Skip to content

Commit 59275f7

Browse files
Merge pull request #1325 from RonnyPfannschmidt/issue-1302-setuptools-build-bootstrap
build: setuptools + setup.py for vcs-versioning and setuptools-scm (#1302)
2 parents 0e5d5b0 + b0dc3c5 commit 59275f7

10 files changed

Lines changed: 116 additions & 113 deletions

File tree

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ repos:
4747
rev: v1.19.1
4848
hooks:
4949
- id: mypy
50+
# Both are named setup.py → duplicate module "setup"; runtime path hacks are not modeled
51+
exclude: ^(vcs-versioning|setuptools-scm)/setup\.py$
5052
args: [--strict]
5153
additional_dependencies:
5254
- types-setuptools

pyproject.toml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,11 @@ no_implicit_reexport = true
6565
# Workspace-specific paths
6666
mypy_path = "vcs-versioning/src:setuptools-scm/src"
6767
explicit_package_bases = true
68-
69-
[[tool.mypy.overrides]]
70-
module = [
71-
"vcs-versioning._own_version_of_vcs_versioning",
72-
"setuptools-scm._own_version_helper",
68+
# setup.py scripts add src/ at runtime; duplicate module name "setup" across packages
69+
exclude = [
70+
"^vcs-versioning/setup\\.py$",
71+
"^setuptools-scm/setup\\.py$",
7372
]
74-
# Version helper files use imports before they're installed
75-
ignore_errors = true
7673

7774
[tool.uv]
7875
package = true
@@ -87,6 +84,10 @@ setuptools-scm = { workspace = true }
8784
[tool.ruff]
8885
lint.extend-select = ["UP", "I", "B"]
8986

87+
[tool.ruff.lint.per-file-ignores]
88+
"vcs-versioning/setup.py" = ["E402"]
89+
"setuptools-scm/setup.py" = ["E402"]
90+
9091
[tool.repo-review]
9192
ignore = [
9293
"PP002", # this is a uv workspace

setuptools-scm/_own_version_helper.py

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Anchor ``get_version`` in ``setup.py`` with ``relative_to`` and ``fallback_root`` so SCM fallbacks (e.g. ``PKG-INFO``) do not resolve against the wrong directory when the build cwd is the workspace or repo root.

setuptools-scm/pyproject.toml

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
2-
31
[build-system]
4-
build-backend = "_own_version_helper:build_meta"
2+
build-backend = "setuptools.build_meta"
53
requires = [
64
"setuptools>=77.0.3",
75
"vcs-versioning>=1.0.0.dev0",
@@ -10,12 +8,8 @@ requires = [
108
'tomli>=1; python_version < "3.11"',
119
'typing-extensions; python_version < "3.11"',
1210
]
13-
backend-path = [
14-
".",
15-
"./src",
16-
]
17-
[tools.uv.sources]
18-
vcs-versioning= "../vcs-versioning"
11+
[tool.uv.sources]
12+
vcs-versioning = { workspace = true }
1913
[project]
2014
name = "setuptools-scm"
2115
description = "the blessed package to manage your versions by scm tags"
@@ -120,8 +114,8 @@ setuptools_scm = "setuptools_scm._integration.setuptools:infer_version"
120114
where = ["src"]
121115
namespaces = false
122116

123-
[tool.setuptools.dynamic]
124-
version = { attr = "_own_version_helper.version"}
117+
[tool.setuptools.package-data]
118+
setuptools_scm = [".git_archival.txt"]
125119

126120
[tool.setuptools_scm]
127121
root = ".."
@@ -148,7 +142,6 @@ lint.preview = true
148142
"**/src/setuptools_scm/hg.py" = ["PLC0414"]
149143
"**/src/setuptools_scm/hg_git.py" = ["PLC0414"]
150144
"**/src/setuptools_scm/fallbacks.py" = ["PLC0414"]
151-
"**/_own_version_helper.py" = ["PLC0414"]
152145
"**/src/setuptools_scm/_integration/pyproject_reading.py" = ["BLE001", "S112"]
153146
"**/testing_scm/conftest.py" = ["BLE001", "S110"]
154147
"**/testing_scm/test_main.py" = ["S102"]

setuptools-scm/setup.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Build entry point: compute ``version`` here so it is not part of the installed package."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import sys
7+
8+
from pathlib import Path
9+
10+
from setuptools import setup
11+
12+
_root = Path(__file__).resolve().parent
13+
sys.path.insert(0, str(_root / "src"))
14+
15+
from setuptools_scm import get_version
16+
from vcs_versioning._backends._git import make_describe_command
17+
18+
19+
def _package_version() -> str:
20+
"""Resolve version using this package's tree, not the process cwd.
21+
22+
``root`` is the monorepo (``..`` relative to ``pyproject.toml``) for SCM
23+
discovery. ``fallback_root`` is ``.`` (the package tree) so ``PKG-INFO``
24+
fallbacks resolve under that directory when building from an sdist.
25+
26+
``get_version`` defaults ``fallback_root`` to ``.``; PEP 517 builds often run
27+
with cwd at the repo or workspace root, so fallbacks (e.g. ``PKG-INFO`` /
28+
``pyproject.toml`` entry-point matching) could pick the wrong directory.
29+
"""
30+
local_scheme = (
31+
"no-local-version"
32+
if os.environ.get("SETUPTOOLS_SCM_NO_LOCAL")
33+
else "node-and-date"
34+
)
35+
pyproject_toml = _root / "pyproject.toml"
36+
return get_version(
37+
root="..",
38+
relative_to=str(pyproject_toml),
39+
fallback_root=".",
40+
version_scheme="guess-next-dev",
41+
local_scheme=local_scheme,
42+
tag_regex=r"^setuptools-scm-(?P<version>v?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$",
43+
scm={"git": {"describe_command": make_describe_command("setuptools-scm-*")}},
44+
)
45+
46+
47+
if __name__ == "__main__":
48+
setup(version=_package_version())
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Build ``vcs-versioning`` with setuptools instead of hatchling to avoid a bootstrap cycle (hatchling → pluggy → setuptools-scm → vcs-versioning) for downstream packagers.

vcs-versioning/pyproject.toml

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
# Build backend: setuptools (not hatchling) until the bootstrap cycle described in
2+
# https://github.com/pypa/setuptools-scm/issues/1302 is gone: hatchling → pluggy →
3+
# setuptools-scm → vcs-versioning → hatchling. Revisit switching back to hatchling
4+
# once hatch/hatchling no longer depend on pluggy (or pluggy no longer pulls
5+
# setuptools-scm into the vcs-versioning build graph).
16
[build-system]
2-
build-backend = "hatchling.build"
7+
build-backend = "setuptools.build_meta"
38
requires = [
4-
"hatchling",
9+
"setuptools>=77.0.3",
510
"packaging>=20",
611
'typing-extensions; python_version < "3.11"',
712
]
@@ -80,25 +85,12 @@ node-and-timestamp = "vcs_versioning._version_schemes:get_local_node_and_timesta
8085
"semver-pep440-release-branch" = "vcs_versioning._version_schemes:release_branch_semver_version"
8186
"towncrier-fragments" = "vcs_versioning._version_schemes._towncrier:version_from_fragments"
8287

83-
[tool.hatch.version]
84-
source = "code"
85-
path = "_own_version_of_vcs_versioning.py"
86-
search-paths = ["src"]
88+
[tool.setuptools.packages.find]
89+
where = ["src"]
90+
namespaces = false
8791

88-
[tool.hatch.build.targets.wheel]
89-
packages = ["src/vcs_versioning"]
90-
91-
[tool.hatch.envs.default]
92-
dependencies = [
93-
"pytest",
94-
"pytest-cov",
95-
]
96-
[tool.hatch.envs.default.scripts]
97-
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=vcs_versioning --cov=tests {args}"
98-
no-cov = "cov --no-cov {args}"
99-
100-
[[tool.hatch.envs.test.matrix]]
101-
python = [ "38", "39", "310", "311", "312", "313" ]
92+
[tool.setuptools.package-data]
93+
"vcs_versioning._cli" = ["git_archival_stable.txt", "git_archival_full.txt"]
10294

10395
[tool.vcs-versioning]
10496
root = ".."

vcs-versioning/_own_version_of_vcs_versioning.py renamed to vcs-versioning/setup.py

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
1-
"""
2-
Version helper for vcs-versioning package.
3-
4-
This module allows vcs-versioning to use VCS metadata for its own version,
5-
with the tag prefix 'vcs-versioning-'.
6-
7-
Used by hatchling's code version source.
8-
"""
1+
"""Build entry point: compute ``version`` here so it is not part of the installed package."""
92

103
from __future__ import annotations
114

125
import logging
136
import os
7+
import sys
148
from collections.abc import Callable
159
from pathlib import Path
1610

11+
from setuptools import setup
12+
13+
_root = Path(__file__).resolve().parent
14+
sys.path.insert(0, str(_root / "src"))
15+
1716
from vcs_versioning import Configuration
1817
from vcs_versioning import _types as _t
1918
from vcs_versioning._backends import _git as git
2019
from vcs_versioning._backends import _hg as hg
2120
from vcs_versioning._fallbacks import fallback_version, parse_pkginfo
22-
from vcs_versioning._get_version_impl import get_version
21+
from vcs_versioning._get_version_impl import get_version, resolved_fallback_root
2322
from vcs_versioning._version_schemes import (
2423
ScmVersion,
2524
get_local_node_and_date,
@@ -29,21 +28,23 @@
2928

3029
log = logging.getLogger("vcs_versioning")
3130

32-
# Try these parsers in order for vcs-versioning's own version
33-
try_parse: list[Callable[[_t.PathT, Configuration], ScmVersion | None]] = [
31+
_try_parse: list[Callable[[_t.PathT, Configuration], ScmVersion | None]] = [
3432
parse_pkginfo,
3533
git.parse,
3634
hg.parse,
3735
git.parse_archival,
3836
hg.parse_archival,
39-
fallback_version, # Last resort: use fallback_version from config
37+
fallback_version,
4038
]
4139

4240

43-
def parse(root: str, config: Configuration) -> ScmVersion | None:
44-
for maybe_parse in try_parse:
41+
def _parse(root: str, config: Configuration) -> ScmVersion | None:
42+
for maybe_parse in _try_parse:
4543
try:
46-
parsed = maybe_parse(root, config)
44+
parse_root = (
45+
resolved_fallback_root(config) if maybe_parse is parse_pkginfo else root
46+
)
47+
parsed = maybe_parse(parse_root, config)
4748
except OSError as e:
4849
log.warning("parse with %s failed with: %s", maybe_parse, e)
4950
else:
@@ -52,26 +53,26 @@ def parse(root: str, config: Configuration) -> ScmVersion | None:
5253
return None
5354

5455

55-
def _get_version() -> str:
56-
"""Get version from VCS with vcs-versioning- tag prefix."""
57-
# Use no-local-version if VCS_VERSIONING_NO_LOCAL is set (for CI uploads)
56+
def _package_version() -> str:
57+
"""Version from VCS with ``vcs-versioning-`` tag prefix.
58+
59+
``root`` is the monorepo (``..`` relative to ``pyproject.toml``) for SCM
60+
discovery. ``fallback_root`` is ``.`` (the package tree next to
61+
``pyproject.toml``) so ``PKG-INFO`` from an sdist is read from the right
62+
place (see ``_parse`` / ``_pkginfo_root``).
63+
"""
5864
local_scheme = (
5965
get_no_local_node
6066
if os.environ.get("VCS_VERSIONING_NO_LOCAL")
6167
else get_local_node_and_date
6268
)
63-
64-
# Resolve repo root from this file's location so version detection works
65-
# regardless of cwd (e.g. when uv build runs from workspace root).
66-
_here = Path(__file__).resolve()
67-
_repo_root = _here.parent.parent
68-
_pyproject_path = _here.parent / "pyproject.toml"
69+
_pyproject_path = _root / "pyproject.toml"
6970

7071
return get_version(
71-
root=str(_repo_root),
72-
fallback_root=str(_here.parent),
72+
root="..",
73+
fallback_root=".",
7374
relative_to=str(_pyproject_path),
74-
parse=parse,
75+
parse=_parse,
7576
version_scheme=guess_next_dev_version,
7677
local_scheme=local_scheme,
7778
tag_regex=r"^vcs-versioning-(?P<version>v?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$",
@@ -82,4 +83,5 @@ def _get_version() -> str:
8283
)
8384

8485

85-
__version__: str = _get_version()
86+
if __name__ == "__main__":
87+
setup(version=_package_version())

vcs-versioning/src/vcs_versioning/_get_version_impl.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
log = logging.getLogger(__name__)
2424

2525

26+
def resolved_fallback_root(config: Configuration) -> str:
27+
"""Absolute path for *fallback_root* when it is relative to *relative_to*'s directory."""
28+
rel = config.relative_to
29+
if rel and not Path(config.fallback_root).is_absolute():
30+
return str((Path(rel).resolve().parent / config.fallback_root).resolve())
31+
return str(Path(config.fallback_root).resolve())
32+
33+
2634
def parse_scm_version(config: Configuration) -> ScmVersion | None:
2735
try:
2836
if config.parse is not None:
@@ -48,7 +56,7 @@ def parse_fallback_version(config: Configuration) -> ScmVersion | None:
4856
return _entrypoints.version_from_entrypoint(
4957
config,
5058
entrypoint="setuptools_scm.parse_scm_fallback",
51-
root=config.fallback_root,
59+
root=resolved_fallback_root(config),
5260
)
5361

5462

0 commit comments

Comments
 (0)