Skip to content

Commit 76a01cd

Browse files
Implement driver for use with setuptools-scm.
Some python projects use `setuptools-scm` to extract version information from git tags instead of setting the version manually inside `pyproject.toml`. This driver adds support for these projects by reading the version information by calling `setuptools-scm` as a library and then setting the appropriate environment variable `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_<DIST_NAME>` in the Sphinx build environment. * pyproject.toml: add `setuptools_scm` and `packaging` as optional dependencies used by the integration. * poetry.lock: Updated using `poetry lock`. * sphinx_polyversion/setuptools_scm.py: Implement `SetuptoolsScmDriver` and `version_for_ref` used by it. * tests/test_setuptools_scm.py: Add unit/integration tests
1 parent a159667 commit 76a01cd

File tree

4 files changed

+360
-9
lines changed

4 files changed

+360
-9
lines changed

poetry.lock

Lines changed: 45 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@ classifiers = [
2525

2626

2727
[tool.poetry.dependencies]
28-
python = ">=3.8"
29-
virtualenv = { version = ">=20", optional = true }
30-
jinja2 = { version = ">=3", optional = true }
28+
python = ">=3.8"
29+
virtualenv = { version = ">=20", optional = true }
30+
jinja2 = { version = ">=3", optional = true }
31+
setuptools_scm = { version = ">=9.2.0", optional = true }
32+
packaging = { version = ">=25.0", optional = true }
3133

3234
[tool.poetry.extras]
33-
virtualenv = ["virtualenv"]
34-
jinja = ["jinja2"]
35+
virtualenv = ["virtualenv"]
36+
jinja = ["jinja2"]
37+
setuptools_scm = ["setuptools_scm", "packaging"]
3538

3639
[tool.poetry.scripts]
3740
sphinx-polyversion = "sphinx_polyversion.main:main"
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""Setuptools SCM integration for sphinx-polyversion."""
2+
3+
from __future__ import annotations
4+
5+
import dataclasses
6+
import shlex
7+
from logging import getLogger
8+
from pathlib import Path
9+
from typing import Tuple, TypeVar
10+
11+
from packaging.utils import canonicalize_name
12+
from setuptools_scm import Configuration, _get_version
13+
from setuptools_scm.git import DEFAULT_DESCRIBE
14+
15+
from sphinx_polyversion.driver import DefaultDriver
16+
from sphinx_polyversion.git import GitRef
17+
from sphinx_polyversion.pyvenv import VirtualPythonEnvironment
18+
from sphinx_polyversion.utils import to_thread
19+
20+
logger = getLogger(__name__)
21+
22+
23+
async def version_for_ref(repo_path: str | Path, ref: str) -> Tuple[str, str] | None:
24+
"""
25+
Get version that `setuptools_scm` determined for a given revision.
26+
27+
Calls `setuptools-scm` using the configuration in the `pyproject.toml`
28+
file. Alters the git describe command configured by appending
29+
the given :paramref:`ref`.
30+
31+
.. warning::
32+
33+
Only works when using git vcs.
34+
35+
.. warning::
36+
37+
Doesn't work for legacy python projects that do not use a `pyproject.toml`
38+
file.
39+
40+
41+
Parameters
42+
----------
43+
repo_path : str | Path
44+
The location of the git repository.
45+
ref : str
46+
The reference of the revision.
47+
48+
Returns
49+
-------
50+
Tuple[str, str] | None
51+
The version determined by `setuptools-scm`
52+
and the canonical distribution name, optional
53+
54+
Raises
55+
------
56+
FileNotFoundError
57+
No `pyproject.toml` file was found in the repo.
58+
59+
"""
60+
# Load project config for `setuptools-scm`
61+
repo_path = Path(repo_path)
62+
pyproject = repo_path / "pyproject.toml"
63+
if not pyproject.exists():
64+
raise FileNotFoundError(f"Could not find configuration file {pyproject}")
65+
config = Configuration.from_file(pyproject)
66+
67+
# determine distribution name
68+
if not config.dist_name:
69+
raise ValueError(
70+
f"Could not determine distribution name from {pyproject}, "
71+
"please add a [project] section with a name field"
72+
)
73+
dist_name = canonicalize_name(config.dist_name)
74+
75+
# Alter `git describe` command to use the ref
76+
cmd = config.scm.git.describe_command
77+
if cmd is None:
78+
# Use the `setuptools-scm`'s default describe command
79+
cmd = list(DEFAULT_DESCRIBE)
80+
elif isinstance(cmd, str):
81+
cmd = shlex.split(cmd)
82+
cmd = list(cmd)
83+
cmd.append(ref)
84+
85+
# remove "--dirty" if present
86+
# its incompatible with describing a specific ref
87+
if "--dirty" in cmd:
88+
cmd.remove("--dirty")
89+
90+
# Update configuration
91+
git_cfg = dataclasses.replace(config.scm.git, describe_command=cmd)
92+
scm_cfg = dataclasses.replace(config.scm, git=git_cfg)
93+
config = dataclasses.replace(config, scm=scm_cfg)
94+
95+
# Get the version (don't write any version files).
96+
version = await to_thread(_get_version, config, force_write_version_files=False)
97+
if not version:
98+
return None
99+
return (version, dist_name)
100+
101+
102+
RT = TypeVar("RT", bound=GitRef)
103+
ENV = TypeVar("ENV", bound=VirtualPythonEnvironment)
104+
S = TypeVar("S")
105+
106+
107+
class SetuptoolsScmDriver(DefaultDriver[RT, ENV, S]):
108+
"""
109+
Driver that uses `setuptools-scm` to determine the version of each revision.
110+
111+
This driver requires that the project uses `setuptools-scm` and has a
112+
`pyproject.toml` file in the root of the repository.
113+
114+
.. note::
115+
116+
Must be used with
117+
:class:`~sphinx_polyversion.git.GitRef` (thus git vcs)
118+
and subclasses of :class:`~sphinx_polyversion.pyvenv.VirtualPythonEnvironment`
119+
120+
.. note::
121+
122+
Doesn't work for legacy python projects that do not use a `pyproject.toml`
123+
file.
124+
125+
Parameters
126+
----------
127+
cwd : Path
128+
The current working directory
129+
output_dir : Path
130+
The directory where to place the built docs.
131+
vcs : VersionProvider[RT]
132+
The version provider to use.
133+
builder : Builder[ENV, Any]
134+
The builder to use.
135+
env : Callable[[Path, str], ENV]
136+
A factory producing the environments to use.
137+
data_factory : Callable[[DefaultDriver[RT, ENV, S], RT, ENV], JSONable], optional
138+
A callable returning the data to pass to the builder.
139+
root_data_factory : Callable[[DefaultDriver[RT, ENV, S]], dict[str, Any]], optional
140+
A callable returning the variables to pass to the jinja templates.
141+
namer : Callable[[RT], str], optional
142+
A callable determining the name of a revision.
143+
selector: Callable[[RT, Iterable[S]], S | Coroutine[Any, Any, S]], optional
144+
The selector to use when either `env` or `builder` are a dict.
145+
encoder : Encoder, optional
146+
The encoder to use for dumping `versions.json` to the output dir.
147+
static_dir : Path, optional
148+
The source directory for root level static files.
149+
template_dir : Path, optional
150+
The source directory for root level templates.
151+
mock : MockData[RT] | None | Literal[False], optional
152+
Only build from local files and mock building all docs using the data provided.
153+
154+
"""
155+
156+
async def init_environment(self, path: Path, rev: RT) -> ENV:
157+
"""
158+
Initialize the build environment for a revision and path.
159+
160+
The environment will be used to build the given revision and
161+
the path specifies the location where the revision is checked out.
162+
163+
This implementation calls `setuptools-scm` to determine the version
164+
for the given revision and sets the environment variable
165+
`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_<DIST_NAME>` in the returned
166+
environment.
167+
168+
Parameters
169+
----------
170+
path : Path
171+
The location of the revisions files.
172+
rev : GitRef
173+
The revision the environment is used for.
174+
175+
Returns
176+
-------
177+
VirtualPythonEnvironment
178+
179+
"""
180+
f = await super().init_environment(path, rev)
181+
182+
logger.info("Calling setuptools-scm to determine version for %s", rev.name)
183+
try:
184+
r = await version_for_ref(self.root, rev.obj)
185+
except FileNotFoundError:
186+
logger.warning(
187+
"Could not find pyproject.toml file in %s, "
188+
"skipping setuptools-scm integration",
189+
self.root,
190+
)
191+
r = None
192+
193+
if r is None:
194+
logger.warning(
195+
"Couldn't determine `setuptools-scm` version for %s", rev.name
196+
)
197+
return f
198+
199+
version, dist_name = r
200+
var_dist_name = dist_name.replace("-", "_").upper()
201+
202+
f.env.setdefault(f"SETUPTOOLS_SCM_PRETEND_VERSION_FOR_{var_dist_name}", version)
203+
return f

0 commit comments

Comments
 (0)