Markerpry
Table of Contents
Installation
pip install markerpry
Usage
Markerpry provides a powerful way to parse, manipulate and evaluate Python package environment markers.
Parsing Markers
Use the parse() method to create a tree structure from a marker string:
from markerpry import parse
# Parse a marker expression into a tree
tree = parse('python_version >= "3.7" and (os_name == "posix" or platform_system == "Linux")')
The parse method returns a tree where each node is one of:
BooleanNode: Represents True/False valuesCompareNode: Represents comparisons likepython_version >= "3.7"(==,===,!=,<,<=,>,>=,~=)ContainsNode: Represents membership tests like"3.7" in python_versionorpython_version in "3.7"OperatorNode: Represents logical operations (and/or) between nodes
Tree Navigation
The tree can be navigated using the left and right properties of nodes. These properties
return None for leaf nodes (BooleanNode, CompareNode, and ContainsNode):
# For operator nodes (and/or), access child nodes
left_expr = tree.left # python_version >= "3.7"
right_expr = tree.right # (os_name == "posix" or platform_system == "Linux")
# For nested expressions, continue traversing
nested_left = right_expr.left # os_name == "posix"
nested_right = right_expr.right # platform_system == "Linux"
# Leaf nodes have no children
assert nested_left.left is None
assert nested_left.right is None
Checking for Keys
You can check if a marker expression contains a specific environment key using the in operator:
# Check if a marker depends on specific environment keys
tree = parse('python_version >= "3.7" and os_name == "posix"')
assert "python_version" in tree
assert "os_name" in tree
assert "platform_machine" not in tree
String Representation
The tree can be converted back to a string using str(), which produces a format compatible with packaging.markers.Marker:
# Convert tree back to string
marker_string = str(tree)
# 'python_version >= "3.7" and (os_name == "posix" or platform_system == "Linux")'
# Use with packaging.markers
from packaging.markers import Marker
marker = Marker(str(tree))
Evaluation
The evaluate() function partially evaluates a tree based on the provided environment:
from markerpry import evaluate
from packaging.version import Version
import re
# Define an environment with known values
env = {
"python_version": [Version("3.8")],
"os_name": ["posix"],
"platform_system": ["Linux"],
"implementation_name": [re.compile("py.*")], # Matches python, pypy, etc.
}
# Evaluate the tree
result = evaluate(tree, env)
# The result will be a simplified tree or a BooleanNode
# In this case, it would evaluate to BooleanNode(True)
The evaluation process:
Environment Values
Each environment key can contain a list of different types of values:
Versionobjects: Used for version comparisons (python_version, etc.)- Work with all comparators (
==,===,!=,<,<=,>,>=,~=) - Version strings are parsed using
packaging.specifiers.SpecifierSet
- Work with all comparators (
strvalues: Used for exact string matching and substring tests==/===/!=do exact string equality;in/not indo substring tests- Other comparators (
<,<=,>,>=,~=) will leave the expression unevaluated
re.Patternobjects: Used for pattern matching- Only work with equality comparators (
==,===,!=) ==/===checks if the pattern matches!=checks if the pattern doesn't match
- Only work with equality comparators (
boolvalues: Unconditionally decide every comparatorTruemeans "yes";Falsemeans "no"
RangeConstraintobjects: Represent an open or closed interval of versionsRangeConstraint(min, max, include_min=True, include_max=False)— either bound may beNonefor "unbounded on this side"- Decidable for
<,<=,>,>=only when the entire interval agrees on the answer (checked by sampling both boundaries); for==/===/!=only when the compared point falls outside the interval, or the interval is a single point equal to it in/not in/~=are always undecidable for an interval- See Range Constraints below
Multiple Values
When multiple values are provided for an environment key:
env = {"python_version": [Version("3.8"), Version("3.9")], "os_name": ["posix", "nt"]}
- The expression is evaluated against each value
- Results are combined with OR logic (any match makes it true)
- If no values match, the expression remains unevaluated
Tree Simplification
The evaluation simplifies boolean operations where possible:
# For OR operations:
True or X => True # Short circuits to True
False or X => X # Continues evaluation with X
# For AND operations:
False and X => False # Short circuits to False
True and X => X # Continues evaluation with X
For example:
# Original: python_version >= "3.7" and (os_name == "posix" or platform_system == "Linux")
env = {"python_version": [Version("3.6")]}
# Evaluates to: False and (os_name == "posix" or platform_system == "Linux")
# Simplifies to: False
env = {"python_version": [Version("3.8")], "os_name": ["posix"]}
# Evaluates to: True and (True or platform_system == "Linux")
# Simplifies to: True
If any parts of the expression can't be evaluated (due to missing environment values or incompatible comparators), they remain as expressions in the resulting tree.
Range Constraints
RangeConstraint represents an open or closed interval of versions, and only resolves an expression when the entire interval agrees on the answer — unlike a list of Version values, which is evaluated existentially and can spuriously resolve interval questions:
from markerpry import RangeConstraint, evaluate, parse
from packaging.version import Version
# An abi3 wheel's floor: python_version >= 3.9, no ceiling
tree = parse('python_version < "3.11"')
env = {"python_version": [RangeConstraint(min=Version("3.9"), max=None)]}
result = evaluate(tree, env)
# result is the SAME unresolved node: "< 3.11" holds for some but not all
# versions >= 3.9, so it stays conditional.
env = {"python_version": [RangeConstraint(min=Version("3.12"), max=None)]}
result = evaluate(tree, env)
# result is BooleanNode(False): every version >= 3.12 fails "< 3.11".
Modifying Trees
modify() is the general-purpose tree-rewrite primitive evaluate() is itself built on. It walks the tree bottom-up and hands every non-operator node to a leaf callback you provide, returning the (possibly different) node it gives back:
from markerpry import BooleanNode, CompareNode, parse
tree = parse('extra == "docs" or python_version >= "3.8"')
def drop_extras(node):
if isinstance(node, CompareNode) and node.key == "extra":
return BooleanNode(False)
return node
result = tree.modify(leaf=drop_extras)
# result is CompareNode(key="python_version", comparator=">=", literal="3.8")
# - the "extra" branch folded to False, and the OR drops it automatically
License
markerpry is distributed under the terms of the MIT license.
Development
This project uses uv for dependency management and packaging.
# Install dependencies (including dev tools) into a local virtualenv
uv sync
# Run the test suite
uv run pytest
# Run linters
uv run isort --check --diff .
uv run black --check --diff .
uv run mypy --check-untyped-defs markerpry tests
# Build the package
uv build
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file markerpry-0.5.0.tar.gz.
File metadata
- Download URL: markerpry-0.5.0.tar.gz
- Upload date:
- Size: 10.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24ce803f3cad80593f372e463a49baa547e175db723b6e81d4a367a5002235df
|
|
| MD5 |
c2ba751f5abfc1badc71d4536a00704f
|
|
| BLAKE2b-256 |
9b30a27295aead52a76ab1c8d57b12a1a94aee7477c16c3f49971360fa75660d
|
Provenance
The following attestation bundles were made for markerpry-0.5.0.tar.gz:
Publisher:
publish.yml on intentionally-left-nil/markerpry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
markerpry-0.5.0.tar.gz -
Subject digest:
24ce803f3cad80593f372e463a49baa547e175db723b6e81d4a367a5002235df - Sigstore transparency entry: 2403809074
- Sigstore integration time:
-
Permalink:
intentionally-left-nil/markerpry@59f7fd8f8d9f14ad8c9db4aef358649286c1e059 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/intentionally-left-nil
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@59f7fd8f8d9f14ad8c9db4aef358649286c1e059 -
Trigger Event:
push
-
Statement type:
File details
Details for the file markerpry-0.5.0-py3-none-any.whl.
File metadata
- Download URL: markerpry-0.5.0-py3-none-any.whl
- Upload date:
- Size: 12.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0dff034f53f6889ddd07063d05d47f78c9df224b37b9b0a5dd5d6e4659fc36a5
|
|
| MD5 |
3285972ed3cf3718202ebc50650ec3af
|
|
| BLAKE2b-256 |
000efce20626216cfe9c771c00c1d22336012e98e5683dc09a6f53c09a26828d
|
Provenance
The following attestation bundles were made for markerpry-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on intentionally-left-nil/markerpry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
markerpry-0.5.0-py3-none-any.whl -
Subject digest:
0dff034f53f6889ddd07063d05d47f78c9df224b37b9b0a5dd5d6e4659fc36a5 - Sigstore transparency entry: 2403809816
- Sigstore integration time:
-
Permalink:
intentionally-left-nil/markerpry@59f7fd8f8d9f14ad8c9db4aef358649286c1e059 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/intentionally-left-nil
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@59f7fd8f8d9f14ad8c9db4aef358649286c1e059 -
Trigger Event:
push
-
Statement type: