Skip to content

Commit 7602a8f

Browse files
Merge pull request #1160 from RonnyPfannschmidt/fix-846-submodule-failing
enhancements around git
2 parents c6e6e3f + c94d797 commit 7602a8f

File tree

8 files changed

+497
-15
lines changed

8 files changed

+497
-15
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- make Mercurial command configurable by environment variable `SETUPTOOLS_SCM_HG_COMMAND`
99
- fix #1099 use file modification times for dirty working directory timestamps instead of current time
1010
- fix #1059: add `SETUPTOOLS_SCM_PRETEND_METADATA` environment variable to override individual ScmVersion fields
11+
- add `scm` parameter support to `get_version()` function for nested SCM configuration
1112
### Changed
1213

1314
- add `pip` to test optional dependencies for improved uv venv compatibility
@@ -27,7 +28,9 @@
2728
- fix #1136: update customizing.md to fix missing import
2829
- fix #1001: document the missing version schemes and add examples in the docs
2930
- fix #1115: explicitly document file finder behaviour
30-
- fix #879: add test that validates caswe differenct behavior
31+
- fix #879: add test that validates case different behavior on windows
32+
- migrate git describe command to new scm config
33+
- add support for failing on missing submodules
3134

3235
## v8.3.1
3336

docs/config.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,30 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
9292
this is a function for advanced use and you should be
9393
familiar with the `setuptools-scm` internals to use it.
9494

95-
`git_describe_command`
95+
`scm.git.describe_command`
9696
: This command will be used instead the default `git describe --long` command.
9797

9898
Defaults to the value set by [setuptools_scm.git.DEFAULT_DESCRIBE][]
9999

100+
`scm.git.pre_parse`
101+
: A string specifying which git pre-parse function to use before parsing version information.
102+
Available options:
103+
104+
- `"warn_on_shallow"` (default): Warns when the repository is shallow
105+
- `"fail_on_shallow"`: Fails with an error when the repository is shallow
106+
- `"fetch_on_shallow"`: Automatically fetches to rectify shallow repositories
107+
- `"fail_on_missing_submodules"`: Fails when submodules are defined but not initialized
108+
109+
The `"fail_on_missing_submodules"` option is useful to prevent packaging incomplete
110+
projects when submodules are required for a complete build.
111+
112+
Note: This setting is overridden by any explicit `pre_parse` parameter passed to the git parse function.
113+
114+
`git_describe_command` (deprecated)
115+
: **Deprecated since 8.4.0**: Use `scm.git.describe_command` instead.
116+
117+
This field is maintained for backward compatibility but will issue a deprecation warning when used.
118+
100119
`normalize`
101120
: A boolean flag indicating if the version string should be normalized.
102121
Defaults to `True`. Setting this to `False` is equivalent to setting

