Dependency Marker Resolution#
Dependency markers (PEP 508) attach conditions to package requirements β e.g., greenlet; platform_machine == "aarch64" or extra == "asyncio". Poetry must correctly evaluate, simplify, and persist these markers when locking and installing packages. The logic is split across poetry-core (marker algebra) and poetry (solver and lockfile generation).
Marker Model (poetry-core)#
The core abstraction lives in src/poetry/core/version/markers.py. The key types are:
| Class | Meaning |
|---|---|
SingleMarkerLike | A single key op value atom |
MultiMarker | AND of multiple markers |
MarkerUnion | OR of multiple markers |
AnyMarker | Always-true (unconstrained) |
EmptyMarker | Always-false (impossible) |
exclude() β stripping a marker variable from an expression#
exclude(marker_name) removes all references to a given marker variable and returns the simplified result. Its semantics differ by type:
SingleMarkerLike.exclude(): if the atom matches, returnsAnyMarker().MarkerUnion.exclude(): iterates over union branches. As soon as it finds a branch matchingmarker_name, it immediately returnsAnyMarker()β because any satisfiable branch in an OR makes the whole union unconstrained with respect to that variable.
This second rule was the site of a historical bug (see Known Bug: Union Exclusion below).
without_extras()#
A convenience wrapper that calls exclude("extra") . Used pervasively in the solver whenever extras-specific conditions must be stripped so that plain environment conditions survive into the lockfile.
Solver Integration (poetry)#
Lockfile marker calculation (DFS traversal)#
During poetry lock, the solver traverses the dependency graph in solver.py. For each package edge, it accumulates the effective marker by calling:
markers[dep][parent][groups] = prev_marker.union(new_marker)
where new_marker is edge.marker.without_extras() for non-root packages . Extras are intentionally stripped here β the goal is to record only platform/Python conditions in the lockfile marker; extra conditions are tracked separately.
Merging duplicate dependencies#
provider.py:_merge_dependencies_by_constraint() consolidates dependencies that share the same version constraint but carry different markers into a single dependency with a unioned marker. This reduces the number of solver "overrides" needed.
Extras + platform marker interaction (solver PR #7175)#
A specific edge case arises when a package declares the same dependency both unconditionally-for-an-extra and conditionally by platform β e.g.:
B ; sys_platform != "linux"(mandatory)B ; extra == "foo"(extra-gated)
Before the fix in PR #7175 (released in Poetry 1.4.0, ), stripping extras too early in _merge_dependencies_by_constraint() caused the extra-gated marker to be discarded, producing an incorrect combined marker. The fix changed this method to union all markers before stripping extras, preserving the sys_platform != "linux" or extra == "foo" union.
Known Bug: Union Exclusion (poetry-core #943)#
Root cause: MarkerUnion.exclude() had a logic error β instead of immediately returning AnyMarker() on finding a matching branch, it previously skipped the branch, rebuilt the union from the remaining branches, and only returned AnyMarker() if all branches were excluded.
Practical impact (greenlet / sqlalchemy): SQLAlchemy declares greenlet>=1 under two separate conditions :
platform_machine == "aarch64" or platform_machine == "x86_64" or ...(platform list)extra == "asyncio"
When Poetry stripped extra == "asyncio" via without_extras() while writing the lockfile, the buggy code kept only the platform branch instead of collapsing to AnyMarker. On Apple Silicon (arm64, which is not in the platform list), the resulting lockfile marker evaluated to false, so greenlet was omitted at install time β causing No module named 'greenlet' errors.
The bug was latent in earlier versions but only became observable in Poetry 2.3+ after the installer was changed to stop re-resolving at install time . The fix was released in poetry-core 2.4.1 .
Key Files & Entry Points#
| File | Purpose |
|---|---|
src/poetry/core/version/markers.py | Marker algebra: SingleMarkerLike, MultiMarker, MarkerUnion, AnyMarker, exclude(), without_extras() |
src/poetry/puzzle/solver.py | DFS traversal; accumulates and unions per-package markers |
src/poetry/puzzle/provider.py | _merge_dependencies_by_constraint() β merges markers for same-constraint deps |
Related PRs#
| PR | Summary |
|---|---|
| poetry-core #943 | Fix MarkerUnion.exclude() to correctly return AnyMarker() on union with excluded branch |
| poetry #7175 | Fix solver to union markers before stripping extras for mandatory+extra-gated deps |
| poetry-core #415 | Earlier fix for without_extras() on marker unions |