moon_pyversion

    Pure MoonBit toolkit for Python versions, metadata, indexes, locks and auditable candidate selection.

    python
    packaging
    metadata
    pylock
    resolver
    Download zip
    Version
    0.4.1
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    61

    No dependencies

    #Moon PyVersion

    Pure MoonBit library for parsing, normalizing, comparing, and filtering Python package versions and PEP 440 version specifiers. Zero third-party package dependencies; it uses only moonbitlang/core.

    #Consumer example

    Published and independently installed from MoonCakes as 123123213weqw/moon_pyversion@0.2.0. Add it to your module, then import it in moon.pkg. For source-based use see the repository README.

    import { "123123213weqw/moon_pyversion" @pyversion, }

    ///|
    fn main {
    let v = try! @pyversion.Version::parse("v01.002-rc3+ABC_007")
    println(@pyversion.Version::to_string(v))
    let spec = try! @pyversion.SpecifierSet::parse(">=1.0, !=1.4.*, <2.0")
    let accepted = @pyversion.SpecifierSet::filter(
    [try! @pyversion.Version::parse("1.5")],
    spec,
    )
    println(accepted.length())
    }

    The source repository contains this working example in examples/basic.

    #Public API

    • Version::parse(String) -> Version raise VersionError
    • Version::normalize(String) -> String raise VersionError
    • Version::to_string(Version) -> String
    • Version::compare(Version, Version) -> Int
    • SpecifierSet::parse(String) -> SpecifierSet raise VersionError
    • SpecifierSet::contains(SpecifierSet, Version, prereleases? : Bool? = None) -> Bool
    • SpecifierSet::filter(Array[Version], SpecifierSet, prereleases? : Bool? = None) -> Array[Version]
    • canonicalize_name(String) -> String
    • canonicalize_version(String, strip_trailing_zero? : Bool = true) -> String
    • parse_wheel_filename(String) -> WheelFilename raise VersionError
    • parse_sdist_filename(String) -> SdistFilename raise VersionError
    • Requirement::parse, Marker::parse, Marker::evaluate
    • canonicalize_license_expression / canonicalize_license_file
    • Toml::parse and Metadata::parse
    • SimpleIndex::parse, LocalIndex::scan, resolve_candidates, select_best, explain_rejection
    • Pylock::parse, Pylock::is_applicable, Pylock::applicable_packages
    • cpython_tags, generic_tags, pure_python_tags, compatible_tags, mac_platforms, tag_rank
    • audit_package(...) -> PackageAudit raise
    • audit_bundle(...) -> AuditBundle raise, AuditBundle::counts and AuditBundle::disposition
    • upgrade_shortlist(...) -> UpgradePlan, UpgradePlan::versions, UpgradePlan::rejection_summary, UpgradeCandidate::parse

    The high-level integration boundaries are built out of the parsers and selection rules above rather than beside them.

    audit_package joins one PEP 508 requirement, one real core-metadata document, one PEP 691 index response and one PEP 751 lock for an explicitly supplied target. It reports cross-document name, Python, environment, version and sha256 inconsistencies without downloading or executing a file. It selects the locked version rather than the newest index candidate, then checks the selected filename and sha256 declaration against the lock. It returns Ready, Blocked or NotRequired with stable issue codes. See examples/audit and docs/audit-scenario.md.

    audit_bundle applies that decision to several explicitly supplied projects against one parsed lock and target, reports duplicate project inputs, and can optionally require every applicable lock entry to have an input. It does not resolve or fetch transitive dependencies. See examples/bundle-audit and docs/bundle-audit-scenario.md.

    upgrade_shortlist answers the version part of "which of these candidates is worth a test run?": given the current version, a target range, a prerelease policy, a target interpreter and a candidate list, it returns the shortlist in ascending order (smallest step first) plus the rule that dropped every other candidate -- unparsable, same, downgrade, out-of-range, prerelease or requires-python, in that order, first match wins. The prerelease policy is SpecifierSet::filter's, so it reaches the candidates through the range; a requires-python that does not parse is ignored rather than treated as a rejection, which is what pip does. The shortlist says nothing about API compatibility, dependency solvability or security, and the example prints that sentence in its own output rather than leaving it in a comment. See examples/upgrade-check and docs/upgrade-scenario.md.

    Version implements Eq, Compare, and Show. Its raw field retains the caller's original string. to_string returns a normalized public form: leading zeroes are removed, pre/post/dev spellings are canonicalized (a, b, rc, .post, .dev), separators - and _ are normalized to the canonical form, local segments are lowercased, and numeric local segments lose their leading zeroes. Release segment count is preserved, so 1.0 normalizes to 1.0 and 1.0.0 to 1.0.0; they still compare equal.

    Parsing supports epochs, v prefixes, leading/trailing ASCII whitespace, pre-release aliases (alpha, beta, c, pre, preview), post-release aliases (post, rev, r), dev releases, implicit numeric zeroes, implicit post releases (1.0-1), and local versions. Each suffix label accepts a single -, _ or . separator on either side, so 1.0post1, 1.0-post1, 1.0_post1, 1.0a1-5 and 1.0a1post2dev3 all parse. Invalid input raises a stable VersionError with a code and a UTF-16 offset. Errors never echo passwords, paths, or unrelated runtime data.

    #Ordering and specifier contract

    Comparison follows the PEP 440 key order: epoch, padded release segments, pre-release phase and number, post-release, dev, then local segments. A bare dev release has a pre-key below alpha; otherwise absent pre sorts after pre. Absent post sorts before post; absent dev sorts after dev. Local segments are compared segment by segment: numerically when both segments are numeric, and otherwise as lowercased byte strings (a numeric segment sorts greater than a textual one).

    SpecifierSet supports ==, !=, <, <=, >, >=, ~=, and ===. Wildcard suffixes are accepted only for == and != and match by epoch plus release prefix. Compatible release (~=) uses an inclusive lower bound and an exclusive upper bound formed by incrementing the penultimate release segment. Arbitrary equality (===) compares the parsed candidate's normalized spelling case-insensitively; arbitrary legacy-string candidates are not supported, and the operand follows packaging's restrictions (no whitespace, ; or )). Whitespace is allowed around a specifier but not inside its version operand. Wildcard release prefixes use zero padding. Ordered constraints ignore candidate local labels and reject local labels in the constraint itself.

    Pre-release policy matches packaging 26.3: contains automatically permits a matching prerelease because it has no alternatives; filter prefers matching finals, falling back to prereleases if none exist. Explicit prerelease boundaries opt in. Pass prereleases=Some(false) to exclude or Some(true) to allow them. Operator-specific exclusions still apply: <1.0 excludes 1.0a1 even when prereleases are enabled; >1.0 excludes 1.0.post1. This is not a resolver or a security assessment of upgrade candidates.

    #Packaging metadata helpers

    canonicalize_name implements PEP 503 key normalization (lowercase, collapse runs of -, _ and . into -; leading and trailing separators survive). canonicalize_version implements PEP 625 in both forms: by default it strips trailing zeros from the release segment, which is the form used as an index lookup key (1.0.0 -> 1, 0.0 -> 0), and with strip_trailing_zero=false it only normalizes the spelling (v1.2.3 -> 1.2.3). Invalid input is returned unaltered rather than raising, matching packaging.

    parse_wheel_filename implements PEP 427 (extension, part count, project name rules, optional build tag, compressed tag set expansion) and returns a WheelFilename with a normalized name, the version, (leading digits, rest) or None, and the expanded tags sorted by code point. parse_sdist_filename implements PEP 625 for .tar.gz and .zip, splitting on the last dash.

    Both order strings with String::lexical_compare, because MoonBit's default String comparison orders by length first and would disagree with PEP 440.

    #Runnable scenarios

    Four programs in examples/ join the pieces into end-to-end decisions, and each one is checked byte for byte across the four backends in CI:

    1. examples/metadata-check -- four real METADATA documents plus a pylock.toml and one target environment: which requirements are out of range, which do not apply here, which are not locked, which cannot be parsed. On the real data one document is rejected for declaring a Metadata-Version the specification never defined, and one requirement genuinely conflicts with the lock.
    2. examples/resolve -- one PEP 691 index response, a target tag list and a wheelhouse directory: the yank policy, the first failing rule for every rejected file, and the chosen candidate.
    3. examples/audit -- requirement + real Flask 0.12.5 metadata + index + lock + CPython 3.11/Linux target: Ready, with one yanked candidate excluded and a sha256 declared.
    4. examples/upgrade-check -- the shortlist for eight sets of inputs, including one that trips every rule at once.

    The library never reads a file, the network, the clock or the host: every interpreter version, ABI, platform and environment is supplied by the caller, so the same input produces the same answer on every backend and on every machine.

    #Differential experiment

    Agreement with CPython packaging is measured rather than asserted. examples/diff emits a deterministic corpus of 122 640 records in 26 kinds, from curated lists, grammar-generated input, single-character mutations and real data: 3000 version strings, 500 constraint strings, 600 real Requires-Dist lines and 900 real distribution filenames from 97 PyPI packages, 239 real METADATA documents, 97 real PEP 691 index responses, 215 lock-file records (including six pylock.toml files written by uv), 83 TOML documents, 62 tag argument tuples and 51 upgrade cases. Corpus content is identical across wasm, wasm-gc, js and native after normalizing the host C runtime's CRLF convention; raw and normalized hashes are both reported.

    • python -B tools/diff_packaging.py --oracle-version 26.3 replays every record against packaging 26.3 and reports differences by source, kind, mode and cause.
    • python -B tools/target_parity.py fails if the backends diverge.
    • python -B tools/oracle_matrix.py --oracle python3 replays one captured corpus against several packaging releases.
    • python -B tools/mutation_probe.py breaks the library on purpose and asserts the corpus notices; all 43 injected defects are detected.

    Current results: 0 differences against the targeted packaging 26.3; 4492, 2754 and 844 differences against 24.2, 25.0 and 26.0 respectively, all attributable to upstream behaviour changes (automatic prerelease admission, the </> range rewrite, the ~= upper bound, the 26.3 filename and marker grammars, and the 26.1/26.3 selector APIs). See docs/experiment.md and docs/experiment-results.md. "Zero differences" only means something when the comparison can fail, so the corpus keeps 43 deliberate defects beside it: the probe refuses to run on a modified tree, and every injection has to be noticed.

    Not every record kind has an external reference. packaging covers versions, specifiers, filenames, requirement lines, markers, licences and core metadata; TOML is checked against tomli. PEP 691 indexes and PEP 751 locks have no reference implementation, so those records are compared against a second reading of the specification text written in this repository -- which cannot catch two readings that are wrong in the same way. For locks, six real uv lock files add independence of input, not a second reading: uv writes them and does not read them. Declared divergences are registered per kind and asserted in both directions rather than tolerated.

    The experiment found seven real defects in the library (suffix separator grammar, textual local-segment ordering, specifier operand restrictions, Name and Provides-Extra trailing underscores, Keywords whitespace, and wheel tag case normalization), plus six PEP 751 relaxations that reading the specification's own Required? lines turned into fixes, and four defects in the test infrastructure. One of them is worth repeating: every wheel filename in the corpus spelled its tags in lower case, so the two sides never disagreed about the spelling and the tag-case defect was invisible until the corpus was given mixed-case filenames.

    License: Apache-2.0. See docs/design.md, docs/provenance.md, and the repository README.md.

    DirectoryLister

    type DirectoryLister = (String) -> Array[String] raise VersionError

    A function returning the entry names of a directory.

    moonbitlang/core on the pinned toolchain has no filesystem package: there is no moonbitlang/core/fs, and no read_dir/readdir/list_dir anywhere in core, so this module cannot call one. A host that does have a filesystem passes a function of this shape instead (a thin adapter around whatever listing API that host can reach).

    MarkerError

    pub(all) suberror MarkerError {
    UndefinedEnvironmentName(String)
    UndefinedComparison(String)
    SetValuedOnLeft(String, String)
    }

    PEP 508 environment markers: python_version < "3.10", extra == "test" and sys_platform == "win32".

    A marker is a boolean expression over a small, closed vocabulary of environment keys. It is the second half of a PEP 508 dependency line, the part after the ;. requirements.mbt splits the line and hands the tail to Marker::parse, so the two modules together accept or reject a Requires-Dist line exactly where PyPA packaging does.

    Semantics are aligned with packaging 26.3 and verified by the differential corpus (marker and marker_eval records). Three details are easy to get wrong, so they are called out here:

    • the vocabulary is closed: ex0tra == "x" is a syntax error, not an unknown-name lookup failure, which is what makes a typo in a Requires-Dist line detectable without evaluating anything.
    • rendering is not the input: every value is re-quoted with ", the dotted spellings (os.name) collapse to their canonical key, and parentheses are emitted only where the and/or nesting needs them, because the same structure also decides precedence.
    • and/or need no surrounding whitespace, while not in does.

    One documented boundary: when a version-like key holds a value that is not a PEP 440 version, packaging lets the underlying InvalidVersion escape, while this module falls back to string comparison.
    impl Show for MarkerError

    MarkerError::output

    fn MarkerError::output(self : MarkerError, logger : &Logger) -> Unit

    MarkerError::to_string

    fn MarkerError::to_string(self : MarkerError) -> String

    VersionError

    pub(all) suberror VersionError {
    InvalidVersion(String, Int)
    InvalidSpecifier(String, Int)
    InvalidName(String, Int)
    InvalidFilename(String, Int)
    InvalidRequirement(String, Int)
    InvalidMarker(String, Int)
    InvalidToml(String, Int)
    InvalidLicense(String, Int)
    InvalidMetadata(String, Int)
    InvalidIndex(String, Int)
    InvalidPylock(String, Int)
    InvalidTag(String, Int)
    } derive(Eq,
    Debug
    )

    A stable, structured error for version, specifier and packaging-metadata parsing. Every variant carries a stable code and, where a position is meaningful, a UTF-16 offset.

    VersionError::diagnostic

    fn VersionError::diagnostic(self : VersionError) -> String

    VersionError::equal

    VersionError::not_equal

    fn VersionError::not_equal(x : VersionError, y : VersionError) -> Bool

    VersionError::output

    fn VersionError::output(self : VersionError, logger : &Logger) -> Unit

    VersionError::to_string

    fn VersionError::to_string(self : VersionError) -> String

    AuditBundle

    pub(all) struct AuditBundle {
    reports : Array[PackageAudit]
    issues : Array[AuditIssue]
    } derive(Eq,
    Debug
    )

    An ordered set of package audits against one lock and one target. This is intentionally not a dependency resolver: it audits exactly the inputs the caller supplies and does not infer or fetch missing transitive projects.

    AuditBundle::counts

    fn AuditBundle::counts(self : AuditBundle) -> (Int, Int, Int)

    AuditBundle::disposition

    fn AuditBundle::disposition(self : AuditBundle) -> AuditDisposition

    AuditBundle::equal

    fn AuditBundle::equal(AuditBundle, AuditBundle) -> Bool

    AuditBundle::not_equal

    fn AuditBundle::not_equal(x : AuditBundle, y : AuditBundle) -> Bool

    AuditBundle::render

    fn AuditBundle::render(self : AuditBundle) -> String

    AuditDisposition

    pub(all) enum AuditDisposition {
    NotRequired
    Ready
    Blocked
    } derive(Eq,
    Debug
    )

    Whether the requested dependency needs no action, is reproducibly ready, or is blocked by at least one cross-document inconsistency.

    AuditDisposition::equal

    AuditDisposition::not_equal

    fn AuditDisposition::not_equal(x : AuditDisposition, y : AuditDisposition) -> Bool

    AuditInput

    pub(all) struct AuditInput {
    requirement_text : String
    metadata_text : String
    index_json : String
    } derive(Eq,
    Debug
    )

    One explicit project request and its two externally supplied artifacts. The shared PEP 751 lock and target environment are passed to audit_bundle.

    AuditInput::equal

    fn AuditInput::equal(AuditInput, AuditInput) -> Bool

    AuditInput::not_equal

    fn AuditInput::not_equal(x : AuditInput, y : AuditInput) -> Bool

    AuditIssue

    pub(all) struct AuditIssue {
    code : String
    subject : String
    } derive(Eq,
    Debug
    )

    A stable machine-readable finding. subject identifies the project, version or filename involved without embedding whole input documents.

    AuditIssue::equal

    fn AuditIssue::equal(AuditIssue, AuditIssue) -> Bool

    AuditIssue::not_equal

    fn AuditIssue::not_equal(x : AuditIssue, y : AuditIssue) -> Bool

    IndexFile

    pub(all) struct IndexFile {
    filename : String
    path : String
    kind : IndexFileKind
    name : String
    version : Version
    requires_python : String?
    } derive(Eq,
    Debug
    )

    One distribution file the resolver may consider.

    name is already PEP 503 normalized, so it compares directly against canonicalize_name(requirement.name). version is the PEP 440 version parsed out of the file name: the only version a listing carries. path is wherever the file was found (a path for a scan, a URL for an index response). requires_python is the PEP 345 / PEP 503 constraint when the source knows it: a bare listing does not and leaves it None, while SimpleIndexFile::to_index_file carries the JSON member over.

    IndexFile::equal

    fn IndexFile::equal(IndexFile, IndexFile) -> Bool

    IndexFile::not_equal

    fn IndexFile::not_equal(x : IndexFile, y : IndexFile) -> Bool

    IndexFile::tags

    fn IndexFile::tags(self : IndexFile) -> Array[String]

    The expanded PEP 425 tag set of a wheel, and the empty array for an sdist. The tags come from parse_wheel_filename, so a compressed tag such as py2.py3-none-any is expanded into its combinations. An IndexFile built by hand around an unparseable wheel name has no tags, which the resolver reads as "matches nothing".

    IndexFileKind

    pub(all) enum IndexFileKind {
    WheelFile
    SdistFile
    } derive(Eq,
    Debug
    )

    Whether an IndexFile is a wheel or a source distribution.

    IndexFileKind::equal

    IndexFileKind::not_equal

    fn IndexFileKind::not_equal(x : IndexFileKind, y : IndexFileKind) -> Bool

    JsonValue

    pub(all) enum JsonValue {
    JsonNull
    JsonBool(Bool)
    JsonNumber(String)
    JsonString(String)
    JsonArray(Array[JsonValue])
    JsonObject(Array[(String, JsonValue)])
    } derive(Eq,
    Debug
    )

    A JSON value. JsonNumber keeps the source text of the number, so "1.40" and "1.4e0" stay distinct and nothing here depends on floating-point formatting or precision. JsonObject keeps document order.

    JsonValue::equal

    fn JsonValue::equal(JsonValue, JsonValue) -> Bool

    JsonValue::field

    fn JsonValue::field(self : JsonValue, key : String) -> JsonValue?

    The member named key of a JsonObject, or None. The answer is unambiguous because parse_json rejects duplicate names.

    JsonValue::not_equal

    fn JsonValue::not_equal(x : JsonValue, y : JsonValue) -> Bool

    LocalIndex

    pub(all) struct LocalIndex {
    directory : String
    files : Array[IndexFile]
    } derive(
    Debug
    )

    The distribution files of one directory.

    LocalIndex::files_for

    fn LocalIndex::files_for(self : LocalIndex, name : String) -> Array[IndexFile]

    Every file of name, normalized with PEP 503 canonicalize_name before the lookup, so "Zope.Interface" finds zope-interface-6.0.zip. An unknown name yields an empty array.

    LocalIndex::packages

    fn LocalIndex::packages(self : LocalIndex) -> Array[String]

    The distinct normalized package names in the index, sorted in code point order with String::lexical_compare.

    Array::sort() is never used on strings here: MoonBit's default ordering for String compares length first, which is not the order a PEP 503 consumer expects.

    LocalIndex::scan

    fn LocalIndex::scan(directory : String, lister? : (String) -> Array[String] raise VersionError) -> LocalIndex raise VersionError

    List directory and index every file in it.

    A file is indexed when its name parses as a PEP 427 wheel or a PEP 625 sdist; every other file is ignored, which includes READMEs, lock files and any name the parsers reject. That is the opposite of the PEP 691 rule in SimpleIndex::wheels, deliberately: an index document claims to list distributions, while a directory is expected to hold anything.

    Classification is by name only -- no stat is performed -- so a subdirectory whose name looks like a distribution is indexed too. Only the entries the lister returns are considered; this never recurses. The listing comes from lister (DirectoryLister); the default raises INDEX_NO_FS rather than pretending the directory is empty.

    LocalPart

    pub(all) struct LocalPart {
    text : String
    numeric : Bool
    } derive(Eq,
    Debug
    )

    One normalized local-version segment. Numeric segments are compared as integers; textual segments are compared case-insensitively.

    LocalPart::equal

    fn LocalPart::equal(LocalPart, LocalPart) -> Bool

    LocalPart::not_equal

    fn LocalPart::not_equal(x : LocalPart, y : LocalPart) -> Bool

    Marker

    pub(all) struct Marker {
    items : Array[MarkerItem]
    } derive(Eq,
    Debug
    )

    A parsed marker expression: a flat sequence of clauses, parenthesised groups and and/or operators.
    impl Show for Marker

    Marker::clauses

    fn Marker::clauses(self : Marker) -> Array[MarkerClause]

    The clauses a marker contains, in source order, including the clauses inside parenthesised groups.

    Marker::equal

    fn Marker::equal(Marker, Marker) -> Bool

    Marker::evaluate

    fn Marker::evaluate(self : Marker, environment : MarkerEnvironment) -> Bool raise MarkerError

    Evaluate a marker.

    The flat items list is folded the way packaging folds it: every or starts a new group, everything inside a group is an and, and the result is true when any group is entirely true.

    Marker::not_equal

    fn Marker::not_equal(x : Marker, y : Marker) -> Bool

    Marker::output

    fn Marker::output(self : Marker, logger : &Logger) -> Unit

    Marker::parse

    fn Marker::parse(input : String) -> Marker raise VersionError

    Parse a complete marker expression. Trailing input is an error, so python_version == "3.8" extra does not parse.

    Marker::to_repr

    Marker::to_string

    fn Marker::to_string(self : Marker) -> String raise VersionError

    Render a marker expression back to text.

    The structure is rendered with the minimum parentheses that preserve it: a parenthesised group keeps its parentheses only when it is nested inside another group, while a group that wraps a single clause loses them entirely because the clause renders on its own.

    Marker::variables

    fn Marker::variables(self : Marker) -> Array[MarkerVar]

    The keys a marker mentions, in first-appearance order. Useful for deciding whether a marker can be evaluated in a given context at all.

    MarkerAtom

    pub(all) enum MarkerAtom {
    Clause(MarkerClause)
    Group(Marker)
    } derive(Eq,
    Debug
    )

    A member of the flat list a marker expression parses into. and/or are members rather than tree nodes because that single choice decides both the rendering and the any(all(...)) evaluation order.

    MarkerAtom::equal

    fn MarkerAtom::equal(MarkerAtom, MarkerAtom) -> Bool

    MarkerAtom::not_equal

    fn MarkerAtom::not_equal(x : MarkerAtom, y : MarkerAtom) -> Bool

    MarkerClause

    pub(all) struct MarkerClause {
    lhs : MarkerOperand
    op : MarkerOp
    rhs : MarkerOperand
    } derive(Eq,
    Debug
    )

    One lhs op rhs clause.

    MarkerClause::equal

    MarkerClause::not_equal

    fn MarkerClause::not_equal(x : MarkerClause, y : MarkerClause) -> Bool

    MarkerClause::render

    fn MarkerClause::render(self : MarkerClause) -> String raise VersionError

    MarkerEnvironment

    pub(all) struct MarkerEnvironment {
    values : Map[String, String]
    names : Map[String, Array[String]]
    }

    The environment a marker is evaluated against. There is no ambient default: the caller supplies every value, which keeps evaluation deterministic and makes the differential corpus reproducible.

    A caller must therefore provide a value for every key its markers mention, or evaluate reports UndefinedEnvironmentName. PyPA packaging differs here: it starts from the host process's real environment and overlays the mapping it is given, so a key the caller omitted silently takes the value of the machine the comparison runs on. That fallback is not reproducible, so this library refuses it instead of reproducing it.

    MarkerEnvironment::lookup

    fn MarkerEnvironment::lookup(self : MarkerEnvironment, variable : MarkerVar) -> MarkerValue raise MarkerError

    Look up a key, preferring the set-valued table so set_names wins for the keys that can be either.

    MarkerEnvironment::new

    MarkerEnvironment::set

    fn MarkerEnvironment::set(self : MarkerEnvironment, key : String, value : String) -> Unit

    Set a single-valued key.

    extra is a project extra, so PEP 685 name normalization applies to the environment half of a comparison as much as to the literal inside the marker: a distribution that declares its optional dependency group as Docs.Build has to satisfy extra == "docs-build". packaging rewrites the value it is handed before evaluating, so this library stores the canonical spelling instead and no comparison path can see the raw one.

    MarkerEnvironment::set_names

    fn MarkerEnvironment::set_names(self : MarkerEnvironment, key : String, value : Array[String]) -> Unit

    Set a set-valued key (extras, dependency_groups).

    The members are canonicalized for the same reason as in set: PEP 685 and PEP 735 compare names, so "docs-build" in extras has to match a provided Docs_Build.

    MarkerItem

    pub(all) enum MarkerItem {
    Atom(MarkerAtom)
    And
    Or
    } derive(Eq,
    Debug
    )

    MarkerItem::equal

    fn MarkerItem::equal(MarkerItem, MarkerItem) -> Bool

    MarkerItem::not_equal

    fn MarkerItem::not_equal(x : MarkerItem, y : MarkerItem) -> Bool

    MarkerOp

    pub(all) enum MarkerOp {
    Less
    LessEqual
    NotEqual
    Equal
    GreaterEqual
    Greater
    Compatible
    ArbitraryEqual
    In
    NotIn
    } derive(Eq,
    Debug
    )

    The comparison operators of a marker clause.

    MarkerOp::equal

    fn MarkerOp::equal(MarkerOp, MarkerOp) -> Bool

    MarkerOp::not_equal

    fn MarkerOp::not_equal(x : MarkerOp, y : MarkerOp) -> Bool

    MarkerOp::parse

    fn MarkerOp::parse(text : String) -> MarkerOp?

    Parse the symbolic operators. in and not in are word operators with their own whitespace rules and are handled by the parser directly.

    MarkerOp::symbol

    fn MarkerOp::symbol(self : MarkerOp) -> String

    MarkerOp::to_repr

    MarkerOperand

    pub(all) enum MarkerOperand {
    Variable(MarkerVar)
    Value(String)
    } derive(Eq,
    Debug
    )

    One side of a comparison: either an environment key or a quoted string.

    MarkerOperand::equal

    MarkerOperand::not_equal

    fn MarkerOperand::not_equal(x : MarkerOperand, y : MarkerOperand) -> Bool

    MarkerValue

    pub(all) enum MarkerValue {
    Text(String)
    Names(Array[String])
    } derive(Eq,
    Debug
    )

    The value of a marker key at evaluation time. Most keys hold plain strings; extras and dependency_groups hold sets of names.

    MarkerValue::equal

    fn MarkerValue::equal(MarkerValue, MarkerValue) -> Bool

    MarkerValue::not_equal

    fn MarkerValue::not_equal(x : MarkerValue, y : MarkerValue) -> Bool

    MarkerVar

    pub(all) enum MarkerVar {
    PythonVersion
    PythonFullVersion
    OsName
    SysPlatform
    PlatformRelease
    PlatformSystem
    PlatformVersion
    PlatformMachine
    PlatformPythonImplementation
    ImplementationName
    ImplementationVersion
    Extra
    Extras
    DependencyGroups
    } derive(Eq,
    Debug
    )

    The complete marker vocabulary. Modelling it as a payload-free enum means a misspelling cannot survive parsing.

    MarkerVar::equal

    fn MarkerVar::equal(MarkerVar, MarkerVar) -> Bool

    MarkerVar::is_set_valued

    fn MarkerVar::is_set_valued(self : MarkerVar) -> Bool

    True for the two keys whose environment value is a set of names rather than a string (PEP 685 for extras, PEP 735 for dependency_groups).

    MarkerVar::is_version_like

    fn MarkerVar::is_version_like(self : MarkerVar) -> Bool

    True for the keys that are compared as versions, where < and > carry their numeric meaning instead of being undefined.

    MarkerVar::key

    fn MarkerVar::key(self : MarkerVar) -> String

    The canonical environment key: what Marker::to_string prints and what a MarkerEnvironment is keyed by.

    MarkerVar::not_equal

    fn MarkerVar::not_equal(x : MarkerVar, y : MarkerVar) -> Bool

    MarkerVar::parse

    fn MarkerVar::parse(text : String) -> MarkerVar?

    Resolve a source spelling to a vocabulary entry. Several keys have a dotted alias that packaging normalizes away, and python_implementation is an alias for platform_python_implementation.

    Metadata

    pub(all) struct Metadata {
    metadata_version : String
    name : String
    version : Version
    summary : String?
    description : String?
    description_content_type : String?
    keywords : Array[String]
    author : String?
    author_email : String?
    author_email_addresses : Array[(String, String)]
    maintainer : String?
    maintainer_email : String?
    maintainer_email_addresses : Array[(String, String)]
    license : String?
    license_expression : String?
    home_page : String?
    download_url : String?
    requires_python : String?
    classifiers : Array[String]
    requires_dist : Array[String]
    requires_external : Array[String]
    project_urls : Array[(String, String)]
    provides_extra : Array[String]
    provides_dist : Array[String]
    obsoletes_dist : Array[String]
    dynamic : Array[String]
    platforms : Array[String]
    supported_platforms : Array[String]
    license_files : Array[String]
    import_names : Array[String]
    import_namespaces : Array[String]
    requires : Array[String]
    provides : Array[String]
    obsoletes : Array[String]
    raw : String
    } derive(Eq,
    Debug
    )

    A parsed and validated core metadata document.

    Everything is stored the way the document spelled it — names, extras and classifiers keep their case, an absent optional field is None, and multi-use fields keep their input order — except for the fields the specification gives a canonical form to: Version is a parsed PEP 440 version, License-Expression holds the canonical SPDX expression, and Dynamic is lower-cased because PEP 643 compares it case insensitively.

    raw is the text handed to Metadata::parse. It is the caller's document, not a canonical form, so it is not part of the to_string fixed point.
    impl Show for Metadata

    Metadata::diagnostics

    fn Metadata::diagnostics(self : Metadata) -> Array[String]

    Warnings about a document that parsed successfully. A warning is never an error: the document is usable, it just says something the current specifications discourage. The list is stable and ordered — the deprecated field first, then one entry per deprecated classifier, then the description-related warning — and the exact strings are:

    • deprecated: Home-page is superseded by Project-URL when Home-page is present (PEP 753 deprecates it in favour of a Project-URL);
    • classifier-not-current: <classifier> for every Classifier that starts with License ::, because PEP 639 replaces those classifiers with License-Expression;
    • description-content-type-missing when a description is present but Description-Content-Type is not, which leaves the format of the body to the reader (PEP 566).

    Metadata::equal

    fn Metadata::equal(Metadata, Metadata) -> Bool

    Metadata::extras

    fn Metadata::extras(self : Metadata) -> Array[String]

    Provides-Extra in the canonical PEP 685 form, in input order: extras are compared after canonicalize_name, so Foo_Bar and foo-bar are one extra. The field itself keeps the spelling of the document.

    Metadata::is_compatible

    fn Metadata::is_compatible(self : Metadata, name : String, version : Version, python : Version?) -> Bool

    True when this metadata describes name at version and can be installed on python:

    • the names are compared after canonicalize_name (PEP 503), so Foo_Bar matches foo-bar;
    • the versions are compared with PEP 440 equality, so 1.0 matches 1.0.0;
    • the Requires-Python set, when there is one and python was given, must contain python. An unknown side is not a mismatch: no Requires-Python means every interpreter, and python = None means the caller does not know the interpreter it will install on.

    Metadata::not_equal

    fn Metadata::not_equal(x : Metadata, y : Metadata) -> Bool

    Metadata::output

    fn Metadata::output(self : Metadata, logger : &Logger) -> Unit

    Metadata::parse

    fn Metadata::parse(text : String) -> Metadata raise VersionError

    Parse and validate a METADATA/PKG-INFO document (PEP 566).

    The checks run in a fixed order, so the error a broken document reports is deterministic: the version fields first (rules 1 and 2), then the structural pass over every header (rules 9 and 10), then each field's own rule in the canonical order of the specification's field list.

    let metadata = Metadata::parse(
    "Metadata-Version: 2.1\nName: demo\nVersion: 1.0\n\nHello.\n",
    )
    metadata.name // "demo"
    metadata.version.to_string() // "1.0"
    metadata.description // Some("Hello.\n")

    Metadata::requirements

    fn Metadata::requirements(self : Metadata) -> Array[Requirement]

    Requires-Dist parsed into PEP 508 requirements, in input order.

    Every value was validated by Metadata::parse, so this cannot fail for a value that came from it; a value a caller wrote into the struct by hand and that does not parse is skipped rather than panicking.

    Metadata::requires_python

    fn Metadata::requires_python(self : Metadata) -> SpecifierSet?

    Requires-Python parsed as a PEP 440 specifier set, or None when the field is absent. As with requirements, an unparsable hand-written value becomes None instead of a panic.

    Metadata::to_repr

    Metadata::to_string

    fn Metadata::to_string(self : Metadata) -> String

    Render the metadata as a METADATA/PKG-INFO document.

    The rendering is canonical, so it re-parses to the same metadata; the unit tests assert both the fixed point to_string(parse(to_string(m))) == to_string(m) and that every field survives a round trip:

    • the headers come in the field order of the specification rather than the order of the input document, and multi-use fields keep their input order;
    • every value sits on a single line, because an unfolded logical value has no line breaks;
    • Version is the normalized PEP 440 spelling and License-Expression is the canonical SPDX expression, the two fields whose value is normalized on the way in;
    • a non-empty Description is rendered as the body after the blank line, which is the canonical 2.x location (PEP 566). An empty Description is rendered as a header instead, because an empty body means "no description" and would not survive the round trip.

    PackageAudit

    pub(all) struct PackageAudit {
    project : String
    requirement : String
    applicable : Bool
    metadata_name : String
    metadata_version : String
    index_name : String
    index_files : Int
    ignored_index_files : Int
    yanked_files : Int
    candidates : Array[String]
    selected_file : String?
    selected_version : String?
    lock_versions : Array[String]
    issues : Array[AuditIssue]
    } derive(Eq,
    Debug
    )

    The result of joining one requirement, one core-metadata document, one PEP 691 index response and one PEP 751 lock file for a concrete target. Strings are retained for reporting; no file is downloaded or executed.

    PackageAudit::disposition

    fn PackageAudit::disposition(self : PackageAudit) -> AuditDisposition

    PackageAudit::equal

    PackageAudit::not_equal

    fn PackageAudit::not_equal(x : PackageAudit, y : PackageAudit) -> Bool

    PackageAudit::render

    fn PackageAudit::render(self : PackageAudit) -> String

    A deterministic human-readable report suitable for CI logs. Documents and URLs are not echoed; the report contains identities, counts and findings.

    Pylock

    pub struct Pylock {
    lock_version : String
    environments : Array[String]
    requires_python : String?
    extras : Array[String]
    dependency_groups : Array[String]
    default_groups : Array[String]
    created_by : String
    packages : Array[PylockPackage]
    } derive(Eq,
    Debug
    )

    A validated pylock.toml document. The fields are private; read them through the accessors, so the spelling of every member lives in one place.

    Pylock::accepts_environment

    fn Pylock::accepts_environment(self : Pylock, environment : MarkerEnvironment) -> Bool raise MarkerError

    True when environment is one the lock file claims to support: the specification's environments check, which is satisfied when the member is absent and when at least one of its marker expressions holds.

    Pylock::applicable_packages

    fn Pylock::applicable_packages(self : Pylock, environment : MarkerEnvironment) -> Array[PylockPackage] raise MarkerError

    Every entry that applies to environment, in document order.

    Pylock::created_by

    fn Pylock::created_by(self : Pylock) -> String

    created-by: the tool that wrote the lock file. The specification requires the member, so this reader does too and the value is a plain string.

    Pylock::default_groups

    fn Pylock::default_groups(self : Pylock) -> Array[String]

    default-groups, in document order; empty when the member is absent. These are the keys a MarkerEnvironment should carry as dependency_groups for the default install.

    Pylock::dependencies_of

    fn Pylock::dependencies_of(self : Pylock, name : String) -> Array[PylockDependency]

    Every dependency record of every entry for name, in document order and without deduplication: two entries for one name contribute two lists, because they are two different resolutions.

    Pylock::dependency_groups

    fn Pylock::dependency_groups(self : Pylock) -> Array[String]

    dependency-groups, in document order; empty when the member is absent.

    Pylock::environments

    fn Pylock::environments(self : Pylock) -> Array[String]

    environments: the marker expressions the lock file is compatible with, in document order. Empty means "no restriction", which is also what an absent member means (rule 2 does not give the member a default of its own).

    Pylock::equal

    fn Pylock::equal(Pylock, Pylock) -> Bool

    Pylock::extras

    fn Pylock::extras(self : Pylock) -> Array[String]

    extras, in document order; empty when the member is absent.

    Pylock::files_for

    fn Pylock::files_for(self : Pylock, name : String) -> Array[PylockFile]

    Every file record of every entry for name, in document order, each entry contributing the order of PylockPackage::all_files. This is the lookup an audit wants: files_for("attrs") finds the wheels of an entry spelled Attrs too, and an unknown name yields an empty array.

    Pylock::is_applicable

    fn Pylock::is_applicable(self : Pylock, name : String, environment : MarkerEnvironment) -> Bool raise MarkerError

    True when at least one entry for name applies to environment; false when no entry has that name. Entries for other names are not considered.

    Pylock::lock_version

    fn Pylock::lock_version(self : Pylock) -> String

    lock-version, always "1.0": this reader implements no other version.

    Pylock::not_equal

    fn Pylock::not_equal(x : Pylock, y : Pylock) -> Bool

    Pylock::packages

    fn Pylock::packages(self : Pylock) -> Array[PylockPackage]

    [[packages]], in document order. The same name may appear more than once with different markers and versions, which is how a lock file covers several environments; the install-time rule that they narrow down to one entry is not this reader's job.

    Pylock::packages_named

    fn Pylock::packages_named(self : Pylock, name : String) -> Array[PylockPackage]

    Every entry whose name matches name after canonicalize_name, in document order. An unknown name yields an empty array.

    Pylock::parse

    fn Pylock::parse(text : String) -> Pylock raise VersionError

    Parse and validate a pylock.toml document.

    The checks run in a fixed order, so a broken document reports the same error every time: the top-level members in the specification's own order (rules 1 to 7), then packages (rule 8) and each entry in document order (rules 9 to 20). Text that is not TOML is rejected by Toml::parse with VersionError::InvalidToml before any pylock rule runs.

    let lock = Pylock::parse(
    "lock-version = \"1.0\"\ncreated-by = \"demo\"\n\n[[packages]]\nname = \"attrs\"\nversion = \"25.1.0\"\n[[packages.wheels]]\nname = \"attrs-25.1.0-py3-none-any.whl\"\nhashes = { sha256 = \"c75a69e2\" }\n",
    )
    lock.files_for("attrs")[0].filename() // Some("attrs-25.1.0-py3-none-any.whl")

    Pylock::requires_python

    fn Pylock::requires_python(self : Pylock) -> String?

    requires-python, the raw specifier set, or None.

    Pylock::requires_python_set

    fn Pylock::requires_python_set(self : Pylock) -> SpecifierSet?

    requires-python as a parsed specifier set, or None when the member is absent (or, which rule 3 makes impossible, unparseable).

    Pylock::supports_python

    fn Pylock::supports_python(self : Pylock, version : Version) -> Bool

    True when version satisfies this lock file's requires-python. A lock file without requires-python supports every version.

    Pylock::to_repr

    PylockDependency

    pub(all) struct PylockDependency {
    name : String
    version : Version?
    marker : String?
    extras : Array[String]
    } derive(Eq,
    Debug
    )

    One entry of [[packages.dependencies]].

    PEP 751 defines the entry as the minimum information that identifies another [[packages]] entry by a key-by-key comparison -- name alone, or name plus version, or name plus a source -- and says a tool MUST NOT use it for installation; it is an audit record. marker and extras are not members the specification's dependency entry defines at all, but the brief for this module records them, so they are read and validated like any other marker and name list. Unknown keys of an entry are ignored.

    PylockDependency::equal

    PylockDependency::not_equal

    fn PylockDependency::not_equal(x : PylockDependency, y : PylockDependency) -> Bool

    PylockDirectory

    pub(all) struct PylockDirectory {
    path : String
    editable : Bool
    subdirectory : String?
    } derive(Eq,
    Debug
    )

    [packages.directory]: a source tree on the local filesystem. editable records what the lock time was, and the specification lets an installer ignore it.

    PylockDirectory::equal

    PylockDirectory::not_equal

    fn PylockDirectory::not_equal(x : PylockDirectory, y : PylockDirectory) -> Bool

    PylockFile

    pub(all) struct PylockFile {
    name : String?
    url : String?
    path : String?
    hashes : Array[(String, String)]
    size : Int64?
    upload_time : String?
    subdirectory : String?
    kind : PylockFileKind
    } derive(Eq,
    Debug
    )

    One distribution file of a package: an entry of [[packages.files]], an entry of [[packages.wheels]], or the [packages.sdist] / [packages.archive] table. All of them share one record shape in the specification, so they share one type here.

    name, url and path are all optional; PylockFile::filename resolves the name the specification allows to be inferred from the last component of url or path. hashes keeps document order and is empty when the member is absent. upload_time is the datetime's source spelling, because no date arithmetic happens anywhere in this library.

    PylockFile::equal

    fn PylockFile::equal(PylockFile, PylockFile) -> Bool

    PylockFile::filename

    fn PylockFile::filename(self : PylockFile) -> String?

    The file name of the record: name when it is set, otherwise the last component of url, otherwise the last component of path. None only when the record names no file, which rule 20 allows for sdist and wheels (an installer is then expected to find the file through index) and never for a record that has neither url nor path either.

    PylockFile::hash_for

    fn PylockFile::hash_for(self : PylockFile, algorithm : String) -> String?

    The digest advertised for algorithm (for example "sha256"), or None. Algorithm names are compared verbatim, the same rule SimpleIndexFile uses.

    PylockFile::not_equal

    fn PylockFile::not_equal(x : PylockFile, y : PylockFile) -> Bool

    PylockFileKind

    pub(all) enum PylockFileKind {
    FilesEntry
    WheelFile
    SdistFile
    ArchiveFile
    } derive(Eq,
    Debug
    )

    Which member of a package a PylockFile came from. The lists keep the specification's own spellings apart so a caller can tell a wheel from an sdist from an archive without re-reading the document.

    PylockFileKind::equal

    PylockFileKind::key

    fn PylockFileKind::key(self : PylockFileKind) -> String

    The TOML key a file record was read from: "files", "wheels", "sdist" or "archive".

    PylockFileKind::not_equal

    fn PylockFileKind::not_equal(x : PylockFileKind, y : PylockFileKind) -> Bool

    PylockPackage

    pub(all) struct PylockPackage {
    name : String
    version : Version?
    marker : String?
    requires_python : String?
    dependencies : Array[PylockDependency]
    files : Array[PylockFile]
    wheels : Array[PylockFile]
    sdist : PylockFile?
    archive : PylockFile?
    vcs : PylockVcs?
    directory : PylockDirectory?
    index : String?
    path : String?
    } derive(Eq,
    Debug
    )

    One entry of [[packages]]: a distribution that may be installed, possibly one of several entries for the same name that different environments select between. Read marker through PylockPackage::is_applicable rather than evaluating it by hand.

    PylockPackage::all_files

    fn PylockPackage::all_files(self : PylockPackage) -> Array[PylockFile]

    Every file record of the entry, in a fixed order: files, then sdist, then wheels, then archive. Empty when the package records no file.

    PylockPackage::equal

    PylockPackage::is_applicable

    fn PylockPackage::is_applicable(self : PylockPackage, environment : MarkerEnvironment) -> Bool raise MarkerError

    True when the entry applies to environment: it has no marker, or its marker evaluates to true there.

    The marker is re-parsed on every call, which is cheap, deterministic and keeps the caller's spelling on the value. MarkerError is raised exactly where Marker::evaluate raises it, so an environment that is missing a key one of the markers mentions is reported instead of being guessed at.

    PylockPackage::is_direct_reference

    fn PylockPackage::is_direct_reference(self : PylockPackage) -> Bool

    True when the entry records a direct reference: a source tree, an archive or a local path. Those are the entries whose version is optional here.

    PylockPackage::is_source_tree

    fn PylockPackage::is_source_tree(self : PylockPackage) -> Bool

    True when the entry records a source tree (vcs or directory), which is the case the specification allows to have no version.

    PylockPackage::not_equal

    fn PylockPackage::not_equal(x : PylockPackage, y : PylockPackage) -> Bool

    PylockPackage::requires_python_set

    fn PylockPackage::requires_python_set(self : PylockPackage) -> SpecifierSet?

    requires-python as a parsed specifier set, or None when the member is absent. The text was validated when the document was parsed, so the None of a failed parse cannot happen; a None member is also None.

    PylockVcs

    pub(all) struct PylockVcs {
    vcs_type : String
    url : String?
    path : String?
    requested_revision : String?
    commit_id : String
    subdirectory : String?
    } derive(Eq,
    Debug
    )

    [packages.vcs]: a source tree in version control.

    vcs_type is one of the registered types (git, hg, bzr, svn), commit_id is required because the specification wants an immutable revision, and at least one of url and path says where the repository is.

    PylockVcs::equal

    fn PylockVcs::equal(PylockVcs, PylockVcs) -> Bool

    PylockVcs::not_equal

    fn PylockVcs::not_equal(x : PylockVcs, y : PylockVcs) -> Bool

    Requirement

    pub(all) struct Requirement {
    name : String
    extras : Array[String]
    specifier : SpecifierSet
    url : String?
    marker_text : String?
    raw : String
    } derive(
    Debug
    )

    A parsed PEP 508 requirement.
    impl Show for Requirement

    Requirement::output

    fn Requirement::output(self : Requirement, logger : &Logger) -> Unit

    Requirement::parse

    fn Requirement::parse(input : String) -> Requirement raise VersionError

    Parse a PEP 508 requirement line.

    let r = try! Requirement::parse("Requests[Security]>=2.0,<3.0 ; python_version >= \"3.8\"")
    r.name // "Requests"
    r.extras // ["Security"]
    r.specifier.to_string() // "<3.0,>=2.0"
    r.marker_text // Some("python_version >= \"3.8\"")

    The two operand forms have different whitespace rules, which is why the URL form is parsed before any ; is looked for: the URL runs to the next space, so a ; can be part of it.

    Requirement::to_string

    fn Requirement::to_string(self : Requirement) -> String

    The normalized rendering: name, sorted extras, normalized specifier, URL and marker, in that order.

    SdistFilename

    pub(all) struct SdistFilename {
    name : String
    version : Version
    } derive(Eq,
    Debug
    )

    A parsed source distribution filename.

    SdistFilename::equal

    SdistFilename::not_equal

    fn SdistFilename::not_equal(x : SdistFilename, y : SdistFilename) -> Bool

    SimpleIndex

    pub struct SimpleIndex {
    name : String
    api_version : String?
    files : Array[SimpleIndexFile]
    versions : Array[String]
    } derive(
    Debug
    )

    A parsed PEP 691 "simple repository API" document. The fields are private; read them through SimpleIndex::name, SimpleIndex::api_version, SimpleIndex::files and SimpleIndex::versions.

    SimpleIndex::api_version

    fn SimpleIndex::api_version(self : SimpleIndex) -> String?

    The PEP 691 meta.api-version, or None when meta or api-version is absent. PEP 691 requires both, but the version only gates optional features, so a missing one is reported as None; an ill-typed one is still rejected.

    SimpleIndex::files

    The files array, in document order.

    SimpleIndex::name

    fn SimpleIndex::name(self : SimpleIndex) -> String

    The name member, verbatim. PEP 691 lets the index choose the spelling, so compare it with canonicalize_name.

    SimpleIndex::parse

    fn SimpleIndex::parse(json : String) -> SimpleIndex raise VersionError

    Parse a PEP 691 simple-repository JSON response.

    PEP 691 requires meta, name and files; versions is the PEP 700 addition and optional. meta must be an object and api-version a string when present; name must be a string; every files[] entry must carry filename, url and hashes. Unknown members are ignored.

    Errors are VersionError::InvalidIndex(code, ordinal), where ordinal is the offending files[] entry, or 0 for a document-level problem -- unlike the JSON layer, which reports UTF-16 offsets.

    SimpleIndex::sdists

    Every files[] entry whose name is a valid source distribution, in document order, on the same "neither wheel nor sdist is an error" rule as SimpleIndex::wheels.

    SimpleIndex::versions

    fn SimpleIndex::versions(self : SimpleIndex) -> Array[String]

    The PEP 700 versions array in document order, or [] when the index publishes none.

    SimpleIndex::wheels

    Every files[] entry whose name is a valid wheel, in document order.

    An index is about distributions, so an entry that is neither a wheel nor an sdist is a defect: it raises INDEX_UNKNOWN_DIST (position = entry ordinal) instead of being silently dropped. Entries that are valid sdists are classified and skipped, not rejected.

    SimpleIndexFile

    pub(all) struct SimpleIndexFile {
    filename : String
    url : String
    hashes : Array[(String, String)]
    requires_python : String?
    yanked : SimpleYanked
    size : Int?
    upload_time : String?
    } derive(Eq,
    Debug
    )

    One files[] entry of a PEP 691 response. hashes keeps document order and may be empty (the member is required, its contents are not); url is carried through verbatim, resolving it against the index root is the caller's job. Members this module does not model -- including PEP 658's dist-info-metadata/core-metadata -- are ignored, as PEP 691 requires of clients.

    SimpleIndexFile::equal

    SimpleIndexFile::hash_for

    fn SimpleIndexFile::hash_for(self : SimpleIndexFile, algorithm : String) -> String?

    The digest advertised for algorithm (for example "sha256"), or None. Names are compared verbatim: PEP 691 does not normalize them.

    SimpleIndexFile::not_equal

    fn SimpleIndexFile::not_equal(x : SimpleIndexFile, y : SimpleIndexFile) -> Bool

    SimpleIndexFile::to_index_file

    fn SimpleIndexFile::to_index_file(self : SimpleIndexFile) -> IndexFile raise VersionError

    The IndexFile of an index entry, with path set to the entry's url. An entry that is neither a wheel nor an sdist raises INDEX_UNKNOWN_DIST, mirroring SimpleIndex::wheels.

    SimpleYanked

    pub(all) enum SimpleYanked {
    NotYanked
    Yanked(String)
    } derive(Eq,
    Debug
    )

    PEP 592 yank state of one file. PEP 592 lets yanked be a boolean or a string; Yanked("") is "yanked with no reason", which is what true means and the two spellings are deliberately collapsed, since PEP 592 gives the boolean no other meaning.

    SimpleYanked::equal

    SimpleYanked::not_equal

    fn SimpleYanked::not_equal(x : SimpleYanked, y : SimpleYanked) -> Bool

    Specifier

    pub(all) struct Specifier {
    op : SpecifierOp
    raw_version : String
    version : Version?
    wildcard : Bool
    } derive(
    Debug
    )

    Specifier::contains

    fn Specifier::contains(self : Specifier, version : Version, prereleases? : Bool?) -> Bool

    test {
    let spec = @moon_pyversion.Specifier::parse(">=1.4")
    let hit = @moon_pyversion.Version::parse("1.5")
    inspect(@moon_pyversion.Specifier::contains(spec, hit), content="true")
    }

    Specifier::parse

    fn Specifier::parse(input : String) -> Specifier raise VersionError

    Parse one constraint term (e.g. ">=1.4" or "!=1.4.*") without the surrounding set syntax; mirrors pypa/packaging's Specifier constructor.

    Specifier::to_string

    fn Specifier::to_string(self : Specifier) -> String

    Render a specifier back to text. The operand keeps the caller's spelling (==V1.0 stays ==V1.0) and only surrounding whitespace is dropped, which is what packaging does.

    SpecifierOp

    pub(all) enum SpecifierOp {
    Equal
    NotEqual
    Less
    LessEqual
    Greater
    GreaterEqual
    Compatible
    ArbitraryEqual
    } derive(Eq,
    Debug
    )

    SpecifierOp::equal

    fn SpecifierOp::equal(SpecifierOp, SpecifierOp) -> Bool

    SpecifierOp::not_equal

    fn SpecifierOp::not_equal(x : SpecifierOp, y : SpecifierOp) -> Bool

    SpecifierSet

    pub(all) struct SpecifierSet {
    specs : Array[Specifier]
    original : String
    } derive(
    Debug
    )

    SpecifierSet::contains

    fn SpecifierSet::contains(self : SpecifierSet, version : Version, prereleases? : Bool?) -> Bool

    SpecifierSet::filter

    fn SpecifierSet::filter(versions : Array[Version], spec : SpecifierSet, prereleases? : Bool?) -> Array[Version]

    test {
    let spec = @moon_pyversion.SpecifierSet::parse(">=1.0, <2.0")
    let candidates = [
    @moon_pyversion.Version::parse("0.9"),
    @moon_pyversion.Version::parse("1.5"),
    ]
    inspect(
    @moon_pyversion.SpecifierSet::filter(candidates, spec).length(),
    content="1",
    )
    }

    SpecifierSet::output

    fn SpecifierSet::output(self : SpecifierSet, logger : &Logger) -> Unit

    SpecifierSet::parse

    fn SpecifierSet::parse(input : String) -> SpecifierSet raise VersionError

    SpecifierSet::to_string

    fn SpecifierSet::to_string(self : SpecifierSet) -> String

    Render a constraint set in its canonical form: clauses are deduplicated and sorted by code point, then joined with commas. >=2.0,<3.0 therefore renders as <3.0,>=2.0.

    Toml

    pub(all) struct Toml {
    entries : Array[(String, TomlValue)]
    } derive(Eq,
    Debug
    )

    An ordered TOML table.

    Entries are kept as an association list in document order. Nothing is ever re-sorted, so Toml::to_string is deterministic; lookups are a linear scan, which is fine for the small tables found in pyproject.toml and pylock.toml.

    Toml::equal

    fn Toml::equal(Toml, Toml) -> Bool

    Toml::get

    fn Toml::get(self : Toml, key : String) -> TomlValue?

    Look up a key, optionally as a dotted path such as "tool.poetry.name".

    A dotted path descends through tables only; it stops at arrays. A key that literally contains a . therefore has to be fetched through its parent table.

    Toml::get_array

    fn Toml::get_array(self : Toml, key : String) -> Array[TomlValue]?

    Toml::get_bool

    fn Toml::get_bool(self : Toml, key : String) -> Bool?

    Toml::get_int

    fn Toml::get_int(self : Toml, key : String) -> Int64?

    An integer value, widened to Int64 because that is TOML's own width. A caller that knows the value fits can narrow it with Int64::to_int.

    Toml::get_string

    fn Toml::get_string(self : Toml, key : String) -> String?

    Toml::get_table

    fn Toml::get_table(self : Toml, key : String) -> Toml?

    Toml::not_equal

    fn Toml::not_equal(x : Toml, y : Toml) -> Bool

    Toml::parse

    fn Toml::parse(input : String) -> Toml raise VersionError

    Parse a TOML 1.0 document.

    \r\n is normalized to \n first, which the specification allows even inside string literals. A lone \r and a leading byte-order mark are rejected.

    Toml::to_repr

    Toml::to_string

    fn Toml::to_string(self : Toml) -> String

    Canonical re-serialization.

    The result re-parses to an equal Toml. Within a table, plain values are written before sub-tables so that TOML's header scoping cannot move a key into the wrong table; sub-tables and arrays of tables keep their relative order.

    TomlValue

    pub(all) enum TomlValue {
    TString(String)
    TInt(Int64)
    TFloat(Double)
    TBool(Bool)
    TDatetime(String)
    TArray(Array[TomlValue])
    TTable(Toml)
    } derive(Eq,
    Debug
    )

    A parsed TOML value.

    TDatetime carries the exact source spelling of an offset date-time, a local date-time, a local date or a local time. No date arithmetic and no timezone normalization is performed, so 1979-05-27 07:32:00Z keeps both its separator and its Z.

    TomlValue::equal

    fn TomlValue::equal(TomlValue, TomlValue) -> Bool

    TomlValue::not_equal

    fn TomlValue::not_equal(x : TomlValue, y : TomlValue) -> Bool

    UpgradeCandidate

    pub(all) struct UpgradeCandidate {
    raw : String
    version : Version?
    requires_python : String?
    } derive(Eq,
    Debug
    )

    One candidate to consider.

    version is None when the caller's spelling is not a PEP 440 version at all: the candidate is then reported as Unparsable instead of being dropped from the list, so a report can account for every input it was given.

    UpgradeCandidate::equal

    UpgradeCandidate::not_equal

    fn UpgradeCandidate::not_equal(x : UpgradeCandidate, y : UpgradeCandidate) -> Bool

    UpgradeCandidate::parse

    fn UpgradeCandidate::parse(raw : String, requires_python : String?) -> UpgradeCandidate

    Parse a candidate's version, and remember the requires-python it declares (if any) as text: this module compares it against the target interpreter, and the comparison is the only thing it needs it for.

    let c = UpgradeCandidate::parse("1.2.3", Some(">=3.8"))
    c.version.unwrap().to_string() // "1.2.3"

    UpgradePlan

    pub(all) struct UpgradePlan {
    shortlist : Array[UpgradeCandidate]
    dropped : Array[(UpgradeCandidate, UpgradeRejection)]
    } derive(
    Debug
    )

    The result: what to test, and what was dropped with the reason.

    UpgradePlan::is_empty

    fn UpgradePlan::is_empty(self : UpgradePlan) -> Bool

    Whether the plan found anything at all. A tool that prints "nothing to test" should ask this rather than check shortlist().length(), because the two cases it distinguishes -- "no candidate is newer" and "no candidate was given" -- read differently to a user.

    UpgradePlan::rejection_summary

    fn UpgradePlan::rejection_summary(self : UpgradePlan) -> String

    One line per dropped candidate, in the order the rules ran, for a report.

    UpgradePlan::versions

    fn UpgradePlan::versions(self : UpgradePlan) -> Array[Version]

    The shortlist as the versions to try, most useful first. A convenience for a caller that only needs the versions and not the spellings.

    UpgradeRejection

    pub(all) enum UpgradeRejection {
    Unparsable
    Same
    Downgrade
    OutOfRange
    Prerelease
    RequiresPython
    } derive(Eq,
    Debug
    )

    The rule that dropped a candidate. Closed on purpose: a report that says "rejected" without saying why is not actionable, and the differential corpus asserts the exact reason for each candidate.

    UpgradeRejection::code

    fn UpgradeRejection::code(self : UpgradeRejection) -> String

    The stable spelling of a rejection rule, for reports and for the corpus.

    UpgradeRejection::equal

    UpgradeRejection::not_equal

    fn UpgradeRejection::not_equal(x : UpgradeRejection, y : UpgradeRejection) -> Bool

    Version

    pub(all) struct Version {
    raw : String
    epoch :
    BigInt

    release : Array[
    BigInt
    ]
    has_pre : Bool
    pre_kind : Int
    pre_num :
    BigInt

    has_post : Bool
    post_num :
    BigInt

    has_dev : Bool
    dev_num :
    BigInt

    local_segments : Array[LocalPart]
    } derive(
    Debug
    )

    A parsed PEP 440 version. raw keeps the caller's original string, while to_string returns the normalized public form.
    impl Compare for Version
    impl Eq for Version
    impl Show for Version

    Version::base_version

    fn Version::base_version(self : Version) -> String

    test {
    let v = @moon_pyversion.Version::parse("1.0rc1.post2+local")
    inspect(@moon_pyversion.Version::base_version(v), content="1.0")
    }

    Version::compare

    fn Version::compare(left : Version, right : Version) -> Int

    Version::epoch_string

    fn Version::epoch_string(self : Version) -> String

    The epoch component as an integer string ("0" for the common case).

    Version::equal

    fn Version::equal(self : Version, other : Version) -> Bool

    Version::is_devrelease

    fn Version::is_devrelease(self : Version) -> Bool

    True when the version carries a dev marker.

    Version::is_postrelease

    fn Version::is_postrelease(self : Version) -> Bool

    True when the version carries a post-release marker.

    Version::is_prerelease

    fn Version::is_prerelease(self : Version) -> Bool

    test {
    let v = @moon_pyversion.Version::parse("1.0rc1")
    inspect(@moon_pyversion.Version::is_prerelease(v), content="true")
    }

    Version::normalize

    fn Version::normalize(input : String) -> String raise VersionError

    Version::not_equal

    fn Version::not_equal(x : Version, y : Version) -> Bool

    Version::op_ge

    fn Version::op_ge(x : Version, y : Version) -> Bool

    Version::op_gt

    fn Version::op_gt(x : Version, y : Version) -> Bool

    Version::op_le

    fn Version::op_le(x : Version, y : Version) -> Bool

    Version::op_lt

    fn Version::op_lt(x : Version, y : Version) -> Bool

    Version::output

    fn Version::output(self : Version, logger : &Logger) -> Unit

    Version::parse

    fn Version::parse(input : String) -> Version raise VersionError

    Version::public

    fn Version::public(self : Version) -> String

    The public form: the version with local segments removed, e.g. 1.0+abc -> 1.0. Mirrors the spec's "public version" notion used for equality against local-ignoring constraints.

    Version::to_repr

    Version::to_string

    fn Version::to_string(self : Version) -> String

    WheelFilename

    pub(all) struct WheelFilename {
    name : String
    version : Version
    build : (Int, String)?
    tags : Array[String]
    } derive(Eq,
    Debug
    )

    A parsed wheel filename: a normalized project name, the version, the optional PEP 427 build tag split into (leading digits, remainder) and the expanded tag set.

    WheelFilename::equal

    WheelFilename::not_equal

    fn WheelFilename::not_equal(x : WheelFilename, y : WheelFilename) -> Bool

    audit_bundle

    fn audit_bundle(inputs : Array[AuditInput], pylock_toml : String, environment : MarkerEnvironment, python_text : String, tags : Array[String], allow_yanked? : Bool, require_lock_coverage? : Bool) -> AuditBundle raise

    Audit a caller-supplied bundle against one lock and one explicit target. The lock is parsed once, then each input receives an independent package report in caller order. Duplicate project inputs are reported at bundle level, because two independently supplied index/metadata pairs for one canonical project make the overall decision ambiguous.

    With require_lock_coverage=true, every applicable, Python-compatible lock entry must have an input. This is useful when the caller claims to provide a complete offline mirror; leave it false for partial audits. No network I/O, recursive dependency solving or content hashing occurs here.

    audit_package

    fn audit_package(requirement_text : String, metadata_text : String, index_json : String, pylock_toml : String, environment : MarkerEnvironment, python_text : String, tags : Array[String], allow_yanked? : Bool) -> PackageAudit raise

    Audit a package decision across four Python packaging artifacts.

    The function is deliberately a join, not a resolver or installer:

    • requirement_text supplies the requested project, constraints and marker;
    • metadata_text supplies the distribution identity and Requires-Python;
    • index_json supplies downloadable filenames, yank state and hashes;
    • pylock_toml supplies the versions locked for the target environment;
    • environment, python_text and tags describe the target explicitly.

    Parsing errors are raised by the existing typed parsers. Successfully parsed but mutually inconsistent artifacts return stable AuditIssue entries. Yanked files are excluded unless allow_yanked is true. The selected file must advertise a sha256 digest, match a file and digest in the one applicable lock entry, and agree with the supplied metadata version for Ready.

    canonicalize_license_expression

    fn canonicalize_license_expression(raw : String) -> String raise VersionError

    Normalize a PEP 639 License-Expression, mirroring packaging.licenses.canonicalize_license_expression.

    VersionError::InvalidLicense carries a stable code and the 0-based index of the offending token: INVALID_EXPRESSION for empty input or a syntax error (packaging uses one message for both), UNKNOWN_LICENSE for an unknown operand, UNKNOWN_LICENSE_EXCEPTION for the identifier after WITH, and INVALID_LICENSE_REF for a bad LicenseRef.

    canonicalize_license_expression("mit") // "MIT"
    canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
    // "MIT AND (Apache-2.0 OR BSD-2-Clause)"

    canonicalize_license_file

    fn canonicalize_license_file(path : String) -> String raise VersionError

    Validate a PEP 639 License-File path and return it unchanged.

    The name follows the request for this module, but note what the oracle actually does: packaging 26.3 has no canonicalize_license_file and never rewrites a license file path. The real rule is the private packaging.metadata.Metadata._process_license_files, which accepts or rejects and otherwise returns the path untouched. That is what is implemented here, with the checks in packaging's order so the reported reason matches.

    VersionError::InvalidLicense carries a stable code, and the offset is always 0 because a path is a single value: PARENT_DIRECTORY for .. anywhere, including inside a..b; UNRESOLVED_GLOB for * anywhere; ABSOLUTE_PATH for pathlib-absolute POSIX (/) or Windows (drive letter plus separator); and NOT_POSIX_PATH when pathlib's Windows flavour does not reproduce the path.

    canonicalize_license_file("LICENSE") // "LICENSE"
    canonicalize_license_file("licenses/PSF-2.0.txt") // unchanged
    canonicalize_license_file("./LICENSE") // raises NOT_POSIX_PATH
    canonicalize_license_file("a/../b") // raises PARENT_DIRECTORY

    canonicalize_name

    fn canonicalize_name(name : String) -> String

    test {
    inspect(
    @moon_pyversion.canonicalize_name("Flask_Login"),
    content="flask-login",
    )
    }

    canonicalize_version

    fn canonicalize_version(version : String, strip_trailing_zero? : Bool) -> String

    PEP 625 canonical version form. With strip_trailing_zero (the default) the release segment loses its trailing zeros, which is the form used as an index lookup key; without it the spelling is only normalized.

    Invalid versions are returned unaltered rather than raising, matching packaging.utils.canonicalize_version.

    canonicalize_version("1.0.0") // "1"
    canonicalize_version("1.4.0.0.0") // "1.4"
    canonicalize_version("1.0.0.post0") // "1.post0"
    canonicalize_version("1!2.0") // "1!2"
    canonicalize_version("0.0") // "0"
    canonicalize_version("nope") // "nope"
    canonicalize_version("1.0.0", strip_trailing_zero=false) // "1.0.0"

    compatible_tags

    fn compatible_tags(python_version : Array[Int], interpreter : String?, platforms : Array[String]) -> Array[String] raise VersionError

    The tags for a non-CPython interpreter that is compatible with the given Python version (packaging.tags.compatible_tags): every py* version against every platform, then <interpreter>-none-any when the caller named an interpreter, then the pure-Python tags.

    interpreter may be None (an unnamed interpreter) or an empty string: the reference tests truthiness, so both mean "do not emit that line".

    cpython_tags

    fn cpython_tags(python_version : Array[Int], abis : Array[String], platforms : Array[String]) -> Array[String] raise VersionError

    The CPython tags for a target, in packaging.tags.cpython_tags order.

    python_version is [3, 12] or [3]; abis is what the interpreter reported (["cp312m"], ["cp313t"], ["abi3"], ...); platforms is the platform list, most specific first.

    The order the reference produces is: the explicit ABIs against every platform, then the stable ABI (abi3, plus abi3t on a free-threaded build) against every platform, then the empty none ABI, and finally the same stable ABIs for the previous minor versions down to cp32 -- older stable-ABI wheels are usable, older version-specific ones are not.

    cpython_tags([3, 12], ["cp312"], ["linux_x86_64"])
    // ["cp312-cp312-linux_x86_64", "cp312-abi3-linux_x86_64",
    // "cp312-none-linux_x86_64", "cp311-abi3-linux_x86_64", ...]

    explain_rejection

    fn explain_rejection(files : Array[IndexFile], requirement : Requirement, tags : Array[String], python : Version?, prereleases? : Bool?) -> Array[(String, String)]

    Why each file was rejected, as (filename, reason) in input order, one entry per rejected element.

    The reason is the first rule that fails, in this fixed order, and these five strings are stable API that a CLI prints verbatim:

    • "name" -- a different project;
    • "version" -- outside the specifier set even with prereleases allowed;
    • "prerelease" -- in range, but dropped by the prerelease policy;
    • "requires-python" -- the interpreter is excluded by the constraint;
    • "tags" -- a wheel that cannot be installed on this platform.

    A file that resolve_candidates would keep is never reported, so the two functions describe the same partition of the input; the "prerelease" case reuses allowed_versions, which is why they cannot drift apart. prereleases has the same meaning as in resolve_candidates, so with Some(true) the "prerelease" reason cannot occur.

    generic_tags

    fn generic_tags(interpreter : String, abis : Array[String], platforms : Array[String]) -> Array[String] raise VersionError

    The tags for an interpreter that needs no specialization (packaging.tags.generic_tags): every ABI against every platform, with the empty none ABI appended when the caller did not list it.

    The none test is exact, exactly as it is there: ["NONE"] does not contain none, so none is appended and the resulting list carries the same tag twice. That is a duplicate in the reference too, and a corpus that silently deduplicated it would be testing a different function.

    is_valid_license_expression

    fn is_valid_license_expression(raw : String) -> Bool

    True when canonicalize_license_expression accepts raw. packaging has no such helper; there this predicate only exists inside the License-Expression metadata validator.

    mac_platforms

    fn mac_platforms(version : (Int, Int), arch : String) -> Array[String]

    The macOS platform tags for a version and architecture (packaging.tags.mac_platforms), most specific first.

    Both arguments are required, unlike the reference, where either may be left out and read from the running machine. The rules are the reference's own: before macOS 11 every release bumped the minor version under major 10; from 11 on the major version is the release and the minor one is a midyear update; and on 11+ the compatible 10.x range is replayed for x86_64 (with the universal2 spelling only for other architectures).

    parse_json

    fn parse_json(text : String) -> JsonValue raise VersionError

    Parse one complete RFC 8259 JSON text.

    Exactly which rules are enforced; the reader accepts a subset of JSON and never guesses.

    • §2: the only insignificant whitespace is space, tab, LF and CR.
    • §2/§3: a document is exactly one value, optionally surrounded by whitespace; anything else is JSON_TRAILING.
    • §4: a member name must be a string, , and : are mandatory, no trailing comma. RFC 8259 says names SHOULD be unique; this reader makes that MUST and raises JSON_DUPLICATE_KEY, because a PEP 691 consumer must never see a shadowed url or hashes. Members keep document order.
    • §6: the full grammar -? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE][+-]?[0-9]+)?. A leading zero is JSON_LEADING_ZERO; a missing integer part (.5), a missing fraction (1.) or a missing exponent (1e) is JSON_BAD_NUMBER. A number is not converted to a numeric type -- JsonNumber carries the source text -- so no precision or formatting is lost and there is no range limit here; a caller that needs an Int parses it itself.
    • §7: double quotes only; U+0000..U+001F must be escaped (JSON_CONTROL_CHAR); the escapes are \" \\ \/ \b \f \n \r \t and \uXXXX (JSON_BAD_ESCAPE); \u needs four hex digits (JSON_BAD_UNICODE); a surrogate pair must be high-then-low with both halves present, else JSON_LONE_SURROGATE (§8.2 permits lone surrogates in the grammar; a MoonBit Char cannot hold one).
    • §8: no NaN, Infinity or -Infinity and no other bare word -- a value starting with an ASCII letter must be true, false or null, else JSON_BAD_LITERAL; any other character that cannot start a value is JSON_UNEXPECTED_CHAR.
    • Unterminated input and a missing ,/:/member name are JSON_UNTERMINATED, JSON_EOF, JSON_EXPECT_COMMA, JSON_EXPECT_COLON or JSON_EXPECT_KEY.
    • Deliberate additions and omissions: a leading byte order mark is not skipped (§8.1 only permits ignoring it, so U+FEFF is JSON_UNEXPECTED_CHAR); no Unicode normalization; and nesting is capped at JSON_MAX_DEPTH (JSON_DEPTH), which RFC 8259 does not require but which bounds the recursion of this reader.

    Codes are VersionError::InvalidIndex(code, offset), where offset counts UTF-16 code units of text.

    parse_sdist_filename

    fn parse_sdist_filename(filename : String) -> SdistFilename raise VersionError

    Parse a PEP 625 source distribution filename. The name is normalized; the version must be a valid PEP 440 version. The stem is split on the last dash, because a PEP 440 version cannot contain one.

    let s = parse_sdist_filename("flask-3.0.3.tar.gz")
    s.name // "flask"
    s.version.to_string() // "3.0.3"

    parse_wheel_filename

    fn parse_wheel_filename(filename : String) -> WheelFilename raise VersionError

    Parse a PEP 427 wheel filename. Only the compressed tag set is expanded: the returned tags are strings in interpreter-abi-platform form, sorted and deduplicated, which is the stable projection of packaging's frozenset of Tag objects.

    let w = parse_wheel_filename("numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.whl")
    w.name // "numpy"
    w.version.to_string() // "1.26.4"
    w.build // None
    w.tags // ["cp312-cp312-manylinux_2_17_x86_64"]

    pure_python_tags

    fn pure_python_tags(python_version : Array[Int]) -> Array[String] raise VersionError

    The pure-Python tags (packaging.tags.pure_python_tags): none ABI, any platform, one per interpreter version in py_interpreter_range order. These are the only tags whose computation does not depend on the target platform at all, which is why a single-platform library can still ship them.

    resolve_candidates

    fn resolve_candidates(files : Array[IndexFile], requirement : Requirement, tags : Array[String], python : Version?, prereleases? : Bool?) -> Array[IndexFile]

    The files that could satisfy requirement on a machine described by tags and python, best candidate first.

    A file is kept when all four hold: its PEP 503 normalized name equals the requirement's; its version passes requirement.specifier and PEP 440's prerelease policy, evaluated over all name-matching files at once (so the automatic policy means "prefer finals, fall back to prereleases", not "drop every prerelease"); requires-python is satisfied whenever both sides know it (requires_python_ok); and, for a wheel, its PEP 425 tags intersect tags -- an sdist always passes, since PEP 425 applies to wheels only.

    tags must be ordered most-specific-first, the order of packaging.tags.sys_tags(): it is both the filter and the ranking of rule 3 in compare_candidates. python is the interpreter version or None. prereleases overrides PEP 440's automatic policy as in SpecifierSet::filter. The input array is not modified.

    select_best

    fn select_best(files : Array[IndexFile], requirement : Requirement, tags : Array[String], python : Version?, prereleases? : Bool?) -> IndexFile?

    The single best candidate, or None when resolve_candidates is empty.

    tag_rank

    fn tag_rank(tags : Array[String], supported : Array[String]) -> Int

    The rank of a file's tag set against an ordered supported list: the smallest index any of tags occupies, or supported.length() when none does.

    This is the selector rule packaging.tags.create_compatible_tags_selector applies -- a thing matched by several tags is ranked by its best one, and supported is ordered most specific first -- and the same rule the resolver uses to order candidates. It is public here so callers who built their tag list with the functions above can rank wheels with it.

    upgrade_shortlist

    fn upgrade_shortlist(current : Version, target : SpecifierSet?, prereleases : Bool?, python : Version?, candidates : Array[UpgradeCandidate]) -> UpgradePlan

    Decide the version part of an upgrade. See the module documentation for the rule order; every input candidate appears exactly once in the result, either in shortlist or in dropped.

    validate_marker_text

    fn validate_marker_text(text : String) -> Unit raise VersionError

    Validate a marker's text, for callers that only need the yes/no answer. requirements.mbt uses this to decide whether a dependency line is well formed.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io