diff --git a/.cursor/rules/test-running.mdc b/.cursor/rules/test-running.mdc index 8c6875c9..0bd595a2 100644 --- a/.cursor/rules/test-running.mdc +++ b/.cursor/rules/test-running.mdc @@ -6,3 +6,7 @@ alwaysApply: true use `uv run pytest` to run tests use uv to manage dependencies + +follow preexisting conventions in the project + +- use the fixtures \ No newline at end of file diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py index 9f53179a..6918bd19 100644 --- a/src/setuptools_scm/_integration/dump_version.py +++ b/src/setuptools_scm/_integration/dump_version.py @@ -6,11 +6,29 @@ from .. import _types as _t from .._log import log as parent_log +from .._node_utils import _format_node_for_output from .._version_cls import _version_as_tuple from ..version import ScmVersion log = parent_log.getChild("dump_version") + +class _TemplateScmVersion: + """Wrapper for ScmVersion that formats node for template output.""" + + def __init__(self, scm_version: ScmVersion) -> None: + self._scm_version = scm_version + + def __getattr__(self, name: str) -> object: + # Delegate all attribute access to the wrapped ScmVersion + return getattr(self._scm_version, name) + + @property + def node(self) -> str | None: + """Return the node formatted for output.""" + return _format_node_for_output(self._scm_version.node) + + TEMPLATES = { ".py": """\ # file generated by setuptools-scm @@ -101,10 +119,12 @@ def write_version_to_path( log.debug("dump %s into %s", version, target) version_tuple = _version_as_tuple(version) if scm_version is not None: + # Wrap ScmVersion to provide formatted node for templates + template_scm_version = _TemplateScmVersion(scm_version) content = final_template.format( version=version, version_tuple=version_tuple, - scm_version=scm_version, + scm_version=template_scm_version, ) else: content = final_template.format(version=version, version_tuple=version_tuple) diff --git a/src/setuptools_scm/_node_utils.py b/src/setuptools_scm/_node_utils.py new file mode 100644 index 00000000..1a7a2274 --- /dev/null +++ b/src/setuptools_scm/_node_utils.py @@ -0,0 +1,46 @@ +"""Private utilities for consistent node ID handling across SCM backends.""" + +from __future__ import annotations + +# Standard node ID length used across all SCM backends +_NODE_ID_LENGTH = 10 + + +def _slice_node_id(node_id: str) -> str: + """ + Slice a node ID to a consistent length. + + This ensures that all SCM backends (git, mercurial, archival) + return the same length node IDs for consistency. + + Args: + node_id: The full node ID/hash from the SCM + + Returns: + The node ID sliced to the standard length + """ + return node_id[:_NODE_ID_LENGTH] + + +def _format_node_for_output(node_id: str | None) -> str | None: + """ + Format a node ID for output, applying consistent slicing. + + Args: + node_id: The full node ID/hash from the SCM or None + + Returns: + The node ID sliced to standard length for output, or None if input was None + """ + if node_id is None: + return None + + # Handle mercurial nodes with 'h' prefix + if node_id.startswith("h"): + # For mercurial nodes, slice the part after 'h' and reconstruct + hg_hash = node_id[1:] # Remove 'h' prefix + sliced_hash = _slice_node_id(hg_hash) + return "h" + sliced_hash + + # For git nodes (with or without 'g' prefix) and others + return _slice_node_id(node_id) diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 150629b1..7d81d40c 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -2,7 +2,6 @@ import dataclasses import logging -import operator import os import re import shlex @@ -47,6 +46,7 @@ "--dirty", "--tags", "--long", + "--abbrev=40", "--match", "*[0-9]*", ] @@ -174,12 +174,10 @@ def fetch_shallow(self) -> None: run_git(["fetch", "--unshallow"], self.path, check=True, timeout=240) def node(self) -> str | None: - unsafe_short_node = operator.itemgetter(slice(7)) - return run_git( ["rev-parse", "--verify", "--quiet", "HEAD"], self.path ).parse_success( - parse=unsafe_short_node, + parse=str, ) def count_all_nodes(self) -> int: diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index bfdfd425..42320516 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -66,7 +66,7 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: if self._is_initial_node(node): return self._create_initial_meta(config, dirty, branch, node_date) - node = "h" + node[:7] + node = "h" + node tags = self._parse_tags(tags_str) # Try to get version from current tags @@ -285,7 +285,7 @@ def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersion: log.debug("data %s", data) - node = data.get("node", "")[:12] + node = data.get("node", "") if node: node = "h" + node if "tag" in data: diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index 47b48954..3e91b20f 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -119,7 +119,7 @@ def node(self) -> str | None: return hg_node - return git_node[:7] + return git_node def count_all_nodes(self) -> int: res = run_hg(["log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 2ae423ed..c3bc1148 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -16,6 +16,7 @@ from . import _entrypoints from . import _modify_version +from ._node_utils import _format_node_for_output if TYPE_CHECKING: import sys @@ -170,7 +171,7 @@ def format_with(self, fmt: str, **kw: object) -> str: time=self.time, tag=self.tag, distance=self.distance, - node=self.node, + node=_format_node_for_output(self.node), dirty=self.dirty, branch=self.branch, node_date=self.node_date, diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 1ebd1e7a..3e15aae8 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -31,12 +31,12 @@ def wd(wd: WorkDir) -> WorkDir: archival_mapping = { "1.0": {"tag": "1.0"}, - "1.1.0.dev3+h000000000000": { + "1.1.0.dev3+h0000000000": { "latesttag": "1.0", "latesttagdistance": "3", "node": "0" * 20, }, - "1.0.1.dev3+h000000000000": { + "1.0.1.dev3+h0000000000": { "latesttag": "1.0.0", "latesttagdistance": "3", "branch": "1.0", @@ -163,7 +163,7 @@ def test_version_from_archival(wd: WorkDir) -> None: """, ) - assert wd.get_version() == "0.2.dev3+h000000000000" + assert wd.get_version() == "0.2.dev3+h0000000000" @pytest.mark.issue("#72")