From dcec38fb0a0c7288b2d9d90ab61dfec37ebfff6e Mon Sep 17 00:00:00 2001 From: Tom Most Date: Wed, 10 Jul 2024 13:43:10 -0700 Subject: [PATCH 1/9] Track whether [tool.incremental] was found As the Hatch plugin is opt-in via some pyproject.toml noise I don't want to require a redundant [tool.incremental] section. --- src/incremental/__init__.py | 45 ++++++--- src/incremental/tests/test_pyproject.py | 118 ++++++++++++++---------- 2 files changed, 100 insertions(+), 63 deletions(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index 9e89b8a..d6bdc95 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -400,7 +400,7 @@ def _get_setuptools_version(dist): # type: (_Distribution) -> None [1]: https://setuptools.pypa.io/en/latest/userguide/extension.html#customizing-distribution-options """ config = _load_pyproject_toml("./pyproject.toml") - if not config: + if not config or not config.has_tool_incremental: return dist.metadata.version = _existing_version(config.path).public() @@ -448,20 +448,26 @@ def _load_toml(f): # type: (BinaryIO) -> Any @dataclass(frozen=True) class _IncrementalConfig: """ - @ivar package: The package name, capitalized as in the package metadata. + Configuration loaded from a ``pyproject.toml`` file. + """ - @ivar path: Path to the package root + has_tool_incremental: bool + """ + Does the pyproject.toml file contain a [tool.incremental] + section? This indicates that the package has explicitly + opted-in to Incremental versioning. """ package: str + """The package name, capitalized as in the package metadata.""" + path: str + """Path to the package root""" def _load_pyproject_toml(toml_path): # type: (str) -> Optional[_IncrementalConfig] """ - Does the pyproject.toml file contain a [tool.incremental] - section? This indicates that the package has opted-in to Incremental - versioning. + Load Incremental configuration from a ``pyproject.toml`` If the [tool.incremental] section is empty we take the project name from the [project] section. Otherwise we require only a C{name} key @@ -474,16 +480,9 @@ def _load_pyproject_toml(toml_path): # type: (str) -> Optional[_IncrementalConf except FileNotFoundError: return None - if "tool" not in data: - return None - if "incremental" not in data["tool"]: - return None + tool_incremental = _extract_tool_incremental(data) - tool_incremental = data["tool"]["incremental"] - if not isinstance(tool_incremental, dict): - raise ValueError("[tool.incremental] must be a table") - - if tool_incremental == {}: + if tool_incremental is None or tool_incremental == {}: try: package = data["project"]["name"] except KeyError: @@ -509,11 +508,27 @@ def _load_pyproject_toml(toml_path): # type: (str) -> Optional[_IncrementalConf ) return _IncrementalConfig( + has_tool_incremental=tool_incremental is not None, package=package, path=_findPath(os.path.dirname(toml_path), package), ) +def _extract_tool_incremental(data): # type: (Dict[str, object]) -> Optional[Dict[str, object]] + if "tool" not in data: + return None + if not isinstance(data["tool"], dict): + raise ValueError("[tool] must be a table") + if "incremental" not in data["tool"]: + return None + + tool_incremental = data["tool"]["incremental"] + if not isinstance(tool_incremental, dict): + raise ValueError("[tool.incremental] must be a table") + + return tool_incremental + + from ._version import __version__ # noqa: E402 diff --git a/src/incremental/tests/test_pyproject.py b/src/incremental/tests/test_pyproject.py index de3b269..0d41194 100644 --- a/src/incremental/tests/test_pyproject.py +++ b/src/incremental/tests/test_pyproject.py @@ -4,6 +4,8 @@ """Test handling of ``pyproject.toml`` configuration""" import os +from typing import Optional +from pathlib import Path from twisted.trial.unittest import TestCase from incremental import _load_pyproject_toml, _IncrementalConfig @@ -12,106 +14,126 @@ class VerifyPyprojectDotTomlTests(TestCase): """Test the `_load_pyproject_toml` helper function""" - def test_fileNotFound(self): + def _loadToml( + self, toml: str, *, path: Optional[os.PathLike] = None + ) -> Optional[_IncrementalConfig]: """ - Verification fails when no ``pyproject.toml`` file exists. + Read a TOML snipped from a temporary file with `_load_pyproject_toml` + + @param toml: TOML content of the temporary file + + @param path: Path to which the TOML is written """ - path = os.path.join(self.mktemp(), "pyproject.toml") - self.assertFalse(_load_pyproject_toml(path)) + path = path or self.mktemp() - def test_noToolIncrementalSection(self): + with open(path, "w") as f: + f.write(toml) + + try: + return _load_pyproject_toml(path) + except Exception as e: + if hasattr(e, "add_note"): + e.add_note(f"While loading:\n\n{toml}") # pragma: no coverage + raise + + def test_fileNotFound(self): """ - Verification fails when there isn't a ``[tool.incremental]`` section. + An absent ``pyproject.toml`` file produces no result """ - path = self.mktemp() - for toml in [ - "\n", - "[tool]\n", - "[tool.notincremental]\n", - '[project]\nname = "foo"\n', - ]: - with open(path, "w") as f: - f.write(toml) - self.assertIsNone(_load_pyproject_toml(path)) + path = os.path.join(self.mktemp(), "pyproject.toml") + self.assertIsNone(_load_pyproject_toml(path)) def test_nameMissing(self): """ `ValueError` is raised when ``[tool.incremental]`` is present but he project name isn't available. """ - path = self.mktemp() for toml in [ + "\n", + "[tool.notincremental]\n", "[tool.incremental]\n", "[project]\n[tool.incremental]\n", ]: - with open(path, "w") as f: - f.write(toml) - - self.assertRaises(ValueError, _load_pyproject_toml, path) + self.assertRaises(ValueError, self._loadToml, toml) def test_nameInvalid(self): """ `TypeError` is raised when the project name isn't a string. """ - path = self.mktemp() for toml in [ "[tool.incremental]\nname = -1\n", "[tool.incremental]\n[project]\nname = 1.0\n", ]: - with open(path, "w") as f: - f.write(toml) - - self.assertRaises(TypeError, _load_pyproject_toml, path) + self.assertRaises(TypeError, self._loadToml, toml) def test_toolIncrementalInvalid(self): """ - `ValueError` is raised when the ``[tool.incremental]`` section isn't - a dict. + `ValueError` is raised when the ``[tool]`` or ``[tool.incremental]`` + isn't a table. """ - path = self.mktemp() for toml in [ + "tool = false\n", "[tool]\nincremental = false\n", "[tool]\nincremental = 123\n", "[tool]\nincremental = null\n", ]: - with open(path, "w") as f: - f.write(toml) - - self.assertRaises(ValueError, _load_pyproject_toml, path) + self.assertRaises(ValueError, self._loadToml, toml) def test_toolIncrementalUnexpecteKeys(self): """ Raise `ValueError` when the ``[tool.incremental]`` section contains keys other than ``"name"`` """ - path = self.mktemp() for toml in [ "[tool.incremental]\nfoo = false\n", '[tool.incremental]\nname = "OK"\nother = false\n', ]: - with open(path, "w") as f: - f.write(toml) - - self.assertRaises(ValueError, _load_pyproject_toml, path) + self.assertRaises(ValueError, self._loadToml, toml) - def test_ok(self): + def test_setuptoolsOptIn(self): """ The package has opted-in to Incremental version management when - the ``[tool.incremental]`` section is an empty dict. + the ``[tool.incremental]`` section is a dict. The project name + is taken from ``[tool.incremental] name`` or ``[project] name``. """ - root = self.mktemp() - path = os.path.join(root, "src", "foo") - os.makedirs(path) - toml_path = os.path.join(root, "pyproject.toml") + root = Path(self.mktemp()) + pkg = root / "src" / "foo" + pkg.mkdir(parents=True) for toml in [ '[project]\nname = "Foo"\n[tool.incremental]\n', '[tool.incremental]\nname = "Foo"\n', ]: - with open(toml_path, "w") as f: - f.write(toml) + config = self._loadToml(toml, path=root / "pyproject.toml") self.assertEqual( - _load_pyproject_toml(toml_path), - _IncrementalConfig(package="Foo", path=path), + config, + _IncrementalConfig( + has_tool_incremental=True, + package="Foo", + path=str(pkg), + ), ) + + def test_noToolIncrementalSection(self): + """ + The ``has_tool_incremental`` flag is false when there + isn't a ``[tool.incremental]`` section. + """ + root = Path(self.mktemp()) + pkg = root / "foo" + pkg.mkdir(parents=True) + + config = self._loadToml( + '[project]\nname = "foo"\n', + path=root / "pyproject.toml", + ) + + self.assertEqual( + config, + _IncrementalConfig( + has_tool_incremental=False, + package="foo", + path=str(pkg), + ), + ) From f964e04a4f6ec3d69141f64d5f7e4d1b95a2f918 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 20 Jul 2024 14:26:52 -0700 Subject: [PATCH 2/9] Hatch version source plugin --- pyproject.toml | 3 ++ src/incremental/_hatch.py | 35 +++++++++++++++++++ .../example_hatchling/__init__.py | 3 ++ .../example_hatchling/_version.py | 11 ++++++ tests/example_hatchling/pyproject.toml | 16 +++++++++ tests/test_examples.py | 17 +++++++++ 6 files changed, 85 insertions(+) create mode 100644 src/incremental/_hatch.py create mode 100644 tests/example_hatchling/example_hatchling/__init__.py create mode 100644 tests/example_hatchling/example_hatchling/_version.py create mode 100644 tests/example_hatchling/pyproject.toml diff --git a/pyproject.toml b/pyproject.toml index d80bd8d..7da6010 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ maintainers = [ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", + "Framework :: Hatch", "Framework :: Setuptools Plugin", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", @@ -50,6 +51,8 @@ Changelog = "https://github.com/twisted/incremental/blob/trunk/NEWS.rst" use_incremental = "incremental:_get_distutils_version" [project.entry-points."setuptools.finalize_distribution_options"] incremental = "incremental:_get_setuptools_version" +[project.entry-points.hatch] +incremental = "incremental._hatch" [tool.incremental] diff --git a/src/incremental/_hatch.py b/src/incremental/_hatch.py new file mode 100644 index 0000000..2f3f2d9 --- /dev/null +++ b/src/incremental/_hatch.py @@ -0,0 +1,35 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +import os +import shlex +from typing import TypedDict + +from hatchling.version.source.plugin.interface import VersionSourceInterface +from hatchling.plugin import hookimpl + +from incremental import _load_pyproject_toml, _existing_version + + +class _VersionData(TypedDict): + version: str + + +class IncrementalVersionSource(VersionSourceInterface): + PLUGIN_NAME = "incremental" + + def get_version_data(self) -> _VersionData: + config = _load_pyproject_toml(os.path.join(self.root, "./pyproject.toml")) + return {"version": _existing_version(config.path).public()} + + def set_version(self, version: str, version_data: dict): + raise NotImplementedError( + f"Run `python -m incremental.version --newversion" + f" {shlex.quote(version)}` to set the version.\n\n" + f" See `python -m incremental.version --help` for more options." + ) + + +@hookimpl +def hatch_register_version_source(): + return [IncrementalVersionSource] diff --git a/tests/example_hatchling/example_hatchling/__init__.py b/tests/example_hatchling/example_hatchling/__init__.py new file mode 100644 index 0000000..26d23ba --- /dev/null +++ b/tests/example_hatchling/example_hatchling/__init__.py @@ -0,0 +1,3 @@ +from ._version import __version__ + +__all__ = ["__version__"] diff --git a/tests/example_hatchling/example_hatchling/_version.py b/tests/example_hatchling/example_hatchling/_version.py new file mode 100644 index 0000000..6f1e6b0 --- /dev/null +++ b/tests/example_hatchling/example_hatchling/_version.py @@ -0,0 +1,11 @@ +""" +Provides example_hatchling version information. +""" + +# This file is auto-generated! Do not edit! +# Use `python -m incremental.update example_hatchling` to change this file. + +from incremental import Version + +__version__ = Version("example_hatchling", 24, 7, 0) +__all__ = ["__version__"] diff --git a/tests/example_hatchling/pyproject.toml b/tests/example_hatchling/pyproject.toml new file mode 100644 index 0000000..fa01745 --- /dev/null +++ b/tests/example_hatchling/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = [ + "hatchling", + "incremental", +] +build-backend = "hatchling.build" + +[project] +name = "example_hatchling" +dependencies = [ + "incremental", +] +dynamic = ["version"] + +[tool.hatch.version] +source = "incremental" diff --git a/tests/test_examples.py b/tests/test_examples.py index 4004850..118b7c0 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -13,6 +13,8 @@ from twisted.python.filepath import FilePath from twisted.trial.unittest import TestCase +from incremental import Version + TEST_DIR = FilePath(os.path.abspath(os.path.dirname(__file__))) @@ -47,3 +49,18 @@ def test_setuptools_version(self): self.assertEqual(example_setuptools.__version__.base(), "2.3.4") self.assertEqual(metadata.version("example_setuptools"), "2.3.4") + + def test_hatchling_get_version(self): + """ + example_hatchling has a version of 24.7.0, which may be retrieved + by the ``hatch version`` command. + """ + build_and_install(TEST_DIR.child("example_hatchling")) + + import example_hatchling + + self.assertEqual( + example_hatchling.__version__, + Version("example_hatchling", 24, 7, 0), + ) + self.assertEqual(metadata.version("example_hatchling"), "24.7.0") From feb24b33598b31e31969bf2ce29887d0bc63c0e2 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 20 Jul 2024 14:29:18 -0700 Subject: [PATCH 3/9] Pin test dependencies --- pyproject.toml | 3 - requirements_mypy.in | 2 + requirements_mypy.txt | 39 +++++++++++ requirements_tests.in | 5 ++ requirements_tests.txt | 153 +++++++++++++++++++++++++++++++++++++++++ tox.ini | 14 ++-- 6 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 requirements_mypy.in create mode 100644 requirements_mypy.txt create mode 100644 requirements_tests.in create mode 100644 requirements_tests.txt diff --git a/pyproject.toml b/pyproject.toml index 7da6010..193e54a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,6 @@ dependencies = [ scripts = [ "click>=6.0", ] -mypy = [ - "mypy==0.812", -] [project.urls] Homepage = "https://github.com/twisted/incremental" diff --git a/requirements_mypy.in b/requirements_mypy.in new file mode 100644 index 0000000..dc49e89 --- /dev/null +++ b/requirements_mypy.in @@ -0,0 +1,2 @@ +mypy==0.812 +twisted diff --git a/requirements_mypy.txt b/requirements_mypy.txt new file mode 100644 index 0000000..dbf10d5 --- /dev/null +++ b/requirements_mypy.txt @@ -0,0 +1,39 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --output-file=requirements_mypy.txt requirements_mypy.in +# +attrs==23.2.0 + # via + # automat + # twisted +automat==22.10.0 + # via twisted +constantly==23.10.4 + # via twisted +hyperlink==21.0.0 + # via twisted +idna==3.7 + # via hyperlink +incremental==22.10.0 + # via twisted +mypy==0.812 + # via -r requirements_mypy.in +mypy-extensions==0.4.4 + # via mypy +six==1.16.0 + # via automat +twisted==24.3.0 + # via -r requirements_mypy.in +typed-ast==1.4.3 + # via mypy +typing-extensions==4.12.2 + # via + # mypy + # twisted +zope-interface==6.4.post2 + # via twisted + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements_tests.in b/requirements_tests.in new file mode 100644 index 0000000..8e00e35 --- /dev/null +++ b/requirements_tests.in @@ -0,0 +1,5 @@ +build +coverage +coverage-p +twisted +hatch diff --git a/requirements_tests.txt b/requirements_tests.txt new file mode 100644 index 0000000..7d812d6 --- /dev/null +++ b/requirements_tests.txt @@ -0,0 +1,153 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --output-file=requirements_tests.txt requirements_tests.in +# +anyio==4.4.0 + # via httpx +attrs==23.2.0 + # via + # automat + # twisted +automat==22.10.0 + # via twisted +backports-tarfile==1.2.0 + # via jaraco-context +build==1.2.1 + # via -r requirements_tests.in +certifi==2024.7.4 + # via + # httpcore + # httpx +cffi==1.16.0 + # via cryptography +click==8.1.7 + # via + # hatch + # userpath +constantly==23.10.4 + # via twisted +coverage==7.5.4 + # via + # -r requirements_tests.in + # coverage-p +coverage-p==24.7.0 + # via -r requirements_tests.in +cryptography==42.0.8 + # via secretstorage +distlib==0.3.8 + # via virtualenv +exceptiongroup==1.2.1 + # via anyio +filelock==3.15.4 + # via virtualenv +h11==0.14.0 + # via httpcore +hatch==1.12.0 + # via -r requirements_tests.in +hatchling==1.25.0 + # via hatch +httpcore==1.0.5 + # via httpx +httpx==0.27.0 + # via hatch +hyperlink==21.0.0 + # via + # hatch + # twisted +idna==3.7 + # via + # anyio + # httpx + # hyperlink +importlib-metadata==8.0.0 + # via keyring +incremental==22.10.0 + # via twisted +jaraco-classes==3.4.0 + # via keyring +jaraco-context==5.3.0 + # via keyring +jaraco-functools==4.0.1 + # via keyring +jeepney==0.8.0 + # via + # keyring + # secretstorage +keyring==25.2.1 + # via hatch +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +more-itertools==10.3.0 + # via + # jaraco-classes + # jaraco-functools +packaging==24.1 + # via + # build + # hatch + # hatchling +pathspec==0.12.1 + # via hatchling +pexpect==4.9.0 + # via hatch +platformdirs==4.2.2 + # via + # hatch + # virtualenv +pluggy==1.5.0 + # via hatchling +ptyprocess==0.7.0 + # via pexpect +pycparser==2.22 + # via cffi +pygments==2.18.0 + # via rich +pyproject-hooks==1.1.0 + # via build +rich==13.7.1 + # via hatch +secretstorage==3.3.3 + # via keyring +shellingham==1.5.4 + # via hatch +six==1.16.0 + # via automat +sniffio==1.3.1 + # via + # anyio + # httpx +tomli==2.0.1 + # via + # build + # hatchling +tomli-w==1.0.0 + # via hatch +tomlkit==0.13.0 + # via hatch +trove-classifiers==2024.7.2 + # via hatchling +twisted==24.3.0 + # via -r requirements_tests.in +typing-extensions==4.12.2 + # via + # anyio + # twisted +userpath==1.9.2 + # via hatch +uv==0.2.24 + # via hatch +virtualenv==20.26.3 + # via hatch +zipp==3.19.2 + # via importlib-metadata +zope-interface==6.4.post2 + # via twisted +zstandard==0.22.0 + # via hatch + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/tox.ini b/tox.ini index 948d0fb..c1d2fc7 100644 --- a/tox.ini +++ b/tox.ini @@ -14,15 +14,16 @@ envlist = [testenv] wheel = true wheel_build_env = build +skip_install = + pindeps: true deps = - tests: build - tests: coverage - tests: coverage-p - tests,mypy: twisted + tests: -rrequirements_tests.txt + mypy: -rrequirements_mypy.txt apidocs: pydoctor lint: pre-commit + pindeps: pip-tools extras = - mypy: mypy,scripts + mypy: scripts tests: scripts setenv = @@ -52,6 +53,9 @@ commands = mypy: mypy src + pindeps: pip-compile -o requirements_tests.txt requirements_tests.in {posargs} + pindeps: pip-compile -o requirements_mypy.txt requirements_mypy.in {posargs} + [testenv:build] # empty environment to build universal wheel once per tox invocation From c4d1a4afdcb706b77167bd4bed58b3cc68b5f0bc Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 20 Jul 2024 14:32:26 -0700 Subject: [PATCH 4/9] Add newsfragment --- src/incremental/newsfragments/93.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/incremental/newsfragments/93.feature diff --git a/src/incremental/newsfragments/93.feature b/src/incremental/newsfragments/93.feature new file mode 100644 index 0000000..10e5873 --- /dev/null +++ b/src/incremental/newsfragments/93.feature @@ -0,0 +1 @@ +Incremental now provides a `Hatch version source plugin `__. From 4971be7a7b9260e7b8e88b25d452a652173d51b9 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 20 Jul 2024 14:57:55 -0700 Subject: [PATCH 5/9] Update documentation --- README.rst | 95 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/README.rst b/README.rst index 6f5c925..394e504 100644 --- a/README.rst +++ b/README.rst @@ -9,55 +9,75 @@ Incremental is a small library that versions your Python projects. API documentation can be found `here `_. +.. contents:: Quick Start ----------- -In your ``pyproject.toml``, add Incremental to your build requirements: +Using setuptools +~~~~~~~~~~~~~~~~ + +Add Incremental to your ``pyproject.toml``: .. code-block:: toml [build-system] - requires = ["setuptools", "incremental>=NEXT"] + requires = [ + "setuptools", + "incremental>=NEXT", # ← Add incremental as a build dependency + ] build-backend = "setuptools.build_meta" -Specify the project's version as dynamic: - -.. code-block:: toml - [project] name = "" - dynamic = ["version"] + dynamic = ["version"] # ← Mark the version dynamic + dependencies = [ + "incremental>=NEXT", # ← Depend on incremental at runtime + ] + # ... -Remove any ``version`` line and any ``[tool.setuptools.dynamic] version = `` entry. + [tool.incremental] # ← Activate Incremental's setuptools plugin -Add this empty block to activate Incremental's setuptools plugin: -.. code-block:: toml +It's fine if the ``[tool.incremental]`` table is empty, but it must be present. - [tool.incremental] +Remove any ``[project] version =`` entry and any ``[tool.setuptools.dynamic] version =`` entry. -Install Incremental to your local environment with ``pip install incremental[scripts]``. -Then run ``python -m incremental.update --create``. -It will create a file in your package named ``_version.py`` and look like this: +Next, `initialize the project`_. -.. code:: python +Using Hatch +~~~~~~~~~~~ - from incremental import Version +If you're using `Hatch `_ to package your project, +activate Incremental's Hatch plugin by altering your ``pyproject.toml``: - __version__ = Version("", 24, 1, 0) - __all__ = ["__version__"] +.. code:: toml + [build-system] + requires = [ + "hatchling", + "incremental>=NEXT", # ← Add incremental as a build dependency + ] + build-backend = "hatchling.build" -Then, so users of your project can find your version, in your root package's ``__init__.py`` add: + [project] + name = "" + dynamic = ["version"] # ← Mark the version dynamic + dependencies = [ + "incremental>=NEXT", # ← Depend on incremental at runtime + ] + # ... -.. code:: python + [tool.hatch.version] + source = "incremental" # ← Activate Incremental's Hatch plugin - from ._version import __version__ +The ``hatch version`` command will report the Incremental-managed version. +Use the ``python -m incremental.update`` command to change the version (setting it with ``hatch version`` is not supported). -Subsequent installations of your project will then use Incremental for versioning. +Incremental can be configured as usual in a ``[tool.incremental]`` table. +Next, `initialize the project`_. Using ``setup.py`` ~~~~~~~~~~~~~~~~~~ @@ -74,7 +94,34 @@ Add this to your ``setup()`` call, removing any other versioning arguments: ... } -Then proceed with the ``incremental.update`` command above. +Then `initialize the project`_. + + +Initialize the project +~~~~~~~~~~~~~~~~~~~~~~ + +Install Incremental to your local environment with ``pip install incremental[scripts]``. +Then run ``python -m incremental.update --create``. +It will create a file in your package named ``_version.py`` like this: + +.. code:: python + + from incremental import Version + + __version__ = Version("", 24, 1, 0) + __all__ = ["__version__"] + + +Then, so users of your project can find your version, in your root package's ``__init__.py`` add: + +.. code:: python + + from ._version import __version__ + + +Subsequent installations of your project will then use Incremental for versioning. + + Incremental Versions -------------------- @@ -90,7 +137,7 @@ It is made up of the following elements (which are given during instantiation): You can extract a PEP-440 compatible version string by using the ``.public()`` method, which returns a ``str`` containing the full version. This is the version you should provide to users, or publicly use. An example output would be ``"13.2.0"``, ``"17.1.2dev1"``, or ``"18.8.0rc2"``. -Calling ``repr()`` with a ``Version`` will give a Python-source-code representation of it, and calling ``str()`` with a ``Version`` will provide a string similar to ``'[Incremental, version 16.10.1]'``. +Calling ``repr()`` with a ``Version`` will give a Python-source-code representation of it, and calling ``str()`` on a ``Version`` produces a string like ``'[Incremental, version 16.10.1]'``. Updating From 66aea8d1da2990f42eeb6a48cb620caac37d19aa Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 20 Jul 2024 15:15:34 -0700 Subject: [PATCH 6/9] Call it "Hatchling" The PyPA packaging user guide refers to it as "Hatchling", so match that terminology. --- README.rst | 15 +++++++-------- src/incremental/newsfragments/93.feature | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 394e504..e8c0126 100644 --- a/README.rst +++ b/README.rst @@ -45,11 +45,11 @@ Remove any ``[project] version =`` entry and any ``[tool.setuptools.dynamic] ver Next, `initialize the project`_. -Using Hatch -~~~~~~~~~~~ +Using Hatchling +~~~~~~~~~~~~~~~ -If you're using `Hatch `_ to package your project, -activate Incremental's Hatch plugin by altering your ``pyproject.toml``: +If you're using `Hatchling `_ to package your project, +activate Incremental's Hatchling plugin by altering your ``pyproject.toml``: .. code:: toml @@ -69,14 +69,13 @@ activate Incremental's Hatch plugin by altering your ``pyproject.toml``: # ... [tool.hatch.version] - source = "incremental" # ← Activate Incremental's Hatch plugin + source = "incremental" # ← Activate Incremental's Hatchling plugin -The ``hatch version`` command will report the Incremental-managed version. +Incremental can be configured as usual in an optional ``[tool.incremental]`` table. +The ``hatch version`` command will report the Incremental-managed version. Use the ``python -m incremental.update`` command to change the version (setting it with ``hatch version`` is not supported). -Incremental can be configured as usual in a ``[tool.incremental]`` table. - Next, `initialize the project`_. Using ``setup.py`` diff --git a/src/incremental/newsfragments/93.feature b/src/incremental/newsfragments/93.feature index 10e5873..9203306 100644 --- a/src/incremental/newsfragments/93.feature +++ b/src/incremental/newsfragments/93.feature @@ -1 +1 @@ -Incremental now provides a `Hatch version source plugin `__. +Incremental now provides a `Hatchling version source plugin `_. From e27a4a7bc575bf707fe54ccc46b460faada8d3d5 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 20 Jul 2024 15:40:19 -0700 Subject: [PATCH 7/9] Address MyPy issues --- requirements_mypy.in | 1 + requirements_mypy.txt | 12 ++++++++++++ src/incremental/_hatch.py | 12 +++++++----- src/incremental/tests/test_pyproject.py | 20 +++++++++++++------- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/requirements_mypy.in b/requirements_mypy.in index dc49e89..1c3f8f8 100644 --- a/requirements_mypy.in +++ b/requirements_mypy.in @@ -1,2 +1,3 @@ mypy==0.812 twisted +hatchling # for types diff --git a/requirements_mypy.txt b/requirements_mypy.txt index dbf10d5..9228c07 100644 --- a/requirements_mypy.txt +++ b/requirements_mypy.txt @@ -12,6 +12,8 @@ automat==22.10.0 # via twisted constantly==23.10.4 # via twisted +hatchling==1.25.0 + # via -r requirements_mypy.in hyperlink==21.0.0 # via twisted idna==3.7 @@ -22,8 +24,18 @@ mypy==0.812 # via -r requirements_mypy.in mypy-extensions==0.4.4 # via mypy +packaging==24.1 + # via hatchling +pathspec==0.12.1 + # via hatchling +pluggy==1.5.0 + # via hatchling six==1.16.0 # via automat +tomli==2.0.1 + # via hatchling +trove-classifiers==2024.7.2 + # via hatchling twisted==24.3.0 # via -r requirements_mypy.in typed-ast==1.4.3 diff --git a/src/incremental/_hatch.py b/src/incremental/_hatch.py index 2f3f2d9..8d677c4 100644 --- a/src/incremental/_hatch.py +++ b/src/incremental/_hatch.py @@ -3,7 +3,7 @@ import os import shlex -from typing import TypedDict +from typing import Any, Dict, List, Type, TypedDict from hatchling.version.source.plugin.interface import VersionSourceInterface from hatchling.plugin import hookimpl @@ -18,11 +18,13 @@ class _VersionData(TypedDict): class IncrementalVersionSource(VersionSourceInterface): PLUGIN_NAME = "incremental" - def get_version_data(self) -> _VersionData: - config = _load_pyproject_toml(os.path.join(self.root, "./pyproject.toml")) + def get_version_data(self) -> _VersionData: # type: ignore[override] + path = os.path.join(self.root, "./pyproject.toml") + config = _load_pyproject_toml(path) + assert config is not None, "Failed to read {}".format(path) return {"version": _existing_version(config.path).public()} - def set_version(self, version: str, version_data: dict): + def set_version(self, version: str, version_data: Dict[Any, Any]) -> None: raise NotImplementedError( f"Run `python -m incremental.version --newversion" f" {shlex.quote(version)}` to set the version.\n\n" @@ -31,5 +33,5 @@ def set_version(self, version: str, version_data: dict): @hookimpl -def hatch_register_version_source(): +def hatch_register_version_source() -> List[Type[VersionSourceInterface]]: return [IncrementalVersionSource] diff --git a/src/incremental/tests/test_pyproject.py b/src/incremental/tests/test_pyproject.py index 0d41194..df5b0ca 100644 --- a/src/incremental/tests/test_pyproject.py +++ b/src/incremental/tests/test_pyproject.py @@ -4,7 +4,7 @@ """Test handling of ``pyproject.toml`` configuration""" import os -from typing import Optional +from typing import cast, Optional, Union from pathlib import Path from twisted.trial.unittest import TestCase @@ -15,7 +15,7 @@ class VerifyPyprojectDotTomlTests(TestCase): """Test the `_load_pyproject_toml` helper function""" def _loadToml( - self, toml: str, *, path: Optional[os.PathLike] = None + self, toml: str, *, path: Union[Path, str, None] = None ) -> Optional[_IncrementalConfig]: """ Read a TOML snipped from a temporary file with `_load_pyproject_toml` @@ -24,23 +24,29 @@ def _loadToml( @param path: Path to which the TOML is written """ - path = path or self.mktemp() + path_: str + if path is None: + path_ = self.mktemp() # type: ignore + else: + path_ = str(path) - with open(path, "w") as f: + with open(path_, "w") as f: f.write(toml) try: - return _load_pyproject_toml(path) + return _load_pyproject_toml(path_) except Exception as e: if hasattr(e, "add_note"): - e.add_note(f"While loading:\n\n{toml}") # pragma: no coverage + e.add_note( # type: ignore[attr-defined] + f"While loading:\n\n{toml}" + ) # pragma: no coverage raise def test_fileNotFound(self): """ An absent ``pyproject.toml`` file produces no result """ - path = os.path.join(self.mktemp(), "pyproject.toml") + path = os.path.join(cast(str, self.mktemp()), "pyproject.toml") self.assertIsNone(_load_pyproject_toml(path)) def test_nameMissing(self): From e1e05d2d9153e17e3505e6c47b0728bfafb6c598 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 20 Jul 2024 15:59:17 -0700 Subject: [PATCH 8/9] Test integration with the Hatch CLI This is documented behavior so it should have tests. --- tests/test_examples.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 118b7c0..5d83e3c 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -52,8 +52,7 @@ def test_setuptools_version(self): def test_hatchling_get_version(self): """ - example_hatchling has a version of 24.7.0, which may be retrieved - by the ``hatch version`` command. + example_hatchling has a version of 24.7.0. """ build_and_install(TEST_DIR.child("example_hatchling")) @@ -64,3 +63,37 @@ def test_hatchling_get_version(self): Version("example_hatchling", 24, 7, 0), ) self.assertEqual(metadata.version("example_hatchling"), "24.7.0") + + def test_hatch_version(self): + """ + The ``hatch version`` command reports the version of a package + packaged with hatchling. + """ + proc = run( + ["hatch", "version"], + cwd=TEST_DIR.child("example_hatchling").path, + check=True, + capture_output=True, + ) + + self.assertEqual(proc.stdout, b"24.7.0\n") + + def test_hatch_version_set(self): + """ + The ``hatch version`` command can't set the version so its output + tells the user to use ``incremental.update`` instead. + """ + proc = run( + ["hatch", "--no-color", "version", "24.8.0"], + cwd=TEST_DIR.child("example_hatchling").path, + check=False, + capture_output=True, + ) + suggestion = b"Run `python -m incremental.version --newversion 24.8.0` to set the version." + + self.assertGreater(proc.returncode, 0) + self.assertRegex( + proc.stdout, + # Hatch may wrap the output, so we are flexible about the specifics of whitespace. + suggestion.replace(b".", rb"\.").replace(b" ", b"\\s+"), + ) From 7d35b39344a547378eb9a7a8e72621c121da1dc0 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 23 Jul 2024 14:46:05 -0700 Subject: [PATCH 9/9] Note the plugin is read-only --- src/incremental/newsfragments/93.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incremental/newsfragments/93.feature b/src/incremental/newsfragments/93.feature index 9203306..85e76cc 100644 --- a/src/incremental/newsfragments/93.feature +++ b/src/incremental/newsfragments/93.feature @@ -1 +1 @@ -Incremental now provides a `Hatchling version source plugin `_. +Incremental now provides a read-only `Hatchling version source plugin `_.