buildstamp 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 buildstamp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ include _build_backend.py
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: buildstamp
3
+ Version: 0.3.0
4
+ Summary: Build-time version metadata injection for Python packages.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/basvandriel/buildstamp
7
+ Project-URL: Repository, https://github.com/basvandriel/buildstamp
8
+ Project-URL: Issues, https://github.com/basvandriel/buildstamp/issues
9
+ Project-URL: Changelog, https://github.com/basvandriel/buildstamp/releases
10
+ Keywords: versioning,build,metadata,pep517,setuptools
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Classifier: Topic :: Software Development :: Version Control
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: ruff>=0.9; extra == "dev"
28
+ Requires-Dist: pyright>=1.1; extra == "dev"
29
+ Requires-Dist: twine>=5; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # buildstamp
33
+
34
+ [![CI](https://github.com/basvandriel/buildstamp/actions/workflows/ci.yml/badge.svg)](https://github.com/basvandriel/buildstamp/actions/workflows/ci.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/buildstamp)](https://pypi.org/project/buildstamp/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/buildstamp)](https://pypi.org/project/buildstamp/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
38
+
39
+ Build-time version metadata injection for Python packages. Every artifact is
40
+ stamped with its version, quality tier, commit SHA, and build date — with no
41
+ metadata drift between source and installed packages, and no git required at
42
+ runtime.
43
+
44
+ ---
45
+
46
+ ## How it works
47
+
48
+ | Environment | Version source |
49
+ |---|---|
50
+ | Git checkout / editable install | Live: `git rev-parse` + `VERSION` file |
51
+ | Installed artifact (no `.git`) | Baked: `_version.json` written at build time |
52
+
53
+ The quality tier (`dev` / `rc` / `stable`) is controlled by the `RELEASE_TYPE`
54
+ environment variable at build time, so no tag ceremony is needed and no branch
55
+ names leak into artifacts.
56
+
57
+ ---
58
+
59
+ ## Quick start
60
+
61
+ **1. Add a thin backend shim** — `_build_backend.py` at your project root:
62
+
63
+ ```python
64
+ from buildstamp.backend import * # noqa: F401, F403
65
+ ```
66
+
67
+ **2. Configure `pyproject.toml`:**
68
+
69
+ ```toml
70
+ [project]
71
+ dynamic = ["version"]
72
+
73
+ [tool.setuptools.dynamic]
74
+ version = {file = "VERSION"}
75
+
76
+ [tool.setuptools.package-data]
77
+ your_package = ["_version.json"]
78
+
79
+ [build-system]
80
+ requires = ["setuptools>=64.0", "wheel", "buildstamp"]
81
+ build-backend = "_build_backend"
82
+ backend-path = ["."]
83
+ ```
84
+
85
+ **3. Add `_version.json` to `.gitignore`:**
86
+
87
+ ```
88
+ your_package/_version.json
89
+ ```
90
+
91
+ **4. Use in `your_package/__init__.py`:**
92
+
93
+ ```python
94
+ from buildstamp import load_metadata
95
+
96
+ _meta = load_metadata(__file__)
97
+ __version__ = _meta.version
98
+ __quality__ = _meta.quality
99
+ __commit__ = _meta.commit
100
+ __build_date__ = _meta.build_date
101
+ ```
102
+
103
+ **5. Create a `VERSION` file** at the project root:
104
+
105
+ ```
106
+ 1.0.0
107
+ ```
108
+
109
+ ---
110
+
111
+ ## `BuildMetadata` fields
112
+
113
+ | Field | Type | Dev (checkout) | Artifact |
114
+ |---|---|---|---|
115
+ | `version` | `str` | `"1.0.0+g701e4ca"` | `"1.0.0"` |
116
+ | `quality` | `str` | `"dev"` | `RELEASE_TYPE` value |
117
+ | `commit` | `str` | short SHA | short SHA baked at build time |
118
+ | `build_date` | `datetime \| None` | `None` | UTC datetime |
119
+
120
+ ---
121
+
122
+ ## Releasing artifacts
123
+
124
+ ```sh
125
+ # stable release
126
+ RELEASE_TYPE=stable uv build
127
+
128
+ # release candidate
129
+ RELEASE_TYPE=rc uv build
130
+ ```
131
+
132
+ If `RELEASE_TYPE` is unset, the baked quality defaults to `"dev"`.
133
+
134
+ ---
135
+
136
+ ## Installation
137
+
138
+ ```sh
139
+ pip install buildstamp
140
+ ```
141
+
142
+ While not yet on PyPI, install from source:
143
+
144
+ ```sh
145
+ uv pip install setuptools
146
+ uv pip install -e /path/to/buildstamp
147
+ uv pip install -e . --no-build-isolation
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Design rationale
153
+
154
+ See [VERSIONING.md](VERSIONING.md) for the full rationale — including why this
155
+ avoids git tags, dirty flags, and branch names as version sources.
@@ -0,0 +1,124 @@
1
+ # buildstamp
2
+
3
+ [![CI](https://github.com/basvandriel/buildstamp/actions/workflows/ci.yml/badge.svg)](https://github.com/basvandriel/buildstamp/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/buildstamp)](https://pypi.org/project/buildstamp/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/buildstamp)](https://pypi.org/project/buildstamp/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ Build-time version metadata injection for Python packages. Every artifact is
9
+ stamped with its version, quality tier, commit SHA, and build date — with no
10
+ metadata drift between source and installed packages, and no git required at
11
+ runtime.
12
+
13
+ ---
14
+
15
+ ## How it works
16
+
17
+ | Environment | Version source |
18
+ |---|---|
19
+ | Git checkout / editable install | Live: `git rev-parse` + `VERSION` file |
20
+ | Installed artifact (no `.git`) | Baked: `_version.json` written at build time |
21
+
22
+ The quality tier (`dev` / `rc` / `stable`) is controlled by the `RELEASE_TYPE`
23
+ environment variable at build time, so no tag ceremony is needed and no branch
24
+ names leak into artifacts.
25
+
26
+ ---
27
+
28
+ ## Quick start
29
+
30
+ **1. Add a thin backend shim** — `_build_backend.py` at your project root:
31
+
32
+ ```python
33
+ from buildstamp.backend import * # noqa: F401, F403
34
+ ```
35
+
36
+ **2. Configure `pyproject.toml`:**
37
+
38
+ ```toml
39
+ [project]
40
+ dynamic = ["version"]
41
+
42
+ [tool.setuptools.dynamic]
43
+ version = {file = "VERSION"}
44
+
45
+ [tool.setuptools.package-data]
46
+ your_package = ["_version.json"]
47
+
48
+ [build-system]
49
+ requires = ["setuptools>=64.0", "wheel", "buildstamp"]
50
+ build-backend = "_build_backend"
51
+ backend-path = ["."]
52
+ ```
53
+
54
+ **3. Add `_version.json` to `.gitignore`:**
55
+
56
+ ```
57
+ your_package/_version.json
58
+ ```
59
+
60
+ **4. Use in `your_package/__init__.py`:**
61
+
62
+ ```python
63
+ from buildstamp import load_metadata
64
+
65
+ _meta = load_metadata(__file__)
66
+ __version__ = _meta.version
67
+ __quality__ = _meta.quality
68
+ __commit__ = _meta.commit
69
+ __build_date__ = _meta.build_date
70
+ ```
71
+
72
+ **5. Create a `VERSION` file** at the project root:
73
+
74
+ ```
75
+ 1.0.0
76
+ ```
77
+
78
+ ---
79
+
80
+ ## `BuildMetadata` fields
81
+
82
+ | Field | Type | Dev (checkout) | Artifact |
83
+ |---|---|---|---|
84
+ | `version` | `str` | `"1.0.0+g701e4ca"` | `"1.0.0"` |
85
+ | `quality` | `str` | `"dev"` | `RELEASE_TYPE` value |
86
+ | `commit` | `str` | short SHA | short SHA baked at build time |
87
+ | `build_date` | `datetime \| None` | `None` | UTC datetime |
88
+
89
+ ---
90
+
91
+ ## Releasing artifacts
92
+
93
+ ```sh
94
+ # stable release
95
+ RELEASE_TYPE=stable uv build
96
+
97
+ # release candidate
98
+ RELEASE_TYPE=rc uv build
99
+ ```
100
+
101
+ If `RELEASE_TYPE` is unset, the baked quality defaults to `"dev"`.
102
+
103
+ ---
104
+
105
+ ## Installation
106
+
107
+ ```sh
108
+ pip install buildstamp
109
+ ```
110
+
111
+ While not yet on PyPI, install from source:
112
+
113
+ ```sh
114
+ uv pip install setuptools
115
+ uv pip install -e /path/to/buildstamp
116
+ uv pip install -e . --no-build-isolation
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Design rationale
122
+
123
+ See [VERSIONING.md](VERSIONING.md) for the full rationale — including why this
124
+ avoids git tags, dirty flags, and branch names as version sources.
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -0,0 +1 @@
1
+ from buildstamp.backend import * # noqa: F401, F403
@@ -0,0 +1,11 @@
1
+ from buildstamp._metadata import BuildMetadata, load_metadata
2
+
3
+ try:
4
+ _meta = load_metadata(__file__)
5
+ __version__ = _meta.version
6
+ except FileNotFoundError:
7
+ # No .git and no _version.json — we're being imported during a build step.
8
+ _meta = None
9
+ __version__ = "unknown"
10
+
11
+ __all__ = ["BuildMetadata", "load_metadata", "__version__"]
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class BuildMetadata:
12
+ version: str
13
+ quality: str
14
+ commit: str
15
+ build_date: datetime | None
16
+
17
+
18
+ def _run_git(*args: str) -> str:
19
+ try:
20
+ return subprocess.check_output(["git", *args], stderr=subprocess.DEVNULL, text=True).strip()
21
+ except Exception:
22
+ return "unknown"
23
+
24
+
25
+ def load_metadata(package_file: str | Path) -> BuildMetadata:
26
+ """Load version metadata for a package.
27
+
28
+ Call from your package's __init__.py:
29
+
30
+ from buildstamp import load_metadata
31
+
32
+ _meta = load_metadata(__file__)
33
+ __version__ = _meta.version
34
+ __quality__ = _meta.quality
35
+ __commit__ = _meta.commit
36
+ __build_date__ = _meta.build_date
37
+
38
+ In a git checkout (development or editable install), metadata is computed
39
+ live from git — always accurate, never stale. In an installed artifact
40
+ (no .git), it is read from _version.json baked in at build time.
41
+ """
42
+ package_dir = Path(package_file).parent
43
+ repo_root = package_dir.parent
44
+
45
+ if (repo_root / ".git").exists():
46
+ base = (repo_root / "VERSION").read_text(encoding="utf-8").strip()
47
+ sha = _run_git("rev-parse", "--short", "HEAD")
48
+ version = f"{base}+g{sha}" if sha != "unknown" else f"{base}.dev0"
49
+ return BuildMetadata(version=version, quality="dev", commit=sha, build_date=None)
50
+
51
+ meta = json.loads((package_dir / "_version.json").read_text(encoding="utf-8"))
52
+ return BuildMetadata(
53
+ version=meta["version"],
54
+ quality=meta["quality"],
55
+ commit=meta["commit"],
56
+ build_date=datetime.fromisoformat(meta["build_date"]),
57
+ )
@@ -0,0 +1,164 @@
1
+ """PEP 517 build backend that injects VCS metadata at build time.
2
+
3
+ Import all hooks from this module in your project's thin _build_backend.py:
4
+
5
+ from buildstamp.backend import * # noqa: F401, F403
6
+
7
+ Then in pyproject.toml:
8
+
9
+ [build-system]
10
+ requires = ["setuptools>=64.0", "wheel", "buildstamp"]
11
+ build-backend = "_build_backend"
12
+ backend-path = ["."]
13
+
14
+ buildstamp reads [tool.buildstamp] from pyproject.toml for configuration.
15
+ If not present, the package name is derived from [project].name.
16
+
17
+ [tool.buildstamp]
18
+ metadata-file = "your_package/_version.json" # optional override
19
+ version-file = "VERSION" # optional override (default: VERSION)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import subprocess
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+
30
+ from setuptools.build_meta import (
31
+ build_sdist as _build_sdist,
32
+ )
33
+ from setuptools.build_meta import (
34
+ build_wheel as _build_wheel,
35
+ )
36
+ from setuptools.build_meta import (
37
+ get_requires_for_build_editable,
38
+ get_requires_for_build_sdist,
39
+ get_requires_for_build_wheel,
40
+ )
41
+ from setuptools.build_meta import (
42
+ prepare_metadata_for_build_wheel as _prepare_wheel,
43
+ )
44
+
45
+ __all__ = [
46
+ "build_editable",
47
+ "build_sdist",
48
+ "build_wheel",
49
+ "get_requires_for_build_editable",
50
+ "get_requires_for_build_sdist",
51
+ "get_requires_for_build_wheel",
52
+ "prepare_metadata_for_build_editable",
53
+ "prepare_metadata_for_build_wheel",
54
+ ]
55
+
56
+
57
+ def _git(*args: str) -> str:
58
+ try:
59
+ return subprocess.check_output(["git", *args], stderr=subprocess.DEVNULL, text=True).strip()
60
+ except Exception:
61
+ return "unknown"
62
+
63
+
64
+ def _read_config() -> tuple[Path, Path]:
65
+ """Return (metadata_file, version_file) from pyproject.toml config."""
66
+ try:
67
+ try:
68
+ import tomllib
69
+ except ImportError:
70
+ import tomli as tomllib # type: ignore[no-redef]
71
+
72
+ with open("pyproject.toml", "rb") as f:
73
+ config = tomllib.load(f)
74
+ except Exception as e:
75
+ raise RuntimeError(f"buildstamp: could not read pyproject.toml — {e}") from e
76
+
77
+ bs = config.get("tool", {}).get("buildstamp", {})
78
+ version_file = Path(bs.get("version-file", "VERSION"))
79
+
80
+ if "metadata-file" in bs:
81
+ return Path(bs["metadata-file"]), version_file
82
+
83
+ # Derive from [project].name: rsync-server → rsync_server/_version.json
84
+ try:
85
+ name = config["project"]["name"].replace("-", "_")
86
+ return Path(name) / "_version.json", version_file
87
+ except KeyError as exc:
88
+ raise RuntimeError(
89
+ "buildstamp: could not determine metadata file path. "
90
+ "Add [tool.buildstamp] metadata-file = 'your_package/_version.json' "
91
+ "to pyproject.toml."
92
+ ) from exc
93
+
94
+
95
+ def write_version_file() -> None:
96
+ """Write _version.json with build-time metadata.
97
+
98
+ Called automatically by all PEP 517 hooks. Can also be called manually.
99
+ """
100
+ metadata_file, version_file = _read_config()
101
+ commit = _git("rev-parse", "--short", "HEAD")
102
+
103
+ # sdist → wheel: no .git dir, reuse the JSON baked in during the sdist step.
104
+ if commit == "unknown" and metadata_file.exists():
105
+ return
106
+
107
+ base = version_file.read_text(encoding="utf-8").strip()
108
+ raw = os.environ.get("RELEASE_TYPE", "dev").strip().lower()
109
+ if raw not in ("dev", "rc", "stable"):
110
+ raise ValueError(f"RELEASE_TYPE must be 'dev', 'rc', or 'stable' — got: {raw!r}")
111
+ if raw == "stable":
112
+ version = base
113
+ elif raw == "rc":
114
+ version = f"{base}rc1"
115
+ else:
116
+ version = f"{base}.dev0"
117
+
118
+ metadata_file.write_text(
119
+ json.dumps(
120
+ {
121
+ "version": version,
122
+ "quality": raw,
123
+ "commit": commit,
124
+ "build_date": datetime.now(timezone.utc).isoformat(),
125
+ },
126
+ indent=2,
127
+ )
128
+ + "\n",
129
+ encoding="utf-8",
130
+ )
131
+
132
+
133
+ # --- PEP 517 hooks -----------------------------------------------------------
134
+
135
+
136
+ def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
137
+ write_version_file()
138
+ return _prepare_wheel(metadata_directory, config_settings)
139
+
140
+
141
+ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
142
+ write_version_file()
143
+ return _build_wheel(wheel_directory, config_settings, metadata_directory)
144
+
145
+
146
+ def build_sdist(sdist_directory, config_settings=None):
147
+ write_version_file()
148
+ return _build_sdist(sdist_directory, config_settings)
149
+
150
+
151
+ def prepare_metadata_for_build_editable(metadata_directory, config_settings=None):
152
+ from setuptools.build_meta import (
153
+ prepare_metadata_for_build_editable as _prepare_editable,
154
+ )
155
+
156
+ write_version_file()
157
+ return _prepare_editable(metadata_directory, config_settings)
158
+
159
+
160
+ def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
161
+ from setuptools.build_meta import build_editable as _build_editable
162
+
163
+ write_version_file()
164
+ return _build_editable(wheel_directory, config_settings, metadata_directory)
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: buildstamp
3
+ Version: 0.3.0
4
+ Summary: Build-time version metadata injection for Python packages.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/basvandriel/buildstamp
7
+ Project-URL: Repository, https://github.com/basvandriel/buildstamp
8
+ Project-URL: Issues, https://github.com/basvandriel/buildstamp/issues
9
+ Project-URL: Changelog, https://github.com/basvandriel/buildstamp/releases
10
+ Keywords: versioning,build,metadata,pep517,setuptools
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Classifier: Topic :: Software Development :: Version Control
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: ruff>=0.9; extra == "dev"
28
+ Requires-Dist: pyright>=1.1; extra == "dev"
29
+ Requires-Dist: twine>=5; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # buildstamp
33
+
34
+ [![CI](https://github.com/basvandriel/buildstamp/actions/workflows/ci.yml/badge.svg)](https://github.com/basvandriel/buildstamp/actions/workflows/ci.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/buildstamp)](https://pypi.org/project/buildstamp/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/buildstamp)](https://pypi.org/project/buildstamp/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
38
+
39
+ Build-time version metadata injection for Python packages. Every artifact is
40
+ stamped with its version, quality tier, commit SHA, and build date — with no
41
+ metadata drift between source and installed packages, and no git required at
42
+ runtime.
43
+
44
+ ---
45
+
46
+ ## How it works
47
+
48
+ | Environment | Version source |
49
+ |---|---|
50
+ | Git checkout / editable install | Live: `git rev-parse` + `VERSION` file |
51
+ | Installed artifact (no `.git`) | Baked: `_version.json` written at build time |
52
+
53
+ The quality tier (`dev` / `rc` / `stable`) is controlled by the `RELEASE_TYPE`
54
+ environment variable at build time, so no tag ceremony is needed and no branch
55
+ names leak into artifacts.
56
+
57
+ ---
58
+
59
+ ## Quick start
60
+
61
+ **1. Add a thin backend shim** — `_build_backend.py` at your project root:
62
+
63
+ ```python
64
+ from buildstamp.backend import * # noqa: F401, F403
65
+ ```
66
+
67
+ **2. Configure `pyproject.toml`:**
68
+
69
+ ```toml
70
+ [project]
71
+ dynamic = ["version"]
72
+
73
+ [tool.setuptools.dynamic]
74
+ version = {file = "VERSION"}
75
+
76
+ [tool.setuptools.package-data]
77
+ your_package = ["_version.json"]
78
+
79
+ [build-system]
80
+ requires = ["setuptools>=64.0", "wheel", "buildstamp"]
81
+ build-backend = "_build_backend"
82
+ backend-path = ["."]
83
+ ```
84
+
85
+ **3. Add `_version.json` to `.gitignore`:**
86
+
87
+ ```
88
+ your_package/_version.json
89
+ ```
90
+
91
+ **4. Use in `your_package/__init__.py`:**
92
+
93
+ ```python
94
+ from buildstamp import load_metadata
95
+
96
+ _meta = load_metadata(__file__)
97
+ __version__ = _meta.version
98
+ __quality__ = _meta.quality
99
+ __commit__ = _meta.commit
100
+ __build_date__ = _meta.build_date
101
+ ```
102
+
103
+ **5. Create a `VERSION` file** at the project root:
104
+
105
+ ```
106
+ 1.0.0
107
+ ```
108
+
109
+ ---
110
+
111
+ ## `BuildMetadata` fields
112
+
113
+ | Field | Type | Dev (checkout) | Artifact |
114
+ |---|---|---|---|
115
+ | `version` | `str` | `"1.0.0+g701e4ca"` | `"1.0.0"` |
116
+ | `quality` | `str` | `"dev"` | `RELEASE_TYPE` value |
117
+ | `commit` | `str` | short SHA | short SHA baked at build time |
118
+ | `build_date` | `datetime \| None` | `None` | UTC datetime |
119
+
120
+ ---
121
+
122
+ ## Releasing artifacts
123
+
124
+ ```sh
125
+ # stable release
126
+ RELEASE_TYPE=stable uv build
127
+
128
+ # release candidate
129
+ RELEASE_TYPE=rc uv build
130
+ ```
131
+
132
+ If `RELEASE_TYPE` is unset, the baked quality defaults to `"dev"`.
133
+
134
+ ---
135
+
136
+ ## Installation
137
+
138
+ ```sh
139
+ pip install buildstamp
140
+ ```
141
+
142
+ While not yet on PyPI, install from source:
143
+
144
+ ```sh
145
+ uv pip install setuptools
146
+ uv pip install -e /path/to/buildstamp
147
+ uv pip install -e . --no-build-isolation
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Design rationale
153
+
154
+ See [VERSIONING.md](VERSIONING.md) for the full rationale — including why this
155
+ avoids git tags, dirty flags, and branch names as version sources.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ VERSION
5
+ _build_backend.py
6
+ pyproject.toml
7
+ buildstamp/__init__.py
8
+ buildstamp/_metadata.py
9
+ buildstamp/backend.py
10
+ buildstamp.egg-info/PKG-INFO
11
+ buildstamp.egg-info/SOURCES.txt
12
+ buildstamp.egg-info/dependency_links.txt
13
+ buildstamp.egg-info/requires.txt
14
+ buildstamp.egg-info/top_level.txt
15
+ tests/test_metadata.py
@@ -0,0 +1,9 @@
1
+
2
+ [:python_version < "3.11"]
3
+ tomli>=2.0
4
+
5
+ [dev]
6
+ pytest>=8
7
+ ruff>=0.9
8
+ pyright>=1.1
9
+ twine>=5
@@ -0,0 +1 @@
1
+ buildstamp
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "buildstamp"
3
+ dynamic = ["version"]
4
+ description = "Build-time version metadata injection for Python packages."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ keywords = ["versioning", "build", "metadata", "pep517", "setuptools"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Intended Audience :: Developers",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.10",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Topic :: Software Development :: Build Tools",
18
+ "Topic :: Software Development :: Version Control",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "tomli>=2.0; python_version < '3.11'",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=8",
28
+ "ruff>=0.9",
29
+ "pyright>=1.1",
30
+ "twine>=5",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/basvandriel/buildstamp"
35
+ Repository = "https://github.com/basvandriel/buildstamp"
36
+ Issues = "https://github.com/basvandriel/buildstamp/issues"
37
+ Changelog = "https://github.com/basvandriel/buildstamp/releases"
38
+
39
+ [build-system]
40
+ requires = ["setuptools>=64.0"]
41
+ build-backend = "_build_backend"
42
+ backend-path = ["."]
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["."]
46
+ include = ["buildstamp*"]
47
+
48
+ [tool.setuptools.dynamic]
49
+ version = {file = "VERSION"}
50
+
51
+ [tool.ruff]
52
+ line-length = 100
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "W", "I", "UP", "B", "SIM"]
56
+ ignore = ["E501"]
57
+
58
+ [tool.pyright]
59
+ pythonVersion = "3.10"
60
+ strict = true
61
+ include = ["buildstamp"]
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+
66
+ [dependency-groups]
67
+ dev = [
68
+ "setuptools>=82.0.1",
69
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,120 @@
1
+ """Tests for buildstamp._metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from unittest.mock import patch
9
+
10
+ import pytest
11
+
12
+ from buildstamp._metadata import BuildMetadata, load_metadata
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # BuildMetadata
16
+ # ---------------------------------------------------------------------------
17
+
18
+
19
+ def test_build_metadata_is_frozen() -> None:
20
+ meta = BuildMetadata(version="1.0.0", quality="dev", commit="abc1234", build_date=None)
21
+ with pytest.raises((AttributeError, TypeError)):
22
+ meta.version = "2.0.0" # type: ignore[misc]
23
+
24
+
25
+ def test_build_metadata_slots() -> None:
26
+ meta = BuildMetadata(version="1.0.0", quality="dev", commit="abc1234", build_date=None)
27
+ assert not hasattr(meta, "__dict__")
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # load_metadata — git checkout path
32
+ # ---------------------------------------------------------------------------
33
+
34
+
35
+ def _make_pkg(root: Path, version: str = "1.2.3") -> Path:
36
+ """Create a minimal package tree under root and return the __init__.py path."""
37
+ pkg = root / "mypkg"
38
+ pkg.mkdir()
39
+ (pkg / "__init__.py").write_text("")
40
+ (root / "VERSION").write_text(f"{version}\n")
41
+ return pkg / "__init__.py"
42
+
43
+
44
+ def test_load_metadata_git_checkout(tmp_path: Path) -> None:
45
+ (tmp_path / ".git").mkdir()
46
+ init = _make_pkg(tmp_path)
47
+
48
+ with patch("buildstamp._metadata._run_git", return_value="abc1234"):
49
+ meta = load_metadata(init)
50
+
51
+ assert meta.version == "1.2.3+gabc1234"
52
+ assert meta.quality == "dev"
53
+ assert meta.commit == "abc1234"
54
+ assert meta.build_date is None
55
+
56
+
57
+ def test_load_metadata_git_unknown_sha(tmp_path: Path) -> None:
58
+ """When git is not available, version should fall back to .dev0."""
59
+ (tmp_path / ".git").mkdir()
60
+ init = _make_pkg(tmp_path)
61
+
62
+ with patch("buildstamp._metadata._run_git", return_value="unknown"):
63
+ meta = load_metadata(init)
64
+
65
+ assert meta.version == "1.2.3.dev0"
66
+ assert meta.commit == "unknown"
67
+ assert meta.build_date is None
68
+
69
+
70
+ def test_load_metadata_git_uses_version_file(tmp_path: Path) -> None:
71
+ (tmp_path / ".git").mkdir()
72
+ init = _make_pkg(tmp_path, version="2.0.0")
73
+
74
+ with patch("buildstamp._metadata._run_git", return_value="what"):
75
+ meta = load_metadata(init)
76
+
77
+ assert meta.version.startswith("2.0.0+g")
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # load_metadata — artifact / installed package path
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ def test_load_metadata_from_version_json(tmp_path: Path) -> None:
86
+ """Without .git, metadata is read from _version.json."""
87
+ pkg = tmp_path / "mypkg"
88
+ pkg.mkdir()
89
+ init = pkg / "__init__.py"
90
+ init.write_text("")
91
+
92
+ build_date = "2026-04-10T12:00:00+00:00"
93
+ (pkg / "_version.json").write_text(
94
+ json.dumps(
95
+ {
96
+ "version": "1.2.3",
97
+ "quality": "stable",
98
+ "commit": "abc1234",
99
+ "build_date": build_date,
100
+ }
101
+ )
102
+ )
103
+
104
+ meta = load_metadata(init)
105
+
106
+ assert meta.version == "1.2.3"
107
+ assert meta.quality == "stable"
108
+ assert meta.commit == "abc1234"
109
+ assert meta.build_date == datetime.fromisoformat(build_date)
110
+
111
+
112
+ def test_load_metadata_missing_json_raises(tmp_path: Path) -> None:
113
+ pkg = tmp_path / "mypkg"
114
+ pkg.mkdir()
115
+ init = pkg / "__init__.py"
116
+ init.write_text("")
117
+ # no _version.json, no .git
118
+
119
+ with pytest.raises((FileNotFoundError, OSError)):
120
+ load_metadata(init)