generated from sco1/py-template
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add initial config parsing & validation
- Loading branch information
Showing
16 changed files
with
458 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,50 @@ | ||
# bumper | ||
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sco1-bumper/0.1.0?logo=python&logoColor=FFD43B)](https://pypi.org/project/sco1-bumper/) | ||
[![PyPI](https://img.shields.io/pypi/v/sco1-bumper?logo=Python&logoColor=FFD43B)](https://pypi.org/project/sco1-bumper/) | ||
[![PyPI - License](https://img.shields.io/pypi/l/bumper?color=magenta)](https://github.com/sco1/bumper/blob/main/LICENSE) | ||
[![PyPI - License](https://img.shields.io/pypi/l/sco1-bumper?color=magenta)](https://github.com/sco1/bumper/blob/main/LICENSE) | ||
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/sco1/bumper/main.svg)](https://results.pre-commit.ci/latest/github/sco1/bumper/main) | ||
|
||
Automatically increment the project's version number. | ||
|
||
Heavily inspired by [`bump2version`](https://github.com/c4urself/bump2version) and [`bumpversion`](https://github.com/peritus/bumpversion). While [`bump-my-version`](https://github.com/callowayproject/bump-my-version) is an excellent modern fork this functionality, I'd like a pared down version of the offered feature set for my personal projects. | ||
|
||
## Configuration | ||
`bumper` searches for its configuration options first in a `.bumper.toml` file, then in `pyproject.toml`; preference is given to whichever configuration is located first. | ||
### Required Fields | ||
#### `tool.bumper` | ||
* `current_version` - The current software version. This is automatically incremented when bumping. | ||
|
||
#### `tool.bumper.files` | ||
* `file` - Path to target file relative to the repository root | ||
* `search` - Replacement string to search for in the target file. Must contain a `{current_version}` tag if you want something to happen. | ||
|
||
### Example Configuration | ||
The basic configuration looks something like the following: | ||
|
||
```toml | ||
[tool.bumper] | ||
current_version = "0.1.0" | ||
|
||
[[tool.bumper.files]] | ||
file = "./pyproject.toml" | ||
search = 'version = "{current_version}"' | ||
``` | ||
|
||
Multiple replacements within the same file can also be specified: | ||
|
||
```toml | ||
[tool.bumper] | ||
current_version = "0.1.0" | ||
|
||
[[tool.bumper.files]] | ||
file = "./pyproject.toml" | ||
search = 'version = "{current_version}"' | ||
|
||
[[tool.bumper.files]] | ||
file = "./README.md" | ||
search = "sco1-bumper/{current_version}" | ||
|
||
[[tool.bumper.files]] | ||
file = "./README.md" | ||
search = "rev: v{current_version}" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
CONFIG_PRIORITY = (".bumper.toml", "pyproject.toml") |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
def main() -> None: | ||
def main() -> None: # noqa: D103 | ||
raise NotImplementedError | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
from __future__ import annotations | ||
|
||
import tomllib | ||
import typing as t | ||
from pathlib import Path | ||
|
||
from packaging import version | ||
|
||
BUMPER_REQUIRED_FIELDS = ("current_version",) | ||
REPLACEMENT_REQUIRED_FIELDS = ("file", "search") | ||
|
||
|
||
class BumperConfigError(Exception): ... # noqa: D101 | ||
|
||
|
||
class BumperFile(t.NamedTuple): # noqa: D101 | ||
file: Path | ||
search: str | ||
|
||
@classmethod | ||
def from_toml(cls, file: str, search: str) -> BumperFile: # noqa: D102 | ||
return cls(file=Path(file), search=search) | ||
|
||
|
||
def _validate_config(cfg: dict) -> None: | ||
""" | ||
Validate the provided parsed TOML output. | ||
The provided TOML is assumed to contain something like the following: | ||
```toml | ||
[tool.bumper] | ||
current_version = "0.1.0" | ||
[[tool.bumper.files]] | ||
file = "./pyproject.toml" | ||
search = 'version = "{current_version}"' | ||
``` | ||
Raises `BumperConfigError` if any of the required information is missing. | ||
""" | ||
if "tool" not in cfg: | ||
raise BumperConfigError("Configuration file does not declare any tools") | ||
|
||
if "bumper" not in cfg["tool"]: | ||
raise BumperConfigError("Configuration does not declare any bumper configuration") | ||
|
||
for rf in BUMPER_REQUIRED_FIELDS: | ||
if rf not in cfg["tool"]["bumper"]: | ||
raise BumperConfigError(f"Bumper tool declaration missing required field: '{rf}'") | ||
|
||
if "files" not in cfg["tool"]["bumper"]: | ||
raise BumperConfigError("Configuration does not declare any file replacements") | ||
|
||
for file in cfg["tool"]["bumper"]["files"]: | ||
for rf in REPLACEMENT_REQUIRED_FIELDS: | ||
if rf not in file: | ||
raise BumperConfigError( | ||
f"File replacement declaration missing required field: '{rf}'" | ||
) | ||
|
||
|
||
PARSED_T: t.TypeAlias = tuple[version.Version, list[BumperFile]] | ||
|
||
|
||
def parse_config(cfg_path: Path) -> PARSED_T: | ||
""" | ||
Parse the provided configuration file for its relevant information. | ||
Incoming information relevant to bumper is validated & extracted for downstream use. | ||
""" | ||
if not cfg_path.exists(): | ||
raise ValueError(f"Configuration file does not exist: '{cfg_path}'") | ||
|
||
with cfg_path.open("rb") as f: | ||
loaded = tomllib.load(f) | ||
|
||
_validate_config(loaded) | ||
current_version = version.parse(loaded["tool"]["bumper"]["current_version"]) | ||
files = [BumperFile.from_toml(**f) for f in loaded["tool"]["bumper"]["files"]] | ||
|
||
return current_version, files |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from pathlib import Path | ||
|
||
TEST_DATA_DIR = Path(__file__).parent / "test_data" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import tomllib | ||
from pathlib import Path | ||
|
||
import pytest | ||
from packaging import version | ||
|
||
from bumper.config import BumperConfigError, BumperFile, PARSED_T, _validate_config, parse_config | ||
from tests import TEST_DATA_DIR | ||
|
||
TOML_NO_TOOLS = """\ | ||
[project] | ||
name = "sco1-bumper" | ||
version = "0.1.0" | ||
""" | ||
|
||
|
||
def test_config_validation_no_tool_raises() -> None: | ||
cfg = tomllib.loads(TOML_NO_TOOLS) | ||
with pytest.raises(BumperConfigError, match="tools"): | ||
_validate_config(cfg) | ||
|
||
|
||
TOML_NO_BUMPER = """\ | ||
[tool.black] | ||
line-length = 100 | ||
""" | ||
|
||
|
||
def test_config_validation_no_bumper_raises() -> None: | ||
cfg = tomllib.loads(TOML_NO_BUMPER) | ||
with pytest.raises(BumperConfigError, match="bumper configuration"): | ||
_validate_config(cfg) | ||
|
||
|
||
TOML_MISSING_BUMPER_INFO = """\ | ||
[tool.bumper] | ||
hello_world = "hi" | ||
""" | ||
|
||
|
||
def test_config_validation_missing_bumper_info_raises() -> None: | ||
cfg = tomllib.loads(TOML_MISSING_BUMPER_INFO) | ||
with pytest.raises(BumperConfigError, match="current_version"): | ||
_validate_config(cfg) | ||
|
||
|
||
TOML_MISSING_FILES = """\ | ||
[tool.bumper] | ||
current_version = "0.1.0" | ||
""" | ||
|
||
|
||
def test_config_validation_no_files_raises() -> None: | ||
cfg = tomllib.loads(TOML_MISSING_FILES) | ||
with pytest.raises(BumperConfigError, match="any file replacements"): | ||
_validate_config(cfg) | ||
|
||
|
||
TOML_MISSING_FILE_INFO = """\ | ||
[tool.bumper] | ||
current_version = "0.1.0" | ||
[[tool.bumper.files]] | ||
file = "./pyproject.toml" | ||
""" | ||
|
||
|
||
def test_config_validation_missing_file_info_raises() -> None: | ||
cfg = tomllib.loads(TOML_MISSING_FILE_INFO) | ||
with pytest.raises(BumperConfigError, match="search"): | ||
_validate_config(cfg) | ||
|
||
|
||
def test_nonexistent_config_raises() -> None: | ||
with pytest.raises(ValueError, match="does not exist"): | ||
parse_config(Path("ooga.booga")) | ||
|
||
|
||
TRUTH_SINGLE_REPLACE = ( | ||
version.Version("0.1.0"), | ||
[BumperFile(file=Path("./pyproject.toml"), search='version = "{current_version}"')], | ||
) | ||
|
||
TRUTH_MULTI_REPLACE = ( | ||
version.Version("0.1.0"), | ||
[ | ||
BumperFile(file=Path("./pyproject.toml"), search='version = "{current_version}"'), | ||
BumperFile(file=Path("./README.md"), search="sco1-bumper/{current_version}"), | ||
BumperFile(file=Path("./README.md"), search="rev: v{current_version}"), | ||
], | ||
) | ||
|
||
CONFIG_PARSER_TEST_CASES = ( | ||
(TEST_DATA_DIR / "sample_config.toml", TRUTH_SINGLE_REPLACE), | ||
(TEST_DATA_DIR / "sample_pyproject.toml", TRUTH_SINGLE_REPLACE), | ||
(TEST_DATA_DIR / "sample_config_multi_replace.toml", TRUTH_MULTI_REPLACE), | ||
(TEST_DATA_DIR / "sample_pyproject_multi_replace.toml", TRUTH_MULTI_REPLACE), | ||
) | ||
|
||
|
||
@pytest.mark.parametrize(("cfg_path", "truth_parsed"), CONFIG_PARSER_TEST_CASES) | ||
def test_config_parse(cfg_path: Path, truth_parsed: PARSED_T) -> None: | ||
assert parse_config(cfg_path) == truth_parsed |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
[tool.bumper] | ||
current_version = "0.1.0" | ||
|
||
[[tool.bumper.files]] | ||
file = "./pyproject.toml" | ||
search = 'version = "{current_version}"' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
[tool.bumper] | ||
current_version = "0.1.0" | ||
|
||
[[tool.bumper.files]] | ||
file = "./pyproject.toml" | ||
search = 'version = "{current_version}"' | ||
|
||
[[tool.bumper.files]] | ||
file = "./README.md" | ||
search = "sco1-bumper/{current_version}" | ||
|
||
[[tool.bumper.files]] | ||
file = "./README.md" | ||
search = "rev: v{current_version}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
[project] | ||
name = "sco1-bumper" | ||
version = "0.1.0" | ||
description = "Automatically increment the project's version number." | ||
authors = [ | ||
{name = "sco1", email = "[email protected]"} | ||
] | ||
maintainers = [ | ||
{name = "sco1", email = "[email protected]"} | ||
] | ||
|
||
readme = "README.md" | ||
classifiers = [ | ||
"Development Status :: 4 - Beta", | ||
"Intended Audience :: Developers", | ||
"License :: OSI Approved :: MIT License", | ||
"Operating System :: OS Independent", | ||
"Programming Language :: Python :: 3 :: Only", | ||
"Programming Language :: Python :: 3.11", | ||
"Programming Language :: Python :: 3.12", | ||
"Programming Language :: Python :: 3.13", | ||
"Typing :: Typed", | ||
] | ||
|
||
requires-python = ">=3.11" | ||
dependencies = [ | ||
"packaging~=24.1", | ||
] | ||
|
||
[project.urls] | ||
Homepage = "https://github.com/sco1/" | ||
Documentation = "https://github.com/sco1/bumper/blob/main/README.md" | ||
Repository = "https://github.com/sco1/bumper" | ||
Issues = "https://github.com/sco1/bumper/issues" | ||
Changelog = "https://github.com/sco1/bumper/blob/main/CHANGELOG.md" | ||
|
||
[project.scripts] | ||
bumper = "bumper.cli:main" | ||
|
||
[tool.uv] | ||
dev-dependencies = [ | ||
"black~=24.10", | ||
"flake8~=7.1", | ||
"flake8-annotations~=3.1", | ||
"isort~=5.13", | ||
"mypy~=1.11", | ||
"pre-commit~=4.0", | ||
"pytest~=8.3", | ||
"pytest-check~=2.4", | ||
"pytest-cov~=5.0", | ||
"pytest-randomly~=3.15", | ||
"ruff~=0.6", | ||
"tox~=4.18", | ||
"tox-uv~=1.11", | ||
] | ||
|
||
[tool.black] | ||
line-length = 100 | ||
|
||
[tool.isort] | ||
case_sensitive = true | ||
known_first_party = "bumper,tests" | ||
no_lines_before = "LOCALFOLDER" | ||
order_by_type = false | ||
profile = "black" | ||
line_length = 100 | ||
|
||
[tool.mypy] | ||
disallow_incomplete_defs = true | ||
disallow_untyped_calls = true | ||
disallow_untyped_decorators = true | ||
disallow_untyped_defs = true | ||
ignore_missing_imports = true | ||
no_implicit_optional = true | ||
show_error_codes = true | ||
warn_redundant_casts = true | ||
warn_return_any = true | ||
warn_unused_configs = true | ||
warn_unused_ignores = true | ||
|
||
[tool.bumper] | ||
current_version = "0.1.0" | ||
|
||
[[tool.bumper.files]] | ||
file = "./pyproject.toml" | ||
search = 'version = "{current_version}"' | ||
|
||
[tool.hatch.build.targets.wheel] | ||
packages = ["bumper/"] | ||
|
||
[build-system] | ||
requires = ["hatchling"] | ||
build-backend = "hatchling.build" |
Oops, something went wrong.