Skip to content

Commit

Permalink
Support hatch version --force (pypa#1645)
Browse files Browse the repository at this point in the history
Co-authored-by: Ofek Lev <[email protected]>
  • Loading branch information
warsaw and ofek authored Aug 17, 2024
1 parent 70b974c commit e07d298
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 29 deletions.
4 changes: 4 additions & 0 deletions backend/src/hatchling/utils/constants.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
DEFAULT_BUILD_SCRIPT = 'hatch_build.py'
DEFAULT_CONFIG_FILE = 'hatch.toml'


class VersionEnvVars:
VALIDATE_BUMP = 'HATCH_VERSION_VALIDATE_BUMP'
30 changes: 28 additions & 2 deletions backend/src/hatchling/version/scheme/plugin/interface.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
"""
2 changes: 1 addition & 1 deletion backend/src/hatchling/version/scheme/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 7 additions & 6 deletions docs/community/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -35,21 +35,22 @@ hatch test --cover
Run the extended test suite with coverage:

```bash
hatch run full
hatch test --cover --all
```

## Lint

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
Expand All @@ -64,4 +65,4 @@ Build and validate the documentation website:

```bash
hatch run docs:build-check
```
```
1 change: 1 addition & 0 deletions docs/history/hatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
1 change: 1 addition & 0 deletions docs/plugins/version-scheme/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
- PLUGIN_NAME
- root
- config
- validate_bump
- update
30 changes: 11 additions & 19 deletions src/hatch/cli/version/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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():
Expand All @@ -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)
4 changes: 4 additions & 0 deletions src/hatch/config/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
11 changes: 10 additions & 1 deletion tests/backend/version/scheme/test_standard.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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), {})

Expand Down
45 changes: 45 additions & 0 deletions tests/cli/version/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down

0 comments on commit e07d298

Please sign in to comment.