///|
/// Scenario 1, end to end: read project metadata, then answer "is every locked
/// dependency acceptable, and is this dependency even for me?".
///
/// The report has three parts per document:
///
/// 1. the document itself is parsed and validated, so a broken `METADATA` is
/// reported with its stable error code instead of being half-read;
/// 2. every `Requires-Dist` line is parsed as a PEP 508 requirement, and the
/// marker decides whether the requirement applies to the target environment
/// at all (a Windows-only dependency is not a problem on Linux, it is simply
/// not applicable);
/// 3. every applicable requirement is checked against the locked version with
/// `SpecifierSet::contains`.
///
/// ## Why the inputs are compiled in
///
/// The library depends on `moonbitlang/core` only, and that package has no
/// filesystem API, so a program cannot open `METADATA` or a lock file. Rather
/// than introduce a dependency (or a platform-specific shim) just for the
/// demonstration, this example takes its documents from the `fixtures` package
/// — the same 239 **real** `METADATA` files that the differential corpus uses,
/// taken from the PEP 658 `.metadata` sidecars of real PyPI wheels — and a small
/// `pylock.toml` document written out below. Swapping the two for a file or a
/// network response is the only change an application needs.
///
/// The lock file is not a stand-in table: it is parsed by this library's own
/// `Pylock::parse` (PEP 751), so the example also shows the lock-file half of the
/// picture — which locked projects apply to this environment at all, and which
/// were recorded for another target. A lock written for Windows is not a broken
/// lock on Linux; it is a lock for a different platform, and the report says so
/// instead of calling every entry in it a conflict.
///
/// Everything here is deterministic: no clock, no environment, no I/O. The
/// output is byte identical on wasm, wasm-gc, js and native.
///
/// ```sh
/// moon run examples/metadata-check --target js
/// ```
///|
/// The environment the markers are evaluated against. Deliberately explicit: the
/// library never reads the host process's environment, so a report produced on
/// one machine can be replayed on another and give the same answer.
fn target_environment() -> @pyversion.MarkerEnvironment {
let environment = @pyversion.MarkerEnvironment::new()
environment.set("python_version", "3.11")
environment.set("python_full_version", "3.11.9")
environment.set("os_name", "posix")
environment.set("sys_platform", "linux")
environment.set("platform_release", "6.1.0")
environment.set("platform_system", "Linux")
environment.set("platform_version", "#1 SMP")
environment.set("platform_machine", "x86_64")
environment.set("platform_python_implementation", "CPython")
environment.set("implementation_name", "cpython")
environment.set("implementation_version", "3.11.9")
// the extra being requested from each distribution; `""` means "no extra"
environment.set("extra", "test")
environment
}
///|
/// What the report says about one dependency line.
enum Verdict {
/// The requirement applies and the locked version satisfies it.
Satisfied(String)
/// The requirement applies and the locked version does not satisfy it.
OutOfRange(String, String, String)
/// The lock file does not mention the project at all.
NotLocked(String)
/// The marker excluded this requirement from the target environment.
NotApplicable(String)
/// The line is not a valid PEP 508 requirement.
Unparsable(String, String)
}
///|
fn Verdict::render(self : Verdict) -> String {
match self {
Satisfied(text) => "ok \{text}"
OutOfRange(name, requirement, locked) =>
"OUT OF RANGE \{name}: locked \{locked}, needs \{requirement}"
NotLocked(name) => "not locked \{name}"
NotApplicable(name) => "not applicable \{name}"
Unparsable(name, code) => "UNPARSABLE \{name}: \{code}"
}
}
///|
/// Whether the verdict is a problem the caller has to act on. `NotApplicable`
/// and `NotLocked` are not: the first is by design, the second is a gap in the
/// lock file rather than a conflict.
fn Verdict::is_problem(self : Verdict) -> Bool {
match self {
OutOfRange(_, _, _) | Unparsable(_, _) => true
Satisfied(_) | NotLocked(_) | NotApplicable(_) => false
}
}
///|
/// Classify one `Requires-Dist` value against a lock table and an environment.
fn classify(
raw : String,
lock : Array[(String, String)],
environment : @pyversion.MarkerEnvironment,
) -> Verdict {
let requirement = @pyversion.Requirement::parse(raw) catch {
@pyversion.VersionError::InvalidRequirement(code, _) =>
return Unparsable(raw, code)
_ => return Unparsable(raw, "INVALID_REQUIREMENT")
}
let key = @pyversion.canonicalize_name(requirement.name)
// The marker decides applicability. A marker the environment cannot answer
// (an undefined key) is reported as not applicable rather than guessed at.
match requirement.marker_text {
None => ()
Some(text) => {
let marker = @pyversion.Marker::parse(text) catch {
_ => return Unparsable(raw, "INVALID_MARKER")
}
// An environment that cannot answer the marker reports "not applicable"
// rather than guessing: the report is about a concrete target.
let applies = marker.evaluate(environment) catch { _ => false }
if !applies {
return NotApplicable(key)
}
}
}
match lookup(lock, key) {
None => NotLocked(key)
Some(locked) => {
let version = @pyversion.Version::parse(locked) catch {
_ => return NotLocked(key)
}
let wanted = requirement.specifier.to_string()
// An empty specifier means "any version", which is what `foo` without a
// constraint means in PEP 508.
let spelled = if wanted.is_empty() { "(any version)" } else { wanted }
if requirement.specifier.contains(version) {
Satisfied("\{key} \{locked} \{spelled}")
} else {
OutOfRange(key, spelled, locked)
}
}
}
}
///|
/// The locked version of `name`, or `None` when the lock does not mention it.
fn lookup(lock : Array[(String, String)], name : String) -> String? {
for entry in lock {
if entry.0 == name {
return Some(entry.1)
}
}
None
}
///|
/// Report one metadata document: the document itself, then one line per
/// dependency, then a one-line summary. Returns the number of problems so the
/// caller can decide on an exit status.
fn report(
name : String,
document : String,
lock : Array[(String, String)],
) -> Int {
println("== \{name}")
let metadata = @pyversion.Metadata::parse(document) catch {
error => {
println(" INVALID METADATA: \{error.diagnostic()}")
return 1
}
}
println(
" \{metadata.name} \{metadata.version.to_string()} (metadata \{metadata.metadata_version})",
)
match metadata.requires_python() {
None => println(" requires-python: (none)")
Some(specifiers) => {
let interpreter = @pyversion.Version::parse("3.11.9") catch {
_ => abort("3.11.9 must parse")
}
let allowed = specifiers.contains(interpreter)
println(
" requires-python: \{specifiers.to_string()} -> \{if allowed { "3.11.9 is allowed" } else { "3.11.9 is NOT allowed" }}",
)
}
}
let warnings = metadata.diagnostics()
if !warnings.is_empty() {
for warning in warnings {
println(" note: \{warning}")
}
}
let problems : Array[Verdict] = []
let requirements = metadata.requirements()
let parsed = requirements.length()
for raw in metadata.requires_dist {
let verdict = classify(raw, lock, target_environment())
println(" \{verdict.render()}")
if verdict.is_problem() {
problems.push(verdict)
}
}
println(
" -> \{parsed}/\{metadata.requires_dist.length()} lines parsed, \{problems.length()} problem(s)",
)
problems.length()
}
///|
/// The lock file this report reads, as a `pylock.toml` document (PEP 751). It is
/// written out rather than fetched so the output is reproducible; the pinned
/// versions are the ones the report explains -- one that satisfies its
/// requirement, one that does not, one project that is only required on another
/// platform -- and the file records the target it was locked for through
/// `environments` and a per-package `marker`.
fn lock_document() -> String {
let text =
#|lock-version = "1.0"
#|environments = ["sys_platform == 'linux'"]
#|extras = ["test"]
#|dependency-groups = ["docs", "lint"]
#|default-groups = ["docs"]
#|created-by = "moon-pyversion metadata-check"
#|
#|[[packages]]
#|name = "requests"
#|version = "2.31.0"
#|
#|[[packages]]
#|name = "werkzeug"
#|version = "2.0.0"
#|
#|[[packages]]
#|name = "jinja2"
#|version = "3.1.4"
#|
#|[[packages]]
#|name = "click"
#|version = "8.1.7"
#|
#|[[packages]]
#|name = "pytest"
#|version = "8.3.5"
#|
#|[[packages]]
#|name = "markupsafe"
#|version = "2.1.5"
#|
#|[[packages]]
#|name = "itsdangerous"
#|version = "2.2.0"
#|
#|[[packages]]
#|name = "blinker"
#|version = "1.8.2"
#|
#|[[packages]]
#|name = "colorama"
#|version = "0.4.6"
#|marker = "sys_platform == 'win32'"
#|
#|[[packages]]
#|name = "demo-linux-only"
#|version = "1.0.0"
#|marker = "sys_platform == 'linux' and python_version >= '3.8'"
text
}
///|
/// The locked projects that apply to `environment`, as `(normalized name,
/// version)`, in document order, plus the number of entries the lock records for
/// another target.
///
/// The document is parsed with `Pylock::parse`, and applicability is the lock's
/// own question: an entry recorded for another platform is *not applicable*
/// here, which is why it is not reported as a missing or conflicting
/// requirement. An entry without a marker applies everywhere.
fn applicable_lock(
document : String,
environment : @pyversion.MarkerEnvironment,
) -> (Array[(String, String)], Int) {
let lock = @pyversion.Pylock::parse(document) catch {
_ => abort("the lock document this example compiles in must parse")
}
let table : Array[(String, String)] = []
let mut skipped = 0
for entry in lock.packages() {
let applies = entry.is_applicable(environment) catch { _ => false }
match entry.version {
Some(version) =>
if applies {
table.push(
(@pyversion.canonicalize_name(entry.name), version.to_string()),
)
} else {
skipped = skipped + 1
}
None => ()
}
}
(table, skipped)
}
///|
/// The documents the report runs on, named explicitly so the output is fixed.
/// All four are real `METADATA` files taken from PyPI wheels, and each one shows
/// a different part of the report:
///
/// - `flask-0.12.5` requires `werkzeug<1.0,>=0.7`, which the lock's 2.0.0 does
/// not satisfy: a real out-of-range conflict;
/// - `alembic-1.5.8` declares
/// `Requires-Python: !=3.0.*,...,>=2.7`, which the target interpreter 3.11.9
/// satisfies, so the interpreter check runs and passes;
/// - `requests-2.24.0` declares `Metadata-Version: 2.0`, which is not a version
/// the core metadata specification defines, so the document is rejected with a
/// stable code (the reference implementation rejects it too);
/// - `kubernetes-10.0.0` carries markers for other platforms, so some of its
/// requirements are reported as not applicable rather than as problems.
fn selected_documents() -> Array[(String, String)] {
let chosen : Array[String] = [
"flask-0.12.5", "alembic-1.5.8", "requests-2.24.0", "kubernetes-10.0.0",
]
let out : Array[(String, String)] = []
for wanted in chosen {
for entry in @fixtures.metadata_pypi_cases {
if entry.0 == wanted {
out.push(entry)
}
}
}
if out.is_empty() {
// The corpus always has these; if it does not, report on the curated case
// that carries every field rather than printing nothing.
for entry in @fixtures.metadata_cases {
if entry.0 == "03_all_fields" {
out.push(entry)
}
}
}
out
}
///|
fn main {
let environment = target_environment()
let lock_file = lock_document()
let (lock, for_another_target) = applicable_lock(lock_file, environment)
let documents = selected_documents()
let parsed = @pyversion.Pylock::parse(lock_file) catch {
_ => abort("the lock document this example compiles in must parse")
}
println(
"lock file: pylock.toml, lock-version \{parsed.lock_version()}, \{parsed.packages().length()} entries, \{lock.length()} for this target, \{for_another_target} for another; target: linux, CPython 3.11.9",
)
let targets = parsed.environments().join(", ")
println(
" lock targets: \{targets}; extras the lock covers: \{parsed.extras().length()}, dependency groups: \{parsed.dependency_groups().length()}",
)
println("")
let mut problems = 0
for entry in documents {
problems = problems + report(entry.0, entry.1, lock)
println("")
}
println(
"\{documents.length()} metadata document(s), \{problems} problem(s) against the lock file",
)
}