From 90656a19072681d8546e273f1000329bfb284ffe Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Sat, 24 Jun 2023 00:28:43 +1200 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 From cf2df80fab3ef249e68f5dca3c8b832a7d92f5f6 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Sun, 28 Jul 2024 12:17:40 +0200 Subject: [PATCH 04/12] review findings and fixes --- src/towncrier/__init__.py | 24 ++++++++++++++++++++++++ src/towncrier/_project.py | 8 +++----- src/towncrier/_shell.py | 3 +-- src/towncrier/_version.py | 14 ++++++-------- src/towncrier/test/test_packaging.py | 18 ++++++++++++++---- src/towncrier/test/test_project.py | 2 +- 6 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/towncrier/__init__.py b/src/towncrier/__init__.py index 0c504e1f..a7578e17 100644 --- a/src/towncrier/__init__.py +++ b/src/towncrier/__init__.py @@ -4,3 +4,27 @@ """ towncrier, a builder for your news files. """ + +from __future__ import annotations + + +__all__ = ["__version__"] + + +def __getattr__(name: str) -> str: + 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 3489e123..3fe6fa94 100644 --- a/src/towncrier/_project.py +++ b/src/towncrier/_project.py @@ -7,13 +7,13 @@ from __future__ import annotations +import contextlib import importlib.metadata as importlib_metadata import sys from importlib import import_module from importlib.metadata import version as metadata_version from types import ModuleType -from typing import Any if sys.version_info >= (3, 10): @@ -62,7 +62,7 @@ def get_version(package_dir: str, package: str) -> str: Try to extract the version from the distribution version metadata that matches `package`, then fall back to looking for the package in `package_dir`. """ - version: Any + version: str # First try to get the version from the package metadata. if version := _get_metadata_version(package): @@ -105,9 +105,7 @@ 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: + with contextlib.suppress(AttributeError): return str(version.package) # type: ignore - except AttributeError: - pass return package.title() diff --git a/src/towncrier/_shell.py b/src/towncrier/_shell.py index 24642e3e..3e36e5cc 100644 --- a/src/towncrier/_shell.py +++ b/src/towncrier/_shell.py @@ -13,10 +13,9 @@ import click -from click_default_group import DefaultGroup - from .build import _main as _build_cmd from .check import _main as _check_cmd +from .click_default_group import DefaultGroup from .create import _main as _create_cmd diff --git a/src/towncrier/_version.py b/src/towncrier/_version.py index a0a1263f..93736bae 100644 --- a/src/towncrier/_version.py +++ b/src/towncrier/_version.py @@ -2,13 +2,11 @@ Provides towncrier version information. """ -from importlib.metadata import PackageNotFoundError, version +# For dev - 23.11.1.dev0 +# For RC - 23.11.1.rc1 +# For final - 23.11.1 +# make sure to follow PEP440 +__version__ = "23.11.1.dev0" - -try: - _version = version("towncrier") -except PackageNotFoundError: # pragma: no cover - _version = "0.0.0.dev" - -_hatchling_version = _version +_hatchling_version = __version__ __all__ = ["_hatchling_version"] diff --git a/src/towncrier/test/test_packaging.py b/src/towncrier/test/test_packaging.py index 2dbcbfbf..199adffd 100644 --- a/src/towncrier/test/test_packaging.py +++ b/src/towncrier/test/test_packaging.py @@ -7,10 +7,20 @@ class TestPackaging(TestCase): - def no_version_attr(self): + def test_version_attr(self): """ - towncrier.__version__ was deprecated, now no longer exists. + towncrier.__version__ was deprecated, but still exists for now. """ - with self.assertRaises(AttributeError): - towncrier.__version__ + def access__version(): + return towncrier.__version__ + + expected_warning = ( + "Accessing towncrier.__version__ is deprecated and will be " + "removed in a future release. Use importlib.metadata directly " + "to query for towncrier's packaging metadata." + ) + + self.assertWarns( + DeprecationWarning, expected_warning, __file__, access__version + ) diff --git a/src/towncrier/test/test_project.py b/src/towncrier/test/test_project.py index 30939d2f..3beda360 100644 --- a/src/towncrier/test/test_project.py +++ b/src/towncrier/test/test_project.py @@ -136,7 +136,7 @@ def test_missing_version(self): get_version(tmp_dir, "missing") self.assertEqual( - ("Package not installed and no missing.__version__ found",), + ("No __version__ or metadata version info for the 'missing' package.",), e.exception.args, ) From 198ad3533432f9abed931c843c78aa9358a916ab Mon Sep 17 00:00:00 2001 From: sigma67 Date: Sun, 28 Jul 2024 21:55:47 +0200 Subject: [PATCH 05/12] review findings --- docs/conf.py | 12 +++++++----- docs/tutorial.rst | 16 +++++++--------- src/towncrier/_project.py | 10 +++------- src/towncrier/_shell.py | 11 ++--------- 4 files changed, 19 insertions(+), 30 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 99574eea..9f2098b4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,13 +29,13 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -import importlib.metadata as importlib_metadata import os from datetime import date +from towncrier import __version__ as towncrier_version + -towncrier_version = importlib_metadata.version("towncrier") extensions = [] # Add any paths that contain templates here, relative to this directory. @@ -55,7 +55,7 @@ project = "Towncrier" copyright = "{}, Towncrier contributors. Ver {}".format( _today.year, - importlib_metadata.version("towncrier"), + towncrier_version.public(), ) author = "Amber Brown" @@ -63,9 +63,11 @@ # |version| and |release|, also used in various other places throughout the # built documents. # The short X.Y version. -version = ".".join(towncrier_version.split(".")[:3]) +version = "{}.{}.{}".format( + towncrier_version.major, towncrier_version.minor, towncrier_version.micro +) # The full version, including alpha/beta/rc tags. -release = towncrier_version +release = towncrier_version.public() # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 7aef8a20..70b9c410 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -52,18 +52,16 @@ Detecting Version ----------------- ``towncrier`` needs to know what version your project is when generating news files. +These are the ways you can provide it, in order of precedence (with the first taking precedence over the second, and so on): -For Python projects, the version can be automatically determined in one of the following ways: +1. Manually pass ``--version=`` when interacting with ``towncrier``. +2. Set a value for the ``version`` option in your configuration file. +3. For Python projects with a ``package`` key in the configuration file: -- 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) + - install the package to use its metadata version information + - add a ``__version__`` in the top level package that is either a string literal, a tuple, or an `Incremental `_ version -For non-Python 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:: +As an example, you can manually specify the version when calling ``towncrier`` on the command line with the ``--version`` flag:: $ towncrier build --version=1.2.3post4 diff --git a/src/towncrier/_project.py b/src/towncrier/_project.py index 3fe6fa94..8d00aa03 100644 --- a/src/towncrier/_project.py +++ b/src/towncrier/_project.py @@ -8,7 +8,6 @@ from __future__ import annotations import contextlib -import importlib.metadata as importlib_metadata import sys from importlib import import_module @@ -72,12 +71,9 @@ def get_version(package_dir: str, package: str) -> str: module = _get_package(package_dir, package) version = getattr(module, "__version__", None) if not version: - try: - version = importlib_metadata.version(package) - except importlib_metadata.PackageNotFoundError: - raise Exception( - f"No __version__ or metadata version info for the '{package}' package." - ) + raise Exception( + f"No __version__ or metadata version info for the '{package}' package." + ) if isinstance(version, str): return version.strip() diff --git a/src/towncrier/_shell.py b/src/towncrier/_shell.py index 3e36e5cc..6e9ae171 100644 --- a/src/towncrier/_shell.py +++ b/src/towncrier/_shell.py @@ -9,24 +9,17 @@ from __future__ import annotations -from importlib.metadata import PackageNotFoundError, version - import click +from ._version import __version__ from .build import _main as _build_cmd from .check import _main as _check_cmd from .click_default_group import DefaultGroup 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) +@click.version_option(__version__) def cli() -> None: """ Towncrier is a utility to produce useful, summarised news files for your project. From 2cf4e76af7f16d8125118cf4fd47f0747741573f Mon Sep 17 00:00:00 2001 From: sigma67 Date: Sun, 28 Jul 2024 21:56:50 +0200 Subject: [PATCH 06/12] revert RELEASE.rst --- RELEASE.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/RELEASE.rst b/RELEASE.rst index 9f19d851..8b457aa7 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 ``pyproject.toml`` the version is set like:: +In ``src/towncrier/_version.py`` the version is set using ``incremental`` such as:: - version = "19.9.0.rc1" + __version__ = Version('towncrier', 19, 9, 0, release_candidate=1) Run ``venv/bin/towncrier build --yes`` to generate the news release NEWS file. Commit and push to the primary repository, not a fork. @@ -65,9 +65,9 @@ Final release Once the PR is approved, you can trigger the final release. Update the version to the final version. -In ``pyproject.toml`` the version is set like:: +In ``src/towncrier/_version.py`` the version is set using ``incremental`` such as:: - version = "19.9.0" + __version__ = Version('towncrier', 19, 9, 0) Manually update the `NEWS.rst` file to include the final release version and date. Usually it will look like this. @@ -96,9 +96,9 @@ Similar to the release candidate, with the difference: No need for another review request. Update the version to the development version. -In ``pyproject.toml`` the version is set like:: +In ``src/towncrier/_version.py`` the version is set using ``incremental`` such as:: - version = "19.9.1.dev0" + __version__ = Version('towncrier', 19, 9, 1, dev=0) Commit and push the changes. From 57022ccbb0441e10734bd2eb4da1f3c7fd5bfe7a Mon Sep 17 00:00:00 2001 From: sigma67 Date: Sun, 28 Jul 2024 22:29:17 +0200 Subject: [PATCH 07/12] fix rtd build --- docs/conf.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9f2098b4..a59bfbf9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,7 +55,7 @@ project = "Towncrier" copyright = "{}, Towncrier contributors. Ver {}".format( _today.year, - towncrier_version.public(), + towncrier_version, ) author = "Amber Brown" @@ -63,11 +63,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. From 7e9d4f0689a8b81dbe79b46e3120c5163af52ed3 Mon Sep 17 00:00:00 2001 From: sigma67 <16363825+sigma67@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:11:58 +0200 Subject: [PATCH 08/12] Update src/towncrier/test/test_project.py --- src/towncrier/test/test_project.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/towncrier/test/test_project.py b/src/towncrier/test/test_project.py index 3beda360..4f1e638e 100644 --- a/src/towncrier/test/test_project.py +++ b/src/towncrier/test/test_project.py @@ -75,8 +75,10 @@ def base(self): def test_not_incremental(self): """ - An incremental-like Version __version__ is picked up, base takes unexpected - arguments. + An exception is raised when the version could not be detected. + For this test we use an incremental-like object, + that has the `base` method, but that method + does not match the return type for `incremental`. """ temp = self.mktemp() os.makedirs(os.path.join(temp, "mytestprojnotinc")) From 03894fe14778ee0293e38588d73426a39ca64547 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Mon, 29 Jul 2024 12:35:59 +0200 Subject: [PATCH 09/12] fix coverage gaps --- src/towncrier/_project.py | 2 +- src/towncrier/test/test_project.py | 59 ++++++++++++++++-------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/towncrier/_project.py b/src/towncrier/_project.py index 8d00aa03..7b3a98c8 100644 --- a/src/towncrier/_project.py +++ b/src/towncrier/_project.py @@ -61,7 +61,7 @@ def get_version(package_dir: str, package: str) -> str: Try to extract the version from the distribution version metadata that matches `package`, then fall back to looking for the package in `package_dir`. """ - version: str + version: str | None # First try to get the version from the package metadata. if version := _get_metadata_version(package): diff --git a/src/towncrier/test/test_project.py b/src/towncrier/test/test_project.py index 4f1e638e..43928c5c 100644 --- a/src/towncrier/test/test_project.py +++ b/src/towncrier/test/test_project.py @@ -44,34 +44,37 @@ def test_tuple(self): version = get_version(temp, "mytestproja") self.assertEqual(version, "1.3.12") - def test_incremental(self): - """ - 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_incremental(self): + """ + 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") + + project = get_project_name(temp, "mytestprojinc") + self.assertEqual(project, "Mytestprojinc") def test_not_incremental(self): """ From 1f35abdbd5c2a5f76359ca4c8f5447495881cff4 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Mon, 29 Jul 2024 14:26:04 +0200 Subject: [PATCH 10/12] fix coverage gap --- src/towncrier/test/test_project.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/towncrier/test/test_project.py b/src/towncrier/test/test_project.py index 43928c5c..a93ef190 100644 --- a/src/towncrier/test/test_project.py +++ b/src/towncrier/test/test_project.py @@ -62,6 +62,7 @@ class Version: def __init__(self, *version_parts): self.version = version_parts + self.package = "mytestprojinc" def base(self): return '.'.join(map(str, self.version)) @@ -74,7 +75,7 @@ def base(self): self.assertEqual(version, "1.3.12rc1") project = get_project_name(temp, "mytestprojinc") - self.assertEqual(project, "Mytestprojinc") + self.assertEqual(project, "mytestprojinc") def test_not_incremental(self): """ From d96226a143c7d39224ec7590cb503889665a6c2c Mon Sep 17 00:00:00 2001 From: sigma67 Date: Mon, 29 Jul 2024 21:49:16 +0200 Subject: [PATCH 11/12] update RELEASE.rst --- RELEASE.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/RELEASE.rst b/RELEASE.rst index 8b457aa7..1a436015 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -14,30 +14,30 @@ Before the final release, a set of release candidates are released. Release candidate ----------------- -Create a release branch with a name of the form ``release-19.9.0`` starting from the main branch. +Create a release branch with a name of the form ``release-23.11.0`` starting from the main branch. 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 ``src/towncrier/_version.py`` the version is set using a PEP440 compliant string: - __version__ = Version('towncrier', 19, 9, 0, release_candidate=1) + __version__ = "23.11.0rc1" Run ``venv/bin/towncrier build --yes`` to generate the news release NEWS file. Commit and push to the primary repository, not a fork. It is important to not use a fork so that pushed tags end up in the primary repository, server provided secrets for publishing to PyPI are available, and maybe more. -Create a PR named in the form ``Release 19.9.0``. +Create a PR named in the form ``Release 23.11.0``. The same PR will be used for the release candidates and the final release. Wait for the tests to be green. Start with the release candidates. Create a new release candidate using `GitHub New release UI `_. -* *Choose a tag*: Type `19.9.0rc1` and select `Create new tag on publish.` +* *Choose a tag*: Type `23.11.0rc1` and select `Create new tag on publish.` * *Target*: Search for the release branch and select it. -* *Title*: "Towncrier 19.9.0rc1". +* *Title*: "Towncrier 23.11.0rc1". * Set the content based on the NEWS file (for now in RST format). * Make sure to check **This is a pre-release**. * Click `Publish release` @@ -65,15 +65,15 @@ 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 ``src/towncrier/_version.py`` the version is set using a PEP440 compliant string: - __version__ = Version('towncrier', 19, 9, 0) + __version__ = "23.11.0" Manually update the `NEWS.rst` file to include the final release version and date. Usually it will look like this. This will replace the release candidate section:: - towncrier 19.9.0 (2019-09-29) + towncrier 23.11.0 (2023-11-01) ============================= No significant changes since the previous release candidate. @@ -85,9 +85,9 @@ Trigger the final release using GitHub Release GUI. Similar to the release candidate, with the difference: -* tag will be named `19.9.0` +* tag will be named `23.11.0` * the target is the same branch -* Title will be `towncrier 19.0.0` +* Title will be `towncrier 23.11.0` * Content can be the content of the final release (RST format). * Check **Set as the latest release**. * Check **Create a discussion for this release**. @@ -96,9 +96,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:: +In ``src/towncrier/_version.py`` the version is set using a PEP440 compliant string: - __version__ = Version('towncrier', 19, 9, 1, dev=0) + __version__ = "23.11.0.dev0" Commit and push the changes. From c23eaded2dbec3ad686be1b65ec43963536664b4 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Tue, 30 Jul 2024 11:37:31 +0100 Subject: [PATCH 12/12] Use 19.9.0 as an example to demonstrate how zero padding is used --- RELEASE.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/RELEASE.rst b/RELEASE.rst index 1a436015..d6611536 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -14,30 +14,30 @@ Before the final release, a set of release candidates are released. Release candidate ----------------- -Create a release branch with a name of the form ``release-23.11.0`` starting from the main branch. -The same branch is used for the release candidated and the final release. +Create a release branch with a name of the form ``release-19.9.0`` starting from the main branch. +The same branch is used for the release candidate 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 a PEP440 compliant string: - __version__ = "23.11.0rc1" + __version__ = "19.9.0rc1" Run ``venv/bin/towncrier build --yes`` to generate the news release NEWS file. Commit and push to the primary repository, not a fork. It is important to not use a fork so that pushed tags end up in the primary repository, server provided secrets for publishing to PyPI are available, and maybe more. -Create a PR named in the form ``Release 23.11.0``. +Create a PR named in the form ``Release 19.9.0``. The same PR will be used for the release candidates and the final release. Wait for the tests to be green. Start with the release candidates. Create a new release candidate using `GitHub New release UI `_. -* *Choose a tag*: Type `23.11.0rc1` and select `Create new tag on publish.` +* *Choose a tag*: Type `19.9.0rc1` and select `Create new tag on publish.` * *Target*: Search for the release branch and select it. -* *Title*: "Towncrier 23.11.0rc1". +* *Title*: "Towncrier 19.9.0rc1". * Set the content based on the NEWS file (for now in RST format). * Make sure to check **This is a pre-release**. * Click `Publish release` @@ -60,20 +60,20 @@ In the future we might create a separate Markdown version. 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 a PEP440 compliant string: - __version__ = "23.11.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. This will replace the release candidate section:: - towncrier 23.11.0 (2023-11-01) + towncrier 19.9.0 (2019-09-01) ============================= No significant changes since the previous release candidate. @@ -85,9 +85,9 @@ Trigger the final release using GitHub Release GUI. Similar to the release candidate, with the difference: -* tag will be named `23.11.0` +* tag will be named `19.9.0` * the target is the same branch -* Title will be `towncrier 23.11.0` +* Title will be `towncrier 19.9.0` * Content can be the content of the final release (RST format). * Check **Set as the latest release**. * Check **Create a discussion for this release**. @@ -98,7 +98,7 @@ No need for another review request. Update the version to the development version. In ``src/towncrier/_version.py`` the version is set using a PEP440 compliant string: - __version__ = "23.11.0.dev0" + __version__ = "19.9.0.dev0" Commit and push the changes. @@ -111,6 +111,6 @@ With a squash merge, the whole branch history is lost. This causes the `pre-commit autoupdate` to fail. See `PR590 `_ for more details. -You can announce the release over IRC or Gitter. +You can announce the release over IRC, Gitter, or Twisted mailing list. Done.