Skip to main content

Markerpry

PyPI - Version PyPI - Python Version


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 values
  • CompareNode: Represents comparisons like python_version >= "3.7" (==, ===, !=, <, <=, >, >=, ~=)
  • ContainsNode: Represents membership tests like "3.7" in python_version or python_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:

  • Version objects: Used for version comparisons (python_version, etc.)
    • Work with all comparators (==, ===, !=, <, <=, >, >=, ~=)
    • Version strings are parsed using packaging.specifiers.SpecifierSet
  • str values: Used for exact string matching and substring tests
    • ==/===/!= do exact string equality; in/not in do substring tests
    • Other comparators (<, <=, >, >=, ~=) will leave the expression unevaluated
  • re.Pattern objects: Used for pattern matching
    • Only work with equality comparators (==, ===, !=)
    • ==/=== checks if the pattern matches
    • != checks if the pattern doesn't match
  • bool values: Unconditionally decide every comparator
    • True means "yes"; False means "no"
  • RangeConstraint objects: Represent an open or closed interval of versions
    • RangeConstraint(min, max, include_min=True, include_max=False) — either bound may be None for "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

markerpry-0.5.1.tar.gz (10.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

markerpry-0.5.1-py3-none-any.whl (12.9 kB view details)

Uploaded Python 3

File details

Details for the file markerpry-0.5.1.tar.gz.

File metadata

  • Download URL: markerpry-0.5.1.tar.gz
  • Upload date:
  • Size: 10.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for markerpry-0.5.1.tar.gz
Algorithm Hash digest
SHA256 ff2ccc7bb4062ae8fce861542ca5305afa830e77efde96075e34d0991c9c87ac
MD5 14e26d107843e9b6bf8350735b6d59ae
BLAKE2b-256 a3f95f3339edbda46aab5bf649180bb48068f2968783c4bb0e7bbfabd61d7683

See more details on using hashes here.

Provenance

The following attestation bundles were made for markerpry-0.5.1.tar.gz:

Publisher: publish.yml on intentionally-left-nil/markerpry

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file markerpry-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: markerpry-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 12.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for markerpry-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f0a9b1be4c35fdb4570b9df6b64e9b2be884140a3c606be59808f3a1dea231c3
MD5 d865556005a682470d150c591e4f842c
BLAKE2b-256 cf66e23613568fc01ca3e44149b3440c92254dd04568d3c835360a96d4f58151

See more details on using hashes here.

Provenance

The following attestation bundles were made for markerpry-0.5.1-py3-none-any.whl:

Publisher: publish.yml on intentionally-left-nil/markerpry

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.5.1 This release

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page