diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d5eb98d..9c4d7cd9 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..8792c3bb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,9 +31,14 @@ # ones. from datetime import date -from towncrier import __version__ as towncrier_version +try: + import importlib.metadata as importlib_metadata +except ImportError: # < Python 3.8 + import importlib_metadata + +towncrier_version = importlib_metadata.version("towncrier") extensions = [] # Add any paths that contain templates here, relative to this directory. @@ -53,7 +58,7 @@ project = "Towncrier" copyright = "{}, Towncrier contributors. Ver {}".format( _today.year, - towncrier_version.public(), + importlib_metadata.version("towncrier"), ) author = "Amber Brown" @@ -61,11 +66,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/noxfile.py b/noxfile.py index f32539d2..1afd57e9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -55,7 +55,7 @@ def check_newsfragment(session: nox.Session) -> None: @nox.session def typecheck(session: nox.Session) -> None: - session.install(".", "mypy", "types-setuptools") + session.install(".", "mypy") session.run("mypy", "src") diff --git a/pyproject.toml b/pyproject.toml index d54647d1..8fe823b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,14 +2,13 @@ requires = [ "hatchling ~= 1.12.2", "wheel ~= 0.38.4", - "incremental == 22.10.0", ] build-backend = "hatchling.build" [project] -dynamic = ["version"] name = "towncrier" +version = "22.12.1.dev0" description = "Building newsfiles for your project." readme = "README.rst" license = "MIT" @@ -33,18 +32,13 @@ dependencies = [ "click", "click-default-group", "importlib-resources>1.3; python_version<'3.9'", - "incremental", + "importlib-metadata; python_version<'3.8'", "jinja2", "tomli; python_version<'3.11'", ] [project.optional-dependencies] -dev = [ - "packaging", - "sphinx >= 5", - "furo", - "twisted", -] +dev = ["packaging", "sphinx >= 5", "furo", "twisted"] [project.scripts] towncrier = "towncrier._shell:cli" @@ -60,11 +54,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", @@ -147,12 +136,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..f157c5f6 100644 --- a/src/towncrier/_project.py +++ b/src/towncrier/_project.py @@ -12,8 +12,13 @@ from importlib import import_module from types import ModuleType +from typing import Any -from incremental import Version as IncrementalVersion + +try: + import importlib.metadata as importlib_metadata +except ImportError: # < Python 3.8 + import importlib_metadata # type: ignore def _get_package(package_dir: str, package: str) -> ModuleType: @@ -38,47 +43,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/_settings/load.py b/src/towncrier/_settings/load.py index 10bde248..07a8ab92 100644 --- a/src/towncrier/_settings/load.py +++ b/src/towncrier/_settings/load.py @@ -171,12 +171,11 @@ def parse_toml(base_path: str, config: Mapping[str, Any]) -> Config: template = str(resource_path) else: template = os.path.join(base_path, template) - - if not os.path.exists(template): - raise ConfigError( - f"The template file '{template}' does not exist.", - failing_option="template", - ) + if not os.path.isfile(template): + raise ConfigError( + f"The template file '{template}' does not exist.", + failing_option="template", + ) return Config( package=config.get("package", ""), diff --git a/src/towncrier/_shell.py b/src/towncrier/_shell.py index 0efe1b69..442620df 100644 --- a/src/towncrier/_shell.py +++ b/src/towncrier/_shell.py @@ -13,14 +13,25 @@ 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: + from importlib.metadata import PackageNotFoundError, version +except ImportError: # < Python 3.8 + from importlib_metadata import PackageNotFoundError, version # type: ignore + + +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 a2529bf2..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", 22, 12, 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 4b420648..d42d631a 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): """ @@ -48,25 +41,61 @@ 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: + 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): """ @@ -92,7 +121,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): @@ -117,7 +147,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): """