fix(python): filter packages by PEP 508 environment markers#491
fix(python): filter packages by PEP 508 environment markers#491ruromero wants to merge 1 commit intoguacsec:mainfrom
Conversation
… poetry providers uv's universal resolver emits all platform variants with inline marker specs. poetry show --tree includes marker-conditional deps without marker info, requiring cross-reference with poetry.lock. Both providers now evaluate markers against the current environment and exclude non-matching packages from the SBOM. Closes: TC-4042 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewer's GuideImplements PEP 508 environment marker evaluation shared utility and wires it into the uv and poetry Python providers so that dependencies whose markers do not match the current platform/python environment are excluded from the SBOM graph, with new fixtures and tests covering direct and transitive dependency filtering and tree pruning. Sequence diagram for Poetry provider dependency resolution with marker filtering (sequence diagram)sequenceDiagram
participant Caller
participant Python_poetry
participant PoetryCLI as Poetry_CLI
participant FileSystem
participant MarkerEvaluator
Caller->>Python_poetry: getDeps(manifestDir, opts)
Python_poetry->>Python_poetry: _getPyproject(manifestDir)
Python_poetry->>Python_poetry: _hasDevGroup(parsed, opts)
Python_poetry->>Python_poetry: _getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts)
Python_poetry->>Python_poetry: _getPoetryShowAllOutput(manifestDir, opts)
Python_poetry->>Python_poetry: _parsePoetryShowAll(showAllOutput) = versionMap
Python_poetry->>Python_poetry: _findLockFileDir(manifestDir, opts) = lockDir
Python_poetry->>Python_poetry: _extractMarkerData(lockDir, parsed)
activate Python_poetry
Python_poetry->>FileSystem: read pyproject.toml
FileSystem-->>Python_poetry: dependencies list
Python_poetry->>Python_poetry: collect directMarkers
Python_poetry->>FileSystem: read poetry.lock (if present)
FileSystem-->>Python_poetry: lock content
Python_poetry->>Python_poetry: parseToml(lockContent)
Python_poetry->>Python_poetry: collect transitiveMarkers
deactivate Python_poetry
Python_poetry->>Python_poetry: _parsePoetryTree(treeOutput, versionMap, markerData)
activate Python_poetry
loop each direct dependency line
Python_poetry->>Python_poetry: lookup directMarkers[name]
alt marker exists
Python_poetry->>MarkerEvaluator: evaluateMarker(marker)
MarkerEvaluator-->>Python_poetry: boolean
alt evaluates to false
Python_poetry->>Python_poetry: skip direct dependency
else evaluates to true
Python_poetry->>Python_poetry: add to graph as directDep
end
else no marker
Python_poetry->>Python_poetry: add to graph as directDep
end
end
loop each transitive dependency line
Python_poetry->>Python_poetry: determine parentKey from stack
Python_poetry->>Python_poetry: lookup transitiveMarkers[parentKey][depKey]
alt marker exists
Python_poetry->>MarkerEvaluator: evaluateMarker(marker)
MarkerEvaluator-->>Python_poetry: boolean
alt evaluates to false
Python_poetry->>Python_poetry: continue (do not add child)
else evaluates to true
Python_poetry->>Python_poetry: add node and parentEntry.children
end
else no marker
Python_poetry->>Python_poetry: add node and parentEntry.children
end
end
deactivate Python_poetry
Python_poetry-->>Caller: {directDeps, graph} (marker-filtered)
Class diagram for Python providers using marker_evaluator (class diagram)classDiagram
class Base_pyproject {
<<abstract>>
}
class Python_poetry {
+getDeps(manifestDir, opts)
-_extractMarkerData(lockDir, parsed)
-_parsePoetryTree(treeOutput, versionMap, markerData)
-_canonicalize(name)
-_getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts)
-_getPoetryShowAllOutput(manifestDir, opts)
-_findLockFileDir(manifestDir, opts)
-_lockFileName()
}
class Python_uv {
+getDeps(manifestDir, opts)
}
class MarkerEvaluator {
+getEnvironmentMarkers() Record
+evaluateMarker(markerExpr) boolean
-getPythonVersion() string
-compareVersions(left, right) number
-evaluateComparison(variable, op, value, env) boolean
-parseAtom(expr) Atom
-evaluateExpr(expr, env) boolean
-splitLogical(expr, sep) string[]
}
Base_pyproject <|-- Python_poetry
Base_pyproject <|-- Python_uv
Python_poetry ..> MarkerEvaluator : uses
Python_uv ..> MarkerEvaluator : uses
class Atom {
+variable string
+op string
+value string
}
Flow diagram for PEP 508 marker evaluation logic (flow diagram)flowchart TD
A["start evaluateMarker(markerExpr)"] --> B{"markerExpr is empty or whitespace?"}
B -->|yes| C["return true"]
B -->|no| D["getEnvironmentMarkers()"]
D --> E["evaluateExpr(expr, env)"]
E --> F["return boolean"]
subgraph EvaluateExpr
E1["receive expr, env"] --> E2["expr contains ' or ' at top level?"]
E2 -->|yes| E3["splitLogical(expr, ' or ')"]
E3 --> E4["recursively evaluateExpr on each part"]
E4 --> E5["return any(part is true)"]
E2 -->|no| E6["expr contains ' and ' at top level?"]
E6 -->|yes| E7["splitLogical(expr, ' and ')"]
E7 --> E8["recursively evaluateExpr on each part"]
E8 --> E9["return all(parts are true)"]
E6 -->|no| E10["expr wrapped in parentheses?"]
E10 -->|yes| E11["strip outer parentheses"]
E11 --> E1
E10 -->|no| E12["atom = parseAtom(expr)"]
E12 --> E13["atom parsed?"]
E13 -->|no| E14["return true"]
E13 -->|yes| E15["evaluateComparison(atom.variable, atom.op, atom.value, env)"]
E15 --> E16["return result"]
end
subgraph EvaluateComparison
C1["lookup envVal = env[variable]"] --> C2["envVal is undefined or empty?"]
C2 -->|yes and variable includes 'version'| C3["return true"]
C2 -->|yes and not version variable| C4["return false"]
C2 -->|no| C5["variable includes 'version'?"]
C5 -->|yes| C6["cmp = compareVersions(envVal, value)"]
C6 --> C7["op"]
C7 -->|==| C8["return cmp == 0"]
C7 -->|!=| C9["return cmp != 0"]
C7 -->|>=| C10["return cmp >= 0"]
C7 -->|<=| C11["return cmp <= 0"]
C7 -->|>| C12["return cmp > 0"]
C7 -->|<| C13["return cmp < 0"]
C7 -->|~=| C14["check prefix match and cmp >= 0"]
C5 -->|no| C15["op"]
C15 -->|==| C16["return envVal == value"]
C15 -->|!=| C17["return envVal != value"]
C15 -->|in| C18["return envVal in value list"]
C15 -->|not in| C19["return envVal not in value list"]
C15 -->|other| C20["return envVal == value"]
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- The marker evaluator silently returns
truefor unparseable atoms and unknown/empty version variables, which can hide incorrect marker expressions or environment detection issues; consider logging or failing closed in these cases to avoid unintentionally including dependencies. - The
getPythonVersionhelper runspython3viaexecSyncwith each process start and assumespython3is available; if this is only used for marker gating, you might want a configuration/override path or a cheaper, non-blocking default for environments without Python. - The regex used in
_extractMarkerDatato pull markers fromproject.dependenciesis quite narrow (no extras, limited name charset) and may miss valid PEP 508 strings, so it could be worth broadening this parsing or reusing an existing marker/dependency parser to avoid silently ignoring markers.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The marker evaluator silently returns `true` for unparseable atoms and unknown/empty version variables, which can hide incorrect marker expressions or environment detection issues; consider logging or failing closed in these cases to avoid unintentionally including dependencies.
- The `getPythonVersion` helper runs `python3` via `execSync` with each process start and assumes `python3` is available; if this is only used for marker gating, you might want a configuration/override path or a cheaper, non-blocking default for environments without Python.
- The regex used in `_extractMarkerData` to pull markers from `project.dependencies` is quite narrow (no extras, limited name charset) and may miss valid PEP 508 strings, so it could be worth broadening this parsing or reusing an existing marker/dependency parser to avoid silently ignoring markers.
## Individual Comments
### Comment 1
<location path="src/providers/marker_evaluator.js" line_range="52-53" />
<code_context>
+
+function evaluateComparison(variable, op, value, env) {
+ let envVal = env[variable]
+ if (envVal === undefined || envVal === '') {
+ return variable.includes('version') ? true : false
+ }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Treating unknown version markers as `true` can incorrectly keep conditional deps enabled when the environment is unknown.
In this branch, `envVal` being `undefined`/empty for version-based markers always returns `true`, so if version detection fails (e.g., `python3` missing) `python_version < "3.8"` is treated as satisfied and its dependencies are always included. It would be safer to return `false` here (or at least surface the uncertainty) so version-guarded dependencies don’t silently become unconditional when environment detection fails.
</issue_to_address>
### Comment 2
<location path="src/providers/marker_evaluator.js" line_range="127-136" />
<code_context>
+ return evaluateComparison(atom.variable, atom.op, atom.value, env)
+}
+
+function splitLogical(expr, sep) {
+ let parts = []
+ let depth = 0
+ let current = ''
+ let i = 0
+ while (i < expr.length) {
+ if (expr[i] === '(') { depth++ }
+ if (expr[i] === ')') { depth-- }
+ if (depth === 0 && expr.substring(i, i + sep.length) === sep) {
+ parts.push(current)
+ current = ''
+ i += sep.length
+ continue
+ }
+ current += expr[i]
+ i++
+ }
+ parts.push(current)
+ return parts.filter(p => p.trim())
+}
</code_context>
<issue_to_address>
**issue:** Logical splitting doesn’t account for parentheses inside quoted strings, which can mis-parse valid marker expressions.
The helper tracks only parenthesis depth and doesn’t know when it’s inside a quoted string. Markers like `platform_version == "10.0 (Build 19045)"` will cause `depth` to change on the string’s parentheses and can trigger splits on `and`/`or` that are actually inside the string. This can mis-evaluate valid marker expressions. Consider also tracking single/double-quote state and ignoring parentheses and logical operators while inside a string literal.
</issue_to_address>
### Comment 3
<location path="test/providers/python_pyproject.test.js" line_range="346-355" />
<code_context>
+ suite('uv projects - PEP 508 marker filtering (TC-4042 reproducer)', () => {
</code_context>
<issue_to_address>
**suggestion (testing):** There are no direct unit tests for the generic marker evaluator; consider adding a focused test suite for it.
The uv and poetry suites exercise `evaluateMarker` only indirectly through provider behavior, but the shared `marker_evaluator` module has substantial logic (version comparisons, boolean ops, parentheses, `in`/`not in`, reversed operands, missing `python_version`) that isn’t explicitly tested.
To better document the intended PEP 508 semantics and catch regressions, consider a dedicated test file for the marker evaluator itself (e.g. `test/providers/marker_evaluator.test.js`) with targeted cases covering:
- Basic marker comparisons (e.g. `sys_platform`, `platform_system`, `os_name`).
- Version comparisons (`>=`, `<`, `~=`, and behavior when `python3` is missing).
- Logical combinations with `and`/`or` and parentheses.
- `in`/`not in` for non-version markers.
- Reversed expressions (e.g. `'linux' == sys_platform`).
These unit-style tests would complement the provider-level integration tests and increase confidence in marker parsing and evaluation changes.
Suggested implementation:
```javascript
suite('uv projects - PEP 508 marker filtering (TC-4042 reproducer)', () => {
const fixtureDir = `${MANIFESTS}/uv_marker_filtering`
function makeUvExport(markerLines) {
let lines = [
'click==8.3.1',
' # via marker-test',
'six==1.16.0',
' # via marker-test',
]
for (let ml of markerLines) {
}
}
// existing uv marker filtering tests...
})
suite('marker evaluator - PEP 508 semantics', () => {
// Helper to evaluate a marker with a default environment, allowing overrides per test
function evalMarker(marker, overrides = {}) {
const baseEnv = {
python_version: '3.11',
python_full_version: '3.11.2',
platform_system: 'Linux',
sys_platform: 'linux',
os_name: 'posix',
}
return evaluateMarker(marker, { ...baseEnv, ...overrides })
}
test('basic non-version comparisons (sys_platform, platform_system, os_name)', () => {
// Positive cases
assert.strictEqual(evalMarker("sys_platform == 'linux'"), true)
assert.strictEqual(evalMarker("platform_system == 'Linux'"), true)
assert.strictEqual(evalMarker("os_name == 'posix'"), true)
// Negative cases
assert.strictEqual(evalMarker("sys_platform == 'win32'"), false)
assert.strictEqual(evalMarker("platform_system != 'Linux'"), false)
assert.strictEqual(evalMarker("os_name != 'posix'"), false)
})
test('version comparisons with python_version and python_full_version', () => {
// python_version uses PEP 440 style normalization
assert.strictEqual(evalMarker("python_version >= '3.10'"), true)
assert.strictEqual(evalMarker("python_version < '3.12'"), true)
assert.strictEqual(evalMarker("python_version < '3.11'"), false)
// python_full_version comparisons
assert.strictEqual(evalMarker("python_full_version == '3.11.2'"), true)
assert.strictEqual(evalMarker("python_full_version != '3.11.2'"), false)
assert.strictEqual(evalMarker("python_full_version >= '3.11.0'"), true)
})
test('version compatible release operator (~=)', () => {
// 3.11.2 ~= 3.11 matches
assert.strictEqual(evalMarker("python_version ~= '3.11'"), true)
// 3.11.2 should not be compatible with 3.10
assert.strictEqual(evalMarker("python_version ~= '3.10'"), false)
})
test('behavior when python_version is missing from environment', () => {
// When python_version is missing, comparisons should generally be false
const envWithoutPython = {
python_version: undefined,
python_full_version: undefined,
}
assert.strictEqual(
evalMarker("python_version >= '3.10'", envWithoutPython),
false
)
assert.strictEqual(
evalMarker("python_full_version == '3.11.2'", envWithoutPython),
false
)
})
test('logical combinations with and/or and parentheses', () => {
// (linux or darwin) and CPython 3.11
assert.strictEqual(
evalMarker(
"(sys_platform == 'linux' or sys_platform == 'darwin') and python_version == '3.11'"
),
true
)
// change platform to win32 -> whole expression becomes false
assert.strictEqual(
evalMarker(
"(sys_platform == 'linux' or sys_platform == 'darwin') and python_version == '3.11'",
{ sys_platform: 'win32' }
),
false
)
// or has lower precedence than and; parentheses change meaning
assert.strictEqual(
evalMarker(
"sys_platform == 'linux' and python_version == '3.11' or python_version == '3.10'"
),
true
)
assert.strictEqual(
evalMarker(
"(sys_platform == 'linux' and python_version == '3.10') or python_version == '3.11'"
),
true
)
assert.strictEqual(
evalMarker(
"(sys_platform == 'linux' and python_version == '3.10') or python_version == '3.10'"
),
false
)
})
test("'in' and 'not in' for non-version markers', () => {
// membership checks
assert.strictEqual(
evalMarker("sys_platform in 'linux darwin'"),
true
)
assert.strictEqual(
evalMarker("sys_platform in 'win32 cygwin'"),
false
)
assert.strictEqual(
evalMarker("platform_system not in 'Windows Darwin'"),
true
)
assert.strictEqual(
evalMarker("platform_system not in 'Linux Darwin'"),
false
)
})
test('reversed comparison operands', () => {
// String literal on the left-hand side
assert.strictEqual(evalMarker("'linux' == sys_platform"), true)
assert.strictEqual(evalMarker("'Linux' == platform_system"), true)
assert.strictEqual(evalMarker("'posix' == os_name"), true)
assert.strictEqual(evalMarker("'win32' == sys_platform"), false)
assert.strictEqual(evalMarker("'Windows' == platform_system"), false)
// Reversed version comparisons
assert.strictEqual(evalMarker("'3.11' <= python_version"), true)
assert.strictEqual(evalMarker("'3.12' <= python_version"), false)
})
```
1. Ensure that `evaluateMarker` and `assert` are imported/available in this test file. For example, near the top of `test/providers/python_pyproject.test.js`, add something like:
- `const assert = require('assert')` (if not already present).
- `const { evaluateMarker } = require('../../../src/providers/python/marker_evaluator')` (adjust the relative path to match your project layout and how other provider utilities are imported).
2. The base environment keys in `evalMarker` (`python_version`, `python_full_version`, `platform_system`, `sys_platform`, `os_name`) must match what `marker_evaluator` actually expects. If the module uses different variable names (e.g. `platform_system` vs `platform_system_name`), update the keys accordingly.
3. If your test runner uses a different global (`describe`/`it` instead of `suite`/`test`), adapt the new suite and test calls to be consistent with the rest of this file.
4. If your marker evaluator has a different signature (e.g. `evaluateMarker(marker, context, opts)`), pass any additional required parameters in `evalMarker` so the tests exercise the real runtime behavior.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if (envVal === undefined || envVal === '') { | ||
| return variable.includes('version') ? true : false |
There was a problem hiding this comment.
issue (bug_risk): Treating unknown version markers as true can incorrectly keep conditional deps enabled when the environment is unknown.
In this branch, envVal being undefined/empty for version-based markers always returns true, so if version detection fails (e.g., python3 missing) python_version < "3.8" is treated as satisfied and its dependencies are always included. It would be safer to return false here (or at least surface the uncertainty) so version-guarded dependencies don’t silently become unconditional when environment detection fails.
| function splitLogical(expr, sep) { | ||
| let parts = [] | ||
| let depth = 0 | ||
| let current = '' | ||
| let i = 0 | ||
| while (i < expr.length) { | ||
| if (expr[i] === '(') { depth++ } | ||
| if (expr[i] === ')') { depth-- } | ||
| if (depth === 0 && expr.substring(i, i + sep.length) === sep) { | ||
| parts.push(current) |
There was a problem hiding this comment.
issue: Logical splitting doesn’t account for parentheses inside quoted strings, which can mis-parse valid marker expressions.
The helper tracks only parenthesis depth and doesn’t know when it’s inside a quoted string. Markers like platform_version == "10.0 (Build 19045)" will cause depth to change on the string’s parentheses and can trigger splits on and/or that are actually inside the string. This can mis-evaluate valid marker expressions. Consider also tracking single/double-quote state and ignoring parentheses and logical operators while inside a string literal.
| suite('uv projects - PEP 508 marker filtering (TC-4042 reproducer)', () => { | ||
| const fixtureDir = `${MANIFESTS}/uv_marker_filtering` | ||
|
|
||
| function makeUvExport(markerLines) { | ||
| let lines = [ | ||
| 'click==8.3.1', | ||
| ' # via marker-test', | ||
| 'six==1.16.0', | ||
| ' # via marker-test', | ||
| ] |
There was a problem hiding this comment.
suggestion (testing): There are no direct unit tests for the generic marker evaluator; consider adding a focused test suite for it.
The uv and poetry suites exercise evaluateMarker only indirectly through provider behavior, but the shared marker_evaluator module has substantial logic (version comparisons, boolean ops, parentheses, in/not in, reversed operands, missing python_version) that isn’t explicitly tested.
To better document the intended PEP 508 semantics and catch regressions, consider a dedicated test file for the marker evaluator itself (e.g. test/providers/marker_evaluator.test.js) with targeted cases covering:
- Basic marker comparisons (e.g.
sys_platform,platform_system,os_name). - Version comparisons (
>=,<,~=, and behavior whenpython3is missing). - Logical combinations with
and/orand parentheses. in/not infor non-version markers.- Reversed expressions (e.g.
'linux' == sys_platform).
These unit-style tests would complement the provider-level integration tests and increase confidence in marker parsing and evaluation changes.
Suggested implementation:
suite('uv projects - PEP 508 marker filtering (TC-4042 reproducer)', () => {
const fixtureDir = `${MANIFESTS}/uv_marker_filtering`
function makeUvExport(markerLines) {
let lines = [
'click==8.3.1',
' # via marker-test',
'six==1.16.0',
' # via marker-test',
]
for (let ml of markerLines) {
}
}
// existing uv marker filtering tests...
})
suite('marker evaluator - PEP 508 semantics', () => {
// Helper to evaluate a marker with a default environment, allowing overrides per test
function evalMarker(marker, overrides = {}) {
const baseEnv = {
python_version: '3.11',
python_full_version: '3.11.2',
platform_system: 'Linux',
sys_platform: 'linux',
os_name: 'posix',
}
return evaluateMarker(marker, { ...baseEnv, ...overrides })
}
test('basic non-version comparisons (sys_platform, platform_system, os_name)', () => {
// Positive cases
assert.strictEqual(evalMarker("sys_platform == 'linux'"), true)
assert.strictEqual(evalMarker("platform_system == 'Linux'"), true)
assert.strictEqual(evalMarker("os_name == 'posix'"), true)
// Negative cases
assert.strictEqual(evalMarker("sys_platform == 'win32'"), false)
assert.strictEqual(evalMarker("platform_system != 'Linux'"), false)
assert.strictEqual(evalMarker("os_name != 'posix'"), false)
})
test('version comparisons with python_version and python_full_version', () => {
// python_version uses PEP 440 style normalization
assert.strictEqual(evalMarker("python_version >= '3.10'"), true)
assert.strictEqual(evalMarker("python_version < '3.12'"), true)
assert.strictEqual(evalMarker("python_version < '3.11'"), false)
// python_full_version comparisons
assert.strictEqual(evalMarker("python_full_version == '3.11.2'"), true)
assert.strictEqual(evalMarker("python_full_version != '3.11.2'"), false)
assert.strictEqual(evalMarker("python_full_version >= '3.11.0'"), true)
})
test('version compatible release operator (~=)', () => {
// 3.11.2 ~= 3.11 matches
assert.strictEqual(evalMarker("python_version ~= '3.11'"), true)
// 3.11.2 should not be compatible with 3.10
assert.strictEqual(evalMarker("python_version ~= '3.10'"), false)
})
test('behavior when python_version is missing from environment', () => {
// When python_version is missing, comparisons should generally be false
const envWithoutPython = {
python_version: undefined,
python_full_version: undefined,
}
assert.strictEqual(
evalMarker("python_version >= '3.10'", envWithoutPython),
false
)
assert.strictEqual(
evalMarker("python_full_version == '3.11.2'", envWithoutPython),
false
)
})
test('logical combinations with and/or and parentheses', () => {
// (linux or darwin) and CPython 3.11
assert.strictEqual(
evalMarker(
"(sys_platform == 'linux' or sys_platform == 'darwin') and python_version == '3.11'"
),
true
)
// change platform to win32 -> whole expression becomes false
assert.strictEqual(
evalMarker(
"(sys_platform == 'linux' or sys_platform == 'darwin') and python_version == '3.11'",
{ sys_platform: 'win32' }
),
false
)
// or has lower precedence than and; parentheses change meaning
assert.strictEqual(
evalMarker(
"sys_platform == 'linux' and python_version == '3.11' or python_version == '3.10'"
),
true
)
assert.strictEqual(
evalMarker(
"(sys_platform == 'linux' and python_version == '3.10') or python_version == '3.11'"
),
true
)
assert.strictEqual(
evalMarker(
"(sys_platform == 'linux' and python_version == '3.10') or python_version == '3.10'"
),
false
)
})
test("'in' and 'not in' for non-version markers', () => {
// membership checks
assert.strictEqual(
evalMarker("sys_platform in 'linux darwin'"),
true
)
assert.strictEqual(
evalMarker("sys_platform in 'win32 cygwin'"),
false
)
assert.strictEqual(
evalMarker("platform_system not in 'Windows Darwin'"),
true
)
assert.strictEqual(
evalMarker("platform_system not in 'Linux Darwin'"),
false
)
})
test('reversed comparison operands', () => {
// String literal on the left-hand side
assert.strictEqual(evalMarker("'linux' == sys_platform"), true)
assert.strictEqual(evalMarker("'Linux' == platform_system"), true)
assert.strictEqual(evalMarker("'posix' == os_name"), true)
assert.strictEqual(evalMarker("'win32' == sys_platform"), false)
assert.strictEqual(evalMarker("'Windows' == platform_system"), false)
// Reversed version comparisons
assert.strictEqual(evalMarker("'3.11' <= python_version"), true)
assert.strictEqual(evalMarker("'3.12' <= python_version"), false)
})- Ensure that
evaluateMarkerandassertare imported/available in this test file. For example, near the top oftest/providers/python_pyproject.test.js, add something like:const assert = require('assert')(if not already present).const { evaluateMarker } = require('../../../src/providers/python/marker_evaluator')(adjust the relative path to match your project layout and how other provider utilities are imported).
- The base environment keys in
evalMarker(python_version,python_full_version,platform_system,sys_platform,os_name) must match whatmarker_evaluatoractually expects. If the module uses different variable names (e.g.platform_systemvsplatform_system_name), update the keys accordingly. - If your test runner uses a different global (
describe/itinstead ofsuite/test), adapt the new suite and test calls to be consistent with the rest of this file. - If your marker evaluator has a different signature (e.g.
evaluateMarker(marker, context, opts)), pass any additional required parameters inevalMarkerso the tests exercise the real runtime behavior.
Summary
marker_evaluator.js) that maps Node.js platform values to Python marker variables and evaluates simple/compound marker expressionsmarker_specnodes from tree-sitter parse and skip packages with non-matching platform markers (e.g.,sys_platform == 'win32'on Linux/macOS)poetry.lockdependency markers andpyproject.tomlPEP 621 dependency strings, filtering out packages whose markers don't match the current environmentTest plan
Fixes: TC-4042 / TC-4240
🤖 Generated with Claude Code
Summary by Sourcery
Implement environment marker-aware dependency filtering for Python uv and Poetry providers to only include packages applicable to the current platform.
Bug Fixes:
Enhancements:
Tests: