From b51bf9d5e8c2c4ae799ef1ce6398724185344f14 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Mon, 1 Jul 2024 16:47:31 -0700 Subject: [PATCH 01/31] First cut of pyproject.toml support --- README.rst | 46 ++++++++++++++++++----- setup.cfg | 5 ++- src/incremental/__init__.py | 75 +++++++++++++++++++++++++++++++------ 3 files changed, 105 insertions(+), 21 deletions(-) diff --git a/README.rst b/README.rst index 7a20d07..47ffb07 100644 --- a/README.rst +++ b/README.rst @@ -7,23 +7,34 @@ Incremental Incremental is a small library that versions your Python projects. -API documentation can be found `here `_. +API documentation can be found `here `_. Quick Start ----------- -Add this to your ``setup.py``\ 's ``setup()`` call, removing any other versioning arguments: +In your ``pyproject.toml``, add Incremental to your build requirements: -.. code:: +```toml +[build-system] +requires = ["setuptools", "incremental>=NEXT"] +build-backend = "setuptools.build_meta" +``` - setup( - use_incremental=True, - setup_requires=['incremental'], - install_requires=['incremental'], # along with any other install dependencies - ... - } +Specify the project's version as dynamic: + +```toml +[project] +dynamic = ["version"] +``` + +Remove any ``version`` specification and any ``[tool.setuptools.dynamic] version = `` block. +Add this empty block to activate Incremental's setuptools plugin: + +```toml +[tool.incremental] +``` Install Incremental to your local environment with ``pip install incremental[scripts]``. Then run ``python -m incremental.update --create``. @@ -47,6 +58,23 @@ Then, so users of your project can find your version, in your root package's ``_ Subsequent installations of your project will then use Incremental for versioning. +Using ``setup.py`` +~~~~~~~~~~~~~~~~ + +Incremental may be used from ``setup.py`` instead of ``pyproject.toml``. +Add this to your ``setup()`` call, removing any other versioning arguments: + +.. code:: + + setup( + use_incremental=True, + setup_requires=['incremental'], + install_requires=['incremental'], # along with any other install dependencies + ... + } + +Then proceed with the ``incremental.update`` command above. + Incremental Versions -------------------- diff --git a/setup.cfg b/setup.cfg index 8695240..d428033 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,7 @@ description = "A small library that versions your Python projects." long_description = file: README.rst install_requires = setuptools + tomli; python_version < '3.11' [options] packages = find: @@ -33,7 +34,9 @@ incremental = py.typed [options.entry_points] distutils.setup_keywords = - use_incremental = incremental:_get_version + use_incremental = incremental:_get_distutils_version +setuptools.finalize_distribution_options = + incremental = incremental:_get_setuptools_version [options.extras_require] scripts = diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index c0c06c0..cf366a4 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -23,11 +23,9 @@ if TYPE_CHECKING: from typing_extensions import Literal from distutils.dist import Distribution as _Distribution + import setuptools -else: - _Distribution = object - if sys.version_info > (3,): def _cmp(a, b): # type: (Any, Any) -> int @@ -167,7 +165,7 @@ def prerelease(self): # type: () -> Optional[int] "Version.release_candidate instead.", DeprecationWarning, stacklevel=2, - ), + ) return self.release_candidate def public(self): # type: () -> str @@ -206,7 +204,6 @@ def public(self): # type: () -> str local = public def __repr__(self): # type: () -> str - if self.release_candidate is None: release_candidate = "" else: @@ -360,14 +357,41 @@ def getVersionString(version): # type: (Version) -> str return result -def _get_version(dist, keyword, value): # type: (_Distribution, object, object) -> None +def _get_distutils_version(dist, keyword, value): # type: (_Distribution, object, object) -> None + """ + Distutils integration: get the version from the package listed in the Distribution. + + This function is invoked when a ``setup.py`` calls `setup(use_incremental=True)`. + + See https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-arguments + """ + if not value: # use_incremental=False + return + + dist.metadata.version = _load_version(dist) + + +def _get_setuptools_version(dist): # type: (setuptools.Distribution) -> None """ - Get the version from the package listed in the Distribution. + Setuptools integration: get the version from the package + + This function is registered as a setuptools.finalize_distribution_options + entry point [1]. It is a no-op unless there is a ``pyproject.toml`` containing + a ``[tool.incremental]`` section. + + [1]: https://setuptools.pypa.io/en/latest/userguide/extension.html#customizing-distribution-options """ - if not value: + if not _verify_pyproject_toml("pyproject.toml"): return - from distutils.command import build_py + dist.metadata.version = _load_version(dist) + + +def _load_version(dist): # type: (_Distribution) -> str + """ + Load the version from ``_version.py`` within the distribution. + """ + from setuptools.command import build_py sp_command = build_py.build_py(dist) sp_command.finalize_options() @@ -379,12 +403,41 @@ def _get_version(dist, keyword, value): # type: (_Distribution, object, object) with open(item[2]) as f: exec(f.read(), version_file) - dist.metadata.version = version_file["__version__"].public() - return None + return version_file["__version__"].public() raise Exception("No _version.py found.") +def _verify_pyproject_toml(path): # type: (str) -> bool + """ + Does the ``pyproject.toml`` file contain an empty ``[tool.incremental]`` + section? This indicates that the package has opted-in to Incremental + versioning. + + We enforce that the section is empty to allow for future extension. + """ + try: + import tomllib + except ImportError: + import tomli as tomllib + + try: + with open(path, "rb") as f: + data = tomllib.load(f) + except FileNotFoundError: + return False + + if "tool" not in data: + return False + if "incremental" not in data["tool"]: + return False + if data["tool"]["incremental"] == {}: + return True + raise ValueError( + "[tool.incremental] table must be empty. Do you need to upgrade Incremental?" + ) + + from ._version import __version__ # noqa: E402 From d383fce19261cef8bc6df821b83067ec0f043728 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Mon, 1 Jul 2024 16:53:05 -0700 Subject: [PATCH 02/31] Add newsfragment --- src/incremental/newsfragments/90.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/incremental/newsfragments/90.feature diff --git a/src/incremental/newsfragments/90.feature b/src/incremental/newsfragments/90.feature new file mode 100644 index 0000000..d17cf87 --- /dev/null +++ b/src/incremental/newsfragments/90.feature @@ -0,0 +1 @@ +Incremental can now be configured using ``pyproject.toml``. From 690ae46e090a4364b200d632b0a4f401a5a02484 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Mon, 1 Jul 2024 17:01:40 -0700 Subject: [PATCH 03/31] Reorganize tests --- pyproject.toml | 2 +- setup.cfg | 2 +- src/exampleproj/_version.py | 8 ------ .../example_setuppy/setup.py | 4 +-- .../src/example_setuppy}/__init__.py | 6 ++--- .../src/example_setuppy/_version.py | 8 ++++++ tests/example_setuptools/pyproject.toml | 9 +++++++ .../src/example_setuptools/__init__.py | 13 ++++++++++ .../src/example_setuptools/_version.py | 8 ++++++ tests/test_exampleproj.py | 20 -------------- tests/test_examples.py | 26 +++++++++++++++++++ tox.ini | 6 +++-- 12 files changed, 75 insertions(+), 37 deletions(-) delete mode 100644 src/exampleproj/_version.py rename examplesetup.py => tests/example_setuppy/setup.py (70%) rename {src/exampleproj => tests/example_setuppy/src/example_setuppy}/__init__.py (50%) create mode 100644 tests/example_setuppy/src/example_setuppy/_version.py create mode 100644 tests/example_setuptools/pyproject.toml create mode 100644 tests/example_setuptools/src/example_setuptools/__init__.py create mode 100644 tests/example_setuptools/src/example_setuptools/_version.py delete mode 100644 tests/test_exampleproj.py create mode 100644 tests/test_examples.py diff --git a/pyproject.toml b/pyproject.toml index 8783b9d..83aeb6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ "setuptools >= 44.1.1", - "wheel >= 0.36.2", + "tomli; python_version < '3.11'", ] build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index d428033..9b83408 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,7 +17,7 @@ license = MIT description = "A small library that versions your Python projects." long_description = file: README.rst install_requires = - setuptools + setuptools >= 44.1.1 tomli; python_version < '3.11' [options] diff --git a/src/exampleproj/_version.py b/src/exampleproj/_version.py deleted file mode 100644 index 85b39a7..0000000 --- a/src/exampleproj/_version.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file is auto-generated! Do not edit! -# Use `python -m incremental.update exampleproj` to change this file. - -from incremental import Version - -__version__ = Version("exampleproj", 1, 2, 3) - -__all__ = ["__version__"] diff --git a/examplesetup.py b/tests/example_setuppy/setup.py similarity index 70% rename from examplesetup.py rename to tests/example_setuppy/setup.py index a8294a7..ad19d62 100644 --- a/examplesetup.py +++ b/tests/example_setuppy/setup.py @@ -1,9 +1,9 @@ from setuptools import setup setup( - name="exampleproj", + name="example_setuppy", package_dir={"": "src"}, - packages=["exampleproj"], + packages=["example_setuppy"], use_incremental=True, zip_safe=False, setup_requires=["incremental"], diff --git a/src/exampleproj/__init__.py b/tests/example_setuppy/src/example_setuppy/__init__.py similarity index 50% rename from src/exampleproj/__init__.py rename to tests/example_setuppy/src/example_setuppy/__init__.py index c188749..cb566be 100644 --- a/src/exampleproj/__init__.py +++ b/tests/example_setuppy/src/example_setuppy/__init__.py @@ -1,7 +1,7 @@ """ -An example project. +An example project that uses setup.py -@added: exampleproj NEXT +@added: example_setuppy NEXT """ from incremental import Version @@ -9,5 +9,5 @@ __all__ = ["__version__"] -if Version("exampleproj", "NEXT", 0, 0) > __version__: +if Version("example_setuppy", "NEXT", 0, 0) > __version__: print("Unreleased!") diff --git a/tests/example_setuppy/src/example_setuppy/_version.py b/tests/example_setuppy/src/example_setuppy/_version.py new file mode 100644 index 0000000..e39c43f --- /dev/null +++ b/tests/example_setuppy/src/example_setuppy/_version.py @@ -0,0 +1,8 @@ +# This file is auto-generated! Do not edit! +# Use `python -m incremental.update example_setuppy` to change this file. + +from incremental import Version + +__version__ = Version("example_setuppy", 1, 2, 3) + +__all__ = ["__version__"] diff --git a/tests/example_setuptools/pyproject.toml b/tests/example_setuptools/pyproject.toml new file mode 100644 index 0000000..9317fc7 --- /dev/null +++ b/tests/example_setuptools/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["setuptools", "incremental"] +build-backend = "setuptools.build_meta" + +[project] +name = "example_setuptools" +dynamic = ["version"] + +[tool.incremental] diff --git a/tests/example_setuptools/src/example_setuptools/__init__.py b/tests/example_setuptools/src/example_setuptools/__init__.py new file mode 100644 index 0000000..b5f6ee2 --- /dev/null +++ b/tests/example_setuptools/src/example_setuptools/__init__.py @@ -0,0 +1,13 @@ +""" +An example project that uses setuptools in pyproject.toml + +@added: example_setuptools NEXT +""" + +from incremental import Version +from ._version import __version__ + +__all__ = ["__version__"] + +if Version("example_setuptools", "NEXT", 0, 0) > __version__: + print("Unreleased!") diff --git a/tests/example_setuptools/src/example_setuptools/_version.py b/tests/example_setuptools/src/example_setuptools/_version.py new file mode 100644 index 0000000..2d606e4 --- /dev/null +++ b/tests/example_setuptools/src/example_setuptools/_version.py @@ -0,0 +1,8 @@ +# This file is auto-generated! Do not edit! +# Use `python -m incremental.update example_setuptools` to change this file. + +from incremental import Version + +__version__ = Version("example_setuptools", 2, 3, 4) + +__all__ = ["__version__"] diff --git a/tests/test_exampleproj.py b/tests/test_exampleproj.py deleted file mode 100644 index 6561e78..0000000 --- a/tests/test_exampleproj.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Tests for L{incremental._versioning}. -""" - -from __future__ import division, absolute_import - -from twisted.trial.unittest import TestCase - - -class ExampleProjTests(TestCase): - def test_version(self): - """ - exampleproj has a version of 1.2.3. - """ - import exampleproj - - self.assertEqual(exampleproj.__version__.base(), "1.2.3") diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 0000000..2fc8801 --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,26 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tests for the packaging examples. +""" + +from twisted.trial.unittest import TestCase + + +class ExampleTests(TestCase): + def test_setuppy_version(self): + """ + example_setuppy has a version of 1.2.3. + """ + import example_setuppy + + self.assertEqual(example_setuppy.__version__.base(), "1.2.3") + + def test_setuptools_version(self): + """ + example_setuptools has a version of 2.3.4. + """ + import example_setuptools + + self.assertEqual(example_setuptools.__version__.base(), "2.3.4") diff --git a/tox.ini b/tox.ini index 81f968e..f322c50 100644 --- a/tox.ini +++ b/tox.ini @@ -32,9 +32,11 @@ commands = tests: coverage --version tests: {envbindir}/trial --version tests: coverage erase + tests: {envpython} -c "import site; open(site.getsitepackages()[0] + '/coveragehack.pth', 'w').write('import coverage; coverage.process_startup()')" tests: coverage run -p {envbindir}/trial incremental - tests: coverage run -p examplesetup.py install - tests: coverage run -p {envbindir}/trial tests/test_exampleproj.py + tests: coverage run -p -m pip install --use-pep517 ./tests/example_setuppy + tests: coverage run -p -m pip install --use-pep517 ./tests/example_setuptools + tests: coverage run -p {envbindir}/trial tests/test_examples.py tests: coverage combine tests: coverage report tests: coverage html From 01bf22be2af5e7d92e60a412345471e194a31894 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Mon, 1 Jul 2024 18:04:16 -0700 Subject: [PATCH 04/31] Make tomli more optional --- src/incremental/__init__.py | 21 +++++++++++++++------ tox.ini | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index cf366a4..2e992d5 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -408,6 +408,20 @@ def _load_version(dist): # type: (_Distribution) -> str raise Exception("No _version.py found.") +def _load_toml(f): # type: (io.BytesIO) -> Dict + """ + Read the content of a TOML file. + """ + # This import is deferred to avoid a hard dependency on tomli + # when no pyproject.toml is present. + if sys.version_info > (3, 11): + import tomllib + else: + import tomli as tomllib + + return tomllib.load(f) + + def _verify_pyproject_toml(path): # type: (str) -> bool """ Does the ``pyproject.toml`` file contain an empty ``[tool.incremental]`` @@ -416,14 +430,9 @@ def _verify_pyproject_toml(path): # type: (str) -> bool We enforce that the section is empty to allow for future extension. """ - try: - import tomllib - except ImportError: - import tomli as tomllib - try: with open(path, "rb") as f: - data = tomllib.load(f) + data = _load_toml(f) except FileNotFoundError: return False diff --git a/tox.ini b/tox.ini index f322c50..3a9ccb2 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ commands = tests: coverage erase tests: {envpython} -c "import site; open(site.getsitepackages()[0] + '/coveragehack.pth', 'w').write('import coverage; coverage.process_startup()')" tests: coverage run -p {envbindir}/trial incremental - tests: coverage run -p -m pip install --use-pep517 ./tests/example_setuppy + tests: coverage run -p -m pip install ./tests/example_setuppy tests: coverage run -p -m pip install --use-pep517 ./tests/example_setuptools tests: coverage run -p {envbindir}/trial tests/test_examples.py tests: coverage combine From 74acefbe80820c6f263e785c54b2bb4ee565a5f5 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Mon, 1 Jul 2024 18:33:43 -0700 Subject: [PATCH 05/31] Clean up MyPy issues --- src/incremental/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index 2e992d5..b4bbf31 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -11,7 +11,7 @@ import sys import warnings -from typing import TYPE_CHECKING, Any, TypeVar, Union, Optional, Dict +from typing import TYPE_CHECKING, Any, TypeVar, Union, Optional, Dict, BinaryIO # # Compat functions @@ -21,9 +21,9 @@ if TYPE_CHECKING: + import io from typing_extensions import Literal from distutils.dist import Distribution as _Distribution - import setuptools if sys.version_info > (3,): @@ -371,7 +371,7 @@ def _get_distutils_version(dist, keyword, value): # type: (_Distribution, objec dist.metadata.version = _load_version(dist) -def _get_setuptools_version(dist): # type: (setuptools.Distribution) -> None +def _get_setuptools_version(dist): # type: (_Distribution) -> None """ Setuptools integration: get the version from the package @@ -391,12 +391,12 @@ def _load_version(dist): # type: (_Distribution) -> str """ Load the version from ``_version.py`` within the distribution. """ - from setuptools.command import build_py + from setuptools.command import build_py # type: ignore sp_command = build_py.build_py(dist) sp_command.finalize_options() - for item in sp_command.find_all_modules(): # type: ignore[attr-defined] + for item in sp_command.find_all_modules(): if item[1] == "_version": version_file = {} # type: Dict[str, Version] @@ -408,7 +408,7 @@ def _load_version(dist): # type: (_Distribution) -> str raise Exception("No _version.py found.") -def _load_toml(f): # type: (io.BytesIO) -> Dict +def _load_toml(f): # type: (BinaryIO) -> Any """ Read the content of a TOML file. """ @@ -417,7 +417,7 @@ def _load_toml(f): # type: (io.BytesIO) -> Dict if sys.version_info > (3, 11): import tomllib else: - import tomli as tomllib + import tomli as tomllib # type: ignore return tomllib.load(f) From 8dea8f63002c7cd3db3a251882cb707862091b8f Mon Sep 17 00:00:00 2001 From: Tom Most Date: Mon, 1 Jul 2024 18:44:19 -0700 Subject: [PATCH 06/31] Runtime dep too --- tests/example_setuptools/pyproject.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/example_setuptools/pyproject.toml b/tests/example_setuptools/pyproject.toml index 9317fc7..beca60e 100644 --- a/tests/example_setuptools/pyproject.toml +++ b/tests/example_setuptools/pyproject.toml @@ -1,9 +1,15 @@ [build-system] -requires = ["setuptools", "incremental"] +requires = [ + "setuptools", + "incremental", +] build-backend = "setuptools.build_meta" [project] name = "example_setuptools" +dependencies = [ + "incremental", +] dynamic = ["version"] [tool.incremental] From 3a8d26d1965c51600f334d411249735944273c50 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Mon, 1 Jul 2024 18:45:28 -0700 Subject: [PATCH 07/31] What version of setuptools? --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 3a9ccb2..de12c43 100644 --- a/tox.ini +++ b/tox.ini @@ -29,6 +29,7 @@ commands = apidocs: pydoctor -q --project-name incremental src/incremental + tests: {envpython} -m pip list tests: coverage --version tests: {envbindir}/trial --version tests: coverage erase From 66ecb82edd3f62424084970407a2aa324679467f Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 16:06:45 -0700 Subject: [PATCH 08/31] Remove some unreachable code This line appears to be nonsense: if the string is bytes, it won't have a decode method on any Python version we support. --- src/incremental/update.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/incremental/update.py b/src/incremental/update.py index 64a5cc8..9867387 100644 --- a/src/incremental/update.py +++ b/src/incremental/update.py @@ -71,7 +71,6 @@ def walk(self): # type: () -> Iterable[FilePath] def _findPath(path, package): # type: (str, str) -> FilePath - cwd = FilePath(path) src_dir = cwd.child("src").child(package.lower()) @@ -112,16 +111,12 @@ def _run( _getcwd=None, # type: Optional[Callable[[], str]] _print=print, # type: Callable[[object], object] ): # type: (...) -> None - if not _getcwd: _getcwd = os.getcwd if not _date: _date = datetime.date.today() - if type(package) != str: - package = package.encode("utf8") # type: ignore[assignment] - _path = FilePath(path) if path else _findPath(_getcwd(), package) if ( @@ -255,7 +250,6 @@ def _run( _print("Updating codebase to %s" % (v.public())) for x in _path.walk(): - if not x.isfile(): continue From 28a1446f68722170ae86e7114763e36c443d37b1 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 16:22:40 -0700 Subject: [PATCH 09/31] Backfill some unit tests --- src/incremental/__init__.py | 2 +- src/incremental/tests/test_pyproject.py | 65 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/incremental/tests/test_pyproject.py diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index b4bbf31..7bb056c 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -417,7 +417,7 @@ def _load_toml(f): # type: (BinaryIO) -> Any if sys.version_info > (3, 11): import tomllib else: - import tomli as tomllib # type: ignore + import tomli as tomllib return tomllib.load(f) diff --git a/src/incremental/tests/test_pyproject.py b/src/incremental/tests/test_pyproject.py new file mode 100644 index 0000000..6eae843 --- /dev/null +++ b/src/incremental/tests/test_pyproject.py @@ -0,0 +1,65 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +"""Test handling of ``pyproject.toml`` configuration""" + +import os +from twisted.trial.unittest import TestCase + +from incremental import _verify_pyproject_toml + + +class VerifyPyprojectDotTomlTests(TestCase): + """Test the `_verify_pyproject_toml` helper function""" + + def test_fileNotFound(self): + """ + Verification fails when no ``pyproject.toml`` file exists. + """ + path = os.path.join(self.mktemp(), "pyproject.toml") + self.assertFalse(_verify_pyproject_toml(path)) + + def test_noToolIncrementalSection(self): + """ + Verification fails when there isn't a ``[tool.incremental]`` section. + """ + path = self.mktemp() + for toml in [ + "\n", + "[tool]\n", + "[tool.notincremental]\n", + "[tool.notincremental]\n", + ]: + with open(path, "w") as f: + f.write(toml) + self.assertFalse(_verify_pyproject_toml(path)) + + def test_toolIncrementalNotEmpty(self): + """ + `ValueError` is raised when the ``[tool.incremental]`` section isn't + an empty dict. + """ + path = self.mktemp() + for toml in [ + '[tool.incremental]\nfoo = "bar"\n', + "[tool]\nincremental = false\n", + ]: + with open(path, "w") as f: + f.write(toml) + + self.assertRaises(ValueError, _verify_pyproject_toml, path) + + def test_ok(self): + """ + The package has opted-in to Incremental version management when + the ``[tool.incremental]`` section is an empty dict. + """ + path = self.mktemp() + for toml in [ + "[tool.incremental]\n", + "[tool]\nincremental = {}\n", + ]: + with open(path, "w") as f: + f.write(toml) + + self.assertTrue(_verify_pyproject_toml(path)) From 1074d55aa7571a22cf3f2fae25e3e8c6cc7e7a98 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 16:44:15 -0700 Subject: [PATCH 10/31] Move packaging metadata to pyproject.toml --- pyproject.toml | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 48 ------------------------------------------------ setup.py | 3 --- tox.ini | 2 +- 4 files changed, 50 insertions(+), 53 deletions(-) delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index 83aeb6b..d91c3c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,56 @@ [build-system] requires = [ + "setuptools >= 61.0", +] +build-backend = "setuptools.build_meta" + +[project] +name = "incremental" +dynamic = ["version"] +maintainers = [ + {name = "Amber Brown", email = "hawkowl@twistedmatrix.com"}, +] +classifiers = [ + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +requires-python = ">=3.8" +license = {file = "LICENSE"} +description = "A small library that versions your Python projects." +readme = "README.rst" +dependencies = [ "setuptools >= 44.1.1", "tomli; python_version < '3.11'", ] -build-backend = "setuptools.build_meta" + +[project.optional-dependencies] +scripts = [ + "click>=6.0", + "twisted>=16.4.0", +] +mypy = [ + "mypy==0.812", +] + +[project.urls] +Homepage = "https://github.com/twisted/incremental" +Documentation = "https://twisted.org/incremental/docs/" +Issues = "https://github.com/twisted/incremental/issues" +Changelog = "https://github.com/twisted/incremental/blob/trunk/NEWS.rst" + +[project.entry-points."distutils.setup_keywords"] +use_incremental = "incremental:_get_distutils_version" +[project.entry-points."setuptools.finalize_distribution_options"] +incremental = "incremental:_get_setuptools_version" + +[tool.setuptools.dynamic] +version = {attr = "incremental._setuptools_version"} [tool.black] target-version = ['py36', 'py37', 'py38'] @@ -12,3 +59,4 @@ target-version = ['py36', 'py37', 'py38'] filename = "NEWS.rst" package_dir = "src/" package = "incremental" +issue_format = "`#{issue} `__" diff --git a/setup.cfg b/setup.cfg index 9b83408..25e2512 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,51 +1,3 @@ -[metadata] -name = incremental -version = attr: incremental._setuptools_version -maintainer = Amber Brown -maintainer_email = hawkowl@twistedmatrix.com -url = https://github.com/twisted/incremental -classifiers = - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 -license = MIT -description = "A small library that versions your Python projects." -long_description = file: README.rst -install_requires = - setuptools >= 44.1.1 - tomli; python_version < '3.11' - -[options] -packages = find: -package_dir = =src -zip_safe = False - -[options.packages.find] -where = src -exclude = exampleproj - -[options.package_data] -incremental = py.typed - -[options.entry_points] -distutils.setup_keywords = - use_incremental = incremental:_get_distutils_version -setuptools.finalize_distribution_options = - incremental = incremental:_get_setuptools_version - -[options.extras_require] -scripts = - click>=6.0 - twisted>=16.4.0 -mypy = - %(scripts)s - mypy==0.812 - [flake8] max-line-length = 88 extend-ignore = diff --git a/setup.py b/setup.py deleted file mode 100644 index d649009..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup # type: ignore[import] - -setup() diff --git a/tox.ini b/tox.ini index de12c43..0b7f69a 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ deps = apidocs: pydoctor lint: pre-commit extras = - mypy: mypy + mypy: mypy,scripts tests: scripts commands = From 8e1ae1d130423ed2ff406cb76ee66e17fcf85326 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 16:57:55 -0700 Subject: [PATCH 11/31] Fix readme syntax --- README.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index 47ffb07..2a0e6e7 100644 --- a/README.rst +++ b/README.rst @@ -15,32 +15,32 @@ Quick Start In your ``pyproject.toml``, add Incremental to your build requirements: -```toml -[build-system] -requires = ["setuptools", "incremental>=NEXT"] -build-backend = "setuptools.build_meta" -``` +.. code-block:: toml + + [build-system] + requires = ["setuptools", "incremental>=NEXT"] + build-backend = "setuptools.build_meta" Specify the project's version as dynamic: -```toml -[project] -dynamic = ["version"] -``` +.. code-block:: toml + + [project] + dynamic = ["version"] Remove any ``version`` specification and any ``[tool.setuptools.dynamic] version = `` block. Add this empty block to activate Incremental's setuptools plugin: -```toml -[tool.incremental] -``` +.. code-block:: toml + + [tool.incremental] 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: -.. code:: +.. code:: python from incremental import Version @@ -50,7 +50,7 @@ It will create a file in your package named ``_version.py`` and look like this: Then, so users of your project can find your version, in your root package's ``__init__.py`` add: -.. code:: +.. code:: python from ._version import __version__ @@ -64,7 +64,7 @@ Using ``setup.py`` Incremental may be used from ``setup.py`` instead of ``pyproject.toml``. Add this to your ``setup()`` call, removing any other versioning arguments: -.. code:: +.. code:: python setup( use_incremental=True, From e1682eb7ab789d72728acc2cce9bd2f80c1fb037 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 20:57:51 -0700 Subject: [PATCH 12/31] A new approach It turns out that, in practice, the --- pyproject.toml | 1 - src/incremental/__init__.py | 270 ++++++++++++++--------- src/incremental/newsfragments/88.removal | 1 + src/incremental/tests/test_pyproject.py | 80 +++++-- src/incremental/update.py | 150 ++++--------- tests/test_examples.py | 3 + tox.ini | 10 +- 7 files changed, 289 insertions(+), 226 deletions(-) create mode 100644 src/incremental/newsfragments/88.removal diff --git a/pyproject.toml b/pyproject.toml index d91c3c9..51826df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ dependencies = [ [project.optional-dependencies] scripts = [ "click>=6.0", - "twisted>=16.4.0", ] mypy = [ "mypy==0.812", diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index 7bb056c..2f47679 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -9,15 +9,11 @@ from __future__ import division, absolute_import +import os import sys import warnings from typing import TYPE_CHECKING, Any, TypeVar, Union, Optional, Dict, BinaryIO - -# -# Compat functions -# - -_T = TypeVar("_T", contravariant=True) +from dataclasses import dataclass if TYPE_CHECKING: @@ -26,25 +22,24 @@ from distutils.dist import Distribution as _Distribution -if sys.version_info > (3,): - - def _cmp(a, b): # type: (Any, Any) -> int - """ - Compare two objects. +# +# Compat functions +# - Returns a negative number if C{a < b}, zero if they are equal, and a - positive number if C{a > b}. - """ - if a < b: - return -1 - elif a == b: - return 0 - else: - return 1 +def _cmp(a, b): # type: (Any, Any) -> int + """ + Compare two objects. -else: - _cmp = cmp # noqa: F821 + Returns a negative number if C{a < b}, zero if they are equal, and a + positive number if C{a > b}. + """ + if a < b: + return -1 + elif a == b: + return 0 + else: + return 1 # @@ -69,19 +64,17 @@ def __cmp__(self, other): # type: (object) -> int return 0 return 1 - if sys.version_info >= (3,): - - def __lt__(self, other): # type: (object) -> bool - return self.__cmp__(other) < 0 + def __lt__(self, other): # type: (object) -> bool + return self.__cmp__(other) < 0 - def __le__(self, other): # type: (object) -> bool - return self.__cmp__(other) <= 0 + def __le__(self, other): # type: (object) -> bool + return self.__cmp__(other) <= 0 - def __gt__(self, other): # type: (object) -> bool - return self.__cmp__(other) > 0 + def __gt__(self, other): # type: (object) -> bool + return self.__cmp__(other) > 0 - def __ge__(self, other): # type: (object) -> bool - return self.__cmp__(other) >= 0 + def __ge__(self, other): # type: (object) -> bool + return self.__cmp__(other) >= 0 _inf = _Inf() @@ -307,43 +300,41 @@ def __cmp__(self, other): # type: (Version) -> int ) return x - if sys.version_info >= (3,): - - def __eq__(self, other): # type: (Any) -> bool - c = self.__cmp__(other) - if c is NotImplemented: - return c # type: ignore[return-value] - return c == 0 - - def __ne__(self, other): # type: (Any) -> bool - c = self.__cmp__(other) - if c is NotImplemented: - return c # type: ignore[return-value] - return c != 0 - - def __lt__(self, other): # type: (Version) -> bool - c = self.__cmp__(other) - if c is NotImplemented: - return c # type: ignore[return-value] - return c < 0 - - def __le__(self, other): # type: (Version) -> bool - c = self.__cmp__(other) - if c is NotImplemented: - return c # type: ignore[return-value] - return c <= 0 - - def __gt__(self, other): # type: (Version) -> bool - c = self.__cmp__(other) - if c is NotImplemented: - return c # type: ignore[return-value] - return c > 0 - - def __ge__(self, other): # type: (Version) -> bool - c = self.__cmp__(other) - if c is NotImplemented: - return c # type: ignore[return-value] - return c >= 0 + def __eq__(self, other): + c = self.__cmp__(other) + if c is NotImplemented: + return c + return c == 0 + + def __ne__(self, other): + c = self.__cmp__(other) + if c is NotImplemented: + return c + return c != 0 + + def __lt__(self, other): + c = self.__cmp__(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self.__cmp__(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self.__cmp__(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self.__cmp__(other) + if c is NotImplemented: + return c + return c >= 0 def getVersionString(version): # type: (Version) -> str @@ -357,40 +348,80 @@ def getVersionString(version): # type: (Version) -> str return result -def _get_distutils_version(dist, keyword, value): # type: (_Distribution, object, object) -> None +def _findPath(path, package): # type: (str, str) -> str """ - Distutils integration: get the version from the package listed in the Distribution. + Determine the package root directory. - This function is invoked when a ``setup.py`` calls `setup(use_incremental=True)`. + The result is one of: - See https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-arguments + - ``src/{package}`` + - ``{package}`` + + Where ``{package}`` is downcased. """ - if not value: # use_incremental=False - return + src_dir = os.path.join(path, "src", package.lower()) + current_dir = os.path.join(path, package.lower()) + + if os.path.isdir(src_dir): + return src_dir + elif os.path.isdir(current_dir): + return current_dir + else: + raise ValueError( + "Can't find the directory of package {}: I looked in {} and {}".format( + package, src_dir, current_dir + ) + ) + - dist.metadata.version = _load_version(dist) +def _existing_version(path): # type: (str) -> Version + """ + Load the current version from ``{path}/_version.py``. + """ + version_info = {} # type: Dict[str, Version] + + versionpath = os.path.join(path, "_version.py") + with open(versionpath, "r") as f: + exec(f.read(), version_info) + + return version_info["__version__"] def _get_setuptools_version(dist): # type: (_Distribution) -> None """ - Setuptools integration: get the version from the package + Setuptools integration: load the version from the working directory This function is registered as a setuptools.finalize_distribution_options entry point [1]. It is a no-op unless there is a ``pyproject.toml`` containing - a ``[tool.incremental]`` section. + an empty ``[tool.incremental]`` section. + + :param dist: + An empty `setuptools.Distribution` instance to mutate. [1]: https://setuptools.pypa.io/en/latest/userguide/extension.html#customizing-distribution-options """ - if not _verify_pyproject_toml("pyproject.toml"): + config = _load_pyproject_toml("./pyproject.toml") + if not config: return - dist.metadata.version = _load_version(dist) + dist.metadata.version = _existing_version(config.path).public() + +# Call this hook after all of the built-in setuptools ones, or the Distribution is empty. +_get_setuptools_version.order = 1 -def _load_version(dist): # type: (_Distribution) -> str + +def _get_distutils_version(dist, keyword, value): # type: (_Distribution, object, object) -> None """ - Load the version from ``_version.py`` within the distribution. + Distutils integration: get the version from the package listed in the Distribution. + + This function is invoked when a ``setup.py`` calls `setup(use_incremental=True)`. + + See https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-arguments """ + if not value: # use_incremental=False + return + from setuptools.command import build_py # type: ignore sp_command = build_py.build_py(dist) @@ -398,12 +429,9 @@ def _load_version(dist): # type: (_Distribution) -> str for item in sp_command.find_all_modules(): if item[1] == "_version": - version_file = {} # type: Dict[str, Version] - - with open(item[2]) as f: - exec(f.read(), version_file) - - return version_file["__version__"].public() + package_path = os.path.dirname(item[2]) + dist.metadata.version = _existing_version(package_path).public() + return raise Exception("No _version.py found.") @@ -422,28 +450,72 @@ def _load_toml(f): # type: (BinaryIO) -> Any return tomllib.load(f) -def _verify_pyproject_toml(path): # type: (str) -> bool +@dataclass(frozen=True) +class _IncrementalConfig: """ - Does the ``pyproject.toml`` file contain an empty ``[tool.incremental]`` + @ivar package: The package name, capitalized as in the package metadata. + + @ivar path: Path to the package root + """ + + package: str + path: str + + +def _load_pyproject_toml(toml_path): # type: (str) -> _IncrementalConfig | None: + """ + Does the ``pyproject.toml`` file contain a ``[tool.incremental]`` section? This indicates that the package has opted-in to Incremental versioning. - We enforce that the section is empty to allow for future extension. + If the ``[tool.incremental]`` section is empty we take the project name + from the ``[project]`` section. Otherwise we require only a ``name`` key + specifying the project name. Other keys are forbidden to allow future + extension and catch typos. """ try: - with open(path, "rb") as f: + with open(toml_path, "rb") as f: data = _load_toml(f) except FileNotFoundError: - return False + return None if "tool" not in data: - return False + return None if "incremental" not in data["tool"]: - return False - if data["tool"]["incremental"] == {}: - return True - raise ValueError( - "[tool.incremental] table must be empty. Do you need to upgrade Incremental?" + return None + + tool_incremental = data["tool"]["incremental"] + if not isinstance(tool_incremental, dict): + raise ValueError("[tool.incremental] must be a table") + + if tool_incremental == {}: + try: + package = data["project"]["name"] + except KeyError: + raise ValueError("""\ +Couldn't extract the package name from pyproject.toml. Specify it like: + + [project] + name = "Foo" + +Or: + + [tool.incremental] + name = "Foo" +""") + elif tool_incremental.keys() == {"name"}: + package = tool_incremental["name"] + else: + raise ValueError("Unexpected key(s) in [tool.incremental]") + + if not isinstance(package, str): + raise TypeError( + "Package name must be a string, but found {}".format(type(package)) + ) + + return _IncrementalConfig( + package=package, + path=_findPath(os.path.dirname(toml_path), package), ) diff --git a/src/incremental/newsfragments/88.removal b/src/incremental/newsfragments/88.removal new file mode 100644 index 0000000..2351050 --- /dev/null +++ b/src/incremental/newsfragments/88.removal @@ -0,0 +1 @@ +``incremental[scripts]`` no longer depends on Twisted. diff --git a/src/incremental/tests/test_pyproject.py b/src/incremental/tests/test_pyproject.py index 6eae843..de3b269 100644 --- a/src/incremental/tests/test_pyproject.py +++ b/src/incremental/tests/test_pyproject.py @@ -6,18 +6,18 @@ import os from twisted.trial.unittest import TestCase -from incremental import _verify_pyproject_toml +from incremental import _load_pyproject_toml, _IncrementalConfig class VerifyPyprojectDotTomlTests(TestCase): - """Test the `_verify_pyproject_toml` helper function""" + """Test the `_load_pyproject_toml` helper function""" def test_fileNotFound(self): """ Verification fails when no ``pyproject.toml`` file exists. """ path = os.path.join(self.mktemp(), "pyproject.toml") - self.assertFalse(_verify_pyproject_toml(path)) + self.assertFalse(_load_pyproject_toml(path)) def test_noToolIncrementalSection(self): """ @@ -28,38 +28,90 @@ def test_noToolIncrementalSection(self): "\n", "[tool]\n", "[tool.notincremental]\n", - "[tool.notincremental]\n", + '[project]\nname = "foo"\n', ]: with open(path, "w") as f: f.write(toml) - self.assertFalse(_verify_pyproject_toml(path)) + self.assertIsNone(_load_pyproject_toml(path)) - def test_toolIncrementalNotEmpty(self): + 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 [ + "[tool.incremental]\n", + "[project]\n[tool.incremental]\n", + ]: + with open(path, "w") as f: + f.write(toml) + + self.assertRaises(ValueError, _load_pyproject_toml, path) + + 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) + + def test_toolIncrementalInvalid(self): """ `ValueError` is raised when the ``[tool.incremental]`` section isn't - an empty dict. + a dict. """ path = self.mktemp() for toml in [ - '[tool.incremental]\nfoo = "bar"\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) + + 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, _verify_pyproject_toml, path) + self.assertRaises(ValueError, _load_pyproject_toml, path) def test_ok(self): """ The package has opted-in to Incremental version management when the ``[tool.incremental]`` section is an empty dict. """ - path = self.mktemp() + root = self.mktemp() + path = os.path.join(root, "src", "foo") + os.makedirs(path) + toml_path = os.path.join(root, "pyproject.toml") + for toml in [ - "[tool.incremental]\n", - "[tool]\nincremental = {}\n", + '[project]\nname = "Foo"\n[tool.incremental]\n', + '[tool.incremental]\nname = "Foo"\n', ]: - with open(path, "w") as f: + with open(toml_path, "w") as f: f.write(toml) - self.assertTrue(_verify_pyproject_toml(path)) + self.assertEqual( + _load_pyproject_toml(toml_path), + _IncrementalConfig(package="Foo", path=path), + ) diff --git a/src/incremental/update.py b/src/incremental/update.py index 9867387..f3e33ee 100644 --- a/src/incremental/update.py +++ b/src/incremental/update.py @@ -6,53 +6,9 @@ import click import os import datetime -from typing import TYPE_CHECKING, Dict, Optional, Callable, Iterable +from typing import Dict, Optional, Callable -from incremental import Version - -if TYPE_CHECKING: - from typing_extensions import Protocol - - class _ReadableWritable(Protocol): - def read(self): # type: () -> bytes - pass - - def write(self, v): # type: (bytes) -> object - pass - - def __enter__(self): # type: () -> _ReadableWritable - pass - - def __exit__(self, *args, **kwargs): # type: (object, object) -> Optional[bool] - pass - - # FilePath is missing type annotations - # https://twistedmatrix.com/trac/ticket/10148 - class FilePath(object): - def __init__(self, path): # type: (str) -> None - self.path = path - - def child(self, v): # type: (str) -> FilePath - pass - - def isdir(self): # type: () -> bool - pass - - def isfile(self): # type: () -> bool - pass - - def getContent(self): # type: () -> bytes - pass - - def open(self, mode): # type: (str) -> _ReadableWritable - pass - - def walk(self): # type: () -> Iterable[FilePath] - pass - - -else: - from twisted.python.filepath import FilePath +from incremental import Version, _findPath, _existing_version _VERSIONPY_TEMPLATE = '''""" Provides {package} version information. @@ -70,34 +26,6 @@ def walk(self): # type: () -> Iterable[FilePath] _YEAR_START = 2000 -def _findPath(path, package): # type: (str, str) -> FilePath - cwd = FilePath(path) - - src_dir = cwd.child("src").child(package.lower()) - current_dir = cwd.child(package.lower()) - - if src_dir.isdir(): - return src_dir - elif current_dir.isdir(): - return current_dir - else: - raise ValueError( - "Can't find under `./src` or `./`. Check the " - "package name is right (note that we expect your " - "package name to be lower cased), or pass it using " - "'--path'." - ) - - -def _existing_version(path): # type: (FilePath) -> Version - version_info = {} # type: Dict[str, Version] - - with path.child("_version.py").open("r") as f: - exec(f.read(), version_info) - - return version_info["__version__"] - - def _run( package, # type: str path, # type: Optional[str] @@ -117,7 +45,8 @@ def _run( if not _date: _date = datetime.date.today() - _path = FilePath(path) if path else _findPath(_getcwd(), package) + if not path: + path = _findPath(_getcwd(), package) if ( newversion @@ -151,7 +80,7 @@ def _run( if newversion: from pkg_resources import parse_version - existing = _existing_version(_path) + existing = _existing_version(path) st_version = parse_version(newversion)._version # type: ignore[attr-defined] release = list(st_version.release) @@ -180,7 +109,7 @@ def _run( existing = v elif rc and not patch: - existing = _existing_version(_path) + existing = _existing_version(path) if existing.release_candidate: v = Version( @@ -194,7 +123,7 @@ def _run( v = Version(package, _date.year - _YEAR_START, _date.month, 0, 1) elif patch: - existing = _existing_version(_path) + existing = _existing_version(path) v = Version( package, existing.major, @@ -204,7 +133,7 @@ def _run( ) elif post: - existing = _existing_version(_path) + existing = _existing_version(path) if existing.post is None: _post = 0 @@ -214,7 +143,7 @@ def _run( v = Version(package, existing.major, existing.minor, existing.micro, post=_post) elif dev: - existing = _existing_version(_path) + existing = _existing_version(path) if existing.dev is None: _dev = 0 @@ -231,7 +160,7 @@ def _run( ) else: - existing = _existing_version(_path) + existing = _existing_version(path) if existing.release_candidate: v = Version(package, existing.major, existing.minor, existing.micro) @@ -249,40 +178,43 @@ def _run( _print("Updating codebase to %s" % (v.public())) - for x in _path.walk(): - if not x.isfile(): - continue - - original_content = x.getContent() - content = original_content + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + with open(filepath, "rb") as f: + original_content = f.read() + content = original_content + + # Replace previous release_candidate calls to the new one + if existing.release_candidate: + content = content.replace( + existing_version_repr_bytes, version_repr_bytes + ) + content = content.replace( + (package.encode("utf8") + b" " + existing.public().encode("utf8")), + (package.encode("utf8") + b" " + v.public().encode("utf8")), + ) + + # Replace NEXT Version calls with the new one + content = content.replace(NEXT_repr_bytes, version_repr_bytes) + content = content.replace( + NEXT_repr_bytes.replace(b"'", b'"'), version_repr_bytes + ) - # Replace previous release_candidate calls to the new one - if existing.release_candidate: - content = content.replace(existing_version_repr_bytes, version_repr_bytes) + # Replace NEXT with content = content.replace( - (package.encode("utf8") + b" " + existing.public().encode("utf8")), + package.encode("utf8") + b" NEXT", (package.encode("utf8") + b" " + v.public().encode("utf8")), ) - # Replace NEXT Version calls with the new one - content = content.replace(NEXT_repr_bytes, version_repr_bytes) - content = content.replace( - NEXT_repr_bytes.replace(b"'", b'"'), version_repr_bytes - ) - - # Replace NEXT with - content = content.replace( - package.encode("utf8") + b" NEXT", - (package.encode("utf8") + b" " + v.public().encode("utf8")), - ) - - if content != original_content: - _print("Updating %s" % (x.path,)) - with x.open("w") as f: - f.write(content) + if content != original_content: + _print("Updating %s" % (filepath,)) + with open(filepath, "wb") as f: + f.write(content) - _print("Updating %s/_version.py" % (_path.path)) - with _path.child("_version.py").open("w") as f: + versionpath = os.path.join(path, "_version.py") + _print("Updating %s" % (versionpath,)) + with open(versionpath, "wb") as f: f.write( ( _VERSIONPY_TEMPLATE.format(package=package, version_repr=version_repr) diff --git a/tests/test_examples.py b/tests/test_examples.py index 2fc8801..b5f694a 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -6,6 +6,7 @@ """ from twisted.trial.unittest import TestCase +from importlib import metadata class ExampleTests(TestCase): @@ -16,6 +17,7 @@ def test_setuppy_version(self): import example_setuppy self.assertEqual(example_setuppy.__version__.base(), "1.2.3") + self.assertEqual(metadata.version("example_setuppy"), "1.2.3") def test_setuptools_version(self): """ @@ -24,3 +26,4 @@ def test_setuptools_version(self): import example_setuptools self.assertEqual(example_setuptools.__version__.base(), "2.3.4") + self.assertEqual(metadata.version("example_setuptools"), "2.3.4") diff --git a/tox.ini b/tox.ini index 0b7f69a..93d80a9 100644 --- a/tox.ini +++ b/tox.ini @@ -16,12 +16,17 @@ wheel = true wheel_build_env = build deps = tests: coverage + tests,mypy: twisted apidocs: pydoctor lint: pre-commit extras = mypy: mypy,scripts tests: scripts +setenv = + ; Suppress pointless chatter in the output. + PIP_DISABLE_PIP_VERSION_CHECK=yes + commands = python -V @@ -29,14 +34,13 @@ commands = apidocs: pydoctor -q --project-name incremental src/incremental - tests: {envpython} -m pip list tests: coverage --version tests: {envbindir}/trial --version tests: coverage erase tests: {envpython} -c "import site; open(site.getsitepackages()[0] + '/coveragehack.pth', 'w').write('import coverage; coverage.process_startup()')" tests: coverage run -p {envbindir}/trial incremental - tests: coverage run -p -m pip install ./tests/example_setuppy - tests: coverage run -p -m pip install --use-pep517 ./tests/example_setuptools + tests: coverage run -p -m pip install --no-build-isolation ./tests/example_setuppy + tests: coverage run -p -m pip install --no-build-isolation --use-pep517 ./tests/example_setuptools tests: coverage run -p {envbindir}/trial tests/test_examples.py tests: coverage combine tests: coverage report From b2d4d8e2d44c61c8d8af3ff5f74f6fae52eaf5c9 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 21:08:19 -0700 Subject: [PATCH 13/31] Bump setuptools dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 51826df..8aa19ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ license = {file = "LICENSE"} description = "A small library that versions your Python projects." readme = "README.rst" dependencies = [ - "setuptools >= 44.1.1", + "setuptools >= 61.0", "tomli; python_version < '3.11'", ] From d08ff9d78cd5dfb4c98c65b2c6702f088e22d973 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 21:28:24 -0700 Subject: [PATCH 14/31] Fix docstring syntax --- src/incremental/__init__.py | 25 ++++++++++++------------- tox.ini | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index 2f47679..fc57d12 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -354,10 +354,10 @@ def _findPath(path, package): # type: (str, str) -> str The result is one of: - - ``src/{package}`` - - ``{package}`` + - src/{package} + - {package} - Where ``{package}`` is downcased. + Where {package} is downcased. """ src_dir = os.path.join(path, "src", package.lower()) current_dir = os.path.join(path, package.lower()) @@ -376,7 +376,7 @@ def _findPath(path, package): # type: (str, str) -> str def _existing_version(path): # type: (str) -> Version """ - Load the current version from ``{path}/_version.py``. + Load the current version from {path}/_version.py. """ version_info = {} # type: Dict[str, Version] @@ -392,11 +392,10 @@ def _get_setuptools_version(dist): # type: (_Distribution) -> None Setuptools integration: load the version from the working directory This function is registered as a setuptools.finalize_distribution_options - entry point [1]. It is a no-op unless there is a ``pyproject.toml`` containing - an empty ``[tool.incremental]`` section. + entry point [1]. It is a no-op unless there is a pyproject.toml containing + an empty [tool.incremental] section. - :param dist: - An empty `setuptools.Distribution` instance to mutate. + @param dist: An empty C{setuptools.Distribution} instance to mutate. [1]: https://setuptools.pypa.io/en/latest/userguide/extension.html#customizing-distribution-options """ @@ -415,9 +414,9 @@ def _get_distutils_version(dist, keyword, value): # type: (_Distribution, objec """ Distutils integration: get the version from the package listed in the Distribution. - This function is invoked when a ``setup.py`` calls `setup(use_incremental=True)`. + This function is invoked when a C{setup.py} calls C{setup(use_incremental=True)}. - See https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-arguments + @see: https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-arguments """ if not value: # use_incremental=False return @@ -464,12 +463,12 @@ class _IncrementalConfig: def _load_pyproject_toml(toml_path): # type: (str) -> _IncrementalConfig | None: """ - Does the ``pyproject.toml`` file contain a ``[tool.incremental]`` + Does the pyproject.toml file contain a [tool.incremental] section? This indicates that the package has opted-in to Incremental versioning. - If the ``[tool.incremental]`` section is empty we take the project name - from the ``[project]`` section. Otherwise we require only a ``name`` key + If the [tool.incremental] section is empty we take the project name + from the [project] section. Otherwise we require only a C{name} key specifying the project name. Other keys are forbidden to allow future extension and catch typos. """ diff --git a/tox.ini b/tox.ini index 93d80a9..962bc3d 100644 --- a/tox.ini +++ b/tox.ini @@ -37,7 +37,7 @@ commands = tests: coverage --version tests: {envbindir}/trial --version tests: coverage erase - tests: {envpython} -c "import site; open(site.getsitepackages()[0] + '/coveragehack.pth', 'w').write('import coverage; coverage.process_startup()')" + tests: {envpython} -c "import site; open(site.getsitepackages()[0] + '/_coveragehack.pth', 'w').write('import coverage; coverage.process_startup()')" tests: coverage run -p {envbindir}/trial incremental tests: coverage run -p -m pip install --no-build-isolation ./tests/example_setuppy tests: coverage run -p -m pip install --no-build-isolation --use-pep517 ./tests/example_setuptools From 54380cab09c71ccfb2b51458b4b43826f85e6897 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 21:35:53 -0700 Subject: [PATCH 15/31] Update readme --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2a0e6e7..859a261 100644 --- a/README.rst +++ b/README.rst @@ -26,9 +26,10 @@ Specify the project's version as dynamic: .. code-block:: toml [project] + name = "" dynamic = ["version"] -Remove any ``version`` specification and any ``[tool.setuptools.dynamic] version = `` block. +Remove any ``version`` line and any ``[tool.setuptools.dynamic] version = `` entry. Add this empty block to activate Incremental's setuptools plugin: @@ -44,7 +45,7 @@ It will create a file in your package named ``_version.py`` and look like this: from incremental import Version - __version__ = Version("widgetbox", 17, 1, 0) + __version__ = Version("", 24, 1, 0) __all__ = ["__version__"] From 895a9a95a84b75b2c2128a4dccb80f859fb8e927 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 21:36:51 -0700 Subject: [PATCH 16/31] Fix type annotation --- src/incremental/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index fc57d12..ec45db0 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -461,7 +461,7 @@ class _IncrementalConfig: path: str -def _load_pyproject_toml(toml_path): # type: (str) -> _IncrementalConfig | None: +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 From 4d5a6f839c5bd260dca8d00244ac6dd46dfae82e Mon Sep 17 00:00:00 2001 From: Tom Most Date: Tue, 2 Jul 2024 21:37:45 -0700 Subject: [PATCH 17/31] Re-enable build isolation Disabling this isn't helping get coverage. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 962bc3d..a7f2163 100644 --- a/tox.ini +++ b/tox.ini @@ -39,8 +39,8 @@ commands = tests: coverage erase tests: {envpython} -c "import site; open(site.getsitepackages()[0] + '/_coveragehack.pth', 'w').write('import coverage; coverage.process_startup()')" tests: coverage run -p {envbindir}/trial incremental - tests: coverage run -p -m pip install --no-build-isolation ./tests/example_setuppy - tests: coverage run -p -m pip install --no-build-isolation --use-pep517 ./tests/example_setuptools + tests: coverage run -p -m pip install ./tests/example_setuppy + tests: coverage run -p -m pip install --use-pep517 ./tests/example_setuptools tests: coverage run -p {envbindir}/trial tests/test_examples.py tests: coverage combine tests: coverage report From 59f0f38d1d6b25db2515054f3a5d916709b97853 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 6 Jul 2024 22:19:02 -0700 Subject: [PATCH 18/31] Use coverage-p MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This package provides the .pth hack to start coverage in sub-processes. By adding it to the build requirements of the example packages we may be able to get coverage within Pip's isolated build environment. 🤞 --- src/incremental/__init__.py | 4 ---- tests/example_setuppy/setup.py | 5 ++++- tests/example_setuptools/pyproject.toml | 1 + tox.ini | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index ec45db0..4feb21b 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -406,10 +406,6 @@ def _get_setuptools_version(dist): # type: (_Distribution) -> None dist.metadata.version = _existing_version(config.path).public() -# Call this hook after all of the built-in setuptools ones, or the Distribution is empty. -_get_setuptools_version.order = 1 - - def _get_distutils_version(dist, keyword, value): # type: (_Distribution, object, object) -> None """ Distutils integration: get the version from the package listed in the Distribution. diff --git a/tests/example_setuppy/setup.py b/tests/example_setuppy/setup.py index ad19d62..5396706 100644 --- a/tests/example_setuppy/setup.py +++ b/tests/example_setuppy/setup.py @@ -6,5 +6,8 @@ packages=["example_setuppy"], use_incremental=True, zip_safe=False, - setup_requires=["incremental"], + setup_requires=[ + "incremental", + "coverage-p", # Capture coverage when building the package. + ], ) diff --git a/tests/example_setuptools/pyproject.toml b/tests/example_setuptools/pyproject.toml index beca60e..e3dfe3c 100644 --- a/tests/example_setuptools/pyproject.toml +++ b/tests/example_setuptools/pyproject.toml @@ -2,6 +2,7 @@ requires = [ "setuptools", "incremental", + "coverage-p", # Capture coverage when building the package. ] build-backend = "setuptools.build_meta" diff --git a/tox.ini b/tox.ini index a7f2163..2be0a0e 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ wheel = true wheel_build_env = build deps = tests: coverage + tests: coverage-p tests,mypy: twisted apidocs: pydoctor lint: pre-commit @@ -37,10 +38,9 @@ commands = tests: coverage --version tests: {envbindir}/trial --version tests: coverage erase - tests: {envpython} -c "import site; open(site.getsitepackages()[0] + '/_coveragehack.pth', 'w').write('import coverage; coverage.process_startup()')" tests: coverage run -p {envbindir}/trial incremental - tests: coverage run -p -m pip install ./tests/example_setuppy - tests: coverage run -p -m pip install --use-pep517 ./tests/example_setuptools + tests: coverage run -p -m pip install --use-pep517 --find-links {distdir} ./tests/example_setuppy + tests: coverage run -p -m pip -vvv install --use-pep517 --find-links {distdir} ./tests/example_setuptools tests: coverage run -p {envbindir}/trial tests/test_examples.py tests: coverage combine tests: coverage report From fe3f0d38ca03865aa0bca0914087c510dff7e3be Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 6 Jul 2024 22:27:31 -0700 Subject: [PATCH 19/31] Actually fix type annotation --- src/incremental/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index 4feb21b..90681f3 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -457,7 +457,7 @@ class _IncrementalConfig: path: str -def _load_pyproject_toml(toml_path): # type: (str) -> Optional[_IncrementalConfig]: +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 From 690994e9cbeba3cf13d5ff7c52666d78796e0cf7 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 6 Jul 2024 22:29:22 -0700 Subject: [PATCH 20/31] Omit redundant license text This is already included in file form, so there's no reason to copy the text of the file into the package metadata too. --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8aa19ec..a7f7812 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] requires-python = ">=3.8" -license = {file = "LICENSE"} description = "A small library that versions your Python projects." readme = "README.rst" dependencies = [ From 799b7f16794aef22d7d543230f605f7298eea8cd Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 6 Jul 2024 23:34:11 -0700 Subject: [PATCH 21/31] Fix coverage capture - Move build/install into the test methods to reduce coupling with tox.ini - Add the missing COVERAGE_PROCESS_START environment variable - Move -p (parallel) to the config file - When combining, pull coverage from the example directories because the package build process uses them as a working directory --- .coveragerc | 1 + tests/test_examples.py | 22 +++++++++++++++++++++- tox.ini | 11 ++++++----- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index ac2cbb9..f97e9b6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,7 @@ source_pkgs = incremental # List of directory names. source = tests branch = True +parallel = True [paths] source = diff --git a/tests/test_examples.py b/tests/test_examples.py index b5f694a..4004850 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -5,8 +5,24 @@ Tests for the packaging examples. """ -from twisted.trial.unittest import TestCase +import os from importlib import metadata +from subprocess import run + +from build import ProjectBuilder +from twisted.python.filepath import FilePath +from twisted.trial.unittest import TestCase + + +TEST_DIR = FilePath(os.path.abspath(os.path.dirname(__file__))) + + +def build_and_install(path): # type: (FilePath) -> None + builder = ProjectBuilder(path.path) + pkgfile = builder.build("wheel", output_directory=os.environ["PIP_FIND_LINKS"]) + + # Force reinstall in case tox reused the venv. + run(["pip", "install", "--force-reinstall", pkgfile], check=True) class ExampleTests(TestCase): @@ -14,6 +30,8 @@ def test_setuppy_version(self): """ example_setuppy has a version of 1.2.3. """ + build_and_install(TEST_DIR.child("example_setuppy")) + import example_setuppy self.assertEqual(example_setuppy.__version__.base(), "1.2.3") @@ -23,6 +41,8 @@ def test_setuptools_version(self): """ example_setuptools has a version of 2.3.4. """ + build_and_install(TEST_DIR.child("example_setuptools")) + import example_setuptools self.assertEqual(example_setuptools.__version__.base(), "2.3.4") diff --git a/tox.ini b/tox.ini index 2be0a0e..a392c6a 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ envlist = wheel = true wheel_build_env = build deps = + tests: build tests: coverage tests: coverage-p tests,mypy: twisted @@ -27,6 +28,8 @@ extras = setenv = ; Suppress pointless chatter in the output. PIP_DISABLE_PIP_VERSION_CHECK=yes + tests: PIP_FIND_LINKS={distdir} + tests: COVERAGE_PROCESS_START={toxinidir}/.coveragerc commands = python -V @@ -38,11 +41,9 @@ commands = tests: coverage --version tests: {envbindir}/trial --version tests: coverage erase - tests: coverage run -p {envbindir}/trial incremental - tests: coverage run -p -m pip install --use-pep517 --find-links {distdir} ./tests/example_setuppy - tests: coverage run -p -m pip -vvv install --use-pep517 --find-links {distdir} ./tests/example_setuptools - tests: coverage run -p {envbindir}/trial tests/test_examples.py - tests: coverage combine + tests: coverage run {envbindir}/trial incremental + tests: coverage run {envbindir}/trial tests/test_examples.py + tests: coverage combine . tests/example_setuppy tests/example_setuptools tests: coverage report tests: coverage html tests: coverage xml From a7e100e5560a3066691a6263728f87d5e3c98c97 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 6 Jul 2024 23:46:01 -0700 Subject: [PATCH 22/31] Fix more MyPy noise --- src/incremental/__init__.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index 90681f3..d8e75c5 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -226,7 +226,7 @@ def __repr__(self): # type: () -> str def __str__(self): # type: () -> str return "[%s, version %s]" % (self.package, self.short()) - def __cmp__(self, other): # type: (Version) -> int + def __cmp__(self, other): # type: (object) -> int """ Compare two versions, considering major versions, minor versions, micro versions, then release candidates, then postreleases, then dev @@ -300,40 +300,40 @@ def __cmp__(self, other): # type: (Version) -> int ) return x - def __eq__(self, other): + def __eq__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c + return c # type: ignore return c == 0 - def __ne__(self, other): + def __ne__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c + return c # type: ignore return c != 0 - def __lt__(self, other): + def __lt__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c + return c # type: ignore return c < 0 - def __le__(self, other): + def __le__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c + return c # type: ignore return c <= 0 - def __gt__(self, other): + def __gt__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c + return c # type: ignore return c > 0 - def __ge__(self, other): + def __ge__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c + return c # type: ignore return c >= 0 From 81bfd06ca15545bb9fa5affc9647aa3931d18be0 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 6 Jul 2024 23:48:26 -0700 Subject: [PATCH 23/31] Skip the remaining coverage holes --- src/incremental/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index d8e75c5..7586e32 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -415,7 +415,7 @@ def _get_distutils_version(dist, keyword, value): # type: (_Distribution, objec @see: https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-arguments """ if not value: # use_incremental=False - return + return # pragma: no cover from setuptools.command import build_py # type: ignore @@ -428,7 +428,7 @@ def _get_distutils_version(dist, keyword, value): # type: (_Distribution, objec dist.metadata.version = _existing_version(package_path).public() return - raise Exception("No _version.py found.") + raise Exception("No _version.py found.") # pragma: no cover def _load_toml(f): # type: (BinaryIO) -> Any From 6f17f0ee70ebcd7aea45dd8e61a1a8ef8d61f8ba Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sun, 7 Jul 2024 12:33:45 -0700 Subject: [PATCH 24/31] Remove unnecessary build deps The examples don't need the coverage-p hack because the tests don't isolate the build. --- tests/example_setuppy/setup.py | 1 - tests/example_setuptools/pyproject.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/example_setuppy/setup.py b/tests/example_setuppy/setup.py index 5396706..4279303 100644 --- a/tests/example_setuppy/setup.py +++ b/tests/example_setuppy/setup.py @@ -8,6 +8,5 @@ zip_safe=False, setup_requires=[ "incremental", - "coverage-p", # Capture coverage when building the package. ], ) diff --git a/tests/example_setuptools/pyproject.toml b/tests/example_setuptools/pyproject.toml index e3dfe3c..beca60e 100644 --- a/tests/example_setuptools/pyproject.toml +++ b/tests/example_setuptools/pyproject.toml @@ -2,7 +2,6 @@ requires = [ "setuptools", "incremental", - "coverage-p", # Capture coverage when building the package. ] build-backend = "setuptools.build_meta" From 6e6e4ee9f742f04a62eef595fa9c7dc9addbb880 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sun, 7 Jul 2024 12:57:08 -0700 Subject: [PATCH 25/31] Write all coverage to {toxinidir} --- .coveragerc | 3 +++ tox.ini | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index f97e9b6..bfe9d69 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,9 @@ source_pkgs = incremental source = tests branch = True parallel = True +# This must be an absolute path because the example tests +# run Python processes with alternative working directories. +data_file = ${TOX_INI_DIR-.}/.coverage [paths] source = diff --git a/tox.ini b/tox.ini index a392c6a..948d0fb 100644 --- a/tox.ini +++ b/tox.ini @@ -28,8 +28,10 @@ extras = setenv = ; Suppress pointless chatter in the output. PIP_DISABLE_PIP_VERSION_CHECK=yes - tests: PIP_FIND_LINKS={distdir} + + tests: TOX_INI_DIR={toxinidir} tests: COVERAGE_PROCESS_START={toxinidir}/.coveragerc + tests: PIP_FIND_LINKS={distdir} commands = python -V @@ -43,7 +45,7 @@ commands = tests: coverage erase tests: coverage run {envbindir}/trial incremental tests: coverage run {envbindir}/trial tests/test_examples.py - tests: coverage combine . tests/example_setuppy tests/example_setuptools + tests: coverage combine tests: coverage report tests: coverage html tests: coverage xml From bd4d87bb1ec94a3a38f41980b3580ea97f467bb3 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sun, 7 Jul 2024 13:25:24 -0700 Subject: [PATCH 26/31] A fresh new setuptools hack --- _build_meta.py | 18 ++++++++++++++++++ pyproject.toml | 8 +++++--- src/incremental/__init__.py | 4 ---- 3 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 _build_meta.py diff --git a/_build_meta.py b/_build_meta.py new file mode 100644 index 0000000..8fc3cd8 --- /dev/null +++ b/_build_meta.py @@ -0,0 +1,18 @@ +""" +Comply with PEP 517's restictions on in-tree backends. + +We use setuptools to package Incremental, but also want to use +Incremental to manage its own version. To do this we specify +``backend-path`` in our ``pyproject.toml``, but PEP 517 requires +that when ``backend-path`` is specified: + +> The backend code MUST be loaded from one of the directories +> specified in backend-path (i.e., it is not permitted to +> specify backend-path and not have in-tree backend code). + +We comply by re-publishing setuptools' build_meta. +""" + +from setuptools import build_meta + +__all__ = ["build_meta"] diff --git a/pyproject.toml b/pyproject.toml index a7f7812..dd693ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,11 @@ [build-system] requires = [ + # Keep this aligned with the project dependencies. "setuptools >= 61.0", + "tomli; python_version < '3.11'", ] -build-backend = "setuptools.build_meta" +backend-path = [".", "./src"] # See _build_meta.py +build-backend = "_build_meta:build_meta" [project] name = "incremental" @@ -47,8 +50,7 @@ use_incremental = "incremental:_get_distutils_version" [project.entry-points."setuptools.finalize_distribution_options"] incremental = "incremental:_get_setuptools_version" -[tool.setuptools.dynamic] -version = {attr = "incremental._setuptools_version"} +[tool.incremental] [tool.black] target-version = ['py36', 'py37', 'py38'] diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index 7586e32..94b6d33 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -517,8 +517,4 @@ def _load_pyproject_toml(toml_path): # type: (str) -> Optional[_IncrementalConf from ._version import __version__ # noqa: E402 -def _setuptools_version(): # type: () -> str - return __version__.public() - - __all__ = ["__version__", "Version", "getVersionString"] From 6cce917e9fa63bb10a26029bf1e377b0c782c819 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sun, 7 Jul 2024 13:42:57 -0700 Subject: [PATCH 27/31] Reduce diff size --- src/incremental/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index 94b6d33..9e89b8a 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -303,37 +303,37 @@ def __cmp__(self, other): # type: (object) -> int def __eq__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c # type: ignore + return c # type: ignore[return-value] return c == 0 def __ne__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c # type: ignore + return c # type: ignore[return-value] return c != 0 def __lt__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c # type: ignore + return c # type: ignore[return-value] return c < 0 def __le__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c # type: ignore + return c # type: ignore[return-value] return c <= 0 def __gt__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c # type: ignore + return c # type: ignore[return-value] return c > 0 def __ge__(self, other): # type: (object) -> bool c = self.__cmp__(other) if c is NotImplemented: - return c # type: ignore + return c # type: ignore[return-value] return c >= 0 From bb8a91809fd558b66ecb6a4efac6731a36c254fa Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sun, 7 Jul 2024 13:46:57 -0700 Subject: [PATCH 28/31] Add Framework :: Setuptools Plugin classifier --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index dd693ee..d80bd8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ maintainers = [ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", + "Framework :: Setuptools Plugin", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From 9ddfbc98f8d23fd89ccab22190506da1f64dae4f Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sun, 7 Jul 2024 14:39:15 -0700 Subject: [PATCH 29/31] Minor docstring tweaks --- README.rst | 2 +- _build_meta.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 859a261..6f5c925 100644 --- a/README.rst +++ b/README.rst @@ -60,7 +60,7 @@ Subsequent installations of your project will then use Incremental for versionin Using ``setup.py`` -~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~ Incremental may be used from ``setup.py`` instead of ``pyproject.toml``. Add this to your ``setup()`` call, removing any other versioning arguments: diff --git a/_build_meta.py b/_build_meta.py index 8fc3cd8..a5bd38c 100644 --- a/_build_meta.py +++ b/_build_meta.py @@ -1,16 +1,16 @@ """ Comply with PEP 517's restictions on in-tree backends. -We use setuptools to package Incremental, but also want to use -Incremental to manage its own version. To do this we specify -``backend-path`` in our ``pyproject.toml``, but PEP 517 requires -that when ``backend-path`` is specified: +We use setuptools to package Incremental and want to activate +the in-tree Incremental plugin to manage its own version. To do +this we specify ``backend-path`` in our ``pyproject.toml``, +but PEP 517 requires that when ``backend-path`` is specified: > The backend code MUST be loaded from one of the directories > specified in backend-path (i.e., it is not permitted to > specify backend-path and not have in-tree backend code). -We comply by re-publishing setuptools' build_meta. +We comply by re-publishing setuptools' ``build_meta``. """ from setuptools import build_meta From 2a50e4c54e25cb28408fbd6aec295b1a0971c707 Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sun, 7 Jul 2024 15:49:57 -0700 Subject: [PATCH 30/31] Fix actions reference --- .github/workflows/tests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 91a5a5e..db25f29 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -86,13 +86,13 @@ jobs: run: | # Assign the coverage file a name unique to this job so that the # uploads don't collide. - mv .coverage ".coverage-job-${{ matrix.python-version }}-${{ matrix.job-name }}" + mv .coverage ".coverage-job-${{ matrix.python-version }}-${{ matrix.tox-env }}" - name: Store coverage file if: ${{ !cancelled() && !matrix.skip-coverage }} uses: actions/upload-artifact@v4 with: - name: coverage-${{ matrix.python-version }}-${{ matrix.job-name }} + name: coverage-${{ matrix.python-version }}-${{ matrix.tox-env }} path: .coverage-job-* coverage-report: From e12b04a79cc9606e11cd7752e1f56731d2ce56ee Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sun, 7 Jul 2024 16:41:39 -0700 Subject: [PATCH 31/31] Fix up MANIFEST.in - Include _build_meta.py so that python -m build works. - Include all the test files in the sdist. Fixes #80. --- MANIFEST.in | 21 ++++++++++++--------- src/incremental/newsfragments/80.bugfix | 1 + 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 src/incremental/newsfragments/80.bugfix diff --git a/MANIFEST.in b/MANIFEST.in index dcb8916..c228f40 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,17 +1,20 @@ -include .coveragerc include LICENSE include NEWS.rst -include towncrier.ini +include SECURITY.md include tox.ini -exclude mypy.ini -include src/incremental/py.typed +include _build_meta.py recursive-include src/incremental *.py +include src/incremental/py.typed +prune src/incremental/newsfragments -prune .travis -prune tests +global-exclude .coverage* +include .coveragerc -exclude examplesetup.py -prune src/exampleproj +graft tests +prune tests/example_*/src/*.egg-info +prune tests/example_*/build +prune tests/example_*/dist + +global-exclude __pycache__ *.py[cod] *~ -prune src/incremental/newsfragments diff --git a/src/incremental/newsfragments/80.bugfix b/src/incremental/newsfragments/80.bugfix new file mode 100644 index 0000000..6d87126 --- /dev/null +++ b/src/incremental/newsfragments/80.bugfix @@ -0,0 +1 @@ +Incremental's tests are now included in the sdist release artifact.