Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .cursor/rules/test-running.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 21 additions & 1 deletion src/setuptools_scm/_integration/dump_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions src/setuptools_scm/_node_utils.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 2 additions & 4 deletions src/setuptools_scm/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import dataclasses
import logging
import operator
import os
import re
import shlex
Expand Down Expand Up @@ -41,12 +40,13 @@
# If testing command in shell make sure to quote the match argument like
# '*[0-9]*' as it will expand before being sent to git if there are any matching
# files in current directory.
DEFAULT_DESCRIBE = [

Check warning on line 43 in src/setuptools_scm/git.py

View workflow job for this annotation

GitHub Actions / Check API stability with griffe

DEFAULT_DESCRIBE

Attribute value was changed: `['git', 'describe', '--dirty', '--tags', '--long', '--match', '*[0-9]*']` -> `['git', 'describe', '--dirty', '--tags', '--long', '--abbrev=40', '--match', '*[0-9]*']`
"git",
"describe",
"--dirty",
"--tags",
"--long",
"--abbrev=40",
"--match",
"*[0-9]*",
]
Expand Down Expand Up @@ -174,12 +174,10 @@
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:
Expand Down
4 changes: 2 additions & 2 deletions src/setuptools_scm/hg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/setuptools_scm/hg_git.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from __future__ import annotations

import logging
Expand Down Expand Up @@ -119,7 +119,7 @@

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)
Expand Down
3 changes: 2 additions & 1 deletion src/setuptools_scm/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from . import _entrypoints
from . import _modify_version
from ._node_utils import _format_node_for_output

if TYPE_CHECKING:
import sys
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions testing/test_mercurial.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down
Loading