Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/trunk' into Better-issue-sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
SmileyChris committed Jun 5, 2024
2 parents 88555d4 + 15d1e25 commit 08e4281
Show file tree
Hide file tree
Showing 17 changed files with 188 additions and 35 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,36 @@ jobs:
if: ${{ matrix.task.run-if }}


pre-commit:
name: Check pre-commit integration
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up python
uses: actions/setup-python@v5
with:
python-version: 3.12

- name: Install dependencies
run: python -m pip install pre-commit

- name: Install pre-commit
run: |
pre-commit install
- name: Update pre-commit
run: |
pre-commit autoupdate
- name: Run pre-commit
run: |
pre-commit run -a
pypi-publish:
name: Check tag and publish
# Only trigger this for tag changes.
Expand Down Expand Up @@ -292,6 +322,7 @@ jobs:
- test-windows
- coverage
- check
- pre-commit
steps:
- name: Require all successes
uses: re-actors/alls-green@3a2de129f0713010a71314c74e33c0e3ef90e696
Expand Down
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ repos:
- id: debug-statements
- id: check-toml
- id: check-yaml

- repo: https://github.com/twisted/towncrier
rev: 23.11.0
hooks:
- id: towncrier-check
9 changes: 8 additions & 1 deletion RELEASE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,14 @@ In ``src/towncrier/_version.py`` the version is set using ``incremental`` such a

Commit and push the changes.

Merge the commit in the main branch.
Merge the commit in the main branch, **without using squash**.

We tag the release based on a commit from the release branch.
If we merge with squash,
the release tag commit will no longer be found in the main branch history.
With a squash merge, the whole branch history is lost.
This causes the `pre-commit autoupdate` to fail.
See `PR590 <https://github.com/twisted/towncrier/pull/590>`_ for more details.

You can announce the release over IRC or Gitter.

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Top level keys
``version``
The version of your project.

Python projects that provide the ``package`` key can have the version to be automatically determined from a ``__version__`` variable in the package's module.
Python projects that provide the ``package`` key, if left empty then the version will be automatically determined from the installed package's version metadata or a ``__version__`` variable in the package's module.

If not provided or able to be determined, the version must be passed explicitly by the command line argument ``--version``.

Expand Down
4 changes: 2 additions & 2 deletions docs/pre-commit.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Usage with the default configuration
repos:
- repo: https://github.com/twisted/towncrier
rev: 22.13.0 # run 'pre-commit autoupdate' to update
rev: 23.11.0 # run 'pre-commit autoupdate' to update
hooks:
- id: towncrier-check
Expand All @@ -30,7 +30,7 @@ News fragments are stored in ``changelog.d/`` in the root of the repository and
repos:
- repo: https://github.com/twisted/towncrier
rev: 22.13.0 # run 'pre-commit autoupdate' to update
rev: 23.11.0 # run 'pre-commit autoupdate' to update
hooks:
- id: towncrier-update
files: $changelog\.d/
Expand Down
22 changes: 15 additions & 7 deletions docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,27 @@ Create this folder::
The ``.gitignore`` will remain and keep Git from not tracking the directory.


Detecting Dates & Versions
--------------------------
Detecting Version
-----------------

``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 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 2/3-compatible projects, a ``__version__`` in the top level package.
This can be either a string literal, a tuple, or an `Incremental <https://github.com/twisted/incremental>`_ version.
- Manually passing ``--version=<myversionhere>`` when interacting with ``towncrier``.
1. Manually pass ``--version=<myversionhere>`` 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:

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::
- 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 <https://github.com/twisted/incremental>`_ version

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


Setting Date
------------

``towncrier`` will also include the current date (in ``YYYY-MM-DD`` format) when generating news files.
You can change this with the ``--date`` flag::

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ requires-python = ">=3.8"
dependencies = [
"click",
"importlib-resources>=5; python_version<'3.10'",
"importlib-metadata>=4.6; python_version<'3.10'",
"incremental",
"jinja2",
"tomli; python_version<'3.11'",
Expand Down
38 changes: 36 additions & 2 deletions src/towncrier/_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@
Responsible for getting the version and name from a project.
"""


from __future__ import annotations

import sys

from importlib import import_module
from importlib.metadata import version as metadata_version
from types import ModuleType
from typing import Any

from incremental import Version as IncrementalVersion


if sys.version_info >= (3, 10):
from importlib.metadata import packages_distributions
else:
from importlib_metadata import packages_distributions # type: ignore


def _get_package(package_dir: str, package: str) -> ModuleType:
try:
module = import_module(package)
Expand All @@ -37,13 +44,40 @@ def _get_package(package_dir: str, package: str) -> ModuleType:
return module


def _get_metadata_version(package: str) -> str | None:
"""
Try to get the version from the package metadata.
"""
distributions = packages_distributions()
distribution_names = distributions.get(package)
if not distribution_names or len(distribution_names) != 1:
# We can only determine the version if there is exactly one matching distribution.
return None
return metadata_version(distribution_names[0])


def get_version(package_dir: str, package: str) -> str:
"""
Get the version of a package.
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

# First try to get the version from the package metadata.
if version := _get_metadata_version(package):
return version

# When no version if found, fall back to looking for the package in `package_dir`.
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")
raise Exception(
f"No __version__ or metadata version info for the '{package}' package."
)

if isinstance(version, str):
return version.strip()
Expand Down
49 changes: 38 additions & 11 deletions src/towncrier/_settings/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,53 @@ def __init__(self, *args: str, **kwargs: str):
def load_config_from_options(
directory: str | None, config_path: str | None
) -> tuple[str, Config]:
"""
Load the configuration from a given directory or specific configuration file.
Unless an explicit configuration file is given, traverse back from the given
directory looking for a configuration file.
Returns a tuple of the base directory and the parsed Config instance.
"""
if config_path is None:
if directory is None:
directory = os.getcwd()
return traverse_for_config(directory)

config_path = os.path.abspath(config_path)

# When a directory is provided (in addition to the config file), use it as the base
# directory. Otherwise use the directory containing the config file.
if directory is not None:
base_directory = os.path.abspath(directory)
config = load_config(base_directory)
else:
config_path = os.path.abspath(config_path)
if directory is None:
base_directory = os.path.dirname(config_path)
else:
base_directory = os.path.abspath(directory)
config = load_config_from_file(os.path.dirname(config_path), config_path)
base_directory = os.path.dirname(config_path)

if config is None:
raise ConfigError(f"No configuration file found.\nLooked in: {base_directory}")
if not os.path.isfile(config_path):
raise ConfigError(f"Configuration file '{config_path}' not found.")
config = load_config_from_file(base_directory, config_path)

return base_directory, config


def traverse_for_config(path: str | None) -> tuple[str, Config]:
"""
Search for a configuration file in the current directory and all parent directories.
Returns the directory containing the configuration file and the parsed configuration.
"""
start_directory = directory = os.path.abspath(path or os.getcwd())
while True:
config = load_config(directory)
if config is not None:
return directory, config

parent = os.path.dirname(directory)
if parent == directory:
raise ConfigError(
f"No configuration file found.\nLooked back from: {start_directory}"
)
directory = parent


def load_config(directory: str) -> Config | None:
towncrier_toml = os.path.join(directory, "towncrier.toml")
pyproject_toml = os.path.join(directory, "pyproject.toml")
Expand Down
1 change: 1 addition & 0 deletions src/towncrier/newsfragments/432.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clarify version discovery behavior.
1 change: 1 addition & 0 deletions src/towncrier/newsfragments/432.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Inferring the version of a Python package now tries to use the metadata of the installed package before importing the package explicitly (which only looks for ``[package].__version__``).
Empty file.
1 change: 1 addition & 0 deletions src/towncrier/newsfragments/601.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Running ``towncrier`` will now traverse back up directories looking for the configuration file.
1 change: 1 addition & 0 deletions src/towncrier/newsfragments/602.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clarify version discovery behavior.
12 changes: 11 additions & 1 deletion src/towncrier/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,21 @@ def test_in_different_dir_dir_option(self, runner):
self.assertEqual(0, result.exit_code)
self.assertTrue((project_dir / "NEWS.rst").exists())

@with_project()
def test_traverse_up_to_find_config(self, runner):
"""
When the current directory doesn't contain the configuration file, Towncrier
will traverse up the directory tree until it finds it.
"""
os.chdir("foo")
result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"])
self.assertEqual(0, result.exit_code, result.output)

@with_project()
def test_in_different_dir_config_option(self, runner):
"""
The current working directory and the location of the configuration
don't matter as long as we pass corrct paths to the directory and the
don't matter as long as we pass correct paths to the directory and the
config file.
"""
project_dir = Path(".").resolve()
Expand Down
29 changes: 20 additions & 9 deletions src/towncrier/test/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import os
import sys

from unittest import skipIf
from importlib.metadata import version as metadata_version
from unittest import mock

from click.testing import CliRunner
from twisted.trial.unittest import TestCase
Expand All @@ -14,12 +15,6 @@
from .helpers import write


try:
from importlib.metadata import version as metadata_version
except ImportError:
metadata_version = None


towncrier_cli.name = "towncrier"


Expand Down Expand Up @@ -50,7 +45,6 @@ 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.
Expand All @@ -60,6 +54,14 @@ def test_incremental(self):
with self.assertWarnsRegex(
DeprecationWarning, "Accessing towncrier.__version__ is deprecated.*"
):
# Previously this triggered towncrier.__version__ but now the first version
# check is from the package metadata. Let's mock out that part to ensure we
# can get incremental versions from __version__ still.
with mock.patch(
"towncrier._project._get_metadata_version", return_value=None
):
version = get_version(pkg, "towncrier")

version = get_version(pkg, "towncrier")

with self.assertWarnsRegex(
Expand All @@ -70,6 +72,13 @@ def test_incremental(self):
self.assertEqual(metadata_version("towncrier"), version)
self.assertEqual("towncrier", name)

def test_version_from_metadata(self):
"""
A version from package metadata is picked up.
"""
version = get_version(".", "towncrier")
self.assertEqual(metadata_version("towncrier"), version)

def _setup_missing(self):
"""
Create a minimalistic project with missing metadata in a temporary
Expand All @@ -91,10 +100,12 @@ def test_missing_version(self):
tmp_dir = self._setup_missing()

with self.assertRaises(Exception) as e:
# The 'missing' package has no __version__ string.
get_version(tmp_dir, "missing")

self.assertEqual(
("No __version__, I don't know how else to look",), e.exception.args
("No __version__ or metadata version info for the 'missing' package.",),
e.exception.args,
)

def test_missing_version_project_name(self):
Expand Down
Loading

0 comments on commit 08e4281

Please sign in to comment.