docs/usage.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ dynamic = ["version"]
3939
[tool.setuptools_scm]
4040
# Configure custom options here (version schemes, file writing, etc.)
4141
version_file = "src/mypackage/_version.py"
42+
43+
# Example: Git-specific configuration
44+
[tool.setuptools_scm.scm.git]
45+
pre_parse = "fail_on_missing_submodules" # Fail if submodules are not initialized
46+
describe_command = "git describe --dirty --tags --long --exclude *js*" # Custom describe command
4247
```
4348

4449
Both approaches will work with projects that support PEP 518 ([pip](https://pypi.org/project/pip) and

src/setuptools_scm/_config.py

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
import warnings
99

1010
from pathlib import Path
11+
from typing import TYPE_CHECKING
1112
from typing import Any
1213
from typing import Pattern
1314
from typing import Protocol
1415

16+
if TYPE_CHECKING:
17+
from . import git
18+
1519
from . import _log
1620
from . import _types as _t
1721
from ._integration.pyproject_reading import PyProjectData
@@ -26,6 +30,57 @@
2630

2731
log = _log.log.getChild("config")
2832

33+
34+
def _is_called_from_dataclasses() -> bool:
35+
"""Check if the current call is from the dataclasses module."""
36+
import inspect
37+
38+
frame = inspect.currentframe()
39+
try:
40+
# Walk up to 7 frames to check for dataclasses calls
41+
current_frame = frame
42+
assert current_frame is not None
43+
for _ in range(7):
44+
current_frame = current_frame.f_back
45+
if current_frame is None:
46+
break
47+
if "dataclasses.py" in current_frame.f_code.co_filename:
48+
return True
49+
return False
50+
finally:
51+
del frame
52+
53+
54+
class _GitDescribeCommandDescriptor:
55+
"""Data descriptor for deprecated git_describe_command field."""
56+
57+
def __get__(
58+
self, obj: Configuration | None, objtype: type[Configuration] | None = None
59+
) -> _t.CMD_TYPE | None:
60+
if obj is None:
61+
return self # type: ignore[return-value]
62+
63+
# Only warn if not being called by dataclasses.replace or similar introspection
64+
is_from_dataclasses = _is_called_from_dataclasses()
65+
if not is_from_dataclasses:
66+
warnings.warn(
67+
"Configuration field 'git_describe_command' is deprecated. "
68+
"Use 'scm.git.describe_command' instead.",
69+
DeprecationWarning,
70+
stacklevel=2,
71+
)
72+
return obj.scm.git.describe_command
73+
74+
def __set__(self, obj: Configuration, value: _t.CMD_TYPE | None) -> None:
75+
warnings.warn(
76+
"Configuration field 'git_describe_command' is deprecated. "
77+
"Use 'scm.git.describe_command' instead.",
78+
DeprecationWarning,
79+
stacklevel=2,
80+
)
81+
obj.scm.git.describe_command = value
82+
83+
2984
DEFAULT_TAG_REGEX = re.compile(
3085
r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
3186
)
@@ -52,6 +107,13 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
52107
return regex
53108

54109

110+
def _get_default_git_pre_parse() -> git.GitPreParse:
111+
"""Get the default git pre_parse enum value"""
112+
from . import git
113+
114+
return git.GitPreParse.WARN_ON_SHALLOW
115+
116+
55117
class ParseFunction(Protocol):
56118
def __call__(
57119
self, root: _t.PathT, *, config: Configuration
@@ -83,6 +145,54 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
83145
return os.path.abspath(root)
84146

85147

148+
@dataclasses.dataclass
149+
class GitConfiguration:
150+
"""Git-specific configuration options"""
151+
152+
pre_parse: git.GitPreParse = dataclasses.field(
153+
default_factory=lambda: _get_default_git_pre_parse()
154+
)
155+
describe_command: _t.CMD_TYPE | None = None
156+
157+
@classmethod
158+
def from_data(cls, data: dict[str, Any]) -> GitConfiguration:
159+
"""Create GitConfiguration from configuration data, converting strings to enums"""
160+
git_data = data.copy()
161+
162+
# Convert string pre_parse values to enum instances
163+
if "pre_parse" in git_data and isinstance(git_data["pre_parse"], str):
164+
from . import git
165+
166+
try:
167+
git_data["pre_parse"] = git.GitPreParse(git_data["pre_parse"])
168+
except ValueError as e:
169+
valid_options = [option.value for option in git.GitPreParse]
170+
raise ValueError(
171+
f"Invalid git pre_parse function '{git_data['pre_parse']}'. "
172+
f"Valid options are: {', '.join(valid_options)}"
173+
) from e
174+
175+
return cls(**git_data)
176+
177+
178+
@dataclasses.dataclass
179+
class ScmConfiguration:
180+
"""SCM-specific configuration options"""
181+
182+
git: GitConfiguration = dataclasses.field(default_factory=GitConfiguration)
183+
184+
@classmethod
185+
def from_data(cls, data: dict[str, Any]) -> ScmConfiguration:
186+
"""Create ScmConfiguration from configuration data"""
187+
scm_data = data.copy()
188+
189+
# Handle git-specific configuration
190+
git_data = scm_data.pop("git", {})
191+
git_config = GitConfiguration.from_data(git_data)
192+
193+
return cls(git=git_config, **scm_data)
194+
195+
86196
@dataclasses.dataclass
87197
class Configuration:
88198
"""Global configuration model"""
@@ -100,16 +210,57 @@ class Configuration:
100210
version_file: _t.PathT | None = None
101211
version_file_template: str | None = None
102212
parse: ParseFunction | None = None
103-
git_describe_command: _t.CMD_TYPE | None = None
213+
git_describe_command: dataclasses.InitVar[_t.CMD_TYPE | None] = (
214+
_GitDescribeCommandDescriptor()
215+
)
216+
104217
dist_name: str | None = None
105218
version_cls: type[_VersionT] = _Version
106219
search_parent_directories: bool = False
107220

108221
parent: _t.PathT | None = None
109222

110-
def __post_init__(self) -> None:
223+
# Nested SCM configurations
224+
scm: ScmConfiguration = dataclasses.field(
225+
default_factory=lambda: ScmConfiguration()
226+
)
227+
228+
# Deprecated fields (handled in __post_init__)
229+
230+
def __post_init__(self, git_describe_command: _t.CMD_TYPE | None) -> None:
111231
self.tag_regex = _check_tag_regex(self.tag_regex)
112232

233+
# Handle deprecated git_describe_command
234+
# Check if it's a descriptor object (happens when no value is passed)
235+
if git_describe_command is not None and not isinstance(
236+
git_describe_command, _GitDescribeCommandDescriptor
237+
):
238+
# Check if this is being called from dataclasses
239+
is_from_dataclasses = _is_called_from_dataclasses()
240+
241+
same_value = (
242+
self.scm.git.describe_command is not None
243+
and self.scm.git.describe_command == git_describe_command
244+
)
245+
246+
if is_from_dataclasses and same_value:
247+
# Ignore the passed value - it's from dataclasses.replace() with same value
248+
pass
249+
else:
250+
warnings.warn(
251+
"Configuration field 'git_describe_command' is deprecated. "
252+
"Use 'scm.git.describe_command' instead.",
253+
DeprecationWarning,
254+
stacklevel=2,
255+
)
256+
# Check for conflicts
257+
if self.scm.git.describe_command is not None:
258+
raise ValueError(
259+
"Cannot specify both 'git_describe_command' (deprecated) and "
260+
"'scm.git.describe_command'. Please use only 'scm.git.describe_command'."
261+
)
262+
self.scm.git.describe_command = git_describe_command
263+
113264
@property
114265
def absolute_root(self) -> str:
115266
return _check_absolute_root(self.root, self.relative_to)
@@ -161,8 +312,17 @@ def from_data(
161312
version_cls = _validate_version_cls(
162313
data.pop("version_cls", None), data.pop("normalize", True)
163314
)
315+
316+
# Handle nested SCM configuration
317+
scm_data = data.pop("scm", {})
318+
319+
# Handle nested SCM configuration
320+
321+
scm_config = ScmConfiguration.from_data(scm_data)
322+
164323
return cls(
165324
relative_to=relative_to,
166325
version_cls=version_cls,
326+
scm=scm_config,
167327
**data,
168328
)

src/setuptools_scm/_get_version_impl.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def get_version(
154154
version_cls: Any | None = None,
155155
normalize: bool = True,
156156
search_parent_directories: bool = False,
157+
scm: dict[str, Any] | None = None,
157158
) -> str:
158159
"""
159160
If supplied, relative_to should be a file from which root may
@@ -165,7 +166,19 @@ def get_version(
165166
version_cls = _validate_version_cls(version_cls, normalize)
166167
del normalize
167168
tag_regex = parse_tag_regex(tag_regex)
168-
config = Configuration(**locals())
169+
170+
# Handle scm parameter by converting it to ScmConfiguration
171+
if scm is not None:
172+
scm_config = _config.ScmConfiguration.from_data(scm)
173+
else:
174+
scm_config = _config.ScmConfiguration()
175+
176+
# Remove scm from locals() since we handle it separately
177+
config_params = locals().copy()
178+
config_params.pop("scm", None)
179+
config_params.pop("scm_config", None)
180+
181+
config = _config.Configuration(scm=scm_config, **config_params)
169182
maybe_version = _get_version(config, force_write_version_files=True)
170183

171184
if maybe_version is None:

src/setuptools_scm/_types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@
2626
VERSION_SCHEME: TypeAlias = Union[str, Callable[["version.ScmVersion"], str]]
2727
VERSION_SCHEMES: TypeAlias = Union[List[str], Tuple[str, ...], VERSION_SCHEME]
2828
SCMVERSION: TypeAlias = "version.ScmVersion"
29+
30+
# Git pre-parse function types
31+
GIT_PRE_PARSE: TypeAlias = Union[str, None]

0 commit comments

Comments
 (0)