From e07d2984aba528262616b70891860e5aefb12775 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 17 Aug 2024 12:49:19 -0700 Subject: [PATCH] Support `hatch version --force` (#1645) Co-authored-by: Ofek Lev --- backend/src/hatchling/utils/constants.py | 4 ++ .../version/scheme/plugin/interface.py | 30 ++++++++++++- .../src/hatchling/version/scheme/standard.py | 2 +- docs/community/contributing.md | 13 +++--- docs/history/hatch.md | 1 + docs/plugins/version-scheme/reference.md | 1 + src/hatch/cli/version/__init__.py | 30 +++++-------- src/hatch/config/constants.py | 4 ++ tests/backend/version/scheme/test_standard.py | 11 ++++- tests/cli/version/test_version.py | 45 +++++++++++++++++++ 10 files changed, 112 insertions(+), 29 deletions(-) diff --git a/backend/src/hatchling/utils/constants.py b/backend/src/hatchling/utils/constants.py index 5c3c4e291..e3349c7b5 100644 --- a/backend/src/hatchling/utils/constants.py +++ b/backend/src/hatchling/utils/constants.py @@ -1,2 +1,6 @@ DEFAULT_BUILD_SCRIPT = 'hatch_build.py' DEFAULT_CONFIG_FILE = 'hatch.toml' + + +class VersionEnvVars: + VALIDATE_BUMP = 'HATCH_VERSION_VALIDATE_BUMP' diff --git a/backend/src/hatchling/version/scheme/plugin/interface.py b/backend/src/hatchling/version/scheme/plugin/interface.py index 0e38b150a..6cf12aa5e 100644 --- a/backend/src/hatchling/version/scheme/plugin/interface.py +++ b/backend/src/hatchling/version/scheme/plugin/interface.py @@ -1,6 +1,8 @@ from __future__ import annotations +import os from abc import ABC, abstractmethod +from functools import cached_property class VersionSchemeInterface(ABC): # no cov @@ -51,9 +53,33 @@ def config(self) -> dict: """ return self.__config + @cached_property + def validate_bump(self) -> bool: + """ + This is the value of the `validate-bump` option, with the `HATCH_VERSION_VALIDATE_BUMP` + environment variable taking precedence. Validation is enabled by default. + + ```toml config-example + [tool.hatch.version] + validate-bump = true + ``` + """ + from hatchling.utils.constants import VersionEnvVars + + if VersionEnvVars.VALIDATE_BUMP in os.environ: + return os.environ[VersionEnvVars.VALIDATE_BUMP] not in {'false', '0'} + + validate_bump = self.config.get('validate-bump', True) + if not isinstance(validate_bump, bool): + message = 'option `validate-bump` must be a boolean' + raise TypeError(message) + + return validate_bump + @abstractmethod def update(self, desired_version: str, original_version: str, version_data: dict) -> str: """ - This should return a normalized form of the desired version and verify that it - is higher than the original version. + This should return a normalized form of the desired version. If the + [validate_bump](reference.md#hatchling.version.scheme.plugin.interface.VersionSchemeInterface.validate_bump) + property is `True`, this method should also verify that the version is higher than the original version. """ diff --git a/backend/src/hatchling/version/scheme/standard.py b/backend/src/hatchling/version/scheme/standard.py index 4ec00893a..5c3caebba 100644 --- a/backend/src/hatchling/version/scheme/standard.py +++ b/backend/src/hatchling/version/scheme/standard.py @@ -57,7 +57,7 @@ def update( raise ValueError(message) next_version = Version(version) - if self.config.get('validate-bump', True) and next_version <= original: + if self.validate_bump and next_version <= original: message = f'Version `{version}` is not higher than the original version `{original_version}`' raise ValueError(message) diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 4a7e9763b..88ad71b5d 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -5,7 +5,7 @@ The usual process to make a contribution is to: 1. Check for existing related issues 2. Fork the repository and create a new branch 3. Make your changes -4. Make sure formatting, linting and tests passes. +4. Make sure formatting, linting and tests passes. 5. Add tests if possible to cover the lines you added. 6. Commit, and send a Pull Request. @@ -15,7 +15,7 @@ Clone the `hatch` repository, `cd` into it, and create a new branch for your con ```bash cd hatch -git checkout -b add-my-contribution +git switch -c add-my-contribution ``` ## Run the tests @@ -35,7 +35,7 @@ hatch test --cover Run the extended test suite with coverage: ```bash -hatch run full +hatch test --cover --all ``` ## Lint @@ -43,13 +43,14 @@ hatch run full Run automated formatting: ```bash -hatch fmt --formatter +hatch fmt ``` Run full linting and type checking: ```bash -hatch fmt +hatch fmt --check +hatch run types:check ``` ## Docs @@ -64,4 +65,4 @@ Build and validate the documentation website: ```bash hatch run docs:build-check -``` \ No newline at end of file +``` diff --git a/docs/history/hatch.md b/docs/history/hatch.md index 0eac19044..cedeee268 100644 --- a/docs/history/hatch.md +++ b/docs/history/hatch.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ***Added:*** - The `version` and `project metadata` commands now support projects that do not use Hatchling as the build backend +- The `version` command accepts a `--force` option, allowing for downgrades when an explicit version number is given. - Build environments can now be configured, the default build environment is `hatch-build` - The environment interface now has the following methods and properties in order to better support builds on remote machines: `project_root`, `sep`, `pathsep`, `fs_context` diff --git a/docs/plugins/version-scheme/reference.md b/docs/plugins/version-scheme/reference.md index 4affdc497..f57404ddd 100644 --- a/docs/plugins/version-scheme/reference.md +++ b/docs/plugins/version-scheme/reference.md @@ -12,4 +12,5 @@ - PLUGIN_NAME - root - config + - validate_bump - update diff --git a/src/hatch/cli/version/__init__.py b/src/hatch/cli/version/__init__.py index 143ce60cc..b89bfc3ec 100644 --- a/src/hatch/cli/version/__init__.py +++ b/src/hatch/cli/version/__init__.py @@ -10,8 +10,14 @@ @click.command(short_help="View or set a project's version") @click.argument('desired_version', required=False) +@click.option( + '--force', + '-f', + is_flag=True, + help='Allow an explicit downgrading version to be given', +) @click.pass_obj -def version(app: Application, desired_version: str | None): +def version(app: Application, *, desired_version: str | None, force: bool): """View or set a project's version.""" if app.project.root is None: if app.project.chosen_name is None: @@ -26,6 +32,7 @@ def version(app: Application, desired_version: str | None): app.display(app.project.metadata.config['project']['version']) return + from hatch.config.constants import VersionEnvVars from hatch.project.constants import BUILD_BACKEND with app.project.location.as_cwd(): @@ -40,33 +47,18 @@ def version(app: Application, desired_version: str | None): project_metadata = app.project.build_frontend.get_core_metadata() app.display(project_metadata['version']) - elif 'version' not in app.project.metadata.dynamic: - source = app.project.metadata.hatch.version.source - - version_data = source.get_version_data() - original_version = version_data['version'] - - if not desired_version: - app.display(original_version) - return - - updated_version = app.project.metadata.hatch.version.scheme.update( - desired_version, original_version, version_data - ) - source.set_version(updated_version, version_data) - - app.display_info(f'Old: {original_version}') - app.display_info(f'New: {updated_version}') else: from hatch.utils.runner import ExecutionContext app.ensure_environment_plugin_dependencies() app.project.prepare_build_environment() + context = ExecutionContext(app.project.build_env) command = ['python', '-u', '-m', 'hatchling', 'version'] if desired_version: command.append(desired_version) + if force: + context.env_vars[VersionEnvVars.VALIDATE_BUMP] = 'false' - context = ExecutionContext(app.project.build_env) context.add_shell_command(command) app.execute_context(context) diff --git a/src/hatch/config/constants.py b/src/hatch/config/constants.py index 2ad442fe1..2f2f73ee3 100644 --- a/src/hatch/config/constants.py +++ b/src/hatch/config/constants.py @@ -33,3 +33,7 @@ class PythonEnvVars: CUSTOM_SOURCE_PREFIX = 'HATCH_PYTHON_CUSTOM_SOURCE_' CUSTOM_PATH_PREFIX = 'HATCH_PYTHON_CUSTOM_PATH_' CUSTOM_VERSION_PREFIX = 'HATCH_PYTHON_CUSTOM_VERSION_' + + +class VersionEnvVars: + VALIDATE_BUMP = 'HATCH_VERSION_VALIDATE_BUMP' diff --git a/tests/backend/version/scheme/test_standard.py b/tests/backend/version/scheme/test_standard.py index 545e6bfeb..fc58123ce 100644 --- a/tests/backend/version/scheme/test_standard.py +++ b/tests/backend/version/scheme/test_standard.py @@ -1,6 +1,8 @@ import pytest from packaging.version import _parse_letter_version # noqa: PLC2701 +from hatch.utils.structures import EnvVars +from hatchling.utils.constants import VersionEnvVars from hatchling.version.scheme.standard import StandardScheme @@ -17,12 +19,19 @@ def test_specific(isolation): assert scheme.update('9000.0.0-rc.1', '1.0', {}) == '9000.0.0rc1' -def test_specific_not_higher_allowed(isolation): +def test_specific_not_higher_allowed_config(isolation): scheme = StandardScheme(str(isolation), {'validate-bump': False}) assert scheme.update('0.24.4', '1.0.0.dev0', {}) == '0.24.4' +def test_specific_not_higher_allowed_env_var(isolation): + scheme = StandardScheme(str(isolation), {}) + + with EnvVars({VersionEnvVars.VALIDATE_BUMP: 'false'}): + assert scheme.update('0.24.4', '1.0.0.dev0', {}) == '0.24.4' + + def test_release(isolation): scheme = StandardScheme(str(isolation), {}) diff --git a/tests/cli/version/test_version.py b/tests/cli/version/test_version.py index c13e5f27f..d4eb0c419 100644 --- a/tests/cli/version/test_version.py +++ b/tests/cli/version/test_version.py @@ -275,6 +275,51 @@ def test_set_dynamic(hatch, helpers, temp_dir): ) +@pytest.mark.usefixtures('mock_backend_process') +def test_set_dynamic_downgrade(hatch, helpers, temp_dir): + project_name = 'My.App' + + with temp_dir.as_cwd(): + hatch('new', project_name) + + path = temp_dir / 'my-app' + data_path = temp_dir / 'data' + data_path.mkdir() + + (path / 'src' / 'my_app' / '__about__.py').write_text('__version__ = "21.1.2"') + + # This one fails, because it's a downgrade without --force + with path.as_cwd(env_vars={ConfigEnvVars.DATA: str(data_path)}): + result = hatch('version', '21.1.0', catch_exceptions=True) + + assert result.exit_code == 1, result.output + assert str(result.exception) == 'Version `21.1.0` is not higher than the original version `21.1.2`' + + # Try again, this time with --force + with path.as_cwd(env_vars={ConfigEnvVars.DATA: str(data_path)}): + result = hatch('version', '--force', '21.1.0') + + assert result.exit_code == 0, result.output + assert result.output == helpers.dedent( + """ + Inspecting build dependencies + Old: 21.1.2 + New: 21.1.0 + """ + ) + + with path.as_cwd(env_vars={ConfigEnvVars.DATA: str(data_path)}): + result = hatch('version') + + assert result.exit_code == 0, result.output + assert result.output == helpers.dedent( + """ + Inspecting build dependencies + 21.1.0 + """ + ) + + def test_show_static(hatch, temp_dir): project_name = 'My.App'