Skip to content

Commit

Permalink
New feature: retrieve and update the PEP 621 project version, if poss…
Browse files Browse the repository at this point in the history
…ible

When determining the current version, and if
`tool.bumpversion.current_version` is not set, attempt to retrieve the
version from `project.version` à la PEP 621. If that setting is not
set, or if the version is explicitly marked as dynamically set, then
continue with querying SCM tags.

When updating the configuration during bumping, if we previously
successfully retrieved a PEP 621 version, then update the
`project.version` field in `pyproject.toml` as well. We always update,
even if the true current version was read from
`tool.bumpversion.current_version` instead of `project.version`.

The docs have been updated; specifically, the "multiple replacements in
one file" howto and the reference for `current_version`.

The tests have been adapted: the new `pep621_info` property would
otherwise trip up the old test output, and the `None` default would trip
up the TOML serializer. Additionally, new tests assert that
`project.version` (and correspondingly, the `pep621_info` property) is
correctly honored or ignored, depending on the other circumstances.
  • Loading branch information
the-13th-letter committed Feb 27, 2025
1 parent ccebdb6 commit 3032450
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 9 deletions.
12 changes: 10 additions & 2 deletions bumpversion/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import TYPE_CHECKING, Any, Optional

from bumpversion.config.files import read_config_file
from bumpversion.config.files import get_pep621_info, read_config_file
from bumpversion.config.models import Config
from bumpversion.exceptions import ConfigurationError
from bumpversion.ui import get_indented_logger
Expand Down Expand Up @@ -32,6 +32,7 @@
"message": "Bump version: {current_version} → {new_version}",
"moveable_tags": [],
"commit_args": None,
"pep621_info": None,
"scm_info": None,
"parts": {},
"files": [],
Expand Down Expand Up @@ -84,6 +85,10 @@ def get_configuration(config_file: Optional[Pathlike] = None, **overrides: Any)
Config.model_rebuild()
config = Config(**config_dict) # type: ignore[arg-type]

# Get the PEP 621 project.version key from pyproject.toml, if possible
config.pep621_info = get_pep621_info(config_file)
logger.debug("config.pep621_info = %r", config.pep621_info)

# Get the information about the SCM
scm_info = SCMInfo(SCMConfig.from_config(config))
config.scm_info = scm_info
Expand Down Expand Up @@ -113,9 +118,12 @@ def check_current_version(config: Config) -> str:
ConfigurationError: If it can't find the current version
"""
current_version = config.current_version
pep621_info = config.pep621_info
scm_info = config.scm_info

if current_version is None and scm_info.current_version:
if current_version is None and pep621_info is not None and pep621_info.version:
return pep621_info.version
elif current_version is None and scm_info.current_version:
return scm_info.current_version
elif current_version and scm_info.current_version and current_version != scm_info.current_version:
logger.warning(
Expand Down
1 change: 1 addition & 0 deletions bumpversion/config/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def get_defaults_from_dest(destination: str) -> Tuple[dict, TOMLDocument]:
project_config = destination_config.get("project", {}).get("version")
config["current_version"] = config["current_version"] or project_config or "0.1.0"
del config["scm_info"]
del config["pep621_info"]
del config["parts"]
del config["files"]

Expand Down
62 changes: 61 additions & 1 deletion bumpversion/config/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import TYPE_CHECKING, Any, Dict, MutableMapping, Optional, Union

from bumpversion.config.files_legacy import read_ini_file
from bumpversion.config.models import PEP621Info
from bumpversion.ui import get_indented_logger, print_warning

if TYPE_CHECKING: # pragma: no-coverage
Expand All @@ -23,6 +24,42 @@
)


def get_pep621_info(config_file: Optional[Pathlike] = None) -> Optional[PEP621Info]:
"""Retrieve the PEP 621 `project` table.
At the moment, only the `project.version` key is handled. Additionally, if the
version is marked as dynamic, then it is explicitly returned as `None`.
Args:
config_file:
The configuration file to explicitly use. Per PEP 621, this file must be
named `pyproject.toml`.
Returns:
A `PEP621Info` structure, if given a `pyproject.toml` file to parse, else `None`.
"""
# We repeat the steps of read_config_file here, except hardcoded to TOML files, and
# without additional error reporting. Then we reimplement read_toml_file, but
# without limiting ourselves to the tool.bumpversion subtable.
if not config_file:
return None

config_path = Path(config_file)
if not config_path.exists():
return None

# PEP 621 explicitly requires pyproject.toml
if config_path.name != "pyproject.toml":
return None

import tomlkit

toml_data = tomlkit.parse(config_path.read_text(encoding="utf-8")).unwrap()
project = toml_data.get("project", {})
return PEP621Info(version=None if "version" in project.get("dynamic", []) else project.get("version"))


def find_config_file(explicit_file: Optional[Pathlike] = None) -> Union[Path, None]:
"""
Find the configuration file, if it exists.
Expand Down Expand Up @@ -139,7 +176,8 @@ def update_config_file(
logger.info("You must have a `.toml` suffix to update the config file: %s.", config_path)
return

# TODO: Eventually this should be transformed into another default "files_to_modify" entry
# TODO: Eventually this config (and datafile_config_pyprojecttoml below) should be
# transformed into another default "files_to_modify" entry
datafile_config = FileChange(
filename=str(config_path),
key_path="tool.bumpversion.current_version",
Expand All @@ -154,4 +192,26 @@ def update_config_file(

updater = DataFileUpdater(datafile_config, config.version_config.part_configs)
updater.update_file(current_version, new_version, context, dry_run)

# Keep PEP 621 `project.version` consistent with `tool.bumpversion.current_version`.
# (At least, if PEP 621 static `project.version` is in use at all.)
if (
config_path.name == "pyproject.toml"
and config.pep621_info is not None
and config.pep621_info.version is not None
):
datafile_config_pyprojecttoml = FileChange(
filename=str(config_path),
key_path="project.version",
search=config.search,
replace=config.replace,
regex=config.regex,
ignore_missing_version=True,
ignore_missing_file=True,
serialize=config.serialize,
parse=config.parse,
)
updater2 = DataFileUpdater(datafile_config_pyprojecttoml, config.version_config.part_configs)
updater2.update_file(current_version, new_version, context, dry_run)

logger.dedent()
9 changes: 9 additions & 0 deletions bumpversion/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import re
from collections import defaultdict
from dataclasses import dataclass
from itertools import chain
from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Tuple, Union

Expand Down Expand Up @@ -97,6 +98,7 @@ class Config(BaseSettings):
commit: bool
message: str
commit_args: Optional[str]
pep621_info: Optional[PEP621Info]
scm_info: Optional[SCMInfo]
parts: Dict[str, VersionComponentSpec]
moveable_tags: list[str] = Field(default_factory=list)
Expand Down Expand Up @@ -179,3 +181,10 @@ def version_spec(self, version: Optional[str] = None) -> "VersionSpec":
from bumpversion.versioning.models import VersionSpec

return VersionSpec(self.parts)


@dataclass
class PEP621Info:
"""PEP 621 info, in particular, the static version number."""

version: Optional[str]
4 changes: 4 additions & 0 deletions docs/howtos/multiple-replacements.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ filename = "CHANGELOG.md"
search = "{current_version}...HEAD"
replace = "{current_version}...{new_version}"
```

??? note "Note: `project.version` in `pyproject.toml`"

This technique is **not** needed to keep `project.version` in `pyproject.toml` up-to-date if you are storing your Bump My Version configuration in `pyproject.toml` as well. Bump My Version will handle this case automatically.
8 changes: 6 additions & 2 deletions docs/reference/configuration/global.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ If you have pre-commit hooks, add an option to turn off your pre-commit hooks. F
::: field-list

required
: **Yes**
: **Yes**

default
: `""`
Expand All @@ -94,7 +94,11 @@ If you have pre-commit hooks, add an option to turn off your pre-commit hooks. F
environment var
: `BUMPVERSION_CURRENT_VERSION`

The current version of the software package before bumping. A value for this is required.
The current version of the software package before bumping. A value for this is required, unless a fallback value is found.

!!! note

‡ If `pyproject.toml` exists, then `current_version` falls back to `project.version` in `pyproject.toml`. This only works if `project.version` is statically set.

## ignore_missing_files

Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/basic_cfg_expected.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
'independent': False,
'optional_value': 'gamma',
'values': ['dev', 'gamma']}},
'pep621_info': {'version': None},
'post_commit_hooks': [],
'pre_commit_hooks': [],
'regex': False,
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/basic_cfg_expected.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ parts:
values:
- "dev"
- "gamma"
pep621_info:
version: null
post_commit_hooks:

pre_commit_hooks:
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/basic_cfg_expected_full.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@
]
}
},
"pep621_info": {
"version": null
},
"post_commit_hooks": [],
"pre_commit_hooks": [],
"regex": false,
Expand Down
Loading

0 comments on commit 3032450

Please sign in to comment.