From 90656a19072681d8546e273f1000329bfb284ffe Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Sat, 24 Jun 2023 00:28:43 +1200 Subject: [PATCH 1/3] Remove incremental --- .github/workflows/ci.yml | 2 +- RELEASE.rst | 13 +++-- admin/canonicalize_version.py | 32 ------------ docs/conf.py | 13 +++-- pyproject.toml | 16 +----- src/towncrier/__init__.py | 26 ---------- src/towncrier/_project.py | 54 ++++++++++--------- src/towncrier/_shell.py | 11 +++- src/towncrier/_version.py | 16 ------ src/towncrier/test/test_packaging.py | 16 ++---- src/towncrier/test/test_project.py | 78 ++++++++++++++++++++-------- 11 files changed, 111 insertions(+), 166 deletions(-) delete mode 100644 admin/canonicalize_version.py delete mode 100644 src/towncrier/_version.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee65930f..eb5bcd44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -215,7 +215,7 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip wheel python -m pip install pep517 - name: Display structure of files to be pushed diff --git a/RELEASE.rst b/RELEASE.rst index 6a4e9918..ebdea07f 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -19,9 +19,9 @@ The same branch is used for the release candidated and the final release. In the end, the release branch is merged into the main branch. Update the version to the release candidate with the first being ``rc1`` (as opposed to 0). -In ``src/towncrier/_version.py`` the version is set using ``incremental`` such as:: +In ``pyproject.toml`` the version is set like:: - __version__ = Version('towncrier', 19, 9, 0, release_candidate=1) + version = "19.9.0.rc1" Run ``venv/bin/towncrier build --yes`` to generate the news release NEWS file. Commit and push to the primary repository, not a fork. @@ -59,9 +59,9 @@ Final release Once the PR is approved, you can trigger the final release. Update the version to the final version. -In ``src/towncrier/_version.py`` the version is set using ``incremental`` such as:: +In ``pyproject.toml`` the version is set like:: - __version__ = Version('towncrier', 19, 9, 0) + version = "19.9.0" Manually update the `NEWS.rst` file to include the final release version and date. Usually it will look like this:: @@ -88,10 +88,9 @@ Similar to the release candidate, with the difference: No need for another review request. Update the version to the development version. -In ``src/towncrier/_version.py`` the version is set using ``incremental`` such as:: - - __version__ = Version('towncrier', 19, 9, 1, dev=0) +In ``pyproject.toml`` the version is set like:: + version = "19.9.1.dev0" Commit and push the changes. diff --git a/admin/canonicalize_version.py b/admin/canonicalize_version.py deleted file mode 100644 index 671b8c23..00000000 --- a/admin/canonicalize_version.py +++ /dev/null @@ -1,32 +0,0 @@ -import click -import incremental -import packaging.utils - - -@click.command() -@click.argument("version") -def cli(version): - """Canonicalizes the passed version according to incremental.""" - - parsed_version = packaging.utils.Version(version) - - release_candidate = None - if parsed_version.pre is not None: - if parsed_version.pre[0] == "rc": - release_candidate = parsed_version.pre[1] - - incremental_version = incremental.Version( - package="", - major=parsed_version.major, - minor=parsed_version.minor, - micro=parsed_version.micro, - release_candidate=release_candidate, - post=parsed_version.post, - dev=parsed_version.dev, - ) - - click.echo(incremental_version.public()) - - -if __name__ == "__main__": - cli() diff --git a/docs/conf.py b/docs/conf.py index ad06fee2..c625d898 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,11 +29,12 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -from datetime import date +import importlib.metadata as importlib_metadata -from towncrier import __version__ as towncrier_version +from datetime import date +towncrier_version = importlib_metadata.version("towncrier") extensions = [] # Add any paths that contain templates here, relative to this directory. @@ -53,7 +54,7 @@ project = "Towncrier" copyright = "{}, Towncrier contributors. Ver {}".format( _today.year, - towncrier_version.public(), + importlib_metadata.version("towncrier"), ) author = "Amber Brown" @@ -61,11 +62,9 @@ # |version| and |release|, also used in various other places throughout the # built documents. # The short X.Y version. -version = "{}.{}.{}".format( - towncrier_version.major, towncrier_version.minor, towncrier_version.micro -) +version = ".".join(towncrier_version.split(".")[:3]) # The full version, including alpha/beta/rc tags. -release = towncrier_version.public() +release = towncrier_version # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/pyproject.toml b/pyproject.toml index 240da5d7..ac70153f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [build-system] requires = [ "hatchling ~= 1.17.1", - "incremental == 22.10.0", + "wheel ~= 0.38.4", ] build-backend = "hatchling.build" [project] -dynamic = ["version"] name = "towncrier" +version = "23.6.1.dev0" description = "Building newsfiles for your project." readme = "README.rst" license = "MIT" @@ -30,7 +30,6 @@ dependencies = [ "click", "click-default-group", "importlib-resources>=5; python_version<'3.10'", - "incremental", "jinja2", "tomli; python_version<'3.11'", ] @@ -57,11 +56,6 @@ Coverage = "https://codecov.io/gh/twisted/towncrier" Distribution = "https://pypi.org/project/towncrier" -[tool.hatch.version] -source = "code" -path = "src/towncrier/_version.py" -expression = "_hatchling_version" - [tool.hatch.build] exclude = [ "admin", @@ -150,12 +144,6 @@ module = 'click_default_group' # 2022-09-04: This library has no type annotations. ignore_missing_imports = true -[[tool.mypy.overrides]] -module = 'incremental' -# No released version with type hints. -ignore_missing_imports = true - - [tool.coverage.run] parallel = true branch = true diff --git a/src/towncrier/__init__.py b/src/towncrier/__init__.py index 47a81404..0c504e1f 100644 --- a/src/towncrier/__init__.py +++ b/src/towncrier/__init__.py @@ -4,29 +4,3 @@ """ towncrier, a builder for your news files. """ - -from __future__ import annotations - -from incremental import Version - - -__all__ = ["__version__"] - - -def __getattr__(name: str) -> Version: - if name != "__version__": - raise AttributeError(f"module {__name__} has no attribute {name}") - - import warnings - - from ._version import __version__ - - warnings.warn( - "Accessing towncrier.__version__ is deprecated and will be " - "removed in a future release. Use importlib.metadata directly " - "to query for towncrier's packaging metadata.", - DeprecationWarning, - stacklevel=2, - ) - - return __version__ diff --git a/src/towncrier/_project.py b/src/towncrier/_project.py index cfaa11fa..2c03b6f2 100644 --- a/src/towncrier/_project.py +++ b/src/towncrier/_project.py @@ -8,12 +8,12 @@ from __future__ import annotations +import importlib.metadata as importlib_metadata import sys from importlib import import_module from types import ModuleType - -from incremental import Version as IncrementalVersion +from typing import Any def _get_package(package_dir: str, package: str) -> ModuleType: @@ -38,47 +38,45 @@ def _get_package(package_dir: str, package: str) -> ModuleType: def get_version(package_dir: str, package: str) -> str: - module = _get_package(package_dir, package) + version: Any + module = _get_package(package_dir, package) version = getattr(module, "__version__", None) - if not version: - raise Exception("No __version__, I don't know how else to look") + try: + version = importlib_metadata.version(package) + except importlib_metadata.PackageNotFoundError: + raise Exception(f"Package not installed and no {package}.__version__ found") if isinstance(version, str): return version.strip() - if isinstance(version, IncrementalVersion): - # FIXME:https://github.com/twisted/incremental/issues/81 - # Incremental uses `.rcN`. - # importlib uses `rcN` (without a dot separation). - # Here we make incremental work like importlib. - return version.base().strip().replace(".rc", "rc") - if isinstance(version, tuple): return ".".join(map(str, version)).strip() + # Try duck-typing as an Incremental version. + if hasattr(version, "base"): + try: + version = str(version.base()).strip() + # Incremental uses `X.Y.rcN`. + # Standardize on importlib (and PEP440) use of `X.YrcN`: + return version.replace(".rc", "rc") # type: ignore + except TypeError: + pass + raise Exception( - "I only know how to look at a __version__ that is a str, " - "an Increment Version, or a tuple. If you can't provide " - "that, use the --version argument and specify one." + "Version must be a string, tuple, or an Incremental Version." + " If you can't provide that, use the --version argument and specify one." ) def get_project_name(package_dir: str, package: str) -> str: module = _get_package(package_dir, package) - version = getattr(module, "__version__", None) + # Incremental has support for package names, try duck-typing it. + try: + return str(version.package) # type: ignore + except AttributeError: + pass - if not version: - # welp idk - return package.title() - - if isinstance(version, str): - return package.title() - - if isinstance(version, IncrementalVersion): - # Incremental has support for package names - return version.package - - raise TypeError(f"Unsupported type for __version__: {type(version)}") + return package.title() diff --git a/src/towncrier/_shell.py b/src/towncrier/_shell.py index 0efe1b69..24642e3e 100644 --- a/src/towncrier/_shell.py +++ b/src/towncrier/_shell.py @@ -9,18 +9,25 @@ from __future__ import annotations +from importlib.metadata import PackageNotFoundError, version + import click from click_default_group import DefaultGroup -from ._version import __version__ from .build import _main as _build_cmd from .check import _main as _check_cmd from .create import _main as _create_cmd +try: + _version = version("towncrier") +except PackageNotFoundError: # pragma: no cover + _version = "unknown" + + @click.group(cls=DefaultGroup, default="build", default_if_no_args=True) -@click.version_option(__version__.public()) +@click.version_option(_version) def cli() -> None: """ Towncrier is a utility to produce useful, summarised news files for your project. diff --git a/src/towncrier/_version.py b/src/towncrier/_version.py deleted file mode 100644 index 7b50856e..00000000 --- a/src/towncrier/_version.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Provides towncrier version information. -""" - -# This file is auto-generated! Do not edit! -# Use `python -m incremental.update towncrier` to change this file. - -from incremental import Version - - -__version__ = Version("towncrier", 23, 6, 1, dev=0) -# The version is exposed in string format to be -# available for the hatching build tools. -_hatchling_version = __version__.short() - -__all__ = ["__version__", "_hatchling_version"] diff --git a/src/towncrier/test/test_packaging.py b/src/towncrier/test/test_packaging.py index 3355ad51..2dbcbfbf 100644 --- a/src/towncrier/test/test_packaging.py +++ b/src/towncrier/test/test_packaging.py @@ -1,22 +1,16 @@ # Copyright (c) Amber Brown, 2015 # See LICENSE for details. -from incremental import Version from twisted.trial.unittest import TestCase -from towncrier._version import _hatchling_version +import towncrier class TestPackaging(TestCase): - def test_version_warning(self): + def no_version_attr(self): """ - Import __version__ from towncrier returns an Incremental version object - and raises a warning. + towncrier.__version__ was deprecated, now no longer exists. """ - with self.assertWarnsRegex( - DeprecationWarning, "Accessing towncrier.__version__ is deprecated.*" - ): - from towncrier import __version__ - self.assertIsInstance(__version__, Version) - self.assertEqual(_hatchling_version, __version__.short()) + with self.assertRaises(AttributeError): + towncrier.__version__ diff --git a/src/towncrier/test/test_project.py b/src/towncrier/test/test_project.py index c41c3a55..3412bb60 100644 --- a/src/towncrier/test/test_project.py +++ b/src/towncrier/test/test_project.py @@ -5,7 +5,6 @@ import sys from subprocess import check_output -from unittest import skipIf from twisted.trial.unittest import TestCase @@ -13,12 +12,6 @@ from .helpers import write -try: - from importlib.metadata import version as metadata_version -except ImportError: - metadata_version = None - - class VersionFetchingTests(TestCase): def test_str(self): """ @@ -46,25 +39,65 @@ def test_tuple(self): version = get_version(temp, "mytestproja") self.assertEqual(version, "1.3.12") - @skipIf(metadata_version is None, "Needs importlib.metadata.") def test_incremental(self): """ - An incremental Version __version__ is picked up. + An incremental-like Version __version__ is picked up. + """ + temp = self.mktemp() + os.makedirs(temp) + os.makedirs(os.path.join(temp, "mytestprojinc")) + + with open(os.path.join(temp, "mytestprojinc", "__init__.py"), "w") as f: + f.write( + """ +class Version: + ''' + This is emulating a Version object from incremental. + ''' + + def __init__(self, *version_parts): + self.version = version_parts + + def base(self): + return '.'.join(map(str, self.version)) + + +__version__ = Version(1, 3, 12, "rc1") +""" + ) + version = get_version(temp, "mytestprojinc") + self.assertEqual(version, "1.3.12rc1") + + def test_not_incremental(self): + """ + An incremental-like Version __version__ is picked up, base takes unexpected + arguments. """ - pkg = "../src" + temp = self.mktemp() + os.makedirs(os.path.join(temp, "mytestprojnotinc")) - with self.assertWarnsRegex( - DeprecationWarning, "Accessing towncrier.__version__ is deprecated.*" - ): - version = get_version(pkg, "towncrier") + with open(os.path.join(temp, "mytestprojnotinc", "__init__.py"), "w") as f: + f.write( + """ +class WeirdVersion: + def base(self, some_arg): + return "shouldn't get here" - with self.assertWarnsRegex( - DeprecationWarning, "Accessing towncrier.__version__ is deprecated.*" - ): - name = get_project_name(pkg, "towncrier") - self.assertEqual(metadata_version("towncrier"), version) - self.assertEqual("towncrier", name) +__version__ = WeirdVersion() +""" + ) + with self.assertRaises(Exception) as e: + get_version(temp, "mytestprojnotinc") + + self.assertEqual( + ( + "Version must be a string, tuple, or an Incremental Version. " + "If you can't provide that, use the --version argument and " + "specify one.", + ), + e.exception.args, + ) def _setup_missing(self): """ @@ -90,7 +123,8 @@ def test_missing_version(self): get_version(tmp_dir, "missing") self.assertEqual( - ("No __version__, I don't know how else to look",), e.exception.args + ("Package not installed and no missing.__version__ found",), + e.exception.args, ) def test_missing_version_project_name(self): @@ -114,7 +148,7 @@ def test_unknown_type(self): self.assertRaises(Exception, get_version, temp, "mytestprojb") - self.assertRaises(TypeError, get_project_name, temp, "mytestprojb") + self.assertEqual("Mytestprojb", get_project_name(temp, "mytestprojb")) def test_import_fails(self): """ From 396a84b6eca42f471bc7c6f0b978b9cc5902e16b Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Sat, 24 Jun 2023 00:28:43 +1200 Subject: [PATCH 2/3] Add news fragment --- src/towncrier/newsfragments/491.misc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/towncrier/newsfragments/491.misc diff --git a/src/towncrier/newsfragments/491.misc b/src/towncrier/newsfragments/491.misc new file mode 100644 index 00000000..1fc092d2 --- /dev/null +++ b/src/towncrier/newsfragments/491.misc @@ -0,0 +1,3 @@ +Remove incremental dependency from towncrier. + +Towncrier can still read incremental versions, it just doesn't rely on the package itself any more. From accc883143fe2d53b005d02485f882c750d250d6 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Sat, 24 Jun 2023 01:16:30 +1200 Subject: [PATCH 3/3] Update tutorial for accuracy around detecting versions --- docs/tutorial.rst | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 0a42ff35..3cbfb63f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -51,13 +51,19 @@ The ``.gitignore`` will remain and keep Git from not tracking the directory. Detecting Dates & Versions -------------------------- -``towncrier`` needs to know what version your project is, and there are two ways you can give it: +``towncrier`` needs to know what version your project is. -- For Python 2/3-compatible projects, a ``__version__`` in the top level package. - This can be either a string literal, a tuple, or an `Incremental `_ version. -- Manually passing ``--version=`` when interacting with ``towncrier``. +For Python projects, the version can be automatically determined in one of the following ways: -As an example, if your package doesn't have a ``__version__``, you can manually specify it when calling ``towncrier`` on the command line with the ``--version`` flag:: +- if the project is installed, the version can be read from the package's metadata +- the version can be provided in a ``__version__`` attribute of the top level package (as a string literal, a tuple, or an `Incremental `_ version) + +For other projects, you can store the version in the ``towncrier.toml`` file:: + + [tool.towncrier] + version = "1.0.0" + +If you don't want to store the version, you can manually pass ``--version=`` whenever interacting with ``towncrier``. For example:: $ towncrier build --version=1.2.3post4