Skip to content

Commit 18aa7f0

Browse files
Merge pull request #1155 from RonnyPfannschmidt/fix-self-enable-consider-build-requires
self enable if setuptools_scm is part of build requires
2 parents d837167 + 1a4dbb5 commit 18aa7f0

File tree

12 files changed

+752
-77
lines changed

12 files changed

+752
-77
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77
- add `setuptools-scm` console_scripts entry point to make the CLI directly executable
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
10-
10+
- fix #1059: add `SETUPTOOLS_SCM_PRETEND_METADATA` environment variable to override individual ScmVersion fields
1111
### Changed
1212

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

1922
### Fixed
2023

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,24 @@ dynamic = ["version"]
5353
[tool.setuptools_scm]
5454
```
5555

56+
!!! note "Simplified Configuration"
57+
58+
Starting with setuptools-scm 8.1+, if `setuptools_scm` (or `setuptools-scm`) is
59+
present in your `build-system.requires`, the `[tool.setuptools_scm]` section
60+
becomes optional! You can now enable setuptools-scm with just:
61+
62+
```toml title="pyproject.toml"
63+
[build-system]
64+
requires = ["setuptools>=64", "setuptools-scm>=8"]
65+
build-backend = "setuptools.build_meta"
66+
67+
[project]
68+
dynamic = ["version"]
69+
```
70+
71+
The `[tool.setuptools_scm]` section is only needed if you want to customize
72+
configuration options.
73+
5674
Additionally, a version file can be written by specifying:
5775

5876
```toml title="pyproject.toml"

docs/config.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Configuration
22

3+
## When is configuration needed?
4+
5+
Starting with setuptools-scm 8.1+, explicit configuration is **optional** in many cases:
6+
7+
- **No configuration needed**: If `setuptools_scm` (or `setuptools-scm`) is in your `build-system.requires`, setuptools-scm will automatically activate with sensible defaults.
8+
9+
- **Configuration recommended**: Use the `[tool.setuptools_scm]` section when you need to:
10+
- Write version files (`version_file`)
11+
- Customize version schemes (`version_scheme`, `local_scheme`)
12+
- Set custom tag patterns (`tag_regex`)
13+
- Configure fallback behavior (`fallback_version`)
14+
- Or any other non-default behavior
315

416
## configuration parameters
517

docs/overrides.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,67 @@ as the override source for the version number unparsed string.
1010
to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}`
1111
where the dist name normalization follows adapted PEP 503 semantics.
1212

13+
## pretend metadata
14+
15+
setuptools-scm provides a mechanism to override individual version metadata fields at build time.
16+
17+
The environment variable `SETUPTOOLS_SCM_PRETEND_METADATA` accepts a TOML inline table
18+
with field overrides for the ScmVersion object.
19+
20+
To be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${NORMALIZED_DIST_NAME}`
21+
where the dist name normalization follows adapted PEP 503 semantics.
22+
23+
### Supported fields
24+
25+
The following ScmVersion fields can be overridden:
26+
27+
- `distance` (int): Number of commits since the tag
28+
- `node` (str): The commit hash/node identifier
29+
- `dirty` (bool): Whether the working directory has uncommitted changes
30+
- `branch` (str): The branch name
31+
- `node_date` (date): The date of the commit (TOML date format: `2024-01-15`)
32+
- `time` (datetime): The version timestamp (TOML datetime format)
33+
- `preformatted` (bool): Whether the version string is preformatted
34+
- `tag`: The version tag (can be string or version object)
35+
36+
### Examples
37+
38+
Override commit hash and distance:
39+
```bash
40+
export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}'
41+
```
42+
43+
Override multiple fields with proper TOML types:
44+
```bash
45+
export SETUPTOOLS_SCM_PRETEND_METADATA='{node="gabcdef12", distance=7, dirty=true, node_date=2024-01-15}'
46+
```
47+
48+
Use with a specific package:
49+
```bash
50+
export SETUPTOOLS_SCM_PRETEND_METADATA_FOR_MY_PACKAGE='{node="g1234567", distance=2}'
51+
```
52+
53+
### Use case: CI/CD environments
54+
55+
This is particularly useful for solving issues where version file templates need access to
56+
commit metadata that may not be available in certain build environments:
57+
58+
```toml
59+
[tool.setuptools_scm]
60+
version_file = "src/mypackage/_version.py"
61+
version_file_template = '''
62+
version = "{version}"
63+
commit_hash = "{scm_version.node}"
64+
commit_count = {scm_version.distance}
65+
'''
66+
```
67+
68+
With pretend metadata, you can ensure the template gets the correct values:
69+
```bash
70+
export SETUPTOOLS_SCM_PRETEND_VERSION="1.2.3.dev4+g1337beef"
71+
export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}'
72+
```
73+
1374
## config overrides
1475

1576
setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}`

docs/usage.md

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,30 @@
22

33
## At build time
44

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

8-
It's necessary to use a setuptools version released after 2022.
7+
### Automatic Configuration (Recommended for Simple Cases)
8+
9+
For projects that don't need custom configuration, simply include `setuptools-scm`
10+
in your build requirements:
11+
12+
```toml title="pyproject.toml"
13+
[build-system]
14+
requires = ["setuptools>=64", "setuptools-scm>=8"]
15+
build-backend = "setuptools.build_meta"
16+
17+
[project]
18+
# version = "0.0.1" # Remove any existing version parameter.
19+
dynamic = ["version"]
20+
```
21+
22+
**That's it!** Starting with setuptools-scm 8.1+, if `setuptools_scm` (or `setuptools-scm`)
23+
is present in your `build-system.requires`, setuptools-scm will automatically activate
24+
with default settings.
25+
26+
### Explicit Configuration
27+
28+
If you need to customize setuptools-scm behavior, use the `tool.setuptools_scm` section:
929

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

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

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

48+
!!! info "How Automatic Detection Works"
49+
50+
When setuptools-scm is listed in `build-system.requires`, it automatically detects this during the build process and activates with default settings. This means:
51+
52+
- ✅ **Automatic activation**: No `[tool.setuptools_scm]` section needed
53+
- ✅ **Default behavior**: Uses standard version schemes and SCM detection
54+
- ✅ **Error handling**: Provides helpful error messages if configuration is missing
55+
- ⚙️ **Customization**: Add `[tool.setuptools_scm]` section when you need custom options
56+
57+
Both package names are detected: `setuptools_scm` and `setuptools-scm` (with dash).
58+
2859
### Version files
2960

3061
Version files can be created with the ``version_file`` directive.

src/setuptools_scm/_config.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from . import _log
1616
from . import _types as _t
17+
from ._integration.pyproject_reading import PyProjectData
1718
from ._integration.pyproject_reading import (
1819
get_args_for_pyproject as _get_args_for_pyproject,
1920
)
@@ -115,17 +116,31 @@ def from_file(
115116
cls,
116117
name: str | os.PathLike[str] = "pyproject.toml",
117118
dist_name: str | None = None,
118-
_require_section: bool = True,
119+
missing_file_ok: bool = False,
119120
**kwargs: Any,
120121
) -> Configuration:
121122
"""
122-
Read Configuration from pyproject.toml (or similar).
123-
Raises exceptions when file is not found or toml is
124-
not installed or the file has invalid format or does
125-
not contain the [tool.setuptools_scm] section.
123+
Read Configuration from pyproject.toml (or similar).
124+
Raises exceptions when file is not found or toml is
125+
not installed or the file has invalid format or does
126+
not contain setuptools_scm configuration (either via
127+
_ [tool.setuptools_scm] section or build-system.requires).
126128
"""
127129

128-
pyproject_data = _read_pyproject(Path(name), require_section=_require_section)
130+
try:
131+
pyproject_data = _read_pyproject(Path(name))
132+
except FileNotFoundError:
133+
if missing_file_ok:
134+
log.warning("File %s not found, using empty configuration", name)
135+
pyproject_data = PyProjectData(
136+
path=Path(name),
137+
tool_name="setuptools_scm",
138+
project={},
139+
section={},
140+
is_required=False,
141+
)
142+
else:
143+
raise
129144
args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs)
130145

131146
args.update(read_toml_overrides(args["dist_name"]))

src/setuptools_scm/_get_version_impl.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,18 @@ def parse_fallback_version(config: Configuration) -> ScmVersion | None:
5656

5757

5858
def parse_version(config: Configuration) -> ScmVersion | None:
59-
return (
59+
# First try to get a version from the normal flow
60+
scm_version = (
6061
_read_pretended_version_for(config)
6162
or parse_scm_version(config)
6263
or parse_fallback_version(config)
6364
)
6465

66+
# Apply any metadata overrides to the version we found
67+
from ._overrides import _apply_metadata_overrides
68+
69+
return _apply_metadata_overrides(scm_version, config)
70+
6571

6672
def write_version_files(
6773
config: Configuration, version: str, scm_version: ScmVersion

src/setuptools_scm/_integration/pyproject_reading.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pathlib import Path
66
from typing import NamedTuple
7+
from typing import Sequence
78

89
from .. import _log
910
from .setuptools import read_dist_name_from_setup_cfg
@@ -20,30 +21,56 @@ class PyProjectData(NamedTuple):
2021
tool_name: str
2122
project: TOML_RESULT
2223
section: TOML_RESULT
24+
is_required: bool
2325

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

2830

31+
def has_build_package(
32+
requires: Sequence[str], build_package_names: Sequence[str]
33+
) -> bool:
34+
for requirement in requires:
35+
import re
36+
37+
# Remove extras like [toml] first
38+
clean_req = re.sub(r"\[.*?\]", "", requirement)
39+
# Split on version operators and take first part
40+
package_name = re.split(r"[><=!~]", clean_req)[0].strip().lower()
41+
if package_name in build_package_names:
42+
return True
43+
return False
44+
45+
2946
def read_pyproject(
3047
path: Path = Path("pyproject.toml"),
3148
tool_name: str = "setuptools_scm",
32-
require_section: bool = True,
49+
build_package_names: Sequence[str] = ("setuptools_scm", "setuptools-scm"),
3350
) -> PyProjectData:
34-
defn = read_toml_content(path, None if require_section else {})
51+
defn = read_toml_content(path)
52+
requires: list[str] = defn.get("build-system", {}).get("requires", [])
53+
is_required = has_build_package(requires, build_package_names)
54+
3555
try:
3656
section = defn.get("tool", {})[tool_name]
3757
except LookupError as e:
38-
error = f"{path} does not contain a tool.{tool_name} section"
39-
if require_section:
58+
if not is_required:
59+
# Enhanced error message that mentions both configuration options
60+
error = (
61+
f"{path} does not contain a tool.{tool_name} section. "
62+
f"setuptools_scm requires configuration via either:\n"
63+
f" 1. [tool.{tool_name}] section in {path}, or\n"
64+
f" 2. {tool_name} (or setuptools-scm) in [build-system] requires"
65+
)
4066
raise LookupError(error) from e
4167
else:
68+
error = f"{path} does not contain a tool.{tool_name} section"
4269
log.warning("toml section missing %r", error, exc_info=True)
4370
section = {}
4471

4572
project = defn.get("project", {})
46-
return PyProjectData(path, tool_name, project, section)
73+
return PyProjectData(path, tool_name, project, section, is_required)
4774

4875

4976
def get_args_for_pyproject(

src/setuptools_scm/_integration/setuptools.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None:
4545
)
4646

4747

48+
def _extract_package_name(requirement: str) -> str:
49+
"""Extract the package name from a requirement string.
50+
51+
Examples:
52+
'setuptools_scm' -> 'setuptools_scm'
53+
'setuptools-scm>=8' -> 'setuptools-scm'
54+
'setuptools_scm[toml]>=7.0' -> 'setuptools_scm'
55+
"""
56+
# Split on common requirement operators and take the first part
57+
# This handles: >=, <=, ==, !=, >, <, ~=
58+
import re
59+
60+
# Remove extras like [toml] first
61+
requirement = re.sub(r"\[.*?\]", "", requirement)
62+
# Split on version operators
63+
package_name = re.split(r"[><=!~]", requirement)[0].strip()
64+
return package_name
65+
66+
4867
def _assign_version(
4968
dist: setuptools.Distribution, config: _config.Configuration
5069
) -> None:
@@ -97,7 +116,7 @@ def version_keyword(
97116

98117
config = _config.Configuration.from_file(
99118
dist_name=dist_name,
100-
_require_section=False,
119+
missing_file_ok=True,
101120
**overrides,
102121
)
103122
_assign_version(dist, config)
@@ -115,6 +134,7 @@ def infer_version(dist: setuptools.Distribution) -> None:
115134
return
116135
if dist_name == "setuptools-scm":
117136
return
137+
118138
try:
119139
config = _config.Configuration.from_file(dist_name=dist_name)
120140
except LookupError as e:

0 commit comments

Comments
 (0)