Skip to content

Clarify that comparisons must have an environment variable and a string literal#2055

Open
danyeaw wants to merge 1 commit into
pypa:mainfrom
danyeaw:clarify-no-double-markers-vars
Open

Clarify that comparisons must have an environment variable and a string literal#2055
danyeaw wants to merge 1 commit into
pypa:mainfrom
danyeaw:clarify-no-double-markers-vars

Conversation

@danyeaw
Copy link
Copy Markdown

@danyeaw danyeaw commented May 19, 2026

This is a clarification to the text and grammar for dependency specifiers to ensure that only a variable and a string is compared, rather than two of one type. All of the examples and usage of specifiers assumes this, but it isn't restricted in the grammar or explicitly stated in the text.

The current grammar doesn't prevent things like python_version > os_name or '3.10' == '3.11'.

I tested this with the following code:

Details

from parsley import makeGrammar

grammar = r"""
    wsp           = ' ' | '\t'
    version_cmp   = wsp* <'<=' | '<' | '!=' | '===' | '==' | '>=' | '>' | '~='>
    marker_cop    = (wsp* 'in') | (wsp* 'not' wsp+ 'in')
    marker_op     = version_cmp | marker_cop
    python_str_c  = (wsp | letter | digit | '(' | ')' | '.' | '{' | '}' |
                     '-' | '_' | '*' | '#' | ':' | ';' | ',' | '/' | '?' |
                     '[' | ']' | '!' | '~' | '`' | '@' | '$' | '%' | '^' |
                     '&' | '=' | '+' | '|' | '<' | '>' )
    dquote        = '"'
    squote        = '\''
    python_str    = (squote <(python_str_c | dquote)*>:s squote |
                     dquote <(python_str_c | squote)*>:s dquote) -> s
    env_var       = ('python_version' | 'python_full_version' |
                     'os_name' | 'sys_platform' | 'platform_release' |
                     'platform_system' | 'platform_version' |
                     'platform_machine' | 'platform_python_implementation' |
                     'implementation_name' | 'implementation_version' |
                     'extras' | 'dependency_groups')
    extra_op      = '==' | '!='
    extra_expr    = wsp* 'extra' wsp* extra_op:o wsp* python_str:s -> (o, 'extra', s)
    marker_expr   = extra_expr
                  | wsp* env_var:l marker_op:o wsp* python_str:r -> (o, l, r)
                  | wsp* python_str:l marker_op:o wsp* env_var:r -> (o, l, r)
                  | wsp* '(' marker:m wsp* ')' -> m
    marker_and    = marker_expr:l wsp* 'and' marker_expr:r -> ('and', l, r)
                  | marker_expr:m -> m
    marker_or     = marker_and:l wsp* 'or' marker_and:r -> ('or', l, r)
                  | marker_and:m -> m
    marker        = marker_or
"""

compiled = makeGrammar(grammar, {})

valid = [
    # basic comparisons
    ("python_version > '3.10'", "env_var on left"),
    ('os_name == "posix"', "double-quoted string"),
    ("sys_platform != 'win32'", "not-equal"),
    ("python_version >= '3.8'", "gte"),
    # parenthesized grouping
    ("(os_name == 'posix')", "simple parens"),
    (
        "(os_name == 'a' or os_name == 'b') and sys_platform == 'linux'",
        "parens override precedence",
    ),
    # operator precedence: (a and b) or c
    ("os_name == 'a' and os_name == 'b' or os_name == 'c'", "and before or"),
    # operator precedence: a and (b or c)
    ("os_name == 'a' and (os_name == 'b' or os_name == 'c')", "explicit or in parens"),
    ("'SMP' in platform_version", "in comparison"),
    ('"dev" not in dependency_groups', "not in"),
    ("'3.10' < python_version", "yoda 1"),
    ("'posix' == os_name", "yoda 2"),
    ("'win32' != sys_platform", "yoda 3"),
]

rejected = [
    # two variables — no string literal on either side
    "python_version > os_name",
    # two string literals — no env_var on either side
    "'3.10' == '3.11'",
]

print("=== Valid expressions (should all parse) ===")
passed = 0
failed = 0
for expr, description in valid:
    try:
        result = compiled(expr).marker()
        print(f"  PASS  {description!r}: {expr!r} -> {result}")
        passed += 1
    except Exception as e:
        print(f"  FAIL  {description!r}: {expr!r} raised {e}")
        failed += 1

print()
print("=== These specifiers should all be rejected ===")
for expr in rejected:
    try:
        result = compiled(expr).marker()
        print(f"  FAIL  Yoda accepted: {expr!r} -> {result}")
        failed += 1
    except Exception:
        print(f"  PASS  Correctly rejected: {expr!r}")
        passed += 1

print()
print(f"Results: {passed} passed, {failed} failed")


📚 Documentation preview 📚: https://python-packaging-user-guide--2055.org.readthedocs.build/en/2055/

Clarification that comparisons must have an environment variable and a string
literal. The current grammar would allow for comparing two of each type
which is counter for the purpose of these specifiers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant