Store the version only in pyproject.toml
Marco Ricci

Marco Ricci commited on 2025-02-08 17:24:28
Zeige 16 geänderte Dateien mit 37 Einfügungen und 50 Löschungen.


Establish pyproject.toml functionally as the single source of truth for
the version number, so users can rely on `importlib.metadata.version` to
query the version programmatically [1, 2].  Storing the version number
in the packaging metadata makes it directly accessible to most build
tools statically, instead of dynamically, which in theory would make
certain kinds of operations (reinstall, querying the metadata from a git
checkout) much more tractable [3].  And frankly, the only reason the
version attribute previously was dynamic was incomplete tooling support.
[4, 5]

We still retain the `__version__` attribute -- we need the version
number for the command-line interface and machinery, and querying it via
the installed packages registry is unnecessarily heavyweight -- but only
on the top-level package.  Same goes for the `__author__` attribute.

In future commits, the `__version__` attribute (and version markers in
other files such as the manpages) will be auto-generated by our build
system, commit hooks, or the like.  In general, what we still need is
a system to update all mentions of a version number across the software
package [6].

Further reading:

 1. Python Packaging Authority: [Single-sourcing the Project
    Version](https://packaging.python.org/en/latest/discussions/single-source-version/).
 2. Hynek Schlawek: [Python Packaging
    Metadata](https://hynek.me/articles/packaging-metadata/).
 3. Armin Ronacher: [Constraints are Good: Python's Metadata
    Dilemma](https://lucumr.pocoo.org/2024/11/26/python-packaging-metadata/).
 4. Barry Warsaw: [`hatch version` should be able to handle static
    version numbers](https://github.com/pypa/hatch/issues/1419).
 5. Henry Schreiner: [Support PEP 621
    version?](https://github.com/callowayproject/bump-my-version/issues/66)
 6. Alex Gaynor: [Scaling Software
    Development](https://alexgaynor.net/2020/feb/18/scaling-software-development/).
    "As the scale of a codebase increases, any properties of it which
    are not programmatically enforced will tend to regress."
... ...
@@ -1,6 +1,6 @@
1
-.Dd 2025-01-11
1
+.Dd 2025-02-02
2 2
 .Dt DERIVEPASSPHRASE-EXPORT-VAULT 1
3
-.Os derivepassphrase 0.4.0
3
+.Os derivepassphrase 0.5a1.dev1
4 4
 .
5 5
 .Sh NAME
6 6
 .
... ...
@@ -1,6 +1,6 @@
1
-.Dd 2025-01-11
1
+.Dd 2025-02-02
2 2
 .Dt DERIVEPASSPHRASE-EXPORT 1
3
-.Os derivepassphrase 0.4.0
3
+.Os derivepassphrase 0.5a1.dev1
4 4
 .
5 5
 .Sh NAME
6 6
 .
... ...
@@ -1,6 +1,6 @@
1
-.Dd 2025-01-11
1
+.Dd 2025-02-02
2 2
 .Dt DERIVEPASSPHRASE-VAULT 1
3
-.Os derivepassphrase 0.4.0
3
+.Os derivepassphrase 0.5a1.dev1
4 4
 .
5 5
 .Sh NAME
6 6
 .
... ...
@@ -1,6 +1,6 @@
1
-.Dd 2025-01-11
1
+.Dd 2025-02-02
2 2
 .Dt DERIVEPASSPHRASE 1
3
-.Os derivepassphrase 0.4.0
3
+.Os derivepassphrase 0.5a1.dev1
4 4
 .
5 5
 .Sh NAME
6 6
 .
... ...
@@ -6,6 +6,7 @@ build-backend = "hatchling.build"
6 6
 name = "derivepassphrase"
7 7
 description = "An almost faithful Python reimplementation of James Coglan's vault."
8 8
 readme = "README.md"
9
+version = "0.5a1.dev1"
9 10
 requires-python = ">= 3.9"
10 11
 license = { text = "zlib/libpng" }
11 12
 keywords = []
... ...
@@ -38,7 +39,6 @@ dependencies = [
38 39
     # is unavailable in the Python standard library until Python 3.11.
39 40
     'tomli; python_version < "3.11"'
40 41
 ]
41
-dynamic = ['version']
42 42
 
43 43
 [project.optional-dependencies]
44 44
 dev = [
... ...
@@ -363,7 +363,7 @@ max-doc-length = 72  # for W505
363 363
 convention = 'google'
364 364
 
365 365
 [tool.scriv]
366
-version = "command: hatch version"
366
+version = "literal: pyproject.toml: project.version"
367 367
 format = "md"
368 368
 fragment_directory = "docs/changelog.d"
369 369
 output_file = "docs/changelog.md"
... ...
@@ -5,4 +5,9 @@
5 5
 """Work-alike of vault(1) – a deterministic, stateless password manager"""  # noqa: D415,RUF002
6 6
 
7 7
 __author__ = 'Marco Ricci <software@the13thletter.info>'
8
-__version__ = '0.4.0'
8
+__distribution_name__ = 'derivepassphrase'
9
+
10
+# Automatically generated.  DO NOT EDIT! Use importlib.metadata instead
11
+# to query the correct values.
12
+__version__ = '0.5a1.dev1'
13
+# END automatically generated.
... ...
@@ -13,7 +13,8 @@ Warning:
13 13
 
14 14
 import derivepassphrase
15 15
 
16
-__author__ = derivepassphrase.__author__
17
-__version__ = derivepassphrase.__version__
18
-
19 16
 __all__ = ()
17
+
18
+PROG_NAME = derivepassphrase.__distribution_name__
19
+VERSION = derivepassphrase.__version__
20
+AUTHOR = derivepassphrase.__author__
... ...
@@ -31,7 +31,6 @@ import click
31 31
 import click.shell_completion
32 32
 from typing_extensions import Any
33 33
 
34
-import derivepassphrase as dpp
35 34
 from derivepassphrase import _types, ssh_agent, vault
36 35
 from derivepassphrase._internals import cli_messages as _msg
37 36
 
... ...
@@ -49,9 +48,6 @@ if TYPE_CHECKING:
49 48
 
50 49
     from typing_extensions import Buffer
51 50
 
52
-__author__ = dpp.__author__
53
-__version__ = dpp.__version__
54
-
55 51
 PROG_NAME = _msg.PROG_NAME
56 52
 KEY_DISPLAY_LENGTH = 50
57 53
 
... ...
@@ -26,7 +26,7 @@ import click
26 26
 import click.shell_completion
27 27
 from typing_extensions import Any, ParamSpec, override
28 28
 
29
-import derivepassphrase as dpp
29
+from derivepassphrase import _internals
30 30
 from derivepassphrase._internals import cli_messages as _msg
31 31
 
32 32
 if TYPE_CHECKING:
... ...
@@ -37,10 +37,8 @@ if TYPE_CHECKING:
37 37
 
38 38
     from typing_extensions import Self
39 39
 
40
-__author__ = dpp.__author__
41
-__version__ = dpp.__version__
42
-
43
-PROG_NAME = _msg.PROG_NAME
40
+PROG_NAME = _internals.PROG_NAME
41
+VERSION = _internals.VERSION
44 42
 
45 43
 # Error messages
46 44
 NOT_AN_INTEGER = 'not an integer'
... ...
@@ -964,7 +962,7 @@ def version_option_callback(
964 962
                 _msg.TranslatedString(
965 963
                     _msg.Label.VERSION_INFO_TEXT,
966 964
                     PROG_NAME=PROG_NAME,
967
-                    __version__=__version__,
965
+                    VERSION=VERSION,
968 966
                 )
969 967
             ),
970 968
         )
... ...
@@ -33,19 +33,18 @@ from typing import TYPE_CHECKING, NamedTuple, Protocol, TextIO, Union, cast
33 33
 
34 34
 from typing_extensions import TypeAlias, override
35 35
 
36
-import derivepassphrase as dpp
36
+from derivepassphrase import _internals
37 37
 
38 38
 if TYPE_CHECKING:
39 39
     from collections.abc import Iterable, Iterator, Mapping, Sequence
40 40
 
41 41
     from typing_extensions import Any, Self
42 42
 
43
-__author__ = dpp.__author__
44
-__version__ = dpp.__version__
45
-
46 43
 __all__ = ('PROG_NAME',)
47 44
 
48
-PROG_NAME = 'derivepassphrase'
45
+PROG_NAME = _internals.PROG_NAME
46
+VERSION = _internals.VERSION
47
+AUTHOR = _internals.AUTHOR
49 48
 
50 49
 
51 50
 def load_translations(
... ...
@@ -1250,7 +1249,7 @@ class Label(enum.Enum):
1250 1249
         '',
1251 1250
     )(
1252 1251
         'Label :: Info Message',
1253
-        '{PROG_NAME!s} {__version__}',  # noqa: RUF027
1252
+        '{PROG_NAME!s} {VERSION}',  # noqa: RUF027
1254 1253
         flags='python-brace-format',
1255 1254
     )
1256 1255
     """"""
... ...
@@ -2226,7 +2225,7 @@ def _write_po_file(  # noqa: C901,PLR0912
2226 2225
     /,
2227 2226
     *,
2228 2227
     is_template: bool = True,
2229
-    version: str = __version__,
2228
+    version: str = VERSION,
2230 2229
 ) -> None:  # pragma: no cover
2231 2230
     r"""Write a .po file to the given file object.
2232 2231
 
... ...
@@ -2284,7 +2283,7 @@ def _write_po_file(  # noqa: C901,PLR0912
2284 2283
         header = (
2285 2284
             inspect.cleandoc(rf"""
2286 2285
             # English debug translation for {PROG_NAME!s}.
2287
-            # Copyright (C) {build_time.strftime('%Y')} {__author__}
2286
+            # Copyright (C) {build_time.strftime('%Y')} {AUTHOR!s}
2288 2287
             # This file is distributed under the same license as {PROG_NAME!s}.
2289 2288
             #
2290 2289
             msgid ""
... ...
@@ -2311,7 +2310,7 @@ def _write_po_file(  # noqa: C901,PLR0912
2311 2310
         })
2312 2311
     else:
2313 2312
         po_info.update({
2314
-            'Last-Translator': __author__,
2313
+            'Last-Translator': AUTHOR,
2315 2314
             'Language': 'en_DEBUG',
2316 2315
             'Language-Team': 'English',
2317 2316
         })
... ...
@@ -2453,7 +2452,7 @@ if __name__ == '__main__':
2453 2452
         '--set-version',
2454 2453
         action='store',
2455 2454
         dest='version',
2456
-        default=__version__,
2455
+        default=VERSION,
2457 2456
         help='Override declared software version',
2458 2457
     )
2459 2458
     args = ap.parse_args()
... ...
@@ -28,8 +28,7 @@ from typing_extensions import (
28 28
     Any,
29 29
 )
30 30
 
31
-import derivepassphrase as dpp
32
-from derivepassphrase import _types, exporter, ssh_agent, vault
31
+from derivepassphrase import _internals, _types, exporter, ssh_agent, vault
33 32
 from derivepassphrase._internals import cli_helpers, cli_machinery
34 33
 from derivepassphrase._internals import cli_messages as _msg
35 34
 
... ...
@@ -38,12 +37,10 @@ if TYPE_CHECKING:
38 37
         Sequence,
39 38
     )
40 39
 
41
-__author__ = dpp.__author__
42
-__version__ = dpp.__version__
43
-
44 40
 __all__ = ('derivepassphrase',)
45 41
 
46
-PROG_NAME = _msg.PROG_NAME
42
+PROG_NAME = _internals.PROG_NAME
43
+VERSION = _internals.VERSION
47 44
 
48 45
 
49 46
 @click.group(
... ...
@@ -11,17 +11,12 @@ import os
11 11
 import pathlib
12 12
 from typing import TYPE_CHECKING, Protocol
13 13
 
14
-import derivepassphrase as dpp
15
-
16 14
 if TYPE_CHECKING:
17 15
     from collections.abc import Callable
18 16
     from typing import Any
19 17
 
20 18
     from typing_extensions import Buffer
21 19
 
22
-__author__ = dpp.__author__
23
-__version__ = dpp.__version__
24
-
25 20
 __all__ = ()
26 21
 
27 22
 
... ...
@@ -31,7 +31,6 @@ if TYPE_CHECKING:
31 31
     from collections.abc import Iterator, Sequence
32 32
 
33 33
 __all__ = ('Sequin', 'SequinExhaustedError')
34
-__author__ = 'Marco Ricci <software@the13thletter.info>'
35 34
 
36 35
 
37 36
 class Sequin:
... ...
@@ -24,7 +24,6 @@ if TYPE_CHECKING:
24 24
     from typing_extensions import Buffer
25 25
 
26 26
 __all__ = ('SSHAgentClient',)
27
-__author__ = 'Marco Ricci <software@the13thletter.info>'
28 27
 
29 28
 # In SSH bytestrings, the "length" of the byte string is stored as
30 29
 # a 4-byte/32-bit unsigned integer at the beginning.
... ...
@@ -24,8 +24,6 @@ if TYPE_CHECKING:
24 24
 
25 25
     from typing_extensions import Buffer
26 26
 
27
-__author__ = 'Marco Ricci <software@the13thletter.info>'
28
-
29 27
 
30 28
 class Vault:
31 29
     """A work-alike of James Coglan's vault.
... ...
@@ -1616,7 +1616,7 @@ class TestCLI:
1616 1616
         assert result.clean_exit(empty_stderr=True, output=cli.PROG_NAME), (
1617 1617
             'expected clean exit, and program name in version text'
1618 1618
         )
1619
-        assert result.clean_exit(empty_stderr=True, output=cli.__version__), (
1619
+        assert result.clean_exit(empty_stderr=True, output=cli.VERSION), (
1620 1620
             'expected clean exit, and version in help text'
1621 1621
         )
1622 1622
 
1623 1623