Skip to content

Commit 295619a

Browse files
committed
fix(parser): handle conda git-describe versions as unconstrained in dependency resolution
1 parent 2315399 commit 295619a

3 files changed

Lines changed: 42 additions & 11 deletions

File tree

docs/architecture/Execution/code-execution.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ flowchart LR
4646

4747
- `PythonInitializer` chooses Pixi first, then pip-backed pyRevit CPython if Pixi cannot run.
4848
- `PythonEmbedded` extracts `Parser.py`, `ToolParser.py`, `PytestRunner.py`, setup scripts, and `pixi.toml`.
49-
- `PythonDepsManager` parses PEP 723 dependencies through `Parser.py`.
49+
- `PythonDepsManager` parses PEP 723 dependencies through `Parser.py`. Installed-state JSON may include conda git-describe versions; Parser treats those as unconstrained instead of failing the resolve.
5050
- Pixi uses conda-forge first and PyPI fallback.
5151
- Pip fallback depends on `pyrevit.exe attached` to locate `bin/cengines/CPY*/python.exe`.
5252

source/DevTools.Execution/Resources/scripts/Parser.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
import re
2828
import sys
2929
from pathlib import Path
30-
import tomllib
3130

32-
from packaging.requirements import Requirement, InvalidRequirement
33-
from packaging.utils import canonicalize_name
31+
import tomllib
32+
from packaging.requirements import InvalidRequirement, Requirement
3433
from packaging.specifiers import SpecifierSet
35-
from packaging.version import Version, InvalidVersion
34+
from packaging.utils import canonicalize_name
35+
from packaging.version import InvalidVersion, Version
3636

3737
_BLOCK_RE = re.compile(
3838
r"^#\s*///\s*script\s*\n"
@@ -63,7 +63,10 @@ def _parse_script(script_path: Path) -> tuple[str, list[Requirement]]:
6363
try:
6464
reqs.append(Requirement(raw.strip()))
6565
except InvalidRequirement as e:
66-
print(json.dumps({"error": f"Invalid dependency '{raw}': {e}"}), file=sys.stderr)
66+
print(
67+
json.dumps({"error": f"Invalid dependency '{raw}': {e}"}),
68+
file=sys.stderr,
69+
)
6770
sys.exit(1)
6871

6972
return metadata.get("requires-python", ""), reqs
@@ -87,7 +90,7 @@ def _parse_specifier(val: object) -> SpecifierSet:
8790

8891
try:
8992
return SpecifierSet(spec_str)
90-
except Exception:
93+
except Exception: # noqa: BLE001
9194
return SpecifierSet()
9295

9396

@@ -117,6 +120,10 @@ def _parse_pip_json(text: str) -> dict[str, SpecifierSet]:
117120
pip list returns ``[{"name": "foo", "version": "1.2.3"}, ...]``.
118121
We build an exact ``==version`` specifier so _needs_install can do
119122
proper version comparison.
123+
124+
conda-forge git-describe versions (e.g. libwinpthread
125+
``12.0.0.r4.gg4f2fc60ca``) are valid in ``pixi list --json`` but not
126+
PEP 440. Treat those as unconstrained rather than failing the resolve.
120127
"""
121128
try:
122129
entries = json.loads(text)
@@ -129,7 +136,7 @@ def _parse_pip_json(text: str) -> dict[str, SpecifierSet]:
129136
version = entry.get("version", "")
130137
if not name:
131138
continue
132-
spec = SpecifierSet(f"=={version}") if version else SpecifierSet()
139+
spec = _parse_specifier(f"=={version}") if version else SpecifierSet()
133140
managed[canonicalize_name(name)] = spec
134141
return managed
135142

@@ -147,8 +154,7 @@ def _parse_pixi_toml(text: str) -> dict[str, SpecifierSet]:
147154
for name, val in data.get(section, {}).items()
148155
}
149156

150-
if "python" in managed:
151-
del managed["python"]
157+
managed.pop("python", None)
152158
return managed
153159

154160

@@ -220,6 +226,6 @@ def main(script_path: Path, stdin_content: str) -> None:
220226

221227
try:
222228
main(path, stdin_content)
223-
except Exception as e:
229+
except Exception as e: # noqa: BLE001
224230
print(json.dumps({"error": str(e)}), file=sys.stderr)
225231
sys.exit(1)

tests/DevTools.Execution.Tests/PipEnvironmentTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,31 @@ public async Task Parser_WithPixiListJson_SkipsAlreadyInstalled()
128128
Assert.Contains(packages, p => p.StartsWith("requests", StringComparison.OrdinalIgnoreCase));
129129
}
130130

131+
[Fact]
132+
public async Task Parser_WithCondaGitDescribeVersion_DoesNotFail()
133+
{
134+
// conda-forge win-64 libwinpthread uses git-describe versions that are
135+
// not PEP 440; pixi list --json still emits them. Parser must skip the
136+
// invalid specifier and keep resolving PEP 723 deps.
137+
var listJson = """
138+
[
139+
{"name": "libwinpthread", "version": "12.0.0.r4.gg4f2fc60ca", "kind": "conda"},
140+
{"name": "packaging", "version": "26.0", "kind": "conda"}
141+
]
142+
""";
143+
144+
var result = await RunParserAsync(Path.Combine(FixturesPath, "pep723_sample.py"), listJson);
145+
Assert.Equal(0, result.ExitCode);
146+
Assert.DoesNotContain("Invalid specifier", result.Stderr, StringComparison.Ordinal);
147+
148+
var packages = JsonDocument.Parse(result.Stdout).RootElement
149+
.GetProperty("to_install").EnumerateArray()
150+
.Select(e => e.GetString()!).ToList();
151+
152+
Assert.DoesNotContain(packages, p => p.Equals("packaging", StringComparison.OrdinalIgnoreCase));
153+
Assert.Contains(packages, p => p.StartsWith("requests", StringComparison.OrdinalIgnoreCase));
154+
}
155+
131156
[Fact]
132157
public async Task Parser_WithEmptyPixiListJson_InstallsAllPep723Deps()
133158
{

0 commit comments

Comments
 (0)