///|
/// 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.
pub(all) suberror MarkerError {
/// The environment passed to `Marker::evaluate` has no value for this key.
UndefinedEnvironmentName(String)
/// The operator is not defined for this key and operand.
UndefinedComparison(String)
/// A set-valued key (`extras`, `dependency_groups`) appeared on the left.
SetValuedOnLeft(String, String)
}
///|
pub impl Show for MarkerError with fn to_string(self) {
match self {
UndefinedEnvironmentName(key) => "no value for environment key \{key}"
UndefinedComparison(detail) => detail
SetValuedOnLeft(key, op) =>
"set-valued marker \{key} can only be used with a membership test, not \{op}"
}
}
///|
pub extend MarkerError with Show::{to_string, output}
///|
/// The complete marker vocabulary. Modelling it as a payload-free enum means a
/// misspelling cannot survive parsing.
pub(all) enum MarkerVar {
PythonVersion
PythonFullVersion
OsName
SysPlatform
PlatformRelease
PlatformSystem
PlatformVersion
PlatformMachine
PlatformPythonImplementation
ImplementationName
ImplementationVersion
Extra
Extras
DependencyGroups
} derive(Eq, @debug.Debug)
///|
pub extend MarkerVar with Eq::{equal, not_equal}
///|
pub extend MarkerVar with @debug.Debug::{to_repr}
///|
/// The canonical environment key: what `Marker::to_string` prints and what a
/// `MarkerEnvironment` is keyed by.
pub fn MarkerVar::key(self : MarkerVar) -> String {
match self {
PythonVersion => "python_version"
PythonFullVersion => "python_full_version"
OsName => "os_name"
SysPlatform => "sys_platform"
PlatformRelease => "platform_release"
PlatformSystem => "platform_system"
PlatformVersion => "platform_version"
PlatformMachine => "platform_machine"
PlatformPythonImplementation => "platform_python_implementation"
ImplementationName => "implementation_name"
ImplementationVersion => "implementation_version"
Extra => "extra"
Extras => "extras"
DependencyGroups => "dependency_groups"
}
}
///|
/// 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`).
pub fn MarkerVar::is_set_valued(self : MarkerVar) -> Bool {
match self {
Extras | DependencyGroups => true
_ => false
}
}
///|
/// True for the keys that are compared as versions, where `<` and `>` carry
/// their numeric meaning instead of being undefined.
pub fn MarkerVar::is_version_like(self : MarkerVar) -> Bool {
match self {
PythonVersion
| PythonFullVersion
| ImplementationVersion
| PlatformRelease => true
_ => false
}
}
///|
/// 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`.
pub fn MarkerVar::parse(text : String) -> MarkerVar? {
match text {
"python_version" => Some(PythonVersion)
"python_full_version" => Some(PythonFullVersion)
"os_name" | "os.name" => Some(OsName)
"sys_platform" | "sys.platform" => Some(SysPlatform)
"platform_release" => Some(PlatformRelease)
"platform_system" => Some(PlatformSystem)
"platform_version" | "platform.version" => Some(PlatformVersion)
"platform_machine" | "platform.machine" => Some(PlatformMachine)
"platform_python_implementation" | "platform.python_implementation" =>
Some(PlatformPythonImplementation)
"python_implementation" => Some(PlatformPythonImplementation)
"implementation_name" => Some(ImplementationName)
"implementation_version" => Some(ImplementationVersion)
"extra" => Some(Extra)
"extras" => Some(Extras)
"dependency_groups" => Some(DependencyGroups)
_ => None
}
}
///|
/// Every vocabulary spelling, longest first. The order matters: a plain `extra`
/// must not be allowed to shadow `extras`.
fn marker_var_spellings() -> Array[String] {
[
"platform_python_implementation", "platform.python_implementation", "implementation_version",
"python_implementation", "python_full_version", "implementation_name", "dependency_groups",
"platform_release", "platform_machine", "platform.machine", "platform_version",
"platform.version", "platform_system", "python_version", "sys_platform", "sys.platform",
"os_name", "os.name", "extras", "extra",
]
}
///|
/// The comparison operators of a marker clause.
pub(all) enum MarkerOp {
Less
LessEqual
NotEqual
Equal
GreaterEqual
Greater
Compatible
ArbitraryEqual
In
NotIn
} derive(Eq, @debug.Debug)
///|
pub extend MarkerOp with Eq::{equal, not_equal}
///|
pub extend MarkerOp with @debug.Debug::{to_repr}
///|
pub fn MarkerOp::symbol(self : MarkerOp) -> String {
match self {
Less => "<"
LessEqual => "<="
NotEqual => "!="
Equal => "=="
GreaterEqual => ">="
Greater => ">"
Compatible => "~="
ArbitraryEqual => "==="
In => "in"
NotIn => "not in"
}
}
///|
/// Parse the symbolic operators. `in` and `not in` are word operators with
/// their own whitespace rules and are handled by the parser directly.
pub fn MarkerOp::parse(text : String) -> MarkerOp? {
match text {
"===" => Some(ArbitraryEqual)
"==" => Some(Equal)
"~=" => Some(Compatible)
"!=" => Some(NotEqual)
"<=" => Some(LessEqual)
">=" => Some(GreaterEqual)
"<" => Some(Less)
">" => Some(Greater)
_ => None
}
}
///|
/// The symbolic operator spellings, longest first so `==` is never read as two
/// `<`-free single characters.
fn marker_op_symbols() -> Array[String] {
["===", "==", "~=", "!=", "<=", ">=", "<", ">"]
}
///|
/// One side of a comparison: either an environment key or a quoted string.
pub(all) enum MarkerOperand {
Variable(MarkerVar)
Value(String)
} derive(Eq, @debug.Debug)
///|
pub extend MarkerOperand with Eq::{equal, not_equal}
///|
pub extend MarkerOperand with @debug.Debug::{to_repr}
///|
/// Render one operand the way `packaging` does: a key by its canonical name, a
/// string re-quoted with `"` unless it already contains one, in which case `'`
/// is used. A value containing both quote characters cannot be rendered, which
/// `packaging` reports as an error.
fn MarkerOperand::render(self : MarkerOperand) -> String raise VersionError {
match self {
Variable(variable) => variable.key()
Value(value) =>
if !value.contains("\"") {
"\"" + value + "\""
} else if !value.contains("'") {
"'" + value + "'"
} else {
raise VersionError::InvalidMarker("UNREPRESENTABLE_VALUE", 0)
}
}
}
///|
/// One `lhs op rhs` clause.
pub(all) struct MarkerClause {
lhs : MarkerOperand
op : MarkerOp
rhs : MarkerOperand
} derive(Eq, @debug.Debug)
///|
pub extend MarkerClause with Eq::{equal, not_equal}
///|
pub extend MarkerClause with @debug.Debug::{to_repr}
///|
pub fn MarkerClause::render(self : MarkerClause) -> String raise VersionError {
self.lhs.render() + " " + self.op.symbol() + " " + self.rhs.render()
}
///|
/// 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.
pub(all) enum MarkerAtom {
Clause(MarkerClause)
Group(Marker)
} derive(Eq, @debug.Debug)
///|
pub extend MarkerAtom with Eq::{equal, not_equal}
///|
pub extend MarkerAtom with @debug.Debug::{to_repr}
///|
pub(all) enum MarkerItem {
Atom(MarkerAtom)
And
Or
} derive(Eq, @debug.Debug)
///|
pub extend MarkerItem with Eq::{equal, not_equal}
///|
pub extend MarkerItem with @debug.Debug::{to_repr}
///|
/// A parsed marker expression: a flat sequence of clauses, parenthesised groups
/// and `and`/`or` operators.
pub(all) struct Marker {
items : Array[MarkerItem]
} derive(Eq, @debug.Debug)
///|
pub extend Marker with Show::{output}
///|
pub extend Marker with Eq::{equal, not_equal}
///|
pub extend Marker with @debug.Debug::{to_repr}
///|
fn marker_is_space(c : Char) -> Bool {
c == ' ' || c == '\t'
}
///|
/// A cursor over the characters of a marker expression.
priv struct MarkerParser {
chars : Array[Char]
mut pos : Int
}
///|
fn MarkerParser::peek(self : MarkerParser) -> Char? {
if self.pos < self.chars.length() {
Some(self.chars[self.pos])
} else {
None
}
}
///|
fn MarkerParser::at_end(self : MarkerParser) -> Bool {
self.pos >= self.chars.length()
}
///|
fn MarkerParser::skip_space(self : MarkerParser) -> Unit {
while self.pos < self.chars.length() && marker_is_space(self.chars[self.pos]) {
self.pos = self.pos + 1
}
}
///|
fn MarkerParser::slice(self : MarkerParser, start : Int, stop : Int) -> String {
String::from_array(self.chars[start:stop].to_owned())
}
///|
/// A word boundary in the sense of the Python `\b` anchor: exactly one of the
/// characters on either side of `index` is a word character. `index` may equal
/// the length, meaning end of input.
fn marker_boundary(chars : Array[Char], index : Int) -> Bool {
let left_word = index > 0 && is_word_char(chars[index - 1])
let right_word = index < chars.length() && is_word_char(chars[index])
left_word != right_word
}
///|
/// Test whether `word` starts at the cursor, with a word boundary on both
/// sides, without consuming it. The trailing boundary is why `extra` does not
/// match inside `extras`.
fn MarkerParser::match_word(self : MarkerParser, word : String) -> Bool {
let target = word.to_array()
let length = target.length()
if self.pos + length > self.chars.length() {
return false
}
if !marker_boundary(self.chars, self.pos) {
return false
}
for i = 0; i < length; i = i + 1 {
if self.chars[self.pos + i] != target[i] {
return false
}
}
marker_boundary(self.chars, self.pos + length)
}
///|
/// Consume a vocabulary word if one starts here. Spellings are tried longest
/// first, so `extras` is never read as `extra` followed by junk.
fn MarkerParser::read_variable(self : MarkerParser) -> MarkerVar? {
for spelling in marker_var_spellings() {
if self.match_word(spelling) {
match MarkerVar::parse(spelling) {
Some(variable) => {
self.pos = self.pos + spelling.length()
return Some(variable)
}
None => ()
}
}
}
None
}
///|
/// Consume a quoted string if one starts here. The literal ends at the first
/// occurrence of its own quote character, which is how `packaging` tokenizes it;
/// the body is then decoded with Python string-literal rules, because that is
/// what `packaging` renders back out. A body the Python parser would reject (a
/// truncated `\\x` escape, a lone trailing backslash) is therefore a marker
/// syntax error here too.
fn MarkerParser::read_quoted(self : MarkerParser) -> String? raise VersionError {
let quote = match self.peek() {
Some(c) => c
None => return None
}
if quote != '\'' && quote != '"' {
return None
}
let start = self.pos + 1
let mut stop = start
while stop < self.chars.length() && self.chars[stop] != quote {
stop = stop + 1
}
if stop >= self.chars.length() {
return None
}
let raw = self.slice(start, stop)
self.pos = stop + 1
Some(decode_python_string(raw, start))
}
///|
/// Decode the body of a Python string literal.
///
/// The recognised escapes are the ones Python's own literal parser understands.
/// An unrecognised escape keeps its backslash, which is what Python does (it
/// only warns). The escapes that must have a fixed shape (`\\x`, `\\u`) are
/// errors when truncated, because Python raises `SyntaxError` for those and
/// `packaging` turns that into `InvalidMarker`. So is a lone trailing
/// backslash, which would have escaped the closing quote. `\\N{...}` is the
/// one documented gap: it is reported as unrepresentable rather than decoded.
fn decode_python_string(
raw : String,
offset : Int,
) -> String raise VersionError {
if !raw.contains("\\") {
return raw
}
let chars = raw.to_array()
let out = StringBuilder()
let mut i = 0
while i < chars.length() {
let c = chars[i]
if c != '\\' {
out.write_char(c)
i = i + 1
continue
}
if i + 1 >= chars.length() {
raise VersionError::InvalidMarker("TRUNCATED_ESCAPE", offset + i)
}
let next = chars[i + 1]
if next == 'n' {
out.write_char('\n')
i = i + 2
} else if next == 'r' {
out.write_char('\r')
i = i + 2
} else if next == 't' {
out.write_char('\t')
i = i + 2
} else if next == 'b' {
out.write_char(Int::unsafe_to_char(8))
i = i + 2
} else if next == 'f' {
out.write_char(Int::unsafe_to_char(12))
i = i + 2
} else if next == 'v' {
out.write_char(Int::unsafe_to_char(11))
i = i + 2
} else if next == 'a' {
out.write_char(Int::unsafe_to_char(7))
i = i + 2
} else if next == '\\' || next == '\'' || next == '"' {
out.write_char(next)
i = i + 2
} else if next == 'x' {
match read_hex(chars, i + 2, 2) {
Some(value) => {
out.write_char(Int::unsafe_to_char(value))
i = i + 4
}
None =>
raise VersionError::InvalidMarker("TRUNCATED_HEX_ESCAPE", offset + i)
}
} else if next == 'u' {
match read_hex(chars, i + 2, 4) {
Some(value) => {
out.write_char(Int::unsafe_to_char(value))
i = i + 6
}
None =>
raise VersionError::InvalidMarker(
"TRUNCATED_UNICODE_ESCAPE",
offset + i,
)
}
} else if next == 'N' {
raise VersionError::InvalidMarker("UNSUPPORTED_NAMED_ESCAPE", offset + i)
} else if is_octal_digit(next) {
let mut value = 0
let mut used = 0
while used < 3 &&
i + 1 + used < chars.length() &&
is_octal_digit(chars[i + 1 + used]) {
value = value * 8 + (chars[i + 1 + used].to_int() - '0'.to_int())
used = used + 1
}
out.write_char(Int::unsafe_to_char(value))
i = i + 1 + used
} else {
// An unrecognised escape keeps its backslash, as Python does.
out.write_char('\\')
i = i + 1
}
}
out.to_string()
}
///|
fn is_octal_digit(c : Char) -> Bool {
c >= '0' && c <= '7'
}
///|
/// Read exactly `width` hexadecimal digits, or nothing.
fn read_hex(chars : Array[Char], start : Int, width : Int) -> Int? {
if start + width > chars.length() {
return None
}
let mut value = 0
for offset = 0; offset < width; offset = offset + 1 {
match hex_value(chars[start + offset]) {
Some(digit) => value = value * 16 + digit
None => return None
}
}
Some(value)
}
///|
fn hex_value(c : Char) -> Int? {
if c >= '0' && c <= '9' {
Some(c.to_int() - '0'.to_int())
} else if c >= 'a' && c <= 'f' {
Some(c.to_int() - 'a'.to_int() + 10)
} else if c >= 'A' && c <= 'F' {
Some(c.to_int() - 'A'.to_int() + 10)
} else {
None
}
}
///|
fn MarkerParser::expect_space(
self : MarkerParser,
code : String,
) -> Unit raise VersionError {
let before = self.pos
self.skip_space()
if self.pos == before {
raise VersionError::InvalidMarker(code, self.pos)
}
}
///|
/// Consume an operator: one of the symbolic spellings, or the word operators
/// `in` / `not in`. `not in` requires whitespace between its two words and
/// `and`/`or` never do.
fn MarkerParser::read_op(self : MarkerParser) -> MarkerOp raise VersionError {
if self.match_word("not") {
self.pos = self.pos + 3
self.expect_space("EXPECTED_WHITESPACE_AFTER_NOT")
if !self.match_word("in") {
raise VersionError::InvalidMarker("EXPECTED_IN_AFTER_NOT", self.pos)
}
self.pos = self.pos + 2
return NotIn
}
if self.match_word("in") {
self.pos = self.pos + 2
return In
}
for symbol in marker_op_symbols() {
if self.starts_with(symbol) {
match MarkerOp::parse(symbol) {
Some(op) => {
self.pos = self.pos + symbol.length()
return op
}
None => ()
}
}
}
raise VersionError::InvalidMarker("EXPECTED_MARKER_OPERATOR", self.pos)
}
///|
fn MarkerParser::starts_with(self : MarkerParser, text : String) -> Bool {
let target = text.to_array()
if self.pos + target.length() > self.chars.length() {
return false
}
for i = 0; i < target.length(); i = i + 1 {
if self.chars[self.pos + i] != target[i] {
return false
}
}
true
}
///|
/// Consume one operand: a vocabulary word or a quoted string.
fn MarkerParser::read_operand(
self : MarkerParser,
) -> MarkerOperand raise VersionError {
match self.read_variable() {
Some(variable) => Variable(variable)
None =>
match self.read_quoted() {
Some(value) => Value(value)
None =>
raise VersionError::InvalidMarker(
"EXPECTED_MARKER_VARIABLE",
self.pos,
)
}
}
}
///|
/// `marker_item = WS? marker_var WS? marker_op WS? marker_var WS?`
fn MarkerParser::parse_clause(
self : MarkerParser,
) -> MarkerClause raise VersionError {
self.skip_space()
let lhs = self.read_operand()
self.skip_space()
let op = self.read_op()
self.skip_space()
let rhs = self.read_operand()
self.skip_space()
{ lhs, op, rhs, }
}
///|
/// `marker_atom = WS? '(' WS? marker WS? ')' WS? | WS? marker_item WS?`
fn MarkerParser::parse_atom(
self : MarkerParser,
) -> MarkerAtom raise VersionError {
self.skip_space()
if self.peek() is Some('(') {
let open = self.pos
self.pos = self.pos + 1
self.skip_space()
let inner = self.parse_marker()
self.skip_space()
match self.peek() {
Some(')') => self.pos = self.pos + 1
_ => raise VersionError::InvalidMarker("UNCLOSED_GROUP", open)
}
self.skip_space()
return Group(inner)
}
Clause(self.parse_clause())
}
///|
/// `marker = marker_atom (BOOLOP marker_atom)+`, built as one flat list.
fn MarkerParser::parse_marker(self : MarkerParser) -> Marker raise VersionError {
let items : Array[MarkerItem] = [Atom(self.parse_atom())]
while true {
if self.match_word("and") {
self.pos = self.pos + 3
items.push(And)
} else if self.match_word("or") {
self.pos = self.pos + 2
items.push(Or)
} else {
break
}
items.push(Atom(self.parse_atom()))
}
{ items, }
}
///|
/// Parse a complete marker expression. Trailing input is an error, so
/// `python_version == "3.8" extra` does not parse.
pub fn Marker::parse(input : String) -> Marker raise VersionError {
let parser : MarkerParser = { chars: input.to_array(), pos: 0, }
let marker = parser.parse_marker()
parser.skip_space()
if !parser.at_end() {
raise VersionError::InvalidMarker("TRAILING_INPUT", parser.pos)
}
marker.normalized()
}
///|
/// 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.
pub fn Marker::to_string(self : Marker) -> String raise VersionError {
self.render(true)
}
///|
fn Marker::render(self : Marker, first : Bool) -> String raise VersionError {
if self.items.length() == 1 {
match self.items[0] {
Atom(atom) => return render_atom(atom, first)
_ => ()
}
}
let parts : Array[String] = []
for item in self.items {
match item {
Atom(atom) => parts.push(render_atom(atom, false))
And => parts.push("and")
Or => parts.push("or")
}
}
let joined = parts.join(" ")
if first {
joined
} else {
"(" + joined + ")"
}
}
///|
fn render_atom(atom : MarkerAtom, first : Bool) -> String raise VersionError {
match atom {
Clause(clause) => clause.render()
Group(inner) => inner.render(first)
}
}
///|
pub impl Show for Marker with fn to_string(self) {
self.render(true) catch {
_ => ""
}
}
///|
/// PEP 685 / PEP 735 name normalization at parse time: a name compared against
/// `extra`, `extras` or `dependency_groups` is stored canonicalized so that
/// rendering and evaluation cannot disagree.
fn Marker::normalized(self : Marker) -> Marker {
let items : Array[MarkerItem] = []
for item in self.items {
match item {
Atom(atom) => items.push(Atom(normalize_atom(atom)))
And => items.push(And)
Or => items.push(Or)
}
}
{ items, }
}
///|
fn normalize_atom(atom : MarkerAtom) -> MarkerAtom {
match atom {
Group(inner) => Group(inner.normalized())
Clause(clause) =>
match (clause.lhs, clause.rhs) {
(Variable(Extra), Value(text)) =>
Clause({
lhs: clause.lhs,
op: clause.op,
rhs: Value(canonicalize_name(text)),
})
(Value(text), Variable(Extra)) =>
Clause({
lhs: Value(canonicalize_name(text)),
op: clause.op,
rhs: clause.rhs,
})
(Value(text), Variable(other)) =>
if other.is_set_valued() {
Clause({
lhs: Value(canonicalize_name(text)),
op: clause.op,
rhs: clause.rhs,
})
} else {
Clause(clause)
}
_ => Clause(clause)
}
}
}
///|
/// The value of a marker key at evaluation time. Most keys hold plain strings;
/// `extras` and `dependency_groups` hold sets of names.
pub(all) enum MarkerValue {
Text(String)
Names(Array[String])
} derive(Eq, @debug.Debug)
///|
pub extend MarkerValue with Eq::{equal, not_equal}
///|
pub extend MarkerValue with @debug.Debug::{to_repr}
///|
/// 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.
pub(all) struct MarkerEnvironment {
values : Map[String, String]
names : Map[String, Array[String]]
}
///|
pub fn MarkerEnvironment::new() -> MarkerEnvironment {
{ values: Map([]), names: Map([]), }
}
///|
/// 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.
pub fn MarkerEnvironment::set(
self : MarkerEnvironment,
key : String,
value : String,
) -> Unit {
match MarkerVar::parse(key) {
Some(Extra) => self.values[key] = canonicalize_name(value)
_ => self.values[key] = value
}
}
///|
/// 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`.
pub fn MarkerEnvironment::set_names(
self : MarkerEnvironment,
key : String,
value : Array[String],
) -> Unit {
let names : Array[String] = []
for name in value {
names.push(canonicalize_name(name))
}
self.names[key] = names
}
///|
/// Look up a key, preferring the set-valued table so `set_names` wins for the
/// keys that can be either.
pub fn MarkerEnvironment::lookup(
self : MarkerEnvironment,
variable : MarkerVar,
) -> MarkerValue raise MarkerError {
let key = variable.key()
match self.names.get(key) {
Some(list) => Names(list)
None =>
match self.values.get(key) {
Some(text) => Text(text)
None => raise MarkerError::UndefinedEnvironmentName(key)
}
}
}
///|
/// 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.
pub fn Marker::evaluate(
self : Marker,
environment : MarkerEnvironment,
) -> Bool raise MarkerError {
evaluate_items(self.items, environment)
}
///|
fn evaluate_items(
items : Array[MarkerItem],
environment : MarkerEnvironment,
) -> Bool raise MarkerError {
let groups : Array[Array[Bool]] = [[]]
for item in items {
match item {
Atom(Clause(clause)) =>
groups[groups.length() - 1].push(evaluate_clause(clause, environment))
Atom(Group(inner)) =>
groups[groups.length() - 1].push(
evaluate_items(inner.items, environment),
)
And => ()
Or => groups.push([])
}
}
for group in groups {
let mut all_true = true
for value in group {
if !value {
all_true = false
}
}
if all_true {
return true
}
}
false
}
///|
/// Evaluate one clause. The key is normally the left operand, but
/// `"3.8" == python_version` is legal, so either side may be the variable.
fn evaluate_clause(
clause : MarkerClause,
environment : MarkerEnvironment,
) -> Bool raise MarkerError {
match clause.lhs {
Variable(variable) => {
let lhs = match environment.lookup(variable) {
Text(text) => text
Names(_) =>
raise MarkerError::SetValuedOnLeft(variable.key(), clause.op.symbol())
}
let rhs = operand_value(clause.rhs, environment)
compare_marker(lhs, clause.op, rhs, Some(variable))
}
Value(left) =>
match clause.rhs {
Variable(variable) => {
let rhs = environment.lookup(variable)
compare_marker(left, clause.op, rhs, Some(variable))
}
Value(right) =>
// Two literals: `packaging` still looks the right hand side up as an
// environment key, so this is an undefined name rather than a plain
// string comparison.
raise MarkerError::UndefinedEnvironmentName(right)
}
}
}
///|
fn operand_value(
operand : MarkerOperand,
environment : MarkerEnvironment,
) -> MarkerValue raise MarkerError {
match operand {
Value(text) => Text(text)
Variable(variable) => environment.lookup(variable)
}
}
///|
/// Compare a left-hand string against the evaluated right-hand side. Version-like
/// keys go through the PEP 440 machinery, so `python_version > "3.7"` compares
/// numerically; everything else uses plain string semantics, where `<` and `>`
/// are defined to be false.
fn compare_marker(
lhs : String,
op : MarkerOp,
rhs : MarkerValue,
variable : MarkerVar?,
) -> Bool raise MarkerError {
if variable is Some(key) && key.is_version_like() {
match rhs {
Text(text) => {
let specifier : SpecifierSet? = Some(
SpecifierSet::parse(op.symbol() + text),
) catch {
_ => None
}
match (specifier, parse_version_opt(lhs)) {
(Some(found), Some(version)) => return found.contains(version)
_ => ()
}
}
Names(_) => ()
}
}
match rhs {
Names(members) => {
let canonical = canonicalize_name(lhs)
let matched = members.contains(canonical)
match op {
In => matched
NotIn => !matched
_ => false
}
}
Text(text) =>
// `lhs in rhs` tests whether the left operand occurs inside the right
// one, so the containment is asked of `text`, not of `lhs`.
match op {
In => text.contains(lhs)
NotIn => !text.contains(lhs)
Equal | LessEqual | GreaterEqual => lhs == text
NotEqual => lhs != text
Less | Greater => false
Compatible | ArbitraryEqual =>
raise MarkerError::UndefinedComparison(
"undefined \{op.symbol()} on \{lhs} and \{text}",
)
}
}
}
///|
/// The clauses a marker contains, in source order, including the clauses inside
/// parenthesised groups.
pub fn Marker::clauses(self : Marker) -> Array[MarkerClause] {
let out : Array[MarkerClause] = []
collect_clauses(self.items, out)
out
}
///|
fn collect_clauses(
items : Array[MarkerItem],
out : Array[MarkerClause],
) -> Unit {
for item in items {
match item {
Atom(Clause(clause)) => out.push(clause)
Atom(Group(inner)) => collect_clauses(inner.items, out)
And => ()
Or => ()
}
}
}
///|
/// The keys a marker mentions, in first-appearance order. Useful for deciding
/// whether a marker can be evaluated in a given context at all.
pub fn Marker::variables(self : Marker) -> Array[MarkerVar] {
let out : Array[MarkerVar] = []
for clause in self.clauses() {
for operand in [clause.lhs, clause.rhs] {
match operand {
Variable(variable) => if !out.contains(variable) { out.push(variable) }
Value(_) => ()
}
}
}
out
}
///|
/// 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.
pub fn validate_marker_text(text : String) -> Unit raise VersionError {
ignore(Marker::parse(text))
}