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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- make Mercurial command configurable by environment variable `SETUPTOOLS_SCM_HG_COMMAND`
- fix #1099 use file modification times for dirty working directory timestamps instead of current time
- fix #1059: add `SETUPTOOLS_SCM_PRETEND_METADATA` environment variable to override individual ScmVersion fields
- add `scm` parameter support to `get_version()` function for nested SCM configuration
### Changed

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

## v8.3.1

Expand Down
21 changes: 20 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,30 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
this is a function for advanced use and you should be
familiar with the `setuptools-scm` internals to use it.

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

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

`scm.git.pre_parse`
: A string specifying which git pre-parse function to use before parsing version information.
Available options:

- `"warn_on_shallow"` (default): Warns when the repository is shallow
- `"fail_on_shallow"`: Fails with an error when the repository is shallow
- `"fetch_on_shallow"`: Automatically fetches to rectify shallow repositories
- `"fail_on_missing_submodules"`: Fails when submodules are defined but not initialized

The `"fail_on_missing_submodules"` option is useful to prevent packaging incomplete
projects when submodules are required for a complete build.

Note: This setting is overridden by any explicit `pre_parse` parameter passed to the git parse function.

`git_describe_command` (deprecated)
: **Deprecated since 8.4.0**: Use `scm.git.describe_command` instead.

This field is maintained for backward compatibility but will issue a deprecation warning when used.

`normalize`
: A boolean flag indicating if the version string should be normalized.
Defaults to `True`. Setting this to `False` is equivalent to setting
Expand Down
5 changes: 5 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ dynamic = ["version"]
[tool.setuptools_scm]
# Configure custom options here (version schemes, file writing, etc.)
version_file = "src/mypackage/_version.py"

# Example: Git-specific configuration
[tool.setuptools_scm.scm.git]
pre_parse = "fail_on_missing_submodules" # Fail if submodules are not initialized
describe_command = "git describe --dirty --tags --long --exclude *js*" # Custom describe command
```

Both approaches will work with projects that support PEP 518 ([pip](https://pypi.org/project/pip) and
Expand Down
164 changes: 162 additions & 2 deletions src/setuptools_scm/_config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""configuration"""

from __future__ import annotations
Expand All @@ -8,10 +8,14 @@
import warnings

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from typing import Pattern
from typing import Protocol

if TYPE_CHECKING:
from . import git

from . import _log
from . import _types as _t
from ._integration.pyproject_reading import PyProjectData
Expand All @@ -26,6 +30,57 @@

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


def _is_called_from_dataclasses() -> bool:
"""Check if the current call is from the dataclasses module."""
import inspect

frame = inspect.currentframe()
try:
# Walk up to 7 frames to check for dataclasses calls
current_frame = frame
assert current_frame is not None
for _ in range(7):
current_frame = current_frame.f_back
if current_frame is None:
break
if "dataclasses.py" in current_frame.f_code.co_filename:
return True
return False
finally:
del frame


class _GitDescribeCommandDescriptor:
"""Data descriptor for deprecated git_describe_command field."""

def __get__(
self, obj: Configuration | None, objtype: type[Configuration] | None = None
) -> _t.CMD_TYPE | None:
if obj is None:
return self # type: ignore[return-value]

# Only warn if not being called by dataclasses.replace or similar introspection
is_from_dataclasses = _is_called_from_dataclasses()
if not is_from_dataclasses:
warnings.warn(
"Configuration field 'git_describe_command' is deprecated. "
"Use 'scm.git.describe_command' instead.",
DeprecationWarning,
stacklevel=2,
)
return obj.scm.git.describe_command

def __set__(self, obj: Configuration, value: _t.CMD_TYPE | None) -> None:
warnings.warn(
"Configuration field 'git_describe_command' is deprecated. "
"Use 'scm.git.describe_command' instead.",
DeprecationWarning,
stacklevel=2,
)
obj.scm.git.describe_command = value


DEFAULT_TAG_REGEX = re.compile(
r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
)
Expand All @@ -52,6 +107,13 @@
return regex


def _get_default_git_pre_parse() -> git.GitPreParse:
"""Get the default git pre_parse enum value"""
from . import git

return git.GitPreParse.WARN_ON_SHALLOW


class ParseFunction(Protocol):
def __call__(
self, root: _t.PathT, *, config: Configuration
Expand Down Expand Up @@ -83,6 +145,54 @@
return os.path.abspath(root)


@dataclasses.dataclass
class GitConfiguration:
"""Git-specific configuration options"""

pre_parse: git.GitPreParse = dataclasses.field(
default_factory=lambda: _get_default_git_pre_parse()
)
describe_command: _t.CMD_TYPE | None = None

@classmethod
def from_data(cls, data: dict[str, Any]) -> GitConfiguration:
"""Create GitConfiguration from configuration data, converting strings to enums"""
git_data = data.copy()

# Convert string pre_parse values to enum instances
if "pre_parse" in git_data and isinstance(git_data["pre_parse"], str):
from . import git

try:
git_data["pre_parse"] = git.GitPreParse(git_data["pre_parse"])
except ValueError as e:
valid_options = [option.value for option in git.GitPreParse]
raise ValueError(
f"Invalid git pre_parse function '{git_data['pre_parse']}'. "
f"Valid options are: {', '.join(valid_options)}"
) from e

return cls(**git_data)


@dataclasses.dataclass
class ScmConfiguration:
"""SCM-specific configuration options"""

git: GitConfiguration = dataclasses.field(default_factory=GitConfiguration)

@classmethod
def from_data(cls, data: dict[str, Any]) -> ScmConfiguration:
"""Create ScmConfiguration from configuration data"""
scm_data = data.copy()

# Handle git-specific configuration
git_data = scm_data.pop("git", {})
git_config = GitConfiguration.from_data(git_data)

return cls(git=git_config, **scm_data)


@dataclasses.dataclass
class Configuration:
"""Global configuration model"""
Expand All @@ -100,21 +210,62 @@
version_file: _t.PathT | None = None
version_file_template: str | None = None
parse: ParseFunction | None = None
git_describe_command: _t.CMD_TYPE | None = None
git_describe_command: dataclasses.InitVar[_t.CMD_TYPE | None] = (
_GitDescribeCommandDescriptor()
)

dist_name: str | None = None
version_cls: type[_VersionT] = _Version
search_parent_directories: bool = False

parent: _t.PathT | None = None

def __post_init__(self) -> None:
# Nested SCM configurations
scm: ScmConfiguration = dataclasses.field(
default_factory=lambda: ScmConfiguration()
)

# Deprecated fields (handled in __post_init__)

def __post_init__(self, git_describe_command: _t.CMD_TYPE | None) -> None:
self.tag_regex = _check_tag_regex(self.tag_regex)

# Handle deprecated git_describe_command
# Check if it's a descriptor object (happens when no value is passed)
if git_describe_command is not None and not isinstance(
git_describe_command, _GitDescribeCommandDescriptor
):
# Check if this is being called from dataclasses
is_from_dataclasses = _is_called_from_dataclasses()

same_value = (
self.scm.git.describe_command is not None
and self.scm.git.describe_command == git_describe_command
)

if is_from_dataclasses and same_value:
# Ignore the passed value - it's from dataclasses.replace() with same value
pass
else:
warnings.warn(
"Configuration field 'git_describe_command' is deprecated. "
"Use 'scm.git.describe_command' instead.",
DeprecationWarning,
stacklevel=2,
)
# Check for conflicts
if self.scm.git.describe_command is not None:
raise ValueError(
"Cannot specify both 'git_describe_command' (deprecated) and "
"'scm.git.describe_command'. Please use only 'scm.git.describe_command'."
)
self.scm.git.describe_command = git_describe_command

@property
def absolute_root(self) -> str:
return _check_absolute_root(self.root, self.relative_to)

@classmethod

Check warning on line 268 in src/setuptools_scm/_config.py

View workflow job for this annotation

GitHub Actions / Check API stability with griffe

Configuration.from_file(_require_section)

Parameter was removed
def from_file(
cls,
name: str | os.PathLike[str] = "pyproject.toml",
Expand Down Expand Up @@ -161,8 +312,17 @@
version_cls = _validate_version_cls(
data.pop("version_cls", None), data.pop("normalize", True)
)

# Handle nested SCM configuration
scm_data = data.pop("scm", {})

# Handle nested SCM configuration

scm_config = ScmConfiguration.from_data(scm_data)

return cls(
relative_to=relative_to,
version_cls=version_cls,
scm=scm_config,
**data,
)
15 changes: 14 additions & 1 deletion src/setuptools_scm/_get_version_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def get_version(
version_cls: Any | None = None,
normalize: bool = True,
search_parent_directories: bool = False,
scm: dict[str, Any] | None = None,
) -> str:
"""
If supplied, relative_to should be a file from which root may
Expand All @@ -165,7 +166,19 @@ def get_version(
version_cls = _validate_version_cls(version_cls, normalize)
del normalize
tag_regex = parse_tag_regex(tag_regex)
config = Configuration(**locals())

# Handle scm parameter by converting it to ScmConfiguration
if scm is not None:
scm_config = _config.ScmConfiguration.from_data(scm)
else:
scm_config = _config.ScmConfiguration()

# Remove scm from locals() since we handle it separately
config_params = locals().copy()
config_params.pop("scm", None)
config_params.pop("scm_config", None)

config = _config.Configuration(scm=scm_config, **config_params)
maybe_version = _get_version(config, force_write_version_files=True)

if maybe_version is None:
Expand Down
3 changes: 3 additions & 0 deletions src/setuptools_scm/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@
VERSION_SCHEME: TypeAlias = Union[str, Callable[["version.ScmVersion"], str]]
VERSION_SCHEMES: TypeAlias = Union[List[str], Tuple[str, ...], VERSION_SCHEME]
SCMVERSION: TypeAlias = "version.ScmVersion"

# Git pre-parse function types
GIT_PRE_PARSE: TypeAlias = Union[str, None]
Loading
Loading