diff --git a/crdt-sync/pyproject.toml b/crdt-sync/pyproject.toml index 44411ec..99ccbc6 100644 --- a/crdt-sync/pyproject.toml +++ b/crdt-sync/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "crdt-sync" -version = "0.9.0" +version = "0.9.1" description = "Shared CRDT merge scheme + GitHub-Contents-API sync transport for cross-device apps" requires-python = ">=3.10" dependencies = ["requests==2.34.2"] diff --git a/dep_freshness/parsers/python.py b/dep_freshness/parsers/python.py index 627050f..3e248fb 100644 --- a/dep_freshness/parsers/python.py +++ b/dep_freshness/parsers/python.py @@ -12,7 +12,7 @@ from pathlib import Path import re import tomllib -from dep_freshness._tables import PYPI, TOOLCHAIN +from dep_freshness._tables import GITTAG, PYPI, TOOLCHAIN from dep_freshness.models import Dep from dep_freshness.parsers._lines import index from dep_freshness.versions import exact_pin @@ -24,6 +24,49 @@ _REQ = re.compile( ) _SKIP_PREFIXES = ("-", "#", "git+", "http://", "https://", ".", "/") +# PEP 508 direct reference: `crdt-sync @ git+https://.../utils@crdt-sync-v0.9.0` +# The shared Python libraries are consumed this way, so "latest" for them is a +# git tag, exactly as for the Dart libraries. Without this the requirement +# parses as an unpinned PyPI package that does not exist on PyPI, the answer +# comes back undeterminable, and four minor versions of drift stay invisible -- +# which is how wake-alarm sat on gatelock-v0.4.1 against v0.7.1 and +# crdt-sync-v0.5.1 against v0.9.0 with a green gate. +_DIRECT = re.compile( + r"^\s*(?P[A-Za-z0-9][A-Za-z0-9._-]*)" + r"(?:\[[^\]]*\])?" + r"\s*@\s*git\+\S+?@(?P[A-Za-z0-9_.-]+-v[0-9][^\s#]*)" +) + + +def _direct_reference(requirement: str, path: Path, line: int) -> Dep | None: + """A ` @ git+@-vX.Y.Z` requirement as a git-tag dep.""" + match = _DIRECT.match(requirement.split(";", 1)[0].strip()) + if not match: + return None + ref = match.group("ref") + package, _, version = ref.rpartition("-v") + return Dep( + ecosystem=GITTAG, name=package or match.group("name"), constraint=ref, + path=path, line=line or _ref_line(path, ref), pinned=version or None, + ) + + +def _ref_line(path: Path, ref: str) -> int: + """Locate `ref` textually. A TOML dependency list has no `key =` to index. + + `"gatelock @ git+...@gatelock-v0.7.1"` is a bare string inside an array, + so the shared key indexer finds nothing and the finding would report + line 0 -- a fix the reader has to go looking for. + """ + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + return 0 + for number, line in enumerate(text.splitlines(), start=1): + if ref in line: + return number + return 0 + def _split(requirement: str) -> tuple[str, str] | None: """`('ruff', '==0.16.5')` from a PEP 508 requirement string.""" @@ -51,6 +94,10 @@ def parse_requirements(path: Path) -> list[Dep]: return [] deps: list[Dep] = [] for number, raw in enumerate(text.splitlines(), start=1): + direct = _direct_reference(raw, path, number) + if direct is not None: + deps.append(direct) + continue split = _split(raw) if split: deps.append(_dep(split[0], split[1], path, number)) @@ -81,7 +128,14 @@ def parse_pyproject(path: Path) -> list[Dep]: for group in groups: for requirement in group: - split = _split(str(requirement)) + text = str(requirement) + name = _REQ.match(text.strip()) + line = lines.get(name.group("name"), 0) if name else 0 + direct = _direct_reference(text, path, line) + if direct is not None: + deps.append(direct) + continue + split = _split(text) if split: deps.append(_dep(split[0], split[1], path, lines.get(split[0], 0))) return deps diff --git a/dep_freshness/tests/test_parsers_edges.py b/dep_freshness/tests/test_parsers_edges.py index da28459..42614df 100644 --- a/dep_freshness/tests/test_parsers_edges.py +++ b/dep_freshness/tests/test_parsers_edges.py @@ -140,3 +140,68 @@ dependencies: for d in pubspec.parse(write(tmp_path, "pubspec.yaml", body)) } assert deps["dart"].line == 3 + + +DIRECT_REQ = """\ +ruff==0.16.5 +crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.0#subdirectory=crdt-sync +gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.1#subdirectory=gatelock +""" + + +def test_a_git_direct_reference_is_a_git_tag_dependency(tmp_path): + """The shared Python libs are consumed by git tag, like the Dart ones. + + Parsed as an ordinary requirement this is an unpinned PyPI package that + does not exist on PyPI, so the answer is undeterminable and the drift is + invisible -- wake-alarm sat four minor versions behind on both with a + green gate. + """ + deps = { + d.name: d + for d in python.parse_requirements( + write(tmp_path, "requirements.txt", DIRECT_REQ) + ) + } + assert deps["crdt-sync"].ecosystem == "gittag" + assert deps["crdt-sync"].pinned == "0.9.0" + assert deps["crdt-sync"].line == 2 + assert deps["gatelock"].pinned == "0.7.1" + assert deps["ruff"].ecosystem == "pypi" + + +def test_a_pyproject_git_direct_reference_is_a_git_tag_dependency(tmp_path): + body = """\ +[project] +name = "demo" +requires-python = ">=3.13" +dependencies = [ + "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.1#subdirectory=gatelock", +] +""" + deps = { + d.name: d + for d in python.parse_pyproject(write(tmp_path, "pyproject.toml", body)) + } + assert deps["gatelock"].ecosystem == "gittag" + assert deps["gatelock"].pinned == "0.7.1" + assert deps["gatelock"].line == 5 + + +def test_a_plain_git_url_without_a_version_tag_is_still_skipped(tmp_path): + """A branch or SHA ref has no version to compare, so it stays unparsed.""" + body = "thing @ git+https://github.com/kuhyx/utils@main#subdirectory=thing\n" + assert python.parse_requirements( + write(tmp_path, "requirements.txt", body) + )[0].ecosystem == "pypi" + + +def test_an_unreadable_manifest_gives_the_direct_reference_no_line(tmp_path): + """`_ref_line` degrades to 0 rather than raising, like the key indexer.""" + missing = tmp_path / "gone.txt" + assert python._ref_line(missing, "gatelock-v0.7.1") == 0 + + +def test_a_direct_reference_the_file_does_not_contain_has_no_line(tmp_path): + path = write(tmp_path, "requirements.txt", "ruff==0.16.5\n") + assert python._ref_line(path, "gatelock-v0.7.1") == 0 diff --git a/gatelock/pyproject.toml b/gatelock/pyproject.toml index fce82d1..3bbb524 100644 --- a/gatelock/pyproject.toml +++ b/gatelock/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "gatelock" -version = "0.7.1" +version = "0.7.2" description = "Shared per-output lock-window + HMAC log-integrity backend for blocking-overlay apps" requires-python = ">=3.10" dependencies = [] # pure stdlib — tkinter is bundled with Python