Skip to content

Commit 3a8ec6c

Browse files
chore: unify git path strip
1 parent 3097c6e commit 3a8ec6c

File tree

4 files changed

+144
-10
lines changed

4 files changed

+144
-10
lines changed

src/setuptools_scm/_compat.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Compatibility utilities for cross-platform functionality."""
2+
3+
from __future__ import annotations
4+
5+
6+
def normalize_path_for_assertion(path: str) -> str:
7+
"""Normalize path separators for cross-platform assertions.
8+
9+
On Windows, this converts backslashes to forward slashes to ensure
10+
path comparisons work correctly. On other platforms, returns the path unchanged.
11+
The length of the string is not changed by this operation.
12+
13+
Args:
14+
path: The path string to normalize
15+
16+
Returns:
17+
The path with normalized separators
18+
"""
19+
return path.replace("\\", "/")
20+
21+
22+
def strip_path_suffix(
23+
full_path: str, suffix_path: str, error_msg: str | None = None
24+
) -> str:
25+
"""Strip a suffix from a path, with cross-platform path separator handling.
26+
27+
This function first normalizes path separators for Windows compatibility,
28+
then asserts that the full path ends with the suffix, and finally returns
29+
the path with the suffix removed. This is the common pattern used for
30+
computing parent directories from git output.
31+
32+
Args:
33+
full_path: The full path string
34+
suffix_path: The suffix path to strip from the end
35+
error_msg: Optional custom error message for the assertion
36+
37+
Returns:
38+
The prefix path with the suffix removed
39+
40+
Raises:
41+
AssertionError: If the full path doesn't end with the suffix
42+
"""
43+
normalized_full = normalize_path_for_assertion(full_path)
44+
45+
if error_msg:
46+
assert normalized_full.endswith(suffix_path), error_msg
47+
else:
48+
assert normalized_full.endswith(suffix_path), (
49+
f"Path assertion failed: {full_path!r} does not end with {suffix_path!r}"
50+
)
51+
52+
return full_path[: -len(suffix_path)]
53+
54+
55+
# Legacy aliases for backward compatibility during transition
56+
def assert_path_endswith(
57+
full_path: str, suffix_path: str, error_msg: str | None = None
58+
) -> None:
59+
"""Legacy alias - use strip_path_suffix instead."""
60+
strip_path_suffix(full_path, suffix_path, error_msg)
61+
62+
63+
def compute_path_prefix(full_path: str, suffix_path: str) -> str:
64+
"""Legacy alias - use strip_path_suffix instead."""
65+
return strip_path_suffix(full_path, suffix_path)

src/setuptools_scm/_file_finders/git.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,9 @@ def _git_toplevel(path: str) -> str | None:
3939
# ``cwd`` is absolute path to current working directory.
4040
# the below method removes the length of ``out`` from
4141
# ``cwd``, which gives the git toplevel
42-
assert cwd.replace("\\", "/").endswith(out), f"cwd={cwd!r}\nout={out!r}"
43-
# In windows cwd contains ``\`` which should be replaced by ``/``
44-
# for this assertion to work. Length of string isn't changed by replace
45-
# ``\\`` is just and escape for `\`
46-
out = cwd[: -len(out)]
42+
from .._compat import strip_path_suffix
43+
44+
out = strip_path_suffix(cwd, out, f"cwd={cwd!r}\nout={out!r}")
4745
log.debug("find files toplevel %s", out)
4846
return norm_real(out)
4947
except subprocess.CalledProcessError:

src/setuptools_scm/git.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,9 @@ def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None:
9393
real_wd = os.fspath(wd)
9494
else:
9595
str_wd = os.fspath(wd)
96-
assert str_wd.replace("\\", "/").endswith(real_wd)
97-
# In windows wd contains ``\`` which should be replaced by ``/``
98-
# for this assertion to work. Length of string isn't changed by replace
99-
# ``\\`` is just and escape for `\`
100-
real_wd = str_wd[: -len(real_wd)]
96+
from ._compat import strip_path_suffix
97+
98+
real_wd = strip_path_suffix(str_wd, real_wd)
10199
log.debug("real root %s", real_wd)
102100
if not samefile(real_wd, wd):
103101
return None

testing/test_compat.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Test compatibility utilities."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from setuptools_scm._compat import normalize_path_for_assertion
8+
from setuptools_scm._compat import strip_path_suffix
9+
10+
11+
def test_normalize_path_for_assertion() -> None:
12+
"""Test path normalization for assertions."""
13+
# Unix-style paths should remain unchanged
14+
assert normalize_path_for_assertion("/path/to/file") == "/path/to/file"
15+
16+
# Windows-style paths should be normalized
17+
assert normalize_path_for_assertion(r"C:\path\to\file") == "C:/path/to/file"
18+
assert normalize_path_for_assertion(r"path\to\file") == "path/to/file"
19+
20+
# Mixed paths should be normalized
21+
assert normalize_path_for_assertion(r"C:\path/to\file") == "C:/path/to/file"
22+
23+
# Already normalized paths should remain unchanged
24+
assert normalize_path_for_assertion("path/to/file") == "path/to/file"
25+
26+
27+
def test_strip_path_suffix_success() -> None:
28+
"""Test successful path suffix stripping."""
29+
# Unix-style paths
30+
assert strip_path_suffix("/home/user/project", "project") == "/home/user/"
31+
assert (
32+
strip_path_suffix("/home/user/project/subdir", "project/subdir")
33+
== "/home/user/"
34+
)
35+
36+
# Windows-style paths
37+
assert (
38+
strip_path_suffix("C:\\Users\\user\\project", "project") == "C:\\Users\\user\\"
39+
)
40+
assert (
41+
strip_path_suffix("C:\\Users\\user\\project\\subdir", "project/subdir")
42+
== "C:\\Users\\user\\"
43+
)
44+
45+
# Mixed paths should work due to normalization
46+
assert (
47+
strip_path_suffix("C:\\Users\\user\\project", "project") == "C:\\Users\\user\\"
48+
)
49+
assert strip_path_suffix("/home/user/project", "project") == "/home/user/"
50+
51+
# Edge cases
52+
assert strip_path_suffix("project", "project") == ""
53+
assert strip_path_suffix("/project", "project") == "/"
54+
55+
56+
def test_strip_path_suffix_failure() -> None:
57+
"""Test failed path suffix stripping."""
58+
with pytest.raises(AssertionError, match="Path assertion failed"):
59+
strip_path_suffix("/home/user/project", "other")
60+
61+
with pytest.raises(AssertionError, match="Custom error"):
62+
strip_path_suffix("/home/user/project", "other", "Custom error")
63+
64+
65+
def test_integration_example() -> None:
66+
"""Test the integration pattern used in the codebase."""
67+
# Simulate the pattern used in git.py and _file_finders/git.py
68+
full_path = r"C:\\Users\\user\\project\\subdir"
69+
suffix = "subdir"
70+
71+
# Now this is a single operation
72+
prefix = strip_path_suffix(full_path, suffix)
73+
assert prefix == r"C:\\Users\\user\\project\\"

0 commit comments

Comments
 (0)