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 @@ -7,14 +7,17 @@
- add `setuptools-scm` console_scripts entry point to make the CLI directly executable
- 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
### Changed

- add `pip` to test optional dependencies for improved uv venv compatibility
- migrate to selectable entrypoints for better extensibility
- improve typing for entry_points
- refactor file modification time logic into shared helper function for better maintainability
- reduce complexity of HgWorkdir.get_meta method by extracting focused helper methods
- fix #1150: enable setuptools-scm when we are a build requirement
- feature #1154: add the commit id the the default version file template


### Fixed

Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ dynamic = ["version"]
[tool.setuptools_scm]
```

!!! note "Simplified Configuration"

Starting with setuptools-scm 8.1+, if `setuptools_scm` (or `setuptools-scm`) is
present in your `build-system.requires`, the `[tool.setuptools_scm]` section
becomes optional! You can now enable setuptools-scm with just:

```toml title="pyproject.toml"
[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[project]
dynamic = ["version"]
```

The `[tool.setuptools_scm]` section is only needed if you want to customize
configuration options.

Additionally, a version file can be written by specifying:

```toml title="pyproject.toml"
Expand Down
12 changes: 12 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Configuration

## When is configuration needed?

Starting with setuptools-scm 8.1+, explicit configuration is **optional** in many cases:

- **No configuration needed**: If `setuptools_scm` (or `setuptools-scm`) is in your `build-system.requires`, setuptools-scm will automatically activate with sensible defaults.

- **Configuration recommended**: Use the `[tool.setuptools_scm]` section when you need to:
- Write version files (`version_file`)
- Customize version schemes (`version_scheme`, `local_scheme`)
- Set custom tag patterns (`tag_regex`)
- Configure fallback behavior (`fallback_version`)
- Or any other non-default behavior

## configuration parameters

Expand Down
61 changes: 61 additions & 0 deletions docs/overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,67 @@ as the override source for the version number unparsed string.
to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}`
where the dist name normalization follows adapted PEP 503 semantics.

## pretend metadata

setuptools-scm provides a mechanism to override individual version metadata fields at build time.

The environment variable `SETUPTOOLS_SCM_PRETEND_METADATA` accepts a TOML inline table
with field overrides for the ScmVersion object.

To be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${NORMALIZED_DIST_NAME}`
where the dist name normalization follows adapted PEP 503 semantics.

### Supported fields

The following ScmVersion fields can be overridden:

- `distance` (int): Number of commits since the tag
- `node` (str): The commit hash/node identifier
- `dirty` (bool): Whether the working directory has uncommitted changes
- `branch` (str): The branch name
- `node_date` (date): The date of the commit (TOML date format: `2024-01-15`)
- `time` (datetime): The version timestamp (TOML datetime format)
- `preformatted` (bool): Whether the version string is preformatted
- `tag`: The version tag (can be string or version object)

### Examples

Override commit hash and distance:
```bash
export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}'
```

Override multiple fields with proper TOML types:
```bash
export SETUPTOOLS_SCM_PRETEND_METADATA='{node="gabcdef12", distance=7, dirty=true, node_date=2024-01-15}'
```

Use with a specific package:
```bash
export SETUPTOOLS_SCM_PRETEND_METADATA_FOR_MY_PACKAGE='{node="g1234567", distance=2}'
```

### Use case: CI/CD environments

This is particularly useful for solving issues where version file templates need access to
commit metadata that may not be available in certain build environments:

```toml
[tool.setuptools_scm]
version_file = "src/mypackage/_version.py"
version_file_template = '''
version = "{version}"
commit_hash = "{scm_version.node}"
commit_count = {scm_version.distance}
'''
```

With pretend metadata, you can ensure the template gets the correct values:
```bash
export SETUPTOOLS_SCM_PRETEND_VERSION="1.2.3.dev4+g1337beef"
export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}'
```

## config overrides

setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}`
Expand Down
43 changes: 37 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,30 @@

## At build time

The preferred way to configure `setuptools-scm` is to author
settings in the `tool.setuptools_scm` section of `pyproject.toml`.
There are two ways to configure `setuptools-scm` at build time, depending on your needs:

It's necessary to use a setuptools version released after 2022.
### Automatic Configuration (Recommended for Simple Cases)

For projects that don't need custom configuration, simply include `setuptools-scm`
in your build requirements:

```toml title="pyproject.toml"
[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[project]
# version = "0.0.1" # Remove any existing version parameter.
dynamic = ["version"]
```

**That's it!** Starting with setuptools-scm 8.1+, if `setuptools_scm` (or `setuptools-scm`)
is present in your `build-system.requires`, setuptools-scm will automatically activate
with default settings.

### Explicit Configuration

If you need to customize setuptools-scm behavior, use the `tool.setuptools_scm` section:

```toml title="pyproject.toml"
[build-system]
Expand All @@ -17,14 +37,25 @@ build-backend = "setuptools.build_meta"
dynamic = ["version"]

[tool.setuptools_scm]
# can be empty if no extra settings are needed, presence enables setuptools-scm
# Configure custom options here (version schemes, file writing, etc.)
version_file = "src/mypackage/_version.py"
```

That will be sufficient to require `setuptools-scm` for projects
that support PEP 518 ([pip](https://pypi.org/project/pip) and
Both approaches will work with projects that support PEP 518 ([pip](https://pypi.org/project/pip) and
[pep517](https://pypi.org/project/pep517/)).
Tools that still invoke `setup.py` must ensure build requirements are installed

!!! info "How Automatic Detection Works"

When setuptools-scm is listed in `build-system.requires`, it automatically detects this during the build process and activates with default settings. This means:

- ✅ **Automatic activation**: No `[tool.setuptools_scm]` section needed
- ✅ **Default behavior**: Uses standard version schemes and SCM detection
- ✅ **Error handling**: Provides helpful error messages if configuration is missing
- ⚙️ **Customization**: Add `[tool.setuptools_scm]` section when you need custom options

Both package names are detected: `setuptools_scm` and `setuptools-scm` (with dash).

### Version files

Version files can be created with the ``version_file`` directive.
Expand Down
27 changes: 21 additions & 6 deletions src/setuptools_scm/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from . import _log
from . import _types as _t
from ._integration.pyproject_reading import PyProjectData
from ._integration.pyproject_reading import (
get_args_for_pyproject as _get_args_for_pyproject,
)
Expand Down Expand Up @@ -115,17 +116,31 @@ def from_file(
cls,
name: str | os.PathLike[str] = "pyproject.toml",
dist_name: str | None = None,
_require_section: bool = True,
missing_file_ok: bool = False,
**kwargs: Any,
) -> Configuration:
"""
Read Configuration from pyproject.toml (or similar).
Raises exceptions when file is not found or toml is
not installed or the file has invalid format or does
not contain the [tool.setuptools_scm] section.
Read Configuration from pyproject.toml (or similar).
Raises exceptions when file is not found or toml is
not installed or the file has invalid format or does
not contain setuptools_scm configuration (either via
_ [tool.setuptools_scm] section or build-system.requires).
"""

pyproject_data = _read_pyproject(Path(name), require_section=_require_section)
try:
pyproject_data = _read_pyproject(Path(name))
except FileNotFoundError:
if missing_file_ok:
log.warning("File %s not found, using empty configuration", name)
pyproject_data = PyProjectData(
path=Path(name),
tool_name="setuptools_scm",
project={},
section={},
is_required=False,
)
else:
raise
args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs)

args.update(read_toml_overrides(args["dist_name"]))
Expand Down
8 changes: 7 additions & 1 deletion src/setuptools_scm/_get_version_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,18 @@ def parse_fallback_version(config: Configuration) -> ScmVersion | None:


def parse_version(config: Configuration) -> ScmVersion | None:
return (
# First try to get a version from the normal flow
scm_version = (
_read_pretended_version_for(config)
or parse_scm_version(config)
or parse_fallback_version(config)
)

# Apply any metadata overrides to the version we found
from ._overrides import _apply_metadata_overrides

return _apply_metadata_overrides(scm_version, config)


def write_version_files(
config: Configuration, version: str, scm_version: ScmVersion
Expand Down
37 changes: 32 additions & 5 deletions src/setuptools_scm/_integration/pyproject_reading.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pathlib import Path
from typing import NamedTuple
from typing import Sequence

from .. import _log
from .setuptools import read_dist_name_from_setup_cfg
Expand All @@ -20,30 +21,56 @@ class PyProjectData(NamedTuple):
tool_name: str
project: TOML_RESULT
section: TOML_RESULT
is_required: bool

@property
def project_name(self) -> str | None:
return self.project.get("name")


def has_build_package(
requires: Sequence[str], build_package_names: Sequence[str]
) -> bool:
for requirement in requires:
import re

# Remove extras like [toml] first
clean_req = re.sub(r"\[.*?\]", "", requirement)
# Split on version operators and take first part
package_name = re.split(r"[><=!~]", clean_req)[0].strip().lower()
if package_name in build_package_names:
return True
return False


def read_pyproject(
path: Path = Path("pyproject.toml"),
tool_name: str = "setuptools_scm",
require_section: bool = True,
build_package_names: Sequence[str] = ("setuptools_scm", "setuptools-scm"),
) -> PyProjectData:
defn = read_toml_content(path, None if require_section else {})
defn = read_toml_content(path)
requires: list[str] = defn.get("build-system", {}).get("requires", [])
is_required = has_build_package(requires, build_package_names)

try:
section = defn.get("tool", {})[tool_name]
except LookupError as e:
error = f"{path} does not contain a tool.{tool_name} section"
if require_section:
if not is_required:
# Enhanced error message that mentions both configuration options
error = (
f"{path} does not contain a tool.{tool_name} section. "
f"setuptools_scm requires configuration via either:\n"
f" 1. [tool.{tool_name}] section in {path}, or\n"
f" 2. {tool_name} (or setuptools-scm) in [build-system] requires"
)
raise LookupError(error) from e
else:
error = f"{path} does not contain a tool.{tool_name} section"
log.warning("toml section missing %r", error, exc_info=True)
section = {}

project = defn.get("project", {})
return PyProjectData(path, tool_name, project, section)
return PyProjectData(path, tool_name, project, section, is_required)


def get_args_for_pyproject(
Expand Down
22 changes: 21 additions & 1 deletion src/setuptools_scm/_integration/setuptools.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,25 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None:
)


def _extract_package_name(requirement: str) -> str:
"""Extract the package name from a requirement string.

Examples:
'setuptools_scm' -> 'setuptools_scm'
'setuptools-scm>=8' -> 'setuptools-scm'
'setuptools_scm[toml]>=7.0' -> 'setuptools_scm'
"""
# Split on common requirement operators and take the first part
# This handles: >=, <=, ==, !=, >, <, ~=
import re

# Remove extras like [toml] first
requirement = re.sub(r"\[.*?\]", "", requirement)
# Split on version operators
package_name = re.split(r"[><=!~]", requirement)[0].strip()
return package_name


def _assign_version(
dist: setuptools.Distribution, config: _config.Configuration
) -> None:
Expand Down Expand Up @@ -97,7 +116,7 @@ def version_keyword(

config = _config.Configuration.from_file(
dist_name=dist_name,
_require_section=False,
missing_file_ok=True,
**overrides,
)
_assign_version(dist, config)
Expand All @@ -115,6 +134,7 @@ def infer_version(dist: setuptools.Distribution) -> None:
return
if dist_name == "setuptools-scm":
return

try:
config = _config.Configuration.from_file(dist_name=dist_name)
except LookupError as e:
Expand Down
Loading
Loading