From 3928550ae0183e316c8530b6e5d6ab48b4fdd159 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sat, 9 Dec 2023 09:19:19 -0600 Subject: [PATCH 01/32] Fixed changelog !minor --- CHANGELOG.md | 2 +- tools/bump.sh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) mode change 100644 => 100755 tools/bump.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 647ef4aa..a93ee7fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased (2023-12-06) +## 0.13.0 (2023-12-06) [Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.12.0...0.13.0) ### Fixes diff --git a/tools/bump.sh b/tools/bump.sh old mode 100644 new mode 100755 index 1c081e54..5b36a8c1 --- a/tools/bump.sh +++ b/tools/bump.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + rm -Rf dist RELEASE_KIND=$(generate-changelog --output release-hint) echo "::notice::Suggested release type is: ${RELEASE_KIND}" From def9b100fd70348fcaaefa54ac8bd568b0179f5b Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sat, 9 Dec 2023 18:36:51 -0600 Subject: [PATCH 02/32] Changed configuration so that refactor is a patch change !minor --- .changelog-config.yaml | 4 ++-- CHANGELOG.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.changelog-config.yaml b/.changelog-config.yaml index 838db4e9..94e3b9dd 100644 --- a/.changelog-config.yaml +++ b/.changelog-config.yaml @@ -80,11 +80,11 @@ commit_classifiers: - action: SummaryRegexMatch category: Updates kwargs: - pattern: (?i)^(?:update|change|rename|remove|delete|improve|refactor|chg|modif)[^\n]*$ + pattern: (?i)^(?:update|change|rename|remove|delete|improve|chg|modif)[^\n]*$ - action: SummaryRegexMatch category: Fixes kwargs: - pattern: (?i)^(?:fix)[^\n]*$ + pattern: (?i)^(?:refactor|fix)[^\n]*$ - action: category: Other diff --git a/CHANGELOG.md b/CHANGELOG.md index a93ee7fe..71b364f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased (2023-12-09) +[Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.13.0...HEAD) + + ## 0.13.0 (2023-12-06) [Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.12.0...0.13.0) From 121ef69b2d73606a4cce744ee3f9f762ec16b29d Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sat, 9 Dec 2023 18:37:16 -0600 Subject: [PATCH 03/32] Fixes generate-requirements.sh to upgrade --- requirements/dev.txt | 130 ++++++++++++++++----------------- requirements/docs.txt | 79 ++++++++++---------- requirements/prod.txt | 27 ++++--- requirements/test.txt | 59 ++++++++------- tools/generate-requirements.sh | 8 +- 5 files changed, 153 insertions(+), 150 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index f7fce9ac..f3863a15 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,66 +1,66 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --extra=dev --extra=docs --extra=test --output-file=requirements/dev.txt pyproject.toml # alabaster==0.7.13 # via sphinx -annotated-types==0.5.0 +annotated-types==0.6.0 # via pydantic argopt==0.8.2 # via git-fame -astroid==2.15.6 +astroid==3.0.1 # via sphinx-autodoc2 -babel==2.12.1 +babel==2.13.1 # via sphinx beautifulsoup4==4.12.2 # via furo -build==0.10.0 +build==1.0.3 # via pip-tools -certifi==2023.5.7 +certifi==2023.11.17 # via requests -cfgv==3.3.1 +cfgv==3.4.0 # via pre-commit -charset-normalizer==3.1.0 +charset-normalizer==3.3.2 # via requests -click==8.1.3 +click==8.1.7 # via # bump-my-version (pyproject.toml) # pip-tools # rich-click # sphinx-click # typer -coverage[toml]==7.2.7 +coverage[toml]==7.3.2 # via # bump-my-version (pyproject.toml) # pytest-cov -distlib==0.3.6 +distlib==0.3.7 # via virtualenv docutils==0.20.1 # via # myst-parser # sphinx # sphinx-click -exceptiongroup==1.1.1 - # via pytest -filelock==3.12.2 +dotted-notation==0.10.0 + # via bump-my-version (pyproject.toml) +filelock==3.13.1 # via virtualenv -furo==2023.5.20 +furo==2023.9.10 # via bump-my-version (pyproject.toml) -generate-changelog==0.9.2 +generate-changelog==0.10.0 # via bump-my-version (pyproject.toml) ghp-import==2.1.0 # via bump-my-version (pyproject.toml) git-fame==2.0.1 # via bump-my-version (pyproject.toml) -gitdb==4.0.10 +gitdb==4.0.11 # via gitpython -gitpython==3.1.31 +gitpython==3.1.40 # via generate-changelog -identify==2.5.24 +identify==2.5.33 # via pre-commit -idna==3.4 +idna==3.6 # via requests imagesize==1.4.1 # via sphinx @@ -71,8 +71,6 @@ jinja2==3.1.2 # generate-changelog # myst-parser # sphinx -lazy-object-proxy==1.9.0 - # via astroid linkify-it-py==2.0.2 # via bump-my-version (pyproject.toml) markdown-it-py==3.0.0 @@ -86,78 +84,80 @@ mdit-py-plugins==0.4.0 # via myst-parser mdurl==0.1.2 # via markdown-it-py -more-itertools==9.1.0 +more-itertools==10.1.0 # via generate-changelog myst-parser==2.0.0 # via bump-my-version (pyproject.toml) nodeenv==1.8.0 # via pre-commit -packaging==23.1 +packaging==23.2 # via # build # pytest # sphinx -pip-tools==6.13.0 +pip-tools==7.3.0 # via bump-my-version (pyproject.toml) -platformdirs==3.6.0 +platformdirs==4.1.0 # via virtualenv -pluggy==1.0.0 +pluggy==1.3.0 # via pytest -pre-commit==3.3.3 +pre-commit==3.5.0 # via bump-my-version (pyproject.toml) -pydantic==2.4.0 +pydantic==2.5.2 # via # bump-my-version (pyproject.toml) # pydantic-settings -pydantic-core==2.10.0 +pydantic-core==2.14.5 # via pydantic -pydantic-settings==2.0.3 +pydantic-settings==2.1.0 # via bump-my-version (pyproject.toml) -pygments==2.15.1 +pygments==2.17.2 # via # furo # rich # sphinx +pyparsing==3.1.1 + # via dotted-notation pyproject-hooks==1.0.0 # via build -pytest==7.3.2 +pytest==7.4.3 # via # bump-my-version (pyproject.toml) # pytest-cov # pytest-mock pytest-cov==4.1.0 # via bump-my-version (pyproject.toml) -pytest-mock==3.11.1 +pytest-mock==3.12.0 # via bump-my-version (pyproject.toml) python-dateutil==2.8.2 # via ghp-import python-dotenv==1.0.0 # via pydantic-settings -pyyaml==6.0 +pyyaml==6.0.1 # via # myst-parser # pre-commit requests==2.31.0 # via sphinx -rich==13.4.2 +rich==13.7.0 # via # bump-my-version (pyproject.toml) # rich-click -rich-click==1.6.1 +rich-click==1.7.2 # via bump-my-version (pyproject.toml) -ruamel-yaml==0.17.32 +ruamel-yaml==0.18.5 # via generate-changelog -ruamel-yaml-clib==0.2.7 +ruamel-yaml-clib==0.2.8 # via ruamel-yaml six==1.16.0 # via python-dateutil -smmap==5.0.0 +smmap==5.0.1 # via gitdb snowballstemmer==2.2.0 # via sphinx -soupsieve==2.4.1 +soupsieve==2.5 # via beautifulsoup4 -sphinx==7.0.1 +sphinx==7.2.6 # via # bump-my-version (pyproject.toml) # furo @@ -166,60 +166,56 @@ sphinx==7.0.1 # sphinx-basic-ng # sphinx-click # sphinx-copybutton -sphinx-autodoc-typehints==1.23.2 + # sphinxcontrib-applehelp + # sphinxcontrib-devhelp + # sphinxcontrib-htmlhelp + # sphinxcontrib-qthelp + # sphinxcontrib-serializinghtml +sphinx-autodoc-typehints==1.25.2 # via bump-my-version (pyproject.toml) -sphinx-autodoc2==0.4.2 +sphinx-autodoc2==0.5.0 # via bump-my-version (pyproject.toml) -sphinx-basic-ng==1.0.0b1 +sphinx-basic-ng==1.0.0b2 # via furo -sphinx-click==4.4.0 +sphinx-click==5.1.0 # via bump-my-version (pyproject.toml) sphinx-copybutton==0.5.2 # via bump-my-version (pyproject.toml) -sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-applehelp==1.0.7 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.5 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.0.4 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.6 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.9 # via sphinx tabulate==0.9.0 # via git-fame -tomli==2.0.1 - # via - # build - # coverage - # pyproject-hooks - # pytest - # sphinx-autodoc2 -tomlkit==0.11.8 +tomlkit==0.12.3 # via bump-my-version (pyproject.toml) -tqdm==4.65.0 +tqdm==4.66.1 # via git-fame typer==0.9.0 # via generate-changelog -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via - # astroid # pydantic # pydantic-core + # rich-click # sphinx-autodoc2 # typer uc-micro-py==1.0.2 # via linkify-it-py -urllib3==2.0.3 +urllib3==2.1.0 # via requests -virtualenv==20.23.1 +virtualenv==20.25.0 # via pre-commit -wheel==0.40.0 +wheel==0.42.0 # via pip-tools -wrapt==1.15.0 - # via astroid # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/docs.txt b/requirements/docs.txt index 59234144..fa9959d3 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,24 +1,24 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --extra=docs --output-file=requirements/docs.txt pyproject.toml # alabaster==0.7.13 # via sphinx -annotated-types==0.5.0 +annotated-types==0.6.0 # via pydantic -astroid==2.15.6 +astroid==3.0.1 # via sphinx-autodoc2 -babel==2.12.1 +babel==2.13.1 # via sphinx beautifulsoup4==4.12.2 # via furo -certifi==2023.5.7 +certifi==2023.11.17 # via requests -charset-normalizer==3.1.0 +charset-normalizer==3.3.2 # via requests -click==8.1.3 +click==8.1.7 # via # bump-my-version (pyproject.toml) # rich-click @@ -28,11 +28,13 @@ docutils==0.20.1 # myst-parser # sphinx # sphinx-click -furo==2023.5.20 +dotted-notation==0.10.0 + # via bump-my-version (pyproject.toml) +furo==2023.9.10 # via bump-my-version (pyproject.toml) ghp-import==2.1.0 # via bump-my-version (pyproject.toml) -idna==3.4 +idna==3.6 # via requests imagesize==1.4.1 # via sphinx @@ -40,8 +42,6 @@ jinja2==3.1.2 # via # myst-parser # sphinx -lazy-object-proxy==1.9.0 - # via astroid linkify-it-py==2.0.2 # via bump-my-version (pyproject.toml) markdown-it-py==3.0.0 @@ -57,42 +57,44 @@ mdurl==0.1.2 # via markdown-it-py myst-parser==2.0.0 # via bump-my-version (pyproject.toml) -packaging==23.1 +packaging==23.2 # via sphinx -pydantic==2.4.0 +pydantic==2.5.2 # via # bump-my-version (pyproject.toml) # pydantic-settings -pydantic-core==2.10.0 +pydantic-core==2.14.5 # via pydantic -pydantic-settings==2.0.3 +pydantic-settings==2.1.0 # via bump-my-version (pyproject.toml) -pygments==2.15.1 +pygments==2.17.2 # via # furo # rich # sphinx +pyparsing==3.1.1 + # via dotted-notation python-dateutil==2.8.2 # via ghp-import python-dotenv==1.0.0 # via pydantic-settings -pyyaml==6.0 +pyyaml==6.0.1 # via myst-parser requests==2.31.0 # via sphinx -rich==13.4.2 +rich==13.7.0 # via # bump-my-version (pyproject.toml) # rich-click -rich-click==1.6.1 +rich-click==1.7.2 # via bump-my-version (pyproject.toml) six==1.16.0 # via python-dateutil snowballstemmer==2.2.0 # via sphinx -soupsieve==2.4.1 +soupsieve==2.5 # via beautifulsoup4 -sphinx==7.0.1 +sphinx==7.2.6 # via # bump-my-version (pyproject.toml) # furo @@ -101,41 +103,42 @@ sphinx==7.0.1 # sphinx-basic-ng # sphinx-click # sphinx-copybutton -sphinx-autodoc-typehints==1.23.2 + # sphinxcontrib-applehelp + # sphinxcontrib-devhelp + # sphinxcontrib-htmlhelp + # sphinxcontrib-qthelp + # sphinxcontrib-serializinghtml +sphinx-autodoc-typehints==1.25.2 # via bump-my-version (pyproject.toml) -sphinx-autodoc2==0.4.2 +sphinx-autodoc2==0.5.0 # via bump-my-version (pyproject.toml) -sphinx-basic-ng==1.0.0b1 +sphinx-basic-ng==1.0.0b2 # via furo -sphinx-click==4.4.0 +sphinx-click==5.1.0 # via bump-my-version (pyproject.toml) sphinx-copybutton==0.5.2 # via bump-my-version (pyproject.toml) -sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-applehelp==1.0.7 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.5 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.0.4 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.6 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.9 # via sphinx -tomli==2.0.1 - # via sphinx-autodoc2 -tomlkit==0.11.8 +tomlkit==0.12.3 # via bump-my-version (pyproject.toml) -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via - # astroid # pydantic # pydantic-core + # rich-click # sphinx-autodoc2 uc-micro-py==1.0.2 # via linkify-it-py -urllib3==2.0.3 +urllib3==2.1.0 # via requests -wrapt==1.15.0 - # via astroid diff --git a/requirements/prod.txt b/requirements/prod.txt index f0ccca20..3fd69fec 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,40 +1,45 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --output-file=requirements/prod.txt pyproject.toml # -annotated-types==0.5.0 +annotated-types==0.6.0 # via pydantic -click==8.1.3 +click==8.1.7 # via # bump-my-version (pyproject.toml) # rich-click +dotted-notation==0.10.0 + # via bump-my-version (pyproject.toml) markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -pydantic==2.4.0 +pydantic==2.5.2 # via # bump-my-version (pyproject.toml) # pydantic-settings -pydantic-core==2.10.0 +pydantic-core==2.14.5 # via pydantic -pydantic-settings==2.0.3 +pydantic-settings==2.1.0 # via bump-my-version (pyproject.toml) -pygments==2.15.1 +pygments==2.17.2 # via rich +pyparsing==3.1.1 + # via dotted-notation python-dotenv==1.0.0 # via pydantic-settings -rich==13.4.2 +rich==13.7.0 # via # bump-my-version (pyproject.toml) # rich-click -rich-click==1.6.1 +rich-click==1.7.2 # via bump-my-version (pyproject.toml) -tomlkit==0.11.8 +tomlkit==0.12.3 # via bump-my-version (pyproject.toml) -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # pydantic # pydantic-core + # rich-click diff --git a/requirements/test.txt b/requirements/test.txt index 05070747..9ff09f39 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,28 +1,28 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --extra=test --output-file=requirements/test.txt pyproject.toml # -annotated-types==0.5.0 +annotated-types==0.6.0 # via pydantic -cfgv==3.3.1 +cfgv==3.4.0 # via pre-commit -click==8.1.3 +click==8.1.7 # via # bump-my-version (pyproject.toml) # rich-click -coverage[toml]==7.2.7 +coverage[toml]==7.3.2 # via # bump-my-version (pyproject.toml) # pytest-cov -distlib==0.3.6 +distlib==0.3.7 # via virtualenv -exceptiongroup==1.1.1 - # via pytest -filelock==3.12.2 +dotted-notation==0.10.0 + # via bump-my-version (pyproject.toml) +filelock==3.13.1 # via virtualenv -identify==2.5.24 +identify==2.5.33 # via pre-commit iniconfig==2.0.0 # via pytest @@ -32,54 +32,53 @@ mdurl==0.1.2 # via markdown-it-py nodeenv==1.8.0 # via pre-commit -packaging==23.1 +packaging==23.2 # via pytest -platformdirs==3.6.0 +platformdirs==4.1.0 # via virtualenv -pluggy==1.0.0 +pluggy==1.3.0 # via pytest -pre-commit==3.3.3 +pre-commit==3.5.0 # via bump-my-version (pyproject.toml) -pydantic==2.4.0 +pydantic==2.5.2 # via # bump-my-version (pyproject.toml) # pydantic-settings -pydantic-core==2.10.0 +pydantic-core==2.14.5 # via pydantic -pydantic-settings==2.0.3 +pydantic-settings==2.1.0 # via bump-my-version (pyproject.toml) -pygments==2.15.1 +pygments==2.17.2 # via rich -pytest==7.3.2 +pyparsing==3.1.1 + # via dotted-notation +pytest==7.4.3 # via # bump-my-version (pyproject.toml) # pytest-cov # pytest-mock pytest-cov==4.1.0 # via bump-my-version (pyproject.toml) -pytest-mock==3.11.1 +pytest-mock==3.12.0 # via bump-my-version (pyproject.toml) python-dotenv==1.0.0 # via pydantic-settings -pyyaml==6.0 +pyyaml==6.0.1 # via pre-commit -rich==13.4.2 +rich==13.7.0 # via # bump-my-version (pyproject.toml) # rich-click -rich-click==1.6.1 +rich-click==1.7.2 # via bump-my-version (pyproject.toml) -tomli==2.0.1 - # via - # coverage - # pytest -tomlkit==0.11.8 +tomlkit==0.12.3 # via bump-my-version (pyproject.toml) -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # pydantic # pydantic-core -virtualenv==20.23.1 + # rich-click +virtualenv==20.25.0 # via pre-commit # The following packages are considered to be unsafe in a requirements file: diff --git a/tools/generate-requirements.sh b/tools/generate-requirements.sh index 39c222b1..f97dece8 100755 --- a/tools/generate-requirements.sh +++ b/tools/generate-requirements.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -pip-compile --output-file=requirements/prod.txt pyproject.toml -pip-compile --extra=docs --output-file=requirements/docs.txt pyproject.toml -pip-compile --extra=test --output-file=requirements/test.txt pyproject.toml -pip-compile --extra=dev --extra=docs --extra=test --output-file=requirements/dev.txt pyproject.toml +pip-compile --upgrade --output-file=requirements/prod.txt pyproject.toml +pip-compile --upgrade --extra=docs --output-file=requirements/docs.txt pyproject.toml +pip-compile --upgrade --extra=test --output-file=requirements/test.txt pyproject.toml +pip-compile --upgrade --extra=dev --extra=docs --extra=test --output-file=requirements/dev.txt pyproject.toml From a4c90b2fdcdf57a17242f67c850446b65a27470a Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sat, 9 Dec 2023 18:40:00 -0600 Subject: [PATCH 04/32] Refactored config file management Moved the INI format stuff into files_legacy.py --- bumpversion/bump.py | 3 +- bumpversion/config/files.py | 145 ++--------- bumpversion/config/files_legacy.py | 127 ++++++++++ tests/test_config/__init__.py | 0 .../test_files.py} | 226 +++++++----------- tests/test_config/test_files_legacy.py | 150 ++++++++++++ tests/test_config/test_init.py | 0 7 files changed, 386 insertions(+), 265 deletions(-) create mode 100644 bumpversion/config/files_legacy.py create mode 100644 tests/test_config/__init__.py rename tests/{test_config.py => test_config/test_files.py} (67%) create mode 100644 tests/test_config/test_files_legacy.py create mode 100644 tests/test_config/test_init.py diff --git a/bumpversion/bump.py b/bumpversion/bump.py index 595be314..34c7d91f 100644 --- a/bumpversion/bump.py +++ b/bumpversion/bump.py @@ -9,7 +9,8 @@ from bumpversion.version_part import Version from bumpversion.config import Config -from bumpversion.config.files import update_config_file, update_ini_config_file +from bumpversion.config.files import update_config_file +from bumpversion.config.files_legacy import update_ini_config_file from bumpversion.exceptions import ConfigurationError from bumpversion.utils import get_context, key_val_string diff --git a/bumpversion/config/files.py b/bumpversion/config/files.py index 124ddb92..257fc1c6 100644 --- a/bumpversion/config/files.py +++ b/bumpversion/config/files.py @@ -3,11 +3,10 @@ from __future__ import annotations import logging -import re -from difflib import context_diff from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, MutableMapping, Union +from bumpversion.config.files_legacy import read_ini_file from bumpversion.ui import print_warning if TYPE_CHECKING: # pragma: no-coverage @@ -17,10 +16,10 @@ logger = logging.getLogger(__name__) CONFIG_FILE_SEARCH_ORDER = ( - Path(".bumpversion.cfg"), - Path(".bumpversion.toml"), - Path("setup.cfg"), - Path("pyproject.toml"), + ".bumpversion.cfg", + ".bumpversion.toml", + "setup.cfg", + "pyproject.toml", ) @@ -37,7 +36,9 @@ def find_config_file(explicit_file: Union[str, Path, None] = None) -> Union[Path Returns: The configuration file path """ - search_paths = [Path(explicit_file)] if explicit_file else CONFIG_FILE_SEARCH_ORDER + search_paths = ( + [Path(explicit_file)] if explicit_file else [Path.cwd().joinpath(path) for path in CONFIG_FILE_SEARCH_ORDER] + ) return next( (cfg_file for cfg_file in search_paths if cfg_file.exists() and "bumpversion]" in cfg_file.read_text()), None, @@ -61,77 +62,21 @@ def read_config_file(config_file: Union[str, Path, None] = None) -> Dict[str, An logger.info("No configuration file found.") return {} - logger.info("Reading config file %s:", config_file) config_path = Path(config_file) + if not config_path.exists(): + logger.info("Configuration file not found: %s.", config_path) + return {} + + logger.info("Reading config file %s:", config_file) + if config_path.suffix == ".cfg": print_warning("The .cfg file format is deprecated. Please use .toml instead.") return read_ini_file(config_path) elif config_path.suffix == ".toml": return read_toml_file(config_path) - return {} - - -def read_ini_file(file_path: Path) -> Dict[str, Any]: # noqa: C901 - """ - Parse an INI file and return a dictionary of sections and their options. - - Args: - file_path: The path to the INI file. - - Returns: - dict: A dictionary of sections and their options. - """ - import configparser - - from bumpversion import autocast - - # Create a ConfigParser object and read the INI file - config_parser = configparser.RawConfigParser() - if file_path.name == "setup.cfg": - config_parser = configparser.ConfigParser() - - config_parser.read(file_path) - - # Create an empty dictionary to hold the parsed sections and options - bumpversion_options: Dict[str, Any] = {"files": [], "parts": {}} - - # Loop through each section in the INI file - for section_name in config_parser.sections(): - if not section_name.startswith("bumpversion"): - continue - - section_parts = section_name.split(":") - num_parts = len(section_parts) - options = {key: autocast.autocast_value(val) for key, val in config_parser.items(section_name)} - - if num_parts == 1: # bumpversion section - bumpversion_options.update(options) - serialize = bumpversion_options.get("serialize", []) - if "message" in bumpversion_options and isinstance(bumpversion_options["message"], list): - bumpversion_options["message"] = ",".join(bumpversion_options["message"]) - if not isinstance(serialize, list): - bumpversion_options["serialize"] = [serialize] - elif num_parts > 1 and section_parts[1].startswith("file"): - file_options = { - "filename": section_parts[2], - } - file_options.update(options) - if "replace" in file_options and isinstance(file_options["replace"], list): - file_options["replace"] = "\n".join(file_options["replace"]) - bumpversion_options["files"].append(file_options) - elif num_parts > 1 and section_parts[1].startswith("glob"): - file_options = { - "glob": section_parts[2], - } - file_options.update(options) - if "replace" in file_options and isinstance(file_options["replace"], list): - file_options["replace"] = "\n".join(file_options["replace"]) - bumpversion_options["files"].append(file_options) - elif num_parts > 1 and section_parts[1].startswith("part"): - bumpversion_options["parts"][section_parts[2]] = options - - # Return the dictionary of sections and options - return bumpversion_options + else: + logger.info("Unknown config file suffix: %s. Using defaults.", config_path.suffix) + return {} def read_toml_file(file_path: Path) -> Dict[str, Any]: @@ -180,7 +125,7 @@ def update_config_file( config_path = Path(config_file) if config_path.suffix != ".toml": - logger.info("Could not find the current version in the config file: %s.", config_path) + 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 @@ -197,57 +142,3 @@ def update_config_file( updater = DataFileUpdater(datafile_config, config.version_config.part_configs) updater.update_file(current_version, new_version, context, dry_run) - - -def update_ini_config_file( - config_file: Union[str, Path], current_version: str, new_version: str, dry_run: bool = False -) -> None: - """ - Update the current_version key in the configuration file. - - Instead of parsing and re-writing the config file with new information, it will use - a regular expression to just replace the current_version value. The idea is it will - avoid unintentional changes (like formatting) to the config file. - - Args: - config_file: The configuration file to explicitly use. - current_version: The serialized current version. - new_version: The serialized new version. - dry_run: True if the update should be a dry run. - """ - cfg_current_version_regex = re.compile( - f"(?P\\[bumpversion]\n[^[]*current_version\\s*=\\s*)(?P{current_version})", - re.MULTILINE, - ) - - config_path = Path(config_file) - existing_config = config_path.read_text() - if config_path.suffix == ".cfg" and cfg_current_version_regex.search(existing_config): - sub_str = f"\\g{new_version}" - new_config = cfg_current_version_regex.sub(sub_str, existing_config) - else: - logger.info("Could not find the current version in the config file: %s.", config_path) - return - - logger.info( - "%s to config file %s:", - "Would write" if dry_run else "Writing", - config_path, - ) - - logger.info( - "\n".join( - list( - context_diff( - existing_config.splitlines(), - new_config.splitlines(), - fromfile=f"before {config_path}", - tofile=f"after {config_path}", - lineterm="", - ) - ) - ) - ) - - if not dry_run: - config_path.write_text(new_config) diff --git a/bumpversion/config/files_legacy.py b/bumpversion/config/files_legacy.py new file mode 100644 index 00000000..6a69b113 --- /dev/null +++ b/bumpversion/config/files_legacy.py @@ -0,0 +1,127 @@ +"""This module handles the legacy config file format.""" +from __future__ import annotations + +import logging +import re +from difflib import context_diff +from pathlib import Path +from typing import Any, Dict, Union + +logger = logging.getLogger(__name__) + + +def read_ini_file(file_path: Path) -> Dict[str, Any]: # noqa: C901 + """ + Parse an INI file and return a dictionary of sections and their options. + + Args: + file_path: The path to the INI file. + + Returns: + dict: A dictionary of sections and their options. + """ + import configparser + + from bumpversion import autocast + + # Create a ConfigParser object and read the INI file + config_parser = configparser.RawConfigParser() + if file_path.name == "setup.cfg": + config_parser = configparser.ConfigParser() + + config_parser.read(file_path) + + # Create an empty dictionary to hold the parsed sections and options + bumpversion_options: Dict[str, Any] = {"files": [], "parts": {}} + + # Loop through each section in the INI file + for section_name in config_parser.sections(): + if not section_name.startswith("bumpversion"): + continue + + section_parts = section_name.split(":") + num_parts = len(section_parts) + options = {key: autocast.autocast_value(val) for key, val in config_parser.items(section_name)} + + if num_parts == 1: # bumpversion section + bumpversion_options.update(options) + serialize = bumpversion_options.get("serialize", []) + if "message" in bumpversion_options and isinstance(bumpversion_options["message"], list): + bumpversion_options["message"] = ",".join(bumpversion_options["message"]) + if not isinstance(serialize, list): + bumpversion_options["serialize"] = [serialize] + elif num_parts > 1 and section_parts[1].startswith("file"): + file_options = { + "filename": section_parts[2], + } + file_options.update(options) + if "replace" in file_options and isinstance(file_options["replace"], list): + file_options["replace"] = "\n".join(file_options["replace"]) + bumpversion_options["files"].append(file_options) + elif num_parts > 1 and section_parts[1].startswith("glob"): + file_options = { + "glob": section_parts[2], + } + file_options.update(options) + if "replace" in file_options and isinstance(file_options["replace"], list): + file_options["replace"] = "\n".join(file_options["replace"]) + bumpversion_options["files"].append(file_options) + elif num_parts > 1 and section_parts[1].startswith("part"): + bumpversion_options["parts"][section_parts[2]] = options + + # Return the dictionary of sections and options + return bumpversion_options + + +def update_ini_config_file( + config_file: Union[str, Path], current_version: str, new_version: str, dry_run: bool = False +) -> None: + """ + Update the current_version key in the configuration file. + + Instead of parsing and re-writing the config file with new information, it will use + a regular expression to just replace the current_version value. The idea is it will + avoid unintentional changes (like formatting) to the config file. + + Args: + config_file: The configuration file to explicitly use. + current_version: The serialized current version. + new_version: The serialized new version. + dry_run: True if the update should be a dry run. + """ + cfg_current_version_regex = re.compile( + f"(?P\\[bumpversion]\n[^[]*current_version\\s*=\\s*)(?P{current_version})", + re.MULTILINE, + ) + + config_path = Path(config_file) + existing_config = config_path.read_text() + if config_path.suffix == ".cfg" and cfg_current_version_regex.search(existing_config): + sub_str = f"\\g{new_version}" + new_config = cfg_current_version_regex.sub(sub_str, existing_config) + else: + logger.info("Could not find the current version in the config file: %s.", config_path) + return + + logger.info( + "%s to config file %s:", + "Would write" if dry_run else "Writing", + config_path, + ) + + logger.info( + "\n".join( + list( + context_diff( + existing_config.splitlines(), + new_config.splitlines(), + fromfile=f"before {config_path}", + tofile=f"after {config_path}", + lineterm="", + ) + ) + ) + ) + + if not dry_run: + config_path.write_text(new_config) diff --git a/tests/test_config/__init__.py b/tests/test_config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_config.py b/tests/test_config/test_files.py similarity index 67% rename from tests/test_config.py rename to tests/test_config/test_files.py index 6516c7ce..13eda01e 100644 --- a/tests/test_config.py +++ b/tests/test_config/test_files.py @@ -2,49 +2,107 @@ import difflib import json from pathlib import Path -from textwrap import dedent -import pytest from click.testing import CliRunner, Result -from pytest import param +import pytest +from pytest import LogCaptureFixture, param -import bumpversion.config.files -import bumpversion.config.utils from bumpversion.utils import get_context from bumpversion import config +from bumpversion.config.files import find_config_file, CONFIG_FILE_SEARCH_ORDER +import bumpversion.config.utils + from tests.conftest import inside_dir, get_config_data -@pytest.fixture(params=[".bumpversion.cfg", "setup.cfg"]) -def cfg_file(request) -> str: - """Return both config-file styles ('.bumpversion.cfg', 'setup.cfg').""" - return request.param +class TestFindConfigFile: + """Tests for finding the config file.""" + class TestWhenExplictConfigFileIsPassed: + """Tests for when an explicit config file is passed.""" -@pytest.fixture( - params=[ - "file", - "file(suffix)", - "file (suffix with space)", - "file (suffix lacking closing paren", - ] -) -def cfg_file_keyword(request): - """Return multiple possible styles for the bumpversion:file keyword.""" - return request.param + def test_returns_path_to_existing_file(self, tmp_path: Path) -> None: + """If an explicit config file is passed, it should be returned.""" + cfg_file = tmp_path / "bump.toml" + cfg_file.write_text('[tool.bumpversion]\ncurrent_version = "1.0.0"') + assert find_config_file(cfg_file) == cfg_file + def test_returns_none_when_missing_file(self, tmp_path: Path) -> None: + """If an explicit config file is passed, it should be returned.""" + cfg_file = tmp_path / "bump.toml" + assert find_config_file(cfg_file) is None -@pytest.mark.parametrize( - ["conf_file", "expected_file"], - [ - param("basic_cfg.cfg", "basic_cfg_expected.json", id="ini basic cfg"), - ], -) -def test_read_ini_file(conf_file: str, expected_file: str, fixtures_path: Path) -> None: - """Parsing the config file should match the expected results.""" - result = bumpversion.config.files.read_ini_file(fixtures_path.joinpath(conf_file)) - expected = json.loads(fixtures_path.joinpath(expected_file).read_text()) - assert result == expected + class TestWhenNoExplicitConfigFileIsPassed: + """Tests for when no explicit config file is passed.""" + + @pytest.mark.parametrize( + ["cfg_file_name"], + (param(item, id=item) for item in CONFIG_FILE_SEARCH_ORDER), + ) + def test_returns_path_to_existing_default_file(self, tmp_path: Path, cfg_file_name: str) -> None: + """If no explicit config file is passed, it returns the path to an existing expected file.""" + cfg_file = tmp_path / cfg_file_name + cfg_file.write_text('[tool.bumpversion]\ncurrent_version = "1.0.0"') + with inside_dir(tmp_path): + assert find_config_file() == cfg_file + + def test_returns_none_when_missing_file(self, tmp_path: Path) -> None: + """If an explicit config file is passed, it should be returned.""" + with inside_dir(tmp_path): + assert find_config_file() is None + + def test_returns_path_to_existing_file_in_correct_order(self, tmp_path: Path) -> None: + """If no explicit config file is passed, it returns the path to an existing expected file.""" + expected_order = list(CONFIG_FILE_SEARCH_ORDER)[:] # make a copy so we can mutate it + cfg_file_paths = [tmp_path / cfg_file_name for cfg_file_name in expected_order] + for cfg_file in cfg_file_paths: # create all the files + cfg_file.write_text('[tool.bumpversion]\ncurrent_version = "1.0.0"') + + with inside_dir(tmp_path): + while expected_order: + expected_file = expected_order.pop(0) # the top item in the list is the next expected file + expected_path = tmp_path / expected_file + assert find_config_file() == expected_path + expected_path.unlink() # remove the file so it doesn't get found again + + +class TestReadConfigFile: + """Tests for reading the config file.""" + + class TestWhenExplictConfigFileIsPassed: + def test_returns_empty_dict_when_missing_file(self, tmp_path: Path, caplog: LogCaptureFixture) -> None: + """If an explicit config file is passed and doesn't exist, it returns an empty dict.""" + cfg_file = tmp_path / "bump.toml" + assert config.read_config_file(cfg_file) == {} + assert "Configuration file not found" in caplog.text + + def test_returns_dict_of_cfg_file(self, fixtures_path: Path) -> None: + """Files with a .cfg suffix is parsed into a dict and returned.""" + cfg_file = fixtures_path / "basic_cfg.cfg" + expected = json.loads(fixtures_path.joinpath("basic_cfg_expected.json").read_text()) + assert config.read_config_file(cfg_file) == expected + + def test_returns_dict_of_toml_file(self, fixtures_path: Path) -> None: + """Files with a .toml suffix is parsed into a dict and returned.""" + cfg_file = fixtures_path / "basic_cfg.toml" + expected = json.loads(fixtures_path.joinpath("basic_cfg_expected.json").read_text()) + assert config.read_config_file(cfg_file) == expected + + def test_returns_empty_dict_with_unknown_suffix(self, tmp_path: Path, caplog: LogCaptureFixture) -> None: + """Files with an unknown suffix return an empty dict.""" + cfg_file = tmp_path / "basic_cfg.unknown" + cfg_file.write_text('[tool.bumpversion]\ncurrent_version = "1.0.0"') + with inside_dir(tmp_path): + assert config.read_config_file(cfg_file) == {} + assert "Unknown config file suffix" in caplog.text + + class TestWhenNoConfigFileIsPassed: + """Tests for when no explicit config file is passed.""" + + def test_returns_empty_dict(self, caplog: LogCaptureFixture) -> None: + """If no explicit config file is passed, it returns an empty dict.""" + assert config.read_config_file() == {} + assert "No configuration file found." in caplog.text @pytest.mark.parametrize( @@ -60,61 +118,6 @@ def test_read_toml_file(conf_file: str, expected_file: str, fixtures_path: Path) assert result == expected -def test_independent_falsy_value_in_config_does_not_bump_independently(tmp_path: Path): - # tmp_path.joinpath("VERSION").write_text("2.1.0-5123") - config_file = tmp_path.joinpath(".bumpversion.cfg") - config_file.write_text( - dedent( - r""" - [bumpversion] - current_version: 2.1.0-5123 - parse = (?P\d+)\.(?P\d+)\.(?P\d+)\-(?P\d+) - serialize = {major}.{minor}.{patch}-{build} - - [bumpversion:file:VERSION] - - [bumpversion:part:build] - independent = 0 - """ - ) - ) - - conf = config.get_configuration(config_file) - assert conf.parts["build"].independent is False - - -def test_correct_interpolation_for_setup_cfg_files(tmp_path: Path, fixtures_path: Path): - """ - Reported here: https://github.com/c4urself/bump2version/issues/21. - """ - test_fixtures_path = fixtures_path.joinpath("interpolation") - setup_cfg = config.get_configuration(test_fixtures_path.joinpath("setup.cfg")) - bumpversion_cfg = config.get_configuration(test_fixtures_path.joinpath(".bumpversion.cfg")) - pyproject_toml = config.get_configuration(test_fixtures_path.joinpath("pyproject.toml")) - - assert setup_cfg.replace == "{now:%m-%d-%Y} v. {new_version}" - assert bumpversion_cfg.replace == "{now:%m-%d-%Y} v. {new_version}" - assert pyproject_toml.replace == "{now:%m-%d-%Y} v. {new_version}" - - -def test_file_keyword_with_suffix_is_accepted(tmp_path: Path, cfg_file: str, cfg_file_keyword: str): - cfg_file_path = tmp_path / cfg_file - cfg_file_path.write_text( - "[bumpversion]\n" - "current_version = 0.10.2\n" - "new_version = 0.10.3\n" - "[bumpversion:file (foobar):file2]\n" - "search = version {current_version}\n" - "replace = version {new_version}\n" - f"[bumpversion:{cfg_file_keyword}:file2]\n" - "search = The current version is {current_version}\n" - "replace = The current version is {new_version}\n" - ) - setup_cfg = config.get_configuration(cfg_file_path) - assert len(setup_cfg.files) == 2 - assert all(f.filename == "file2" for f in setup_cfg.files) - - def test_multiple_config_files(tmp_path: Path): """If there are multiple config files, the first one with content wins.""" setup_cfg = tmp_path / "setup.cfg" @@ -140,32 +143,6 @@ def test_multiple_config_files(tmp_path: Path): assert cfg.serialize == ["{major}.{minor}.{patch}-{release}", "{major}.{minor}.{patch}"] -def test_utf8_message_from_config_file(tmp_path: Path, cfg_file): - cfg_path = tmp_path / cfg_file - initial_config = ( - "[bumpversion]\n" - "current_version = 500.0.0\n" - "commit = True\n" - "message = Nová verze: {current_version} ☃, {new_version} ☀\n" - ) - cfg_path.write_bytes(initial_config.encode("utf-8")) - - with inside_dir(tmp_path): - cfg = config.get_configuration(cfg_path) - - assert cfg.message == "Nová verze: {current_version} ☃, {new_version} ☀" - - -CFG_EXPECTED_DIFF = ( - "*** \n" - "--- \n" - "***************\n" - "*** 11 ****\n" - "! current_version = 1.0.0\n" - "--- 11 ----\n" - "! current_version = 1.0.1\n" -) - TOML_EXPECTED_DIFF = ( "*** \n" "--- \n" @@ -205,31 +182,6 @@ def test_update_config_file(tmp_path: Path, cfg_file_name: str, fixtures_path: P assert "".join(difference) == expected_diff -@pytest.mark.parametrize( - [ - "cfg_file_name", - ], - [ - (".bumpversion.cfg",), - ("setup.cfg",), - ], -) -def test_update_ini_config_file(tmp_path: Path, cfg_file_name: str, fixtures_path: Path) -> None: - """ - Make sure only the version string is updated in the config file. - """ - expected_diff = CFG_EXPECTED_DIFF - cfg_path = tmp_path / cfg_file_name - orig_path = fixtures_path / f"basic_cfg{cfg_path.suffix}" - cfg_path.write_text(orig_path.read_text()) - original_content = orig_path.read_text().splitlines(keepends=True) - - bumpversion.config.files.update_ini_config_file(cfg_path, "1.0.0", "1.0.1") - new_content = cfg_path.read_text().splitlines(keepends=True) - difference = difflib.context_diff(original_content, new_content, n=0) - assert "".join(difference) == expected_diff - - def test_pep440_config(git_repo: Path, fixtures_path: Path): """ Check the PEP440 config file. diff --git a/tests/test_config/test_files_legacy.py b/tests/test_config/test_files_legacy.py new file mode 100644 index 00000000..33c2a4fd --- /dev/null +++ b/tests/test_config/test_files_legacy.py @@ -0,0 +1,150 @@ +import difflib +import json +from pathlib import Path +from textwrap import dedent + +import pytest +from _pytest.mark import param + +from bumpversion.config.files_legacy import read_ini_file, update_ini_config_file +from bumpversion import config +from ..conftest import inside_dir + + +@pytest.fixture( + params=[ + "file", + "file(suffix)", + "file (suffix with space)", + "file (suffix lacking closing paren", + ] +) +def cfg_file_keyword(request): + """Return multiple possible styles for the bumpversion:file keyword.""" + return request.param + + +@pytest.fixture(params=[".bumpversion.cfg", "setup.cfg"]) +def cfg_file(request) -> str: + """Return both config-file styles ('.bumpversion.cfg', 'setup.cfg').""" + return request.param + + +@pytest.mark.parametrize( + ["conf_file", "expected_file"], + [ + param("basic_cfg.cfg", "basic_cfg_expected.json", id="ini basic cfg"), + ], +) +def test_read_ini_file(conf_file: str, expected_file: str, fixtures_path: Path) -> None: + """Parsing the config file should match the expected results.""" + result = read_ini_file(fixtures_path.joinpath(conf_file)) + expected = json.loads(fixtures_path.joinpath(expected_file).read_text()) + assert result == expected + + +def test_independent_falsy_value_in_config_does_not_bump_independently(tmp_path: Path): + # tmp_path.joinpath("VERSION").write_text("2.1.0-5123") + config_file = tmp_path.joinpath(".bumpversion.cfg") + config_file.write_text( + dedent( + r""" + [bumpversion] + current_version: 2.1.0-5123 + parse = (?P\d+)\.(?P\d+)\.(?P\d+)\-(?P\d+) + serialize = {major}.{minor}.{patch}-{build} + + [bumpversion:file:VERSION] + + [bumpversion:part:build] + independent = 0 + """ + ) + ) + + conf = config.get_configuration(config_file) + assert conf.parts["build"].independent is False + + +def test_correct_interpolation_for_setup_cfg_files(tmp_path: Path, fixtures_path: Path): + """ + Reported here: https://github.com/c4urself/bump2version/issues/21. + """ + test_fixtures_path = fixtures_path.joinpath("interpolation") + setup_cfg = config.get_configuration(test_fixtures_path.joinpath("setup.cfg")) + bumpversion_cfg = config.get_configuration(test_fixtures_path.joinpath(".bumpversion.cfg")) + pyproject_toml = config.get_configuration(test_fixtures_path.joinpath("pyproject.toml")) + + assert setup_cfg.replace == "{now:%m-%d-%Y} v. {new_version}" + assert bumpversion_cfg.replace == "{now:%m-%d-%Y} v. {new_version}" + assert pyproject_toml.replace == "{now:%m-%d-%Y} v. {new_version}" + + +CFG_EXPECTED_DIFF = ( + "*** \n" + "--- \n" + "***************\n" + "*** 11 ****\n" + "! current_version = 1.0.0\n" + "--- 11 ----\n" + "! current_version = 1.0.1\n" +) + + +@pytest.mark.parametrize( + [ + "cfg_file_name", + ], + [ + (".bumpversion.cfg",), + ("setup.cfg",), + ], +) +def test_update_ini_config_file(tmp_path: Path, cfg_file_name: str, fixtures_path: Path) -> None: + """ + Make sure only the version string is updated in the config file. + """ + expected_diff = CFG_EXPECTED_DIFF + cfg_path = tmp_path / cfg_file_name + orig_path = fixtures_path / f"basic_cfg{cfg_path.suffix}" + cfg_path.write_text(orig_path.read_text()) + original_content = orig_path.read_text().splitlines(keepends=True) + + update_ini_config_file(cfg_path, "1.0.0", "1.0.1") + new_content = cfg_path.read_text().splitlines(keepends=True) + difference = difflib.context_diff(original_content, new_content, n=0) + assert "".join(difference) == expected_diff + + +def test_file_keyword_with_suffix_is_accepted(tmp_path: Path, cfg_file: str, cfg_file_keyword: str): + cfg_file_path = tmp_path / cfg_file + cfg_file_path.write_text( + "[bumpversion]\n" + "current_version = 0.10.2\n" + "new_version = 0.10.3\n" + "[bumpversion:file (foobar):file2]\n" + "search = version {current_version}\n" + "replace = version {new_version}\n" + f"[bumpversion:{cfg_file_keyword}:file2]\n" + "search = The current version is {current_version}\n" + "replace = The current version is {new_version}\n" + ) + setup_cfg = config.get_configuration(cfg_file_path) + assert len(setup_cfg.files) == 2 + assert all(f.filename == "file2" for f in setup_cfg.files) + + +def test_utf8_message_from_config_file(tmp_path: Path, cfg_file): + cfg_path = tmp_path / cfg_file + initial_config = ( + "[bumpversion]\n" + "current_version = 500.0.0\n" + "commit = True\n" + "message = Nová verze: {current_version} ☃, {new_version} ☀\n" + ) + cfg_path.write_bytes(initial_config.encode("utf-8")) + + with inside_dir(tmp_path): + cfg = config.get_configuration(cfg_path) + + assert cfg.message == "Nová verze: {current_version} ☃, {new_version} ☀" diff --git a/tests/test_config/test_init.py b/tests/test_config/test_init.py new file mode 100644 index 00000000..e69de29b From 249a99992088caf5c2c05bfa9a38d10795c0c896 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sat, 9 Dec 2023 19:21:11 -0600 Subject: [PATCH 05/32] Refactored FileConfig to FileChange. This better describes what the class does: describe a file change. Also moved `get_search_pattern` to the class, since it is specific to each instance --- bumpversion/bump.py | 2 +- bumpversion/config/files.py | 4 +- bumpversion/config/models.py | 47 +++++++++-- bumpversion/config/utils.py | 8 +- bumpversion/files.py | 149 ++++++++++++++++------------------- tests/test_bump.py | 2 +- tests/test_cli.py | 4 +- tests/test_files.py | 4 +- 8 files changed, 119 insertions(+), 101 deletions(-) diff --git a/bumpversion/bump.py b/bumpversion/bump.py index 34c7d91f..a36d381b 100644 --- a/bumpversion/bump.py +++ b/bumpversion/bump.py @@ -115,7 +115,7 @@ def commit_and_tag( extra_args = shlex.split(config.commit_args) if config.commit_args else [] - commit_files = {f.path for f in configured_files} + commit_files = {f.file_change.filename for f in configured_files} if config_file: commit_files |= {str(config_file)} diff --git a/bumpversion/config/files.py b/bumpversion/config/files.py index 257fc1c6..49c523c7 100644 --- a/bumpversion/config/files.py +++ b/bumpversion/config/files.py @@ -116,7 +116,7 @@ def update_config_file( context: The context to use for serialization. dry_run: True if the update should be a dry run. """ - from bumpversion.config.models import FileConfig + from bumpversion.config.models import FileChange from bumpversion.files import DataFileUpdater if not config_file: @@ -129,7 +129,7 @@ def update_config_file( return # TODO: Eventually this should be transformed into another default "files_to_modify" entry - datafile_config = FileConfig( + datafile_config = FileChange( filename=str(config_path), key_path="tool.bumpversion.current_version", search=config.search, diff --git a/bumpversion/config/models.py b/bumpversion/config/models.py index efc88832..5914e3bc 100644 --- a/bumpversion/config/models.py +++ b/bumpversion/config/models.py @@ -1,7 +1,9 @@ """Bump My Version configuration models.""" from __future__ import annotations -from typing import TYPE_CHECKING, Dict, List, Optional, Union +import logging +import re +from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Tuple, Union from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict @@ -10,6 +12,8 @@ from bumpversion.scm import SCMInfo from bumpversion.version_part import VersionConfig +logger = logging.getLogger(__name__) + class VersionPartConfig(BaseModel): """Configuration of a part of the version.""" @@ -21,8 +25,8 @@ class VersionPartConfig(BaseModel): independent: bool = False -class FileConfig(BaseModel): - """Search and replace file config.""" +class FileChange(BaseModel): + """A change to make to a file.""" parse: str serialize: List[str] @@ -34,6 +38,35 @@ class FileConfig(BaseModel): glob: Optional[str] = None # Conflicts with filename. If both are specified, glob wins key_path: Optional[str] = None # If specified, and has an appropriate extension, will be treated as a data file + def get_search_pattern(self, context: MutableMapping) -> Tuple[re.Pattern, str]: + """ + Render the search pattern and return the compiled regex pattern and the raw pattern. + + Args: + context: The context to use for rendering the search pattern + + Returns: + A tuple of the compiled regex pattern and the raw pattern as a string. + """ + # the default search pattern is escaped, so we can still use it in a regex + raw_pattern = self.search.format(**context) + default = re.compile(re.escape(raw_pattern), re.MULTILINE | re.DOTALL) + if not self.regex: + logger.debug("No RegEx flag detected. Searching for the default pattern: '%s'", default.pattern) + return default, raw_pattern + + re_context = {key: re.escape(str(value)) for key, value in context.items()} + regex_pattern = self.search.format(**re_context) + try: + search_for_re = re.compile(regex_pattern, re.MULTILINE | re.DOTALL) + logger.debug("Searching for the regex: '%s'", search_for_re.pattern) + return search_for_re, raw_pattern + except re.error as e: + logger.error("Invalid regex '%s': %s.", default, e) + + logger.debug("Invalid regex. Searching for the default pattern: '%s'", raw_pattern) + return default, raw_pattern + class Config(BaseSettings): """Bump Version configuration.""" @@ -55,7 +88,7 @@ class Config(BaseSettings): commit_args: Optional[str] scm_info: Optional["SCMInfo"] parts: Dict[str, VersionPartConfig] - files: List[FileConfig] + files: List[FileChange] included_paths: List[str] = Field(default_factory=list) excluded_paths: List[str] = Field(default_factory=list) model_config = SettingsConfigDict(env_prefix="bumpversion_") @@ -67,7 +100,7 @@ def add_files(self, filename: Union[str, List[str]]) -> None: if name in self.resolved_filemap: continue self.files.append( - FileConfig( + FileChange( filename=name, glob=None, key_path=None, @@ -81,7 +114,7 @@ def add_files(self, filename: Union[str, List[str]]) -> None: ) @property - def resolved_filemap(self) -> Dict[str, FileConfig]: + def resolved_filemap(self) -> Dict[str, FileChange]: """Return a map of filenames to file configs, expanding any globs.""" from bumpversion.config.utils import resolve_glob_files @@ -95,7 +128,7 @@ def resolved_filemap(self) -> Dict[str, FileConfig]: return {file_cfg.filename: file_cfg for file_cfg in new_files} @property - def files_to_modify(self) -> List[FileConfig]: + def files_to_modify(self) -> List[FileChange]: """Return a list of files to modify.""" files_not_excluded = [ file_cfg.filename diff --git a/bumpversion/config/utils.py b/bumpversion/config/utils.py index a0b41e63..61fb1b7d 100644 --- a/bumpversion/config/utils.py +++ b/bumpversion/config/utils.py @@ -5,11 +5,11 @@ import itertools from typing import Dict, List -from bumpversion.config.models import FileConfig, VersionPartConfig +from bumpversion.config.models import FileChange, VersionPartConfig from bumpversion.utils import labels_for_format -def get_all_file_configs(config_dict: dict) -> List[FileConfig]: +def get_all_file_configs(config_dict: dict) -> List[FileChange]: """Make sure all version parts are included.""" defaults = { "parse": config_dict["parse"], @@ -22,7 +22,7 @@ def get_all_file_configs(config_dict: dict) -> List[FileConfig]: files = [{k: v for k, v in filecfg.items() if v is not None} for filecfg in config_dict["files"]] for f in files: f.update({k: v for k, v in defaults.items() if k not in f}) - return [FileConfig(**f) for f in files] + return [FileChange(**f) for f in files] def get_all_part_configs(config_dict: dict) -> Dict[str, VersionPartConfig]: @@ -36,7 +36,7 @@ def get_all_part_configs(config_dict: dict) -> Dict[str, VersionPartConfig]: } -def resolve_glob_files(file_cfg: FileConfig) -> List[FileConfig]: +def resolve_glob_files(file_cfg: FileChange) -> List[FileChange]: """ Return a list of file configurations that match the glob pattern. diff --git a/bumpversion/files.py b/bumpversion/files.py index 855e786f..ed2bd7cc 100644 --- a/bumpversion/files.py +++ b/bumpversion/files.py @@ -4,47 +4,15 @@ from copy import deepcopy from difflib import context_diff from pathlib import Path -from typing import Dict, List, MutableMapping, Optional, Tuple +from typing import Dict, List, MutableMapping, Optional -from bumpversion.config.models import FileConfig, VersionPartConfig +from bumpversion.config.models import FileChange, VersionPartConfig from bumpversion.exceptions import VersionNotFoundError from bumpversion.version_part import Version, VersionConfig logger = logging.getLogger(__name__) -def get_search_pattern(search_str: str, context: MutableMapping, use_regex: bool = False) -> Tuple[re.Pattern, str]: - """ - Render the search pattern and return the compiled regex pattern and the raw pattern. - - Args: - search_str: A string containing the search pattern as a format string - context: The context to use for rendering the search pattern - use_regex: If True, the search pattern is treated as a regex pattern - - Returns: - A tuple of the compiled regex pattern and the raw pattern as a string. - """ - # the default search pattern is escaped, so we can still use it in a regex - raw_pattern = search_str.format(**context) - default = re.compile(re.escape(raw_pattern), re.MULTILINE | re.DOTALL) - if not use_regex: - logger.debug("No RegEx flag detected. Searching for the default pattern: '%s'", default.pattern) - return default, raw_pattern - - re_context = {key: re.escape(str(value)) for key, value in context.items()} - regex_pattern = search_str.format(**re_context) - try: - search_for_re = re.compile(regex_pattern, re.MULTILINE | re.DOTALL) - logger.debug("Searching for the regex: '%s'", search_for_re.pattern) - return search_for_re, raw_pattern - except re.error as e: - logger.error("Invalid regex '%s': %s.", default, e) - - logger.debug("Invalid regex. Searching for the default pattern: '%s'", raw_pattern) - return default, raw_pattern - - def contains_pattern(search: re.Pattern, contents: str) -> bool: """Does the search pattern match any part of the contents?""" if not search or not contents: @@ -96,26 +64,34 @@ class ConfiguredFile: def __init__( self, - file_cfg: FileConfig, + file_change: FileChange, version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None, ) -> None: - self.path = file_cfg.filename - self.parse = file_cfg.parse or version_config.parse_regex.pattern - self.serialize = file_cfg.serialize or version_config.serialize_formats - self.search = search or file_cfg.search or version_config.search - self.replace = replace or file_cfg.replace or version_config.replace - self.regex = file_cfg.regex or False - self.ignore_missing_version = file_cfg.ignore_missing_version or False + self.file_change = FileChange( + parse=file_change.parse or version_config.parse_regex.pattern, + serialize=file_change.serialize or version_config.serialize_formats, + search=search or file_change.search or version_config.search, + replace=replace or file_change.replace or version_config.replace, + regex=file_change.regex or False, + ignore_missing_version=file_change.ignore_missing_version or False, + filename=file_change.filename, + glob=file_change.glob, + key_path=file_change.key_path, + ) self.version_config = VersionConfig( - self.parse, self.serialize, self.search, self.replace, version_config.part_configs + self.file_change.parse, + self.file_change.serialize, + self.file_change.search, + self.file_change.replace, + version_config.part_configs, ) self._newlines: Optional[str] = None def get_file_contents(self) -> str: """Return the contents of the file.""" - with open(self.path, "rt", encoding="utf-8") as f: + with open(self.file_change.filename, "rt", encoding="utf-8") as f: contents = f.read() self._newlines = f.newlines[0] if isinstance(f.newlines, tuple) else f.newlines return contents @@ -125,7 +101,7 @@ def write_file_contents(self, contents: str) -> None: if self._newlines is None: _ = self.get_file_contents() - with open(self.path, "wt", encoding="utf-8", newline=self._newlines) as f: + with open(self.file_change.filename, "wt", encoding="utf-8", newline=self._newlines) as f: f.write(contents) def contains_version(self, version: Version, context: MutableMapping) -> bool: @@ -142,7 +118,7 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool: Returns: True if the version number is in fact present. """ - search_expression, raw_search_expression = get_search_pattern(self.search, context, self.regex) + search_expression, raw_search_expression = self.file_change.get_search_pattern(context) file_contents = self.get_file_contents() if contains_pattern(search_expression, file_contents): return True @@ -153,7 +129,7 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool: # check whether `search` isn't customized, i.e. should match only # very specific parts of the file - search_pattern_is_default = self.search == self.version_config.search + search_pattern_is_default = self.file_change.search == self.version_config.search if search_pattern_is_default and contains_pattern(re.compile(re.escape(version.original)), file_contents): # The original version is present, and we're not looking for something @@ -161,9 +137,9 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool: return True # version not found - if self.ignore_missing_version: + if self.file_change.ignore_missing_version: return False - raise VersionNotFoundError(f"Did not find '{raw_search_expression}' in file: '{self.path}'") + raise VersionNotFoundError(f"Did not find '{raw_search_expression}' in file: '{self.file_change.filename}'") def replace_version( self, current_version: Version, new_version: Version, context: MutableMapping, dry_run: bool = False @@ -175,7 +151,7 @@ def replace_version( if new_version: context["new_version"] = self.version_config.serialize(new_version, context) - search_for, raw_search_pattern = get_search_pattern(self.search, context, self.regex) + search_for, raw_search_pattern = self.file_change.get_search_pattern(context) replace_with = self.version_config.replace.format(**context) file_content_after = search_for.sub(replace_with, file_content_before) @@ -183,23 +159,23 @@ def replace_version( if file_content_before == file_content_after and current_version.original: og_context = deepcopy(context) og_context["current_version"] = current_version.original - search_for_og, og_raw_search_pattern = get_search_pattern(self.search, og_context, self.regex) + search_for_og, og_raw_search_pattern = self.file_change.get_search_pattern(og_context) file_content_after = search_for_og.sub(replace_with, file_content_before) - log_changes(self.path, file_content_before, file_content_after, dry_run) + log_changes(self.file_change.filename, file_content_before, file_content_after, dry_run) if not dry_run: # pragma: no-coverage self.write_file_contents(file_content_after) def __str__(self) -> str: # pragma: no-coverage - return self.path + return self.file_change.filename def __repr__(self) -> str: # pragma: no-coverage - return f"" + return f"" def resolve_file_config( - files: List[FileConfig], version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None + files: List[FileChange], version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None ) -> List[ConfiguredFile]: """ Resolve the files, searching and replacing values according to the FileConfig. @@ -244,7 +220,7 @@ def _check_files_contain_version( """Make sure files exist and contain version string.""" logger.info( "Asserting files %s contain the version string...", - ", ".join({str(f.path) for f in files}), + ", ".join({str(f.file_change.filename) for f in files}), ) for f in files: context["current_version"] = f.version_config.serialize(current_version, context) @@ -256,21 +232,28 @@ class FileUpdater: def __init__( self, - file_cfg: FileConfig, + file_change: FileChange, version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None, ) -> None: - self.path = file_cfg.filename - self.version_config = version_config - self.parse = file_cfg.parse or version_config.parse_regex.pattern - self.serialize = file_cfg.serialize or version_config.serialize_formats - self.search = search or file_cfg.search or version_config.search - self.replace = replace or file_cfg.replace or version_config.replace - self.regex = file_cfg.regex or False - self.ignore_missing_version = file_cfg.ignore_missing_version or False + self.file_change = FileChange( + parse=file_change.parse or version_config.parse_regex.pattern, + serialize=file_change.serialize or version_config.serialize_formats, + search=search or file_change.search or version_config.search, + replace=replace or file_change.replace or version_config.replace, + regex=file_change.regex or False, + ignore_missing_version=file_change.ignore_missing_version or False, + filename=file_change.filename, + glob=file_change.glob, + key_path=file_change.key_path, + ) self.version_config = VersionConfig( - self.parse, self.serialize, self.search, self.replace, version_config.part_configs + self.file_change.parse, + self.file_change.serialize, + self.file_change.search, + self.file_change.replace, + version_config.part_configs, ) self._newlines: Optional[str] = None @@ -287,18 +270,19 @@ class DataFileUpdater: def __init__( self, - file_cfg: FileConfig, + file_change: FileChange, version_part_configs: Dict[str, VersionPartConfig], ) -> None: - self.path = Path(file_cfg.filename) - self.key_path = file_cfg.key_path - self.search = file_cfg.search - self.replace = file_cfg.replace - self.regex = file_cfg.regex - self.ignore_missing_version = file_cfg.ignore_missing_version + self.file_change = file_change self.version_config = VersionConfig( - file_cfg.parse, file_cfg.serialize, file_cfg.search, file_cfg.replace, version_part_configs + self.file_change.parse, + self.file_change.serialize, + self.file_change.search, + self.file_change.replace, + version_part_configs, ) + self.path = Path(self.file_change.filename) + self._newlines: Optional[str] = None def update_file( self, current_version: Version, new_version: Version, context: MutableMapping, dry_run: bool = False @@ -307,8 +291,8 @@ def update_file( new_context = deepcopy(context) new_context["current_version"] = self.version_config.serialize(current_version, context) new_context["new_version"] = self.version_config.serialize(new_version, context) - search_for, raw_search_pattern = get_search_pattern(self.search, new_context, self.regex) - replace_with = self.replace.format(**new_context) + search_for, raw_search_pattern = self.file_change.get_search_pattern(new_context) + replace_with = self.file_change.replace.format(**new_context) if self.path.suffix == ".toml": self._update_toml_file(search_for, raw_search_pattern, replace_with, dry_run) @@ -320,20 +304,21 @@ def _update_toml_file( import tomlkit toml_data = tomlkit.parse(self.path.read_text()) - value_before = dotted.get(toml_data, self.key_path) + value_before = dotted.get(toml_data, self.file_change.key_path) if value_before is None: - raise KeyError(f"Key path '{self.key_path}' does not exist in {self.path}") - elif not contains_pattern(search_for, value_before) and not self.ignore_missing_version: + raise KeyError(f"Key path '{self.file_change.key_path}' does not exist in {self.path}") + elif not contains_pattern(search_for, value_before) and not self.file_change.ignore_missing_version: raise ValueError( - f"Key '{self.key_path}' in {self.path} does not contain the correct contents: {raw_search_pattern}" + f"Key '{self.file_change.key_path}' in {self.path} does not contain the correct contents: " + f"{raw_search_pattern}" ) new_value = search_for.sub(replace_with, value_before) - log_changes(f"{self.path}:{self.key_path}", value_before, new_value, dry_run) + log_changes(f"{self.path}:{self.file_change.key_path}", value_before, new_value, dry_run) if dry_run: return - dotted.update(toml_data, self.key_path, new_value) + dotted.update(toml_data, self.file_change.key_path, new_value) self.path.write_text(tomlkit.dumps(toml_data)) diff --git a/tests/test_bump.py b/tests/test_bump.py index 0affee64..bfd47f05 100644 --- a/tests/test_bump.py +++ b/tests/test_bump.py @@ -76,7 +76,7 @@ def test_do_bump_with_version_part(mock_update_config_file, mock_modify_files): # Assert mock_modify_files.assert_called_once() mock_update_config_file.assert_called_once() - assert {f.path for f in mock_modify_files.call_args[0][0]} == { + assert {f.file_change.filename for f in mock_modify_files.call_args[0][0]} == { "foo.txt", "bar.txt", } diff --git a/tests/test_cli.py b/tests/test_cli.py index e1d99271..28ab3acf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -426,7 +426,7 @@ def test_replace(mocker, tmp_path, fixtures_path): call_args = mocked_modify_files.call_args[0] configured_files = call_args[0] assert len(configured_files) == 3 - actual_filenames = {f.path for f in configured_files} + actual_filenames = {f.file_change.filename for f in configured_files} assert actual_filenames == {"setup.py", "CHANGELOG.md", "bumpversion/__init__.py"} @@ -471,7 +471,7 @@ def test_replace_specific_files(mocker, git_repo, fixtures_path): call_args = mocked_modify_files.call_args[0] configured_files = call_args[0] assert len(configured_files) == 1 - assert configured_files[0].path == "VERSION" + assert configured_files[0].file_change.filename == "VERSION" TEST_REPLACE_CONFIG = { diff --git a/tests/test_files.py b/tests/test_files.py index 3227bb05..d3cf9a72 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -11,7 +11,7 @@ from pytest import param from bumpversion import exceptions, files, config, bump -from bumpversion.config.models import FileConfig +from bumpversion.config.models import FileChange from bumpversion.utils import get_context from bumpversion.exceptions import VersionNotFoundError from bumpversion.version_part import VersionConfig @@ -446,7 +446,7 @@ def test_datafileupdater_replaces_key(tmp_path: Path, fixtures_path: Path) -> No version_config = VersionConfig(conf.parse, conf.serialize, conf.search, conf.replace, conf.parts) current_version = version_config.parse(conf.current_version) new_version = current_version.bump("minor", version_config.order) - datafile_config = FileConfig( + datafile_config = FileChange( filename=str(config_path), key_path="tool.bumpversion.current_version", search=conf.search, From 4e682145817cbe2db9d60645fca0d1b17846b7a7 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sun, 10 Dec 2023 07:59:38 -0600 Subject: [PATCH 06/32] Refactored logging to provide indented output --- bumpversion/indented_logger.py | 73 ++++++++++++++++++++++++++++++++++ bumpversion/ui.py | 9 ++++- tests/test_indented_logger.py | 71 +++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 bumpversion/indented_logger.py create mode 100644 tests/test_indented_logger.py diff --git a/bumpversion/indented_logger.py b/bumpversion/indented_logger.py new file mode 100644 index 00000000..3ff1b281 --- /dev/null +++ b/bumpversion/indented_logger.py @@ -0,0 +1,73 @@ +"""A logger adapter that adds an indent to the beginning of each message.""" +import logging +from contextvars import ContextVar +from typing import Any, MutableMapping, Optional, Tuple + +CURRENT_INDENT = ContextVar("current_indent", default=0) + + +class IndentedLoggerAdapter(logging.LoggerAdapter): + """ + Logger adapter that adds an indent to the beginning of each message. + + Parameters: + logger: The logger to adapt. + extra: Extra values to add to the logging context. + depth: The number of `indent_char` to generate for each indent level. + indent_char: The character or string to use for indenting. + reset: `True` if the indent level should be reset to zero. + """ + + def __init__( + self, + logger: logging.Logger, + extra: Optional[dict] = None, + depth: int = 2, + indent_char: str = " ", + reset: bool = False, + ): + super().__init__(logger, extra or {}) + self._depth = depth + self._indent_char = indent_char + if reset: + self.reset() + + def indent(self, amount: int = 1) -> None: + """ + Increase the indent level by `amount`. + """ + CURRENT_INDENT.set(CURRENT_INDENT.get() + amount) + + def dedent(self, amount: int = 1) -> None: + """ + Decrease the indent level by `amount`. + """ + CURRENT_INDENT.set(max(0, CURRENT_INDENT.get() - amount)) + + def reset(self) -> None: + """ + Reset the indent level to zero. + """ + CURRENT_INDENT.set(0) + + @property + def indent_str(self) -> str: + """ + The indent string. + """ + return (self._indent_char * self._depth) * CURRENT_INDENT.get() + + def process(self, msg: str, kwargs: Optional[MutableMapping[str, Any]]) -> Tuple[str, MutableMapping[str, Any]]: + """ + Process the message and add the indent. + + Args: + msg: The logging message. + kwargs: Keyword arguments passed to the logger. + + Returns: + A tuple containing the message and keyword arguments. + """ + msg = self.indent_str + msg + + return msg, kwargs diff --git a/bumpversion/ui.py b/bumpversion/ui.py index fa0278a3..a1a367e3 100644 --- a/bumpversion/ui.py +++ b/bumpversion/ui.py @@ -5,6 +5,8 @@ from click import UsageError, secho from rich.logging import RichHandler +from bumpversion.indented_logger import IndentedLoggerAdapter + logger = logging.getLogger("bumpversion") VERBOSITY = { @@ -14,6 +16,11 @@ } +def get_indented_logger(name: str) -> "IndentedLoggerAdapter": + """Get a logger with indentation.""" + return IndentedLoggerAdapter(logging.getLogger(name)) + + def setup_logging(verbose: int = 0) -> None: """Configure the logging.""" logging.basicConfig( @@ -26,7 +33,7 @@ def setup_logging(verbose: int = 0) -> None: ) ], ) - root_logger = logging.getLogger("") + root_logger = get_indented_logger("") root_logger.setLevel(VERBOSITY.get(verbose, logging.DEBUG)) diff --git a/tests/test_indented_logger.py b/tests/test_indented_logger.py new file mode 100644 index 00000000..404e37c5 --- /dev/null +++ b/tests/test_indented_logger.py @@ -0,0 +1,71 @@ +import pytest + +from bumpversion.indented_logger import IndentedLoggerAdapter +import logging + + +class TestIndentedLogger: + def test_does_not_indent_without_intent(self, caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.debug("test debug") + logger.info("test info") + logger.warning("test warning") + logger.error("test error") + logger.critical("test critical") + + assert caplog.record_tuples == [ + ("root", 10, "test debug"), + ("root", 20, "test info"), + ("root", 30, "test warning"), + ("root", 40, "test error"), + ("root", 50, "test critical"), + ] + + def test_indents(self, caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.info("test 1") + logger.indent(2) + logger.error("test %d", 2) + logger.indent() + logger.debug("test 3") + logger.warning("test 4") + logger.indent() + logger.critical("test 5") + logger.critical("test 6") + + assert caplog.record_tuples == [ + ("root", 20, "test 1"), + ("root", 40, " test 2"), + ("root", 10, " test 3"), + ("root", 30, " test 4"), + ("root", 50, " test 5"), + ("root", 50, " test 6"), + ] + + def test_dedents(self, caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.indent(3) + logger.info("test 1") + logger.dedent(2) + logger.error("test %d", 2) + logger.dedent() + logger.debug("test 3") + logger.warning("test 4") + + assert caplog.record_tuples == [ + ("root", 20, " test 1"), + ("root", 40, " test 2"), + ("root", 10, "test 3"), + ("root", 30, "test 4"), + ] + + def test_cant_dedent_below_zero(self, caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger()) + logger.dedent(4) + logger.info("test 1") + + assert caplog.record_tuples == [("root", 20, "test 1")] From d1d19e3d06b6ea9f3e0e4967ce5f79b05729adcd Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 06:51:48 -0600 Subject: [PATCH 07/32] Added indented logger to improve console output --- bumpversion/bump.py | 16 ++- bumpversion/cli.py | 6 +- bumpversion/config/__init__.py | 9 +- bumpversion/config/files.py | 14 +-- bumpversion/config/files_legacy.py | 5 +- bumpversion/indented_logger.py | 7 ++ bumpversion/scm.py | 6 +- bumpversion/version_part.py | 15 ++- tests/test_indented_logger.py | 156 +++++++++++++++++------------ tests/test_version_part.py | 2 +- 10 files changed, 148 insertions(+), 88 deletions(-) diff --git a/bumpversion/bump.py b/bumpversion/bump.py index a36d381b..62fe2c13 100644 --- a/bumpversion/bump.py +++ b/bumpversion/bump.py @@ -1,5 +1,4 @@ """Version changing methods.""" -import logging import shlex from pathlib import Path from typing import TYPE_CHECKING, ChainMap, List, Optional @@ -12,9 +11,10 @@ from bumpversion.config.files import update_config_file from bumpversion.config.files_legacy import update_ini_config_file from bumpversion.exceptions import ConfigurationError +from bumpversion.ui import get_indented_logger from bumpversion.utils import get_context, key_val_string -logger = logging.getLogger("bumpversion") +logger = get_indented_logger(__name__) def get_next_version( @@ -36,14 +36,18 @@ def get_next_version( ConfigurationError: If it can't generate the next version. """ if new_version: + logger.info("Attempting to set new version '%s'", new_version) + logger.indent() next_version = config.version_config.parse(new_version) elif version_part: logger.info("Attempting to increment part '%s'", version_part) + logger.indent() next_version = current_version.bump(version_part, config.version_config.order) else: raise ConfigurationError("Unable to get the next version.") logger.info("Values are now: %s", key_val_string(next_version.values)) + logger.dedent() return next_version @@ -66,8 +70,13 @@ def do_bump( """ from bumpversion.files import modify_files, resolve_file_config + logger.indent() + ctx = get_context(config) + logger.info("Parsing current version '%s'", config.current_version) + logger.indent() version = config.version_config.parse(config.current_version) + logger.dedent() next_version = get_next_version(version, config, version_part, new_version) next_version_str = config.version_config.serialize(next_version, ctx) logger.info("New version will be '%s'", next_version_str) @@ -76,6 +85,8 @@ def do_bump( logger.info("Version is already '%s'", next_version_str) return + logger.dedent() + if dry_run: logger.info("Dry run active, won't touch any files.") @@ -91,6 +102,7 @@ def do_bump( ctx = get_context(config, version, next_version) ctx["new_version"] = next_version_str commit_and_tag(config, config_file, configured_files, ctx, dry_run) + logger.info("Done.") def commit_and_tag( diff --git a/bumpversion/cli.py b/bumpversion/cli.py index 6a82c5b2..7b9cc86e 100644 --- a/bumpversion/cli.py +++ b/bumpversion/cli.py @@ -1,5 +1,4 @@ """bump-my-version Command line interface.""" -import logging from typing import List, Optional import rich_click as click @@ -12,10 +11,10 @@ from bumpversion.config.files import find_config_file from bumpversion.files import ConfiguredFile, modify_files from bumpversion.show import do_show, log_list -from bumpversion.ui import print_warning, setup_logging +from bumpversion.ui import get_indented_logger, print_warning, setup_logging from bumpversion.utils import get_context, get_overrides -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) @click.group( @@ -307,6 +306,7 @@ def bump( config.add_files(files) config.included_paths = files + logger.dedent() do_bump(version_part, new_version, config, found_config_file, dry_run) diff --git a/bumpversion/config/__init__.py b/bumpversion/config/__init__.py index 21cbda1d..12de9b7e 100644 --- a/bumpversion/config/__init__.py +++ b/bumpversion/config/__init__.py @@ -1,17 +1,17 @@ """Configuration management.""" from __future__ import annotations -import logging from typing import TYPE_CHECKING, Union from bumpversion.config.files import read_config_file from bumpversion.config.models import Config from bumpversion.exceptions import ConfigurationError +from bumpversion.ui import get_indented_logger if TYPE_CHECKING: # pragma: no-coverage from pathlib import Path -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) DEFAULTS = { "current_version": None, @@ -49,6 +49,9 @@ def get_configuration(config_file: Union[str, Path, None] = None, **overrides) - from bumpversion.config.utils import get_all_file_configs, get_all_part_configs from bumpversion.scm import SCMInfo, SourceCodeManager, get_scm_info # noqa: F401 + logger.info("Reading configuration") + logger.indent() + config_dict = DEFAULTS.copy() parsed_config = read_config_file(config_file) if config_file else {} @@ -75,6 +78,8 @@ def get_configuration(config_file: Union[str, Path, None] = None, **overrides) - # Update and verify the current_version config.current_version = check_current_version(config) + logger.dedent() + return config diff --git a/bumpversion/config/files.py b/bumpversion/config/files.py index 49c523c7..c8287d7a 100644 --- a/bumpversion/config/files.py +++ b/bumpversion/config/files.py @@ -2,18 +2,17 @@ from __future__ import annotations -import logging from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, MutableMapping, Union from bumpversion.config.files_legacy import read_ini_file -from bumpversion.ui import print_warning +from bumpversion.ui import get_indented_logger, print_warning if TYPE_CHECKING: # pragma: no-coverage from bumpversion.config.models import Config from bumpversion.version_part import Version -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) CONFIG_FILE_SEARCH_ORDER = ( ".bumpversion.cfg", @@ -67,7 +66,7 @@ def read_config_file(config_file: Union[str, Path, None] = None) -> Dict[str, An logger.info("Configuration file not found: %s.", config_path) return {} - logger.info("Reading config file %s:", config_file) + logger.info("Reading config file: %s", config_file) if config_path.suffix == ".cfg": print_warning("The .cfg file format is deprecated. Please use .toml instead.") @@ -120,9 +119,11 @@ def update_config_file( from bumpversion.files import DataFileUpdater if not config_file: - logger.info("No configuration file found to update.") + logger.info("\n%sNo configuration file found to update.", logger.indent_str) return - + else: + logger.info("\n%sProcessing config file: %s", logger.indent_str, config_file) + logger.indent() config_path = Path(config_file) if config_path.suffix != ".toml": logger.info("You must have a `.toml` suffix to update the config file: %s.", config_path) @@ -142,3 +143,4 @@ def update_config_file( updater = DataFileUpdater(datafile_config, config.version_config.part_configs) updater.update_file(current_version, new_version, context, dry_run) + logger.dedent() diff --git a/bumpversion/config/files_legacy.py b/bumpversion/config/files_legacy.py index 6a69b113..7dcaba91 100644 --- a/bumpversion/config/files_legacy.py +++ b/bumpversion/config/files_legacy.py @@ -1,13 +1,14 @@ """This module handles the legacy config file format.""" from __future__ import annotations -import logging import re from difflib import context_diff from pathlib import Path from typing import Any, Dict, Union -logger = logging.getLogger(__name__) +from bumpversion.ui import get_indented_logger + +logger = get_indented_logger(__name__) def read_ini_file(file_path: Path) -> Dict[str, Any]: # noqa: C901 diff --git a/bumpversion/indented_logger.py b/bumpversion/indented_logger.py index 3ff1b281..a8b8faad 100644 --- a/bumpversion/indented_logger.py +++ b/bumpversion/indented_logger.py @@ -32,6 +32,13 @@ def __init__( if reset: self.reset() + @property + def current_indent(self) -> int: + """ + The current indent level. + """ + return CURRENT_INDENT.get() + def indent(self, amount: int = 1) -> None: """ Increase the indent level by `amount`. diff --git a/bumpversion/scm.py b/bumpversion/scm.py index 75240b2f..43f1e16c 100644 --- a/bumpversion/scm.py +++ b/bumpversion/scm.py @@ -1,6 +1,5 @@ """Version control system management.""" -import logging import os import re import subprocess @@ -9,6 +8,7 @@ from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, ClassVar, List, MutableMapping, Optional, Type, Union +from bumpversion.ui import get_indented_logger from bumpversion.utils import extract_regex_flags if TYPE_CHECKING: # pragma: no-coverage @@ -16,7 +16,7 @@ from bumpversion.exceptions import DirtyWorkingDirectoryError, SignedTagsError -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) @dataclass @@ -145,6 +145,7 @@ def commit_to_scm( "Preparing" if do_commit else "Would prepare", cls.__name__, ) + logger.indent() for path in files: logger.info( "%s changes in file '%s' to %s", @@ -171,6 +172,7 @@ def commit_to_scm( new_version=context["new_version"], extra_args=extra_args, ) + logger.dedent() @classmethod def tag_in_scm(cls, config: "Config", context: MutableMapping, dry_run: bool = False) -> None: diff --git a/bumpversion/version_part.py b/bumpversion/version_part.py index 41c2d054..038c0fcb 100644 --- a/bumpversion/version_part.py +++ b/bumpversion/version_part.py @@ -1,5 +1,4 @@ """Module for managing Versions and their internal parts.""" -import logging import re import string from copy import copy @@ -10,9 +9,10 @@ from bumpversion.config.models import VersionPartConfig from bumpversion.exceptions import FormattingError, InvalidVersionPartError, MissingValueError from bumpversion.functions import NumericFunction, PartFunction, ValuesFunction +from bumpversion.ui import get_indented_logger from bumpversion.utils import key_val_string, labels_for_format -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) class VersionPart: @@ -183,6 +183,7 @@ def parse(self, version_string: Optional[str] = None) -> Optional[Version]: version_string, regexp_one_line, ) + logger.indent() match = self.parse_regex.search(version_string) @@ -202,6 +203,7 @@ def parse(self, version_string: Optional[str] = None) -> Optional[Version]: v = Version(_parsed, version_string) logger.info("Parsed the following values: %s", key_val_string(v.values)) + logger.dedent() return v @@ -268,8 +270,8 @@ def _serialize( def _choose_serialize_format(self, version: Version, context: MutableMapping) -> str: chosen = None - logger.debug("Available serialization formats: '%s'", "', '".join(self.serialize_formats)) - + logger.debug("Evaluating serialization formats") + logger.indent() for serialize_format in self.serialize_formats: try: self._serialize(version, serialize_format, context, raise_if_incomplete=True) @@ -291,7 +293,7 @@ def _choose_serialize_format(self, version: Version, context: MutableMapping) -> if not chosen: raise KeyError("Did not find suitable serialization format") - + logger.dedent() logger.debug("Selected serialization format '%s'", chosen) return chosen @@ -307,6 +309,9 @@ def serialize(self, version: Version, context: MutableMapping) -> str: Returns: The serialized version as a string """ + logger.debug("Serializing version '%s'", version) + logger.indent() serialized = self._serialize(version, self._choose_serialize_format(version, context), context) logger.debug("Serialized to '%s'", serialized) + logger.dedent() return serialized diff --git a/tests/test_indented_logger.py b/tests/test_indented_logger.py index 404e37c5..ba87f3f5 100644 --- a/tests/test_indented_logger.py +++ b/tests/test_indented_logger.py @@ -4,68 +4,94 @@ import logging -class TestIndentedLogger: - def test_does_not_indent_without_intent(self, caplog: pytest.LogCaptureFixture): - caplog.set_level(logging.DEBUG) - logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) - logger.debug("test debug") - logger.info("test info") - logger.warning("test warning") - logger.error("test error") - logger.critical("test critical") - - assert caplog.record_tuples == [ - ("root", 10, "test debug"), - ("root", 20, "test info"), - ("root", 30, "test warning"), - ("root", 40, "test error"), - ("root", 50, "test critical"), - ] - - def test_indents(self, caplog: pytest.LogCaptureFixture): - caplog.set_level(logging.DEBUG) - logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) - logger.info("test 1") - logger.indent(2) - logger.error("test %d", 2) - logger.indent() - logger.debug("test 3") - logger.warning("test 4") - logger.indent() - logger.critical("test 5") - logger.critical("test 6") - - assert caplog.record_tuples == [ - ("root", 20, "test 1"), - ("root", 40, " test 2"), - ("root", 10, " test 3"), - ("root", 30, " test 4"), - ("root", 50, " test 5"), - ("root", 50, " test 6"), - ] - - def test_dedents(self, caplog: pytest.LogCaptureFixture): - caplog.set_level(logging.DEBUG) - logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) - logger.indent(3) - logger.info("test 1") - logger.dedent(2) - logger.error("test %d", 2) - logger.dedent() - logger.debug("test 3") - logger.warning("test 4") - - assert caplog.record_tuples == [ - ("root", 20, " test 1"), - ("root", 40, " test 2"), - ("root", 10, "test 3"), - ("root", 30, "test 4"), - ] - - def test_cant_dedent_below_zero(self, caplog: pytest.LogCaptureFixture): - caplog.set_level(logging.DEBUG) - logger = IndentedLoggerAdapter(logging.getLogger()) - logger.dedent(4) - logger.info("test 1") - - assert caplog.record_tuples == [("root", 20, "test 1")] +def test_does_not_indent_without_intent(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.debug("test debug") + logger.info("test info") + logger.warning("test warning") + logger.error("test error") + logger.critical("test critical") + + assert caplog.record_tuples == [ + ("root", 10, "test debug"), + ("root", 20, "test info"), + ("root", 30, "test warning"), + ("root", 40, "test error"), + ("root", 50, "test critical"), + ] + + +def test_indents(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.info("test 1") + logger.indent(2) + logger.error("test %d", 2) + logger.indent() + logger.debug("test 3") + logger.warning("test 4") + logger.indent() + logger.critical("test 5") + logger.critical("test 6") + + assert caplog.record_tuples == [ + ("root", 20, "test 1"), + ("root", 40, " test 2"), + ("root", 10, " test 3"), + ("root", 30, " test 4"), + ("root", 50, " test 5"), + ("root", 50, " test 6"), + ] + + +def test_dedents(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.indent(3) + logger.info("test 1") + logger.dedent(2) + logger.error("test %d", 2) + logger.dedent() + logger.debug("test 3") + logger.warning("test 4") + + assert caplog.record_tuples == [ + ("root", 20, " test 1"), + ("root", 40, " test 2"), + ("root", 10, "test 3"), + ("root", 30, "test 4"), + ] + + +def test_cant_dedent_below_zero(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger()) + logger.dedent(4) + logger.info("test 1") + + assert caplog.record_tuples == [("root", 20, "test 1")] + + +def test_current_indent_shared_by_multiple_loggers(caplog: pytest.LogCaptureFixture): + """Indenting one logger indents all loggers.""" + caplog.set_level(logging.DEBUG) + logger1 = IndentedLoggerAdapter(logging.getLogger("logger1"), reset=True) + logger1.info("test 1") + logger1.indent() + logger2 = IndentedLoggerAdapter(logging.getLogger("logger2")) + logger2.info("test 2") + logger3 = IndentedLoggerAdapter(logging.getLogger("logger3")) + logger3.indent() + logger3.info("test 3") + logger3.info("test 4") + logger2.dedent() + logger2.info("test 5") + + assert caplog.record_tuples == [ + ("logger1", 20, "test 1"), + ("logger2", 20, " test 2"), + ("logger3", 20, " test 3"), + ("logger3", 20, " test 4"), + ("logger2", 20, " test 5"), + ] diff --git a/tests/test_version_part.py b/tests/test_version_part.py index 085101ed..4875fc05 100644 --- a/tests/test_version_part.py +++ b/tests/test_version_part.py @@ -299,7 +299,7 @@ def test_parse_doesnt_parse_current_version(tmp_path: Path, caplog: LogCaptureFi with inside_dir(tmp_path): get_config_data(overrides) - assert "Evaluating 'parse' option: 'xxx' does not parse current version '12'" in caplog.messages + assert " Evaluating 'parse' option: 'xxx' does not parse current version '12'" in caplog.messages def test_part_does_not_revert_to_zero_if_optional(tmp_path: Path) -> None: From e7a7629b19e89e6272e1dbee5b20692656ff197f Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 06:58:21 -0600 Subject: [PATCH 08/32] Fixed regression regarding multiple changes in one file. Changed the method of marking changes from a dict keyed by the file name to a list of FileChanges. FileChanges encapsulate a single change to a file. --- bumpversion/config/models.py | 34 ++++++++++------ bumpversion/files.py | 76 ++++++++++++++++++++---------------- tests/test_files.py | 12 +++--- 3 files changed, 71 insertions(+), 51 deletions(-) diff --git a/bumpversion/config/models.py b/bumpversion/config/models.py index 5914e3bc..3a4cebf5 100644 --- a/bumpversion/config/models.py +++ b/bumpversion/config/models.py @@ -1,18 +1,21 @@ """Bump My Version configuration models.""" from __future__ import annotations -import logging import re +from collections import defaultdict +from itertools import chain from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Tuple, Union from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict +from bumpversion.ui import get_indented_logger + if TYPE_CHECKING: from bumpversion.scm import SCMInfo from bumpversion.version_part import VersionConfig -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) class VersionPartConfig(BaseModel): @@ -48,11 +51,14 @@ def get_search_pattern(self, context: MutableMapping) -> Tuple[re.Pattern, str]: Returns: A tuple of the compiled regex pattern and the raw pattern as a string. """ + logger.debug("Rendering search pattern with context") + logger.indent() # the default search pattern is escaped, so we can still use it in a regex raw_pattern = self.search.format(**context) default = re.compile(re.escape(raw_pattern), re.MULTILINE | re.DOTALL) if not self.regex: logger.debug("No RegEx flag detected. Searching for the default pattern: '%s'", default.pattern) + logger.dedent() return default, raw_pattern re_context = {key: re.escape(str(value)) for key, value in context.items()} @@ -60,11 +66,14 @@ def get_search_pattern(self, context: MutableMapping) -> Tuple[re.Pattern, str]: try: search_for_re = re.compile(regex_pattern, re.MULTILINE | re.DOTALL) logger.debug("Searching for the regex: '%s'", search_for_re.pattern) + logger.dedent() return search_for_re, raw_pattern except re.error as e: logger.error("Invalid regex '%s': %s.", default, e) logger.debug("Invalid regex. Searching for the default pattern: '%s'", raw_pattern) + logger.dedent() + return default, raw_pattern @@ -97,8 +106,6 @@ def add_files(self, filename: Union[str, List[str]]) -> None: """Add a filename to the list of files.""" filenames = [filename] if isinstance(filename, str) else filename for name in filenames: - if name in self.resolved_filemap: - continue self.files.append( FileChange( filename=name, @@ -114,10 +121,11 @@ def add_files(self, filename: Union[str, List[str]]) -> None: ) @property - def resolved_filemap(self) -> Dict[str, FileChange]: + def resolved_filemap(self) -> Dict[str, List[FileChange]]: """Return a map of filenames to file configs, expanding any globs.""" from bumpversion.config.utils import resolve_glob_files + output = defaultdict(list) new_files = [] for file_cfg in self.files: if file_cfg.glob: @@ -125,18 +133,20 @@ def resolved_filemap(self) -> Dict[str, FileChange]: else: new_files.append(file_cfg) - return {file_cfg.filename: file_cfg for file_cfg in new_files} + for file_cfg in new_files: + output[file_cfg.filename].append(file_cfg) + return output @property def files_to_modify(self) -> List[FileChange]: """Return a list of files to modify.""" - files_not_excluded = [ - file_cfg.filename - for file_cfg in self.resolved_filemap.values() - if file_cfg.filename not in self.excluded_paths - ] + files_not_excluded = [filename for filename in self.resolved_filemap if filename not in self.excluded_paths] inclusion_set = set(self.included_paths) | set(files_not_excluded) - return [file_cfg for file_cfg in self.resolved_filemap.values() if file_cfg.filename in inclusion_set] + return list( + chain.from_iterable( + file_cfg_list for key, file_cfg_list in self.resolved_filemap.items() if key in inclusion_set + ) + ) @property def version_config(self) -> "VersionConfig": diff --git a/bumpversion/files.py b/bumpversion/files.py index ed2bd7cc..efccdd3a 100644 --- a/bumpversion/files.py +++ b/bumpversion/files.py @@ -1,5 +1,4 @@ """Methods for changing files.""" -import logging import re from copy import deepcopy from difflib import context_diff @@ -8,9 +7,10 @@ from bumpversion.config.models import FileChange, VersionPartConfig from bumpversion.exceptions import VersionNotFoundError +from bumpversion.ui import get_indented_logger from bumpversion.version_part import Version, VersionConfig -logger = logging.getLogger(__name__) +logger = get_indented_logger(__name__) def contains_pattern(search: re.Pattern, contents: str) -> bool: @@ -22,7 +22,7 @@ def contains_pattern(search: re.Pattern, contents: str) -> bool: line_no = contents.count("\n", 0, m.start(0)) + 1 logger.info( "Found '%s' at line %s: %s", - search, + search.pattern, line_no, m.string[m.start() : m.end(0)], ) @@ -42,8 +42,11 @@ def log_changes(file_path: str, file_content_before: str, file_content_after: st """ if file_content_before != file_content_after: logger.info("%s file %s:", "Would change" if dry_run else "Changing", file_path) + logger.indent() + indent_str = logger.indent_str + logger.info( - "\n".join( + f"\n{indent_str}".join( list( context_diff( file_content_before.splitlines(), @@ -53,8 +56,9 @@ def log_changes(file_path: str, file_content_before: str, file_content_after: st lineterm="", ) ) - ) + ), ) + logger.dedent() else: logger.info("%s file %s", "Would not change" if dry_run else "Not changing", file_path) @@ -104,12 +108,16 @@ def write_file_contents(self, contents: str) -> None: with open(self.file_change.filename, "wt", encoding="utf-8", newline=self._newlines) as f: f.write(contents) - def contains_version(self, version: Version, context: MutableMapping) -> bool: + def _contains_change_pattern( + self, search_expression: re.Pattern, raw_search_expression: str, version: Version, context: MutableMapping + ) -> bool: """ - Check whether the version is present in the file. + Does the file contain the change pattern? Args: - version: The version to check + search_expression: The compiled search expression + raw_search_expression: The raw search expression + version: The version to check, in case it's not the same as the original context: The context to use Raises: @@ -118,17 +126,15 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool: Returns: True if the version number is in fact present. """ - search_expression, raw_search_expression = self.file_change.get_search_pattern(context) file_contents = self.get_file_contents() if contains_pattern(search_expression, file_contents): return True - # the `search` pattern did not match, but the original supplied + # The `search` pattern did not match, but the original supplied # version number (representing the same version part values) might - # match instead. + # match instead. This is probably the case if environment variables are used. - # check whether `search` isn't customized, i.e. should match only - # very specific parts of the file + # check whether `search` isn't customized search_pattern_is_default = self.file_change.search == self.version_config.search if search_pattern_is_default and contains_pattern(re.compile(re.escape(version.original)), file_contents): @@ -141,19 +147,36 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool: return False raise VersionNotFoundError(f"Did not find '{raw_search_expression}' in file: '{self.file_change.filename}'") - def replace_version( + def make_file_change( self, current_version: Version, new_version: Version, context: MutableMapping, dry_run: bool = False ) -> None: - """Replace the current version with the new version.""" - file_content_before = self.get_file_contents() - + """Make the change to the file.""" + logger.info( + "\n%sFile %s: replace `%s` with `%s`", + logger.indent_str, + self.file_change.filename, + self.file_change.search, + self.file_change.replace, + ) + logger.indent() + logger.debug("Serializing the current version") + logger.indent() context["current_version"] = self.version_config.serialize(current_version, context) + logger.dedent() if new_version: + logger.debug("Serializing the new version") + logger.indent() context["new_version"] = self.version_config.serialize(new_version, context) + logger.dedent() search_for, raw_search_pattern = self.file_change.get_search_pattern(context) replace_with = self.version_config.replace.format(**context) + if not self._contains_change_pattern(search_for, raw_search_pattern, current_version, context): + return + + file_content_before = self.get_file_contents() + file_content_after = search_for.sub(replace_with, file_content_before) if file_content_before == file_content_after and current_version.original: @@ -163,7 +186,7 @@ def replace_version( file_content_after = search_for_og.sub(replace_with, file_content_before) log_changes(self.file_change.filename, file_content_before, file_content_after, dry_run) - + logger.dedent() if not dry_run: # pragma: no-coverage self.write_file_contents(file_content_after) @@ -209,22 +232,9 @@ def modify_files( context: The context used for rendering the version dry_run: True if this should be a report-only job """ - _check_files_contain_version(files, current_version, context) - for f in files: - f.replace_version(current_version, new_version, context, dry_run) - - -def _check_files_contain_version( - files: List[ConfiguredFile], current_version: Version, context: MutableMapping -) -> None: - """Make sure files exist and contain version string.""" - logger.info( - "Asserting files %s contain the version string...", - ", ".join({str(f.file_change.filename) for f in files}), - ) + # _check_files_contain_version(files, current_version, context) for f in files: - context["current_version"] = f.version_config.serialize(current_version, context) - f.contains_version(current_version, context) + f.make_file_change(current_version, new_version, context, dry_run) class FileUpdater: diff --git a/tests/test_files.py b/tests/test_files.py index d3cf9a72..f8865104 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -53,7 +53,7 @@ def test_single_file_processed_twice(tmp_path: Path): assert len(conf.files) == 2 for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, new_version, ctx) + cfg_file.make_file_change(current_version, new_version, ctx) assert filepath.read_text() == "dots: 0.10.3\ndashes: 0-10-3" @@ -106,7 +106,7 @@ def test_multi_file_configuration(tmp_path: Path): for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, major_version, ctx) + cfg_file.make_file_change(current_version, major_version, ctx) assert full_vers_path.read_text() == "2.0.0" assert maj_vers_path.read_text() == "2" @@ -123,7 +123,7 @@ def test_multi_file_configuration(tmp_path: Path): major_patch_version = major_version.bump("patch", version_config.order) for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(major_version, major_patch_version, ctx) + cfg_file.make_file_change(major_version, major_patch_version, ctx) assert full_vers_path.read_text() == "2.0.1" assert maj_vers_path.read_text() == "2" @@ -220,7 +220,7 @@ def test_search_replace_to_avoid_updating_unconcerned_lines(tmp_path: Path, capl for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, new_version, get_context(conf)) + cfg_file.make_file_change(current_version, new_version, get_context(conf)) utc_today = datetime.now(timezone.utc).strftime("%Y-%m-%d") expected_chglog = dedent( @@ -291,7 +291,7 @@ def test_simple_replacement_in_utf8_file(tmp_path: Path): # Act for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, new_version, get_context(conf)) + cfg_file.make_file_change(current_version, new_version, get_context(conf)) # Assert out = version_path.read_text() @@ -317,7 +317,7 @@ def test_multi_line_search_is_found(tmp_path: Path) -> None: # Act for file_cfg in conf.files: cfg_file = files.ConfiguredFile(file_cfg, version_config) - cfg_file.replace_version(current_version, new_version, get_context(conf)) + cfg_file.make_file_change(current_version, new_version, get_context(conf)) # Assert assert alphabet_path.read_text() == "A\nB\nC\n10.0.0\n" From 4ce17a9716e259357d0097ee8e8302b9400c4683 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 07:56:44 -0600 Subject: [PATCH 09/32] Added custom GitHub action --- .../actions/setup-python-and-git/action.yaml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/actions/setup-python-and-git/action.yaml diff --git a/.github/actions/setup-python-and-git/action.yaml b/.github/actions/setup-python-and-git/action.yaml new file mode 100644 index 00000000..2471ab09 --- /dev/null +++ b/.github/actions/setup-python-and-git/action.yaml @@ -0,0 +1,23 @@ +name: checkout-and-setup-python +description: 'Checkout the repository and setup Python' +inputs: + python-version: + description: 'Python version to use' + required: false + default: '3.11' +runs: + using: 'composite' + steps: + - uses: actions/setup-python@v4 + name: Setup Python + with: + python-version: ${{ inputs.python-version }} + cache: 'pip' # caching pip dependencies + + - name: Git check + run: | + git config --global user.email "bump-my-version@github.actions" + git config --global user.name "Testing Git" + git --version + git config --list + shell: bash From 89686b89659814878ed782f21b73c308efbbaf0e Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 07:57:21 -0600 Subject: [PATCH 10/32] Removed some commented lines --- tests/test_config/test_files.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test_config/test_files.py b/tests/test_config/test_files.py index 13eda01e..823ab068 100644 --- a/tests/test_config/test_files.py +++ b/tests/test_config/test_files.py @@ -220,18 +220,6 @@ def test_pep440_config(git_repo: Path, fixtures_path: Path): cfg = config.get_configuration(cfg_path) assert cfg.current_version == "1.0.0.dev0+myreallylongbranchna" - # try: - # subprocess.run(["git", "add", "README.md"], check=True, capture_output=True) - # subprocess.run(["git", "commit", "-am", "my branch commit"], check=True, capture_output=True) - # except subprocess.CalledProcessError as e: - # print(e.stdout) - # print(e.stderr) - # raise - # result: Result = runner.invoke(cli.cli, ["bump", "dev_label", "--no-tag"]) - # assert result.exit_code == 0 - # cfg = config.get_configuration(cfg_path) - # assert cfg.current_version == "1.0.0.dev1+myreallylongbranchna" - @pytest.mark.parametrize( ["glob_pattern", "file_list"], From c96e0bd125168c70d42c0c353d9ff9f1a2faaf87 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 07:58:11 -0600 Subject: [PATCH 11/32] Added caching to the resolved filemap --- bumpversion/config/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bumpversion/config/models.py b/bumpversion/config/models.py index 3a4cebf5..230a71bf 100644 --- a/bumpversion/config/models.py +++ b/bumpversion/config/models.py @@ -101,6 +101,7 @@ class Config(BaseSettings): included_paths: List[str] = Field(default_factory=list) excluded_paths: List[str] = Field(default_factory=list) model_config = SettingsConfigDict(env_prefix="bumpversion_") + _resolved_filemap: Optional[Dict[str, List[FileChange]]] = None def add_files(self, filename: Union[str, List[str]]) -> None: """Add a filename to the list of files.""" @@ -122,6 +123,12 @@ def add_files(self, filename: Union[str, List[str]]) -> None: @property def resolved_filemap(self) -> Dict[str, List[FileChange]]: + """Return the cached resolved filemap.""" + if self._resolved_filemap is None: + self._resolved_filemap = self._resolve_filemap() + return self._resolved_filemap + + def _resolve_filemap(self) -> Dict[str, List[FileChange]]: """Return a map of filenames to file configs, expanding any globs.""" from bumpversion.config.utils import resolve_glob_files From 42bab8328a42237c360ff7079b5dc92ecf816a55 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 08:04:13 -0600 Subject: [PATCH 12/32] Fixed missing requirement in GH action --- .github/workflows/bumpversion-pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bumpversion-pr.yaml b/.github/workflows/bumpversion-pr.yaml index 51856910..c85ad0c8 100644 --- a/.github/workflows/bumpversion-pr.yaml +++ b/.github/workflows/bumpversion-pr.yaml @@ -22,7 +22,7 @@ jobs: - name: Install requirements run: | - python -m pip install . bump-my-version + python -m pip install generate-changelog bump-my-version - name: Get the release hint id: generate-changelog From 84556f8afefde297c3e34547a23379321fe26be1 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 08:14:19 -0600 Subject: [PATCH 13/32] Fixed issue when adding files --- bumpversion/config/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bumpversion/config/models.py b/bumpversion/config/models.py index 230a71bf..5b8b22b1 100644 --- a/bumpversion/config/models.py +++ b/bumpversion/config/models.py @@ -120,6 +120,7 @@ def add_files(self, filename: Union[str, List[str]]) -> None: ignore_missing_version=self.ignore_missing_version, ) ) + self._resolved_filemap = None @property def resolved_filemap(self) -> Dict[str, List[FileChange]]: From 909396d7541a5876e19dfd948e5468d90ef43640 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 09:43:56 -0600 Subject: [PATCH 14/32] Changed the management of file changes File changes are hashable to weed out duplication. --- bumpversion/config/models.py | 15 ++++++++++---- bumpversion/version_part.py | 4 ++-- bumpversion/yaml_dump.py | 11 +++++----- tests/fixtures/basic_cfg_expected.txt | 14 ++++++------- tests/test_cli.py | 30 +++++++++++++-------------- tests/test_config/test_files.py | 4 ++-- tests/test_yaml.py | 13 +++++++++--- 7 files changed, 53 insertions(+), 38 deletions(-) diff --git a/bumpversion/config/models.py b/bumpversion/config/models.py index 5b8b22b1..a69ff0ef 100644 --- a/bumpversion/config/models.py +++ b/bumpversion/config/models.py @@ -32,7 +32,7 @@ class FileChange(BaseModel): """A change to make to a file.""" parse: str - serialize: List[str] + serialize: tuple search: str replace: str regex: bool @@ -41,6 +41,10 @@ class FileChange(BaseModel): glob: Optional[str] = None # Conflicts with filename. If both are specified, glob wins key_path: Optional[str] = None # If specified, and has an appropriate extension, will be treated as a data file + def __hash__(self): + """Return a hash of the model.""" + return hash(tuple(sorted(self.model_dump().items()))) + def get_search_pattern(self, context: MutableMapping) -> Tuple[re.Pattern, str]: """ Render the search pattern and return the compiled regex pattern and the raw pattern. @@ -82,7 +86,7 @@ class Config(BaseSettings): current_version: Optional[str] parse: str - serialize: List[str] = Field(min_length=1) + serialize: tuple = Field(min_length=1) search: str replace: str regex: bool @@ -97,7 +101,7 @@ class Config(BaseSettings): commit_args: Optional[str] scm_info: Optional["SCMInfo"] parts: Dict[str, VersionPartConfig] - files: List[FileChange] + files: List[FileChange] = Field(default_factory=list) included_paths: List[str] = Field(default_factory=list) excluded_paths: List[str] = Field(default_factory=list) model_config = SettingsConfigDict(env_prefix="bumpversion_") @@ -106,8 +110,9 @@ class Config(BaseSettings): def add_files(self, filename: Union[str, List[str]]) -> None: """Add a filename to the list of files.""" filenames = [filename] if isinstance(filename, str) else filename + files = set(self.files) for name in filenames: - self.files.append( + files.add( FileChange( filename=name, glob=None, @@ -120,6 +125,8 @@ def add_files(self, filename: Union[str, List[str]]) -> None: ignore_missing_version=self.ignore_missing_version, ) ) + self.files = list(files) + self._resolved_filemap = None @property diff --git a/bumpversion/version_part.py b/bumpversion/version_part.py index 038c0fcb..6058fd59 100644 --- a/bumpversion/version_part.py +++ b/bumpversion/version_part.py @@ -2,7 +2,7 @@ import re import string from copy import copy -from typing import Any, Dict, List, MutableMapping, Optional, Union +from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union from click import UsageError @@ -134,7 +134,7 @@ class VersionConfig: def __init__( self, parse: str, - serialize: List[str], + serialize: Tuple[str], search: str, replace: str, part_configs: Optional[Dict[str, VersionPartConfig]] = None, diff --git a/bumpversion/yaml_dump.py b/bumpversion/yaml_dump.py index 4836dbdd..c4ab0acf 100644 --- a/bumpversion/yaml_dump.py +++ b/bumpversion/yaml_dump.py @@ -3,7 +3,7 @@ from collections import UserDict from io import StringIO from textwrap import indent -from typing import Any, Callable +from typing import Any, Callable, Union DumperFunc = Callable[[Any], str] @@ -89,7 +89,7 @@ def format_dict(val: dict) -> str: for key, value in sorted(val.items()): rendered_value = dump(value).strip() - if isinstance(value, (dict, list)): + if isinstance(value, (dict, list, tuple)): rendered_value = f"\n{indent(rendered_value, INDENT)}" else: rendered_value = f" {rendered_value}" @@ -101,7 +101,7 @@ def format_dict(val: dict) -> str: YAML_DUMPERS.add_dumper(dict, format_dict) -def format_list(val: list) -> str: +def format_sequence(val: Union[list, tuple]) -> str: """Return a string representation of a value.""" buffer = StringIO() @@ -110,7 +110,7 @@ def format_list(val: list) -> str: if isinstance(item, dict): rendered_value = indent(rendered_value, INDENT).strip() - if isinstance(item, list): + if isinstance(item, (list, tuple)): rendered_value = f"\n{indent(rendered_value, INDENT)}" else: rendered_value = f" {rendered_value}" @@ -119,7 +119,8 @@ def format_list(val: list) -> str: return buffer.getvalue() -YAML_DUMPERS.add_dumper(list, format_list) +YAML_DUMPERS.add_dumper(list, format_sequence) +YAML_DUMPERS.add_dumper(tuple, format_sequence) def format_none(_: None) -> str: diff --git a/tests/fixtures/basic_cfg_expected.txt b/tests/fixtures/basic_cfg_expected.txt index b8d31ca7..b1d24f9d 100644 --- a/tests/fixtures/basic_cfg_expected.txt +++ b/tests/fixtures/basic_cfg_expected.txt @@ -11,8 +11,8 @@ 'regex': False, 'replace': '{new_version}', 'search': '{current_version}', - 'serialize': ['{major}.{minor}.{patch}-{release}', - '{major}.{minor}.{patch}']}, + 'serialize': ('{major}.{minor}.{patch}-{release}', + '{major}.{minor}.{patch}')}, {'filename': 'bumpversion/__init__.py', 'glob': None, 'ignore_missing_version': False, @@ -21,8 +21,8 @@ 'regex': False, 'replace': '{new_version}', 'search': '{current_version}', - 'serialize': ['{major}.{minor}.{patch}-{release}', - '{major}.{minor}.{patch}']}, + 'serialize': ('{major}.{minor}.{patch}-{release}', + '{major}.{minor}.{patch}')}, {'filename': 'CHANGELOG.md', 'glob': None, 'ignore_missing_version': False, @@ -31,8 +31,8 @@ 'regex': False, 'replace': '**unreleased**\n**v{new_version}**', 'search': '**unreleased**', - 'serialize': ['{major}.{minor}.{patch}-{release}', - '{major}.{minor}.{patch}']}], + 'serialize': ('{major}.{minor}.{patch}-{release}', + '{major}.{minor}.{patch}')}], 'ignore_missing_version': False, 'included_paths': [], 'message': 'Bump version: {current_version} → {new_version}', @@ -63,7 +63,7 @@ 'short_branch_name': None, 'tool': None}, 'search': '{current_version}', - 'serialize': ['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'], + 'serialize': ('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'), 'sign_tags': False, 'tag': True, 'tag_message': 'Bump version: {current_version} → {new_version}', diff --git a/tests/test_cli.py b/tests/test_cli.py index 28ab3acf..14314cac 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -139,7 +139,7 @@ def test_cli_options_override_config(tmp_path: Path, fixtures_path: Path, mocker assert the_config.current_version == "1.1.0" assert the_config.allow_dirty assert the_config.parse == r"XXX(?P\d+);(?P\d+);(?P\d+)" - assert the_config.serialize == ["XXX{spam};{blob};{slurp}"] + assert the_config.serialize == ("XXX{spam};{blob};{slurp}",) assert the_config.search == "my-search" assert the_config.replace == "my-replace" assert the_config.commit is False @@ -203,7 +203,7 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path): "current_version=1.0.0", "excluded_paths=[]", "parse=(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?", - "serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']", + "serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')", "search={current_version}", "replace={new_version}", "regex=False", @@ -220,19 +220,19 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path): ( "files=[{'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': " "'{new_version}', 'regex': False, 'ignore_missing_version': False, " "'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': " "'{new_version}', 'regex': False, 'ignore_missing_version': False, " "'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, " "{'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '**unreleased**', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': " "'**unreleased**\\n**v{new_version}**', 'regex': False, " "'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, " "'key_path': None}]" @@ -263,7 +263,7 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path): "current_version=1.0.0", "excluded_paths=[]", "parse=(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?", - "serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']", + "serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')", "search={current_version}", "replace={new_version}", "regex=False", @@ -280,19 +280,19 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path): ( "files=[{'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': " "'{new_version}', 'regex': False, 'ignore_missing_version': False, " "'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': " "'{new_version}', 'regex': False, 'ignore_missing_version': False, " "'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, " "{'parse': " "'(?P\\\\d+)\\\\.(?P\\\\d+)\\\\.(?P\\\\d+)(\\\\-(?P[a-z]+))?', " - "'serialize': ['{major}.{minor}.{patch}-{release}', " - "'{major}.{minor}.{patch}'], 'search': '**unreleased**', 'replace': " + "'serialize': ('{major}.{minor}.{patch}-{release}', " + "'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': " "'**unreleased**\\n**v{new_version}**', 'regex': False, " "'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, " "'key_path': None}]" diff --git a/tests/test_config/test_files.py b/tests/test_config/test_files.py index 823ab068..d75fa66f 100644 --- a/tests/test_config/test_files.py +++ b/tests/test_config/test_files.py @@ -140,7 +140,7 @@ def test_multiple_config_files(tmp_path: Path): assert cfg.current_version == "0.10.5" assert cfg.parse == "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?" - assert cfg.serialize == ["{major}.{minor}.{patch}-{release}", "{major}.{minor}.{patch}"] + assert cfg.serialize == ("{major}.{minor}.{patch}-{release}", "{major}.{minor}.{patch}") TOML_EXPECTED_DIFF = ( @@ -283,7 +283,7 @@ def test_file_overrides_config(fixtures_path: Path): assert file_map["should_override_parse.txt"].ignore_missing_version == conf.ignore_missing_version assert file_map["should_override_serialize.txt"].parse == conf.parse - assert file_map["should_override_serialize.txt"].serialize == ["{major}"] + assert file_map["should_override_serialize.txt"].serialize == ("{major}",) assert file_map["should_override_serialize.txt"].search == conf.search assert file_map["should_override_serialize.txt"].replace == conf.replace assert file_map["should_override_serialize.txt"].regex == conf.regex diff --git a/tests/test_yaml.py b/tests/test_yaml.py index 7037f67f..b006b43d 100644 --- a/tests/test_yaml.py +++ b/tests/test_yaml.py @@ -6,7 +6,7 @@ def test_dump_unknown(): - assert yaml_dump.dump((1, 2)) == '"(1, 2)"' + assert yaml_dump.dump({1, 2}) == '"{1, 2}"' def test_format_str(): @@ -42,10 +42,15 @@ def test_format_dict(): "key8": True, "key9": False, "key10": 1.43, + "key11": (1, 2, 3), } expected = ( 'key: "strval"\n' "key10: 1.43\n" + "key11:\n" + " - 1\n" + " - 2\n" + " - 3\n" "key2: 30\n" "key3: 2023-06-19 13:45:30\n" "key4: 2023-06-19\n" @@ -63,8 +68,10 @@ def test_format_dict(): def test_format_list(): - assert yaml_dump.format_list(["item"]) == '- "item"\n' - assert yaml_dump.format_list(["item", ["item2"]]) == '- "item"\n-\n - "item2"\n' + assert yaml_dump.format_sequence(["item"]) == '- "item"\n' + assert yaml_dump.format_sequence(["item", ["item2"]]) == '- "item"\n-\n - "item2"\n' + assert yaml_dump.format_sequence(("item",)) == '- "item"\n' + assert yaml_dump.format_sequence(("item", ("item2",))) == '- "item"\n-\n - "item2"\n' def test_dump_none_val(): From fc505e526e28033b1eb929787ae45b4a0065e505 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 10:39:37 -0600 Subject: [PATCH 15/32] Version updated from 0.13.0 to 0.14.0 --- CHANGELOG.md | 42 ++++++++++++++++++++- bumpversion/__init__.py | 2 +- docsrc/reference/bumpversion/bumpversion.md | 2 +- pyproject.toml | 3 +- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71b364f6..acf51d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,46 @@ # Changelog -## Unreleased (2023-12-09) -[Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.13.0...HEAD) +## 0.14.0 (2023-12-15) +[Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.13.0...0.14.0) +### Fixes + +- Fixed issue when adding files. [84556f8](https://github.com/callowayproject/bump-my-version/commit/84556f8afefde297c3e34547a23379321fe26be1) + +- Fixed missing requirement in GH action. [42bab83](https://github.com/callowayproject/bump-my-version/commit/42bab8328a42237c360ff7079b5dc92ecf816a55) + +- Fixed regression regarding multiple changes in one file. [e7a7629](https://github.com/callowayproject/bump-my-version/commit/e7a7629b19e89e6272e1dbee5b20692656ff197f) + + Changed the method of marking changes from a dict keyed by the file name to a list of FileChanges. + + FileChanges encapsulate a single change to a file. +- Refactored logging to provide indented output. [4e68214](https://github.com/callowayproject/bump-my-version/commit/4e682145817cbe2db9d60645fca0d1b17846b7a7) + +- Refactored FileConfig to FileChange. [249a999](https://github.com/callowayproject/bump-my-version/commit/249a99992088caf5c2c05bfa9a38d10795c0c896) + + This better describes what the class does: describe a file change. + + Also moved `get_search_pattern` to the class, since it is specific to each instance +- Refactored config file management. [a4c90b2](https://github.com/callowayproject/bump-my-version/commit/a4c90b2fdcdf57a17242f67c850446b65a27470a) + + Moved the INI format stuff into files_legacy.py +- Fixes generate-requirements.sh to upgrade. [121ef69](https://github.com/callowayproject/bump-my-version/commit/121ef69b2d73606a4cce744ee3f9f762ec16b29d) + +### New + +- Added caching to the resolved filemap. [c96e0bd](https://github.com/callowayproject/bump-my-version/commit/c96e0bd125168c70d42c0c353d9ff9f1a2faaf87) + +- Added custom GitHub action. [4ce17a9](https://github.com/callowayproject/bump-my-version/commit/4ce17a9716e259357d0097ee8e8302b9400c4683) + +- Added indented logger to improve console output. [d1d19e3](https://github.com/callowayproject/bump-my-version/commit/d1d19e3d06b6ea9f3e0e4967ce5f79b05729adcd) + +### Updates + +- Changed the management of file changes. [909396d](https://github.com/callowayproject/bump-my-version/commit/909396d7541a5876e19dfd948e5468d90ef43640) + + File changes are hashable to weed out duplication. +- Removed some commented lines. [89686b8](https://github.com/callowayproject/bump-my-version/commit/89686b89659814878ed782f21b73c308efbbaf0e) + ## 0.13.0 (2023-12-06) [Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.12.0...0.13.0) diff --git a/bumpversion/__init__.py b/bumpversion/__init__.py index 621576f7..caf9fddb 100644 --- a/bumpversion/__init__.py +++ b/bumpversion/__init__.py @@ -1,3 +1,3 @@ """Top-level package for bump-my-version.""" -__version__: str = "0.13.0" +__version__: str = "0.14.0" diff --git a/docsrc/reference/bumpversion/bumpversion.md b/docsrc/reference/bumpversion/bumpversion.md index 6c29bb5e..68a32d1e 100644 --- a/docsrc/reference/bumpversion/bumpversion.md +++ b/docsrc/reference/bumpversion/bumpversion.md @@ -51,7 +51,7 @@ bumpversion.__main__ :canonical: bumpversion.__version__ :type: str :value: > - '0.13.0' + '0.14.0' ```{autodoc2-docstring} bumpversion.__version__ ``` diff --git a/pyproject.toml b/pyproject.toml index c6926ff4..c9b3b864 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -208,7 +208,7 @@ order-by-type = true convention = "google" [tool.bumpversion] -current_version = "0.13.0" +current_version = "0.14.0" commit = true commit_args = "--no-verify" tag = true @@ -240,6 +240,7 @@ search = "{current_version}...HEAD" replace = "{current_version}...{new_version}" + [tool.pydoclint] style = "google" exclude = '\.git|tests' From d96e07a79aef643a363449b86847008152599f64 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 10:46:59 -0600 Subject: [PATCH 16/32] Fixed requirements for github action --- .github/workflows/bumpversion.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bumpversion.yaml b/.github/workflows/bumpversion.yaml index 5720ed33..b605eeec 100644 --- a/.github/workflows/bumpversion.yaml +++ b/.github/workflows/bumpversion.yaml @@ -21,7 +21,7 @@ jobs: - name: Install requirements run: | - python -m pip install . bump-my-version + python -m pip install generate-changelog bump-my-version - name: Generate the changelog and get the release hint id: generate-changelog From 93191f3c20e0f91224f1c2e1df70c3d477cc67ec Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 15 Dec 2023 12:17:03 -0600 Subject: [PATCH 17/32] Changed default regex CLI value to None. Fixes #64 The default value of False was overriding other values. --- bumpversion/cli.py | 4 ++-- bumpversion/files.py | 14 +++++++++++++- bumpversion/version_part.py | 12 ++++++++++++ tests/fixtures/basic_cfg.toml | 4 ++-- tests/fixtures/regex_test_config.toml | 8 ++++++++ tests/test_cli.py | 27 +++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/regex_test_config.toml diff --git a/bumpversion/cli.py b/bumpversion/cli.py index 7b9cc86e..bcb08d26 100644 --- a/bumpversion/cli.py +++ b/bumpversion/cli.py @@ -141,7 +141,7 @@ def cli(ctx: Context) -> None: ) @click.option( "--regex/--no-regex", - default=False, + default=None, envvar="BUMPVERSION_REGEX", help="Treat the search parameter as a regular expression or explicitly do not treat it as a regular expression.", ) @@ -232,7 +232,7 @@ def bump( serialize: Optional[List[str]], search: Optional[str], replace: Optional[str], - regex: bool, + regex: Optional[bool], no_configured_files: bool, ignore_missing_version: bool, dry_run: bool, diff --git a/bumpversion/files.py b/bumpversion/files.py index efccdd3a..a5f8c1e1 100644 --- a/bumpversion/files.py +++ b/bumpversion/files.py @@ -1,4 +1,5 @@ """Methods for changing files.""" +import os.path import re from copy import deepcopy from difflib import context_diff @@ -94,7 +95,18 @@ def __init__( self._newlines: Optional[str] = None def get_file_contents(self) -> str: - """Return the contents of the file.""" + """ + Return the contents of the file. + + Raises: + FileNotFoundError: if the file doesn't exist + + Returns: + The contents of the file + """ + if not os.path.exists(self.file_change.filename): + raise FileNotFoundError(f"File not found: '{self.file_change.filename}'") + with open(self.file_change.filename, "rt", encoding="utf-8") as f: contents = f.read() self._newlines = f.newlines[0] if isinstance(f.newlines, tuple) else f.newlines diff --git a/bumpversion/version_part.py b/bumpversion/version_part.py index 6058fd59..13430d35 100644 --- a/bumpversion/version_part.py +++ b/bumpversion/version_part.py @@ -150,6 +150,18 @@ def __init__( self.search = search self.replace = replace + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: Any) -> bool: + return ( + self.parse_regex.pattern == other.parse_regex.pattern + and self.serialize_formats == other.serialize_formats + and self.part_configs == other.part_configs + and self.search == other.search + and self.replace == other.replace + ) + @property def order(self) -> List[str]: """ diff --git a/tests/fixtures/basic_cfg.toml b/tests/fixtures/basic_cfg.toml index a1b14936..7808ae95 100644 --- a/tests/fixtures/basic_cfg.toml +++ b/tests/fixtures/basic_cfg.toml @@ -41,6 +41,6 @@ replace = """**unreleased** [tool.bumpversion.parts.release] optional_value = "gamma" values =[ - "dev", - "gamma", + "dev", + "gamma", ] diff --git a/tests/fixtures/regex_test_config.toml b/tests/fixtures/regex_test_config.toml new file mode 100644 index 00000000..46eeca1a --- /dev/null +++ b/tests/fixtures/regex_test_config.toml @@ -0,0 +1,8 @@ +[tool.bumpversion] +current_version = "4.7.1" +regex = true + +[[tool.bumpversion.files]] +filename = "./citation.cff" +search = "date-released: \\d{{4}}-\\d{{2}}-\\d{{2}}" +replace = "date-released: {utcnow:%Y-%m-%d}" diff --git a/tests/test_cli.py b/tests/test_cli.py index 14314cac..638d7df4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,6 +2,7 @@ import shutil import subprocess import traceback +from datetime import datetime from pathlib import Path import pytest @@ -45,6 +46,32 @@ def test_bump_no_configured_files(mocker, tmp_path): assert len(call_args[2].files) == 0 +def test_bump_nested_regex(tmp_path: Path, fixtures_path: Path, caplog): + """ + Arrange/Act: Run the `bump` subcommand with --no-configured-files. + + Assert: There is no configured files specified to modify + """ + cff_path = tmp_path / "citation.cff" + cff_path.write_text("cff-version: 1.2.0\ndate-released: 2023-09-19\n") + content = fixtures_path.joinpath("regex_test_config.toml").read_text() + config_path = tmp_path / ".bumpversion.toml" + config_path.write_text(content) + + runner: CliRunner = CliRunner() + with inside_dir(tmp_path): + result: Result = runner.invoke(cli.cli, ["bump", "-vv", "patch"]) + + if result.exit_code != 0: + print(result.output) + print(caplog.text) + + assert result.exit_code == 0 + + now = datetime.now().isoformat()[:10] + assert cff_path.read_text() == f"cff-version: 1.2.0\ndate-released: {now}\n" + + def test_bump_legacy(mocker, tmp_path): """ Arrange/Act: Run the `bump` subcommand with --no-configured-files. From f2c343b83b537b0ad42d186a9bc0362a517170d1 Mon Sep 17 00:00:00 2001 From: Testing Git Date: Sat, 16 Dec 2023 13:17:37 +0000 Subject: [PATCH 18/32] Version updated from 0.14.0 to 0.15.0 --- CHANGELOG.md | 15 +++++++++++++++ bumpversion/__init__.py | 2 +- docsrc/reference/bumpversion/bumpversion.md | 2 +- pyproject.toml | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acf51d4d..fde85063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## Unreleased (2023-12-16) +[Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.14.0...0.15.0) + +### Fixes + +- Fixed requirements for github action. [d96e07a](https://github.com/callowayproject/bump-my-version/commit/d96e07a79aef643a363449b86847008152599f64) + +### Updates + +- Changed default regex CLI value to None. [93191f3](https://github.com/callowayproject/bump-my-version/commit/93191f3c20e0f91224f1c2e1df70c3d477cc67ec) + + Fixes #64 + + The default value of False was overriding other values. + ## 0.14.0 (2023-12-15) [Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.13.0...0.14.0) diff --git a/bumpversion/__init__.py b/bumpversion/__init__.py index caf9fddb..b90a12be 100644 --- a/bumpversion/__init__.py +++ b/bumpversion/__init__.py @@ -1,3 +1,3 @@ """Top-level package for bump-my-version.""" -__version__: str = "0.14.0" +__version__: str = "0.15.0" diff --git a/docsrc/reference/bumpversion/bumpversion.md b/docsrc/reference/bumpversion/bumpversion.md index 68a32d1e..ac20d224 100644 --- a/docsrc/reference/bumpversion/bumpversion.md +++ b/docsrc/reference/bumpversion/bumpversion.md @@ -51,7 +51,7 @@ bumpversion.__main__ :canonical: bumpversion.__version__ :type: str :value: > - '0.14.0' + '0.15.0' ```{autodoc2-docstring} bumpversion.__version__ ``` diff --git a/pyproject.toml b/pyproject.toml index c9b3b864..72ed4693 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -208,7 +208,7 @@ order-by-type = true convention = "google" [tool.bumpversion] -current_version = "0.14.0" +current_version = "0.15.0" commit = true commit_args = "--no-verify" tag = true From d2f30a8e649d099668c03a3e292c2e5a41e3c670 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sun, 17 Dec 2023 09:46:48 -0600 Subject: [PATCH 19/32] Refactored workflows --- .../package-and-upload-artifacts/action.yaml | 45 ++++++++++ .github/actions/release/action.yaml | 37 +++++++++ .github/workflows/release.yaml | 83 ------------------- .github/workflows/{ci.yaml => test.yaml} | 0 ...mpversion-pr.yaml => version-preview.yaml} | 0 .../{bumpversion.yaml => version.yaml} | 8 +- 6 files changed, 88 insertions(+), 85 deletions(-) create mode 100644 .github/actions/package-and-upload-artifacts/action.yaml create mode 100644 .github/actions/release/action.yaml delete mode 100644 .github/workflows/release.yaml rename .github/workflows/{ci.yaml => test.yaml} (100%) rename .github/workflows/{bumpversion-pr.yaml => version-preview.yaml} (100%) rename .github/workflows/{bumpversion.yaml => version.yaml} (88%) diff --git a/.github/actions/package-and-upload-artifacts/action.yaml b/.github/actions/package-and-upload-artifacts/action.yaml new file mode 100644 index 00000000..82d4bad4 --- /dev/null +++ b/.github/actions/package-and-upload-artifacts/action.yaml @@ -0,0 +1,45 @@ +name: Package and upload artifacts +description: Package a Python project and upload the artifacts and release notes +runs: + using: 'composite' + steps: + - name: Parse changelog for release notes + shell: bash + run: | + function extract_version_content() { + changelog=$1 + target_version=$2 + + awk -v target="$target_version" ' + /^## / { + if (found) exit; + version=$2; + if (version == target) found=1; + next; + } + found { print; } + ' <<< "$changelog" + } + + changelog=$(cat "CHANGELOG.md") + target_version=${GITHUB_REF#refs/tags/} + echo "TAG_NAME=$target_version" >> $GITHUB_ENV + content=$(extract_version_content "$changelog" "$target_version") + + if [ -n "$content" ]; then + echo "::notice::Found release notes for ${target_version}" + echo "$content" >> release-notes.md + else + echo "::warning::Did not find release notes for ${target_version}" + touch release-notes.md + fi + + - name: Upload release notes + uses: actions/upload-artifact@v3 + with: + name: release-notes + path: release-notes.md + + - name: Package and upload artifacts + if: ${{ env.PACKAGE == 'true' }} + uses: hynek/build-and-inspect-python-package@v1 diff --git a/.github/actions/release/action.yaml b/.github/actions/release/action.yaml new file mode 100644 index 00000000..87b94be1 --- /dev/null +++ b/.github/actions/release/action.yaml @@ -0,0 +1,37 @@ +name: Release +description: Create a GitHub release and upload the package to PyPI +inputs: + pypi-api-token: + description: 'PyPI API token' + required: true + tag-name: + description: 'The name of the tag for the GitHub release' + required: true + +runs: + using: "composite" + steps: + - name: Download packages built by build-and-inspect-python-package + uses: actions/download-artifact@v3 + with: + name: Packages + path: dist + if-no-files-found: warn + + - name: Download release notes + uses: actions/download-artifact@v3 + with: + name: release-notes + if-no-files-found: warn + + - name: Create a GitHub release + uses: softprops/action-gh-release@v1 + with: + files: dist/* + tag_name: "${{ env.TAG_NAME }}" + body_path: release-notes.md + + - name: Upload package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ inputs.pypi-api-token }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index cfea6ac8..00000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,83 +0,0 @@ -name: Release -on: - push: - tags: ["*"] - -jobs: - # Create a GitHub release - release: - name: Create a GitHub release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - name: Checkout the repository - with: - fetch-depth: 0 - token: ${{ secrets.PAT }} - - - name: Setup Python and Git - uses: ./.github/actions/setup-python-and-git - with: - python-version: '3.11' - - - name: Parse changelog - shell: bash - run: | - function extract_version_content() { - changelog=$1 - target_version=$2 - - awk -v target="$target_version" ' - /^## / { - if (found) exit; - version=$2; - if (version == target) found=1; - next; - } - found { print; } - ' <<< "$changelog" - } - - changelog=$(cat "CHANGELOG.md") - target_version=${GITHUB_REF#refs/tags/} - echo "TAG_NAME=$target_version" >> $GITHUB_ENV - content=$(extract_version_content "$changelog" "$target_version") - - if [ -n "$content" ]; then - echo "::notice::Found release notes for ${target_version}" - echo "$content" >> release-notes.md - else - echo "::warning::Did not find release notes for ${target_version}" - touch release-notes.md - fi - - - name: Download packages built by build-and-inspect-python-package - uses: actions/download-artifact@v3 - with: - name: Packages - path: dist - if-no-files-found: warn - - - name: Release - uses: softprops/action-gh-release@v1 - with: - files: dist/* - tag_name: "${{ env.TAG_NAME }}" - body_path: release-notes.md - - # Upload to real PyPI on GitHub Releases. - release-pypi: - name: Publish released package to pypi.org - runs-on: ubuntu-latest - needs: build-package - steps: - - name: Download packages built by build-and-inspect-python-package - uses: actions/download-artifact@v3 - with: - name: Packages - path: dist - - - name: Upload package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/test.yaml similarity index 100% rename from .github/workflows/ci.yaml rename to .github/workflows/test.yaml diff --git a/.github/workflows/bumpversion-pr.yaml b/.github/workflows/version-preview.yaml similarity index 100% rename from .github/workflows/bumpversion-pr.yaml rename to .github/workflows/version-preview.yaml diff --git a/.github/workflows/bumpversion.yaml b/.github/workflows/version.yaml similarity index 88% rename from .github/workflows/bumpversion.yaml rename to .github/workflows/version.yaml index b605eeec..b56f2231 100644 --- a/.github/workflows/bumpversion.yaml +++ b/.github/workflows/version.yaml @@ -51,6 +51,10 @@ jobs: ;; esac - - name: Package + - name: Package and upload artifacts if: ${{ env.PACKAGE == 'true' }} - uses: hynek/build-and-inspect-python-package@v1 + uses: ./.github/actions/package-and-upload-artifacts + + - name: Create a GitHub release + if: ${{ env.PACKAGE == 'true' }} + uses: ./.github/actions/release From 6ac064eb9b1889981c32782a915cd25b86e4ffe9 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sun, 17 Dec 2023 11:53:13 -0600 Subject: [PATCH 20/32] Put in temporary debugging steps --- .../package-and-upload-artifacts/action.yaml | 6 +++++- .github/workflows/version-preview.yaml | 2 +- .github/workflows/version.yaml | 13 ++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/actions/package-and-upload-artifacts/action.yaml b/.github/actions/package-and-upload-artifacts/action.yaml index 82d4bad4..830578a5 100644 --- a/.github/actions/package-and-upload-artifacts/action.yaml +++ b/.github/actions/package-and-upload-artifacts/action.yaml @@ -1,5 +1,9 @@ name: Package and upload artifacts description: Package a Python project and upload the artifacts and release notes +inputs: + tag-name: + description: 'The name of the tag for the GitHub release' + required: true runs: using: 'composite' steps: @@ -22,7 +26,7 @@ runs: } changelog=$(cat "CHANGELOG.md") - target_version=${GITHUB_REF#refs/tags/} + target_version=${{ inputs.tag-name }} echo "TAG_NAME=$target_version" >> $GITHUB_ENV content=$(extract_version_content "$changelog" "$target_version") diff --git a/.github/workflows/version-preview.yaml b/.github/workflows/version-preview.yaml index c85ad0c8..02336a94 100644 --- a/.github/workflows/version-preview.yaml +++ b/.github/workflows/version-preview.yaml @@ -5,7 +5,7 @@ on: branches: [master] jobs: - bumpversion: + preview-version-hint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index b56f2231..cd68e25e 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -42,18 +42,25 @@ jobs: case "$RELEASE_KIND" in major|minor|patch) bump-my-version bump --allow-dirty --verbose "$RELEASE_KIND" - git push - git push --tags + echo "TAG_NAME=$(bump-my-version show current_version)" >> $GITHUB_ENV + # git push + # git push --tags echo "PACKAGE=true" >> $GITHUB_ENV ;; dev) - echo "Intentionally not bumping version for dev release" + echo "Temporary dev release for testing" + bump-my-version bump --allow-dirty --verbose "$RELEASE_KIND" + echo "TAG_NAME=$(bump-my-version show current_version)" >> $GITHUB_ENV + echo "PACKAGE=true" >> $GITHUB_ENV + # echo "Intentionally not bumping version for dev release" ;; esac - name: Package and upload artifacts if: ${{ env.PACKAGE == 'true' }} uses: ./.github/actions/package-and-upload-artifacts + with: + tag-name: ${{ env.TAG_NAME }} - name: Create a GitHub release if: ${{ env.PACKAGE == 'true' }} From 23e6c18cd941e7a8717a8ff62ee9e8aa23a5c242 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sun, 17 Dec 2023 11:56:35 -0600 Subject: [PATCH 21/32] Changed the triggers to cause runs --- .github/workflows/test.yaml | 7 ++++--- .github/workflows/version.yaml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ec8e0cbc..5a5188f9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,9 +1,10 @@ name: CI on: - pull_request: - types: [opened, synchronize] - branches: [master] + workflow_call: +# pull_request: +# types: [opened, synchronize] +# branches: [master] defaults: run: diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index cd68e25e..7ee003cb 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -1,7 +1,7 @@ name: Bump the version on merge on: pull_request: - types: [closed] +# types: [closed] branches: [master] jobs: From 12ba54f21e5f1c1853fcd38fca5d23c7350b41a2 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sun, 17 Dec 2023 12:08:29 -0600 Subject: [PATCH 22/32] Fixes committing and download-artifact --- .github/actions/release/action.yaml | 6 ++---- .github/workflows/version.yaml | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/actions/release/action.yaml b/.github/actions/release/action.yaml index 87b94be1..1e16fe97 100644 --- a/.github/actions/release/action.yaml +++ b/.github/actions/release/action.yaml @@ -12,17 +12,15 @@ runs: using: "composite" steps: - name: Download packages built by build-and-inspect-python-package - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: Packages path: dist - if-no-files-found: warn - name: Download release notes - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: release-notes - if-no-files-found: warn - name: Create a GitHub release uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index 7ee003cb..14093303 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -5,7 +5,7 @@ on: branches: [master] jobs: - version-hint: + version: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -49,7 +49,7 @@ jobs: ;; dev) echo "Temporary dev release for testing" - bump-my-version bump --allow-dirty --verbose "$RELEASE_KIND" + bump-my-version bump --allow-dirty --verbose --no-commit "$RELEASE_KIND" echo "TAG_NAME=$(bump-my-version show current_version)" >> $GITHUB_ENV echo "PACKAGE=true" >> $GITHUB_ENV # echo "Intentionally not bumping version for dev release" From 85a8b4852e55c493f43db8a5e8dd2c2fcaa5fb14 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 06:44:33 -0600 Subject: [PATCH 23/32] Fixed PR_NUMBER retrieval --- .github/workflows/version.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index 14093303..c584649b 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -37,8 +37,9 @@ jobs: shell: bash run: | PR_NUMBER=$(gh pr view --json number -q .number || echo "") - REVISION=$(git describe --tags --long | awk -F- '{print $2}') - export PR_NUMBER REVISION + echo "::notice::PR_NUMBER is: ${PR_NUMBER}" + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + export PR_NUMBER case "$RELEASE_KIND" in major|minor|patch) bump-my-version bump --allow-dirty --verbose "$RELEASE_KIND" @@ -55,6 +56,8 @@ jobs: # echo "Intentionally not bumping version for dev release" ;; esac + env: + GH_TOKEN: ${{ github.token }} - name: Package and upload artifacts if: ${{ env.PACKAGE == 'true' }} From a53162837561a2cfc673fa4a93a795f087ec0113 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 06:55:59 -0600 Subject: [PATCH 24/32] downloading all artifacts !minor --- .github/actions/release/action.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/actions/release/action.yaml b/.github/actions/release/action.yaml index 1e16fe97..6c16bc3b 100644 --- a/.github/actions/release/action.yaml +++ b/.github/actions/release/action.yaml @@ -11,6 +11,11 @@ inputs: runs: using: "composite" steps: + - name: Download packages built by build-and-inspect-python-package + uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Download packages built by build-and-inspect-python-package uses: actions/download-artifact@v4 with: From f90d86fb61b72415dc6a0908157fbba2574f74e4 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 07:06:33 -0600 Subject: [PATCH 25/32] Showing run id !minor --- .github/actions/package-and-upload-artifacts/action.yaml | 4 ++++ .github/workflows/version.yaml | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.github/actions/package-and-upload-artifacts/action.yaml b/.github/actions/package-and-upload-artifacts/action.yaml index 830578a5..5a894f16 100644 --- a/.github/actions/package-and-upload-artifacts/action.yaml +++ b/.github/actions/package-and-upload-artifacts/action.yaml @@ -7,6 +7,10 @@ inputs: runs: using: 'composite' steps: + - name: show runid + shell: bash + run: | + echo "::notice::GitHub Run ID is ${{ github.run_id }}" - name: Parse changelog for release notes shell: bash run: | diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index c584649b..c969b1ef 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -58,6 +58,9 @@ jobs: esac env: GH_TOKEN: ${{ github.token }} + - name: show runid + run: | + echo "::notice::GitHub Run ID is ${{ github.run_id }}" - name: Package and upload artifacts if: ${{ env.PACKAGE == 'true' }} From 3f6174297498b641e19ce6a02e48496e6e5c97c8 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 07:34:03 -0600 Subject: [PATCH 26/32] fixes mismatched artifact up/downloading versions --- .github/actions/package-and-upload-artifacts/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/package-and-upload-artifacts/action.yaml b/.github/actions/package-and-upload-artifacts/action.yaml index 5a894f16..858b7cba 100644 --- a/.github/actions/package-and-upload-artifacts/action.yaml +++ b/.github/actions/package-and-upload-artifacts/action.yaml @@ -43,11 +43,11 @@ runs: fi - name: Upload release notes - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: release-notes path: release-notes.md - name: Package and upload artifacts if: ${{ env.PACKAGE == 'true' }} - uses: hynek/build-and-inspect-python-package@v1 + uses: hynek/build-and-inspect-python-package@v2 From db3d046faf5ad54f9e585359d1dae4fad0cd1b89 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 07:47:08 -0600 Subject: [PATCH 27/32] Added id-token write permission to job !minor --- .github/workflows/version.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index c969b1ef..de41699f 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -6,6 +6,8 @@ on: jobs: version: + permissions: + id-token: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 8aed9595195224ab81d6d9c991a0214266c3088c Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 07:56:42 -0600 Subject: [PATCH 28/32] fixing permissions and pr stuff !minor --- .github/workflows/version.yaml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index de41699f..8804dad9 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -8,6 +8,7 @@ jobs: version: permissions: id-token: write + pull-requests: read runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -34,14 +35,24 @@ jobs: echo "release-kind=$RELEASE_KIND" >> $GITHUB_OUTPUT echo "PACKAGE=false" >> $GITHUB_ENV + - name: Get Pull Request Number + id: pr + run: | + PR_NUMBER=$(gh pr view --json number -q .number || echo "") + echo "::set-output name=pull_request_number::${PR_NUMBER}" + echo "::notice::PR_NUMBER is ${PR_NUMBER}" + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + env: + GITHUB_TOKEN: ${{ secrets.PAT }} + - name: Bump version if: ${{ env.RELEASE_KIND != 'no-release' }} shell: bash run: | - PR_NUMBER=$(gh pr view --json number -q .number || echo "") - echo "::notice::PR_NUMBER is: ${PR_NUMBER}" - echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV - export PR_NUMBER + # PR_NUMBER=$(gh pr view --json number -q .number || echo "") + # echo "::notice::PR_NUMBER is: ${PR_NUMBER}" + # echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + # export PR_NUMBER case "$RELEASE_KIND" in major|minor|patch) bump-my-version bump --allow-dirty --verbose "$RELEASE_KIND" From c8f0d45ec40345ebd1c4bd5fbdc70136c51bb31b Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 08:04:33 -0600 Subject: [PATCH 29/32] Testing the PyPI release !minor --- .github/actions/release/action.yaml | 12 ++++++------ .github/workflows/version.yaml | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/actions/release/action.yaml b/.github/actions/release/action.yaml index 6c16bc3b..eff5d9bf 100644 --- a/.github/actions/release/action.yaml +++ b/.github/actions/release/action.yaml @@ -27,12 +27,12 @@ runs: with: name: release-notes - - name: Create a GitHub release - uses: softprops/action-gh-release@v1 - with: - files: dist/* - tag_name: "${{ env.TAG_NAME }}" - body_path: release-notes.md +# - name: Create a GitHub release +# uses: softprops/action-gh-release@v1 +# with: +# files: dist/* +# tag_name: "${{ env.TAG_NAME }}" +# body_path: release-notes.md - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index 8804dad9..f771b04c 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -39,9 +39,10 @@ jobs: id: pr run: | PR_NUMBER=$(gh pr view --json number -q .number || echo "") - echo "::set-output name=pull_request_number::${PR_NUMBER}" + echo "pull_request_number=${PR_NUMBER}" >> $GITHUB_OUTPUT echo "::notice::PR_NUMBER is ${PR_NUMBER}" echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + echo "::notice::event number is ${{ github.event.number }}" env: GITHUB_TOKEN: ${{ secrets.PAT }} From 67ab83d79d67e729bec62a1d25c735ecad41afdb Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 08:19:55 -0600 Subject: [PATCH 30/32] testing PR acquisition --- .../package-and-upload-artifacts/action.yaml | 4 --- .github/actions/release/action.yaml | 12 ++++---- .github/workflows/version-preview.yaml | 14 +++++++--- .github/workflows/version.yaml | 28 ++++++------------- 4 files changed, 25 insertions(+), 33 deletions(-) diff --git a/.github/actions/package-and-upload-artifacts/action.yaml b/.github/actions/package-and-upload-artifacts/action.yaml index 858b7cba..a06b83ca 100644 --- a/.github/actions/package-and-upload-artifacts/action.yaml +++ b/.github/actions/package-and-upload-artifacts/action.yaml @@ -7,10 +7,6 @@ inputs: runs: using: 'composite' steps: - - name: show runid - shell: bash - run: | - echo "::notice::GitHub Run ID is ${{ github.run_id }}" - name: Parse changelog for release notes shell: bash run: | diff --git a/.github/actions/release/action.yaml b/.github/actions/release/action.yaml index eff5d9bf..6c16bc3b 100644 --- a/.github/actions/release/action.yaml +++ b/.github/actions/release/action.yaml @@ -27,12 +27,12 @@ runs: with: name: release-notes -# - name: Create a GitHub release -# uses: softprops/action-gh-release@v1 -# with: -# files: dist/* -# tag_name: "${{ env.TAG_NAME }}" -# body_path: release-notes.md + - name: Create a GitHub release + uses: softprops/action-gh-release@v1 + with: + files: dist/* + tag_name: "${{ env.TAG_NAME }}" + body_path: release-notes.md - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/version-preview.yaml b/.github/workflows/version-preview.yaml index 02336a94..e65a9abf 100644 --- a/.github/workflows/version-preview.yaml +++ b/.github/workflows/version-preview.yaml @@ -32,14 +32,20 @@ jobs: echo "RELEASE_KIND=$RELEASE_KIND" >> $GITHUB_ENV echo "release-kind=$RELEASE_KIND" >> $GITHUB_OUTPUT + - name: Get Pull Request Number + id: pr + run: | + PR_NUMBER=$(gh pr view --json number -q .number || echo "${{ github.event.number }}") + echo "pull_request_number=${PR_NUMBER}" >> $GITHUB_OUTPUT + echo "::notice::PR_NUMBER is ${PR_NUMBER}" + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + env: + GITHUB_TOKEN: ${{ secrets.PAT }} + - name: Bump version dry run if: ${{ env.RELEASE_KIND != 'no-release' }} shell: bash run: | - PR_NUMBER=$(gh pr view --json number -q .number || echo "") - REVISION=$(git describe --tags --long | awk -F- '{print $2}') - export PR_NUMBER REVISION - # This will display a full log of what would happen if we were to bump the version. bump-my-version bump --dry-run --verbose "$RELEASE_KIND" diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index f771b04c..6d78ec2c 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -38,11 +38,10 @@ jobs: - name: Get Pull Request Number id: pr run: | - PR_NUMBER=$(gh pr view --json number -q .number || echo "") + PR_NUMBER=$(gh pr view --json number -q .number || echo "${{ github.event.number }}") echo "pull_request_number=${PR_NUMBER}" >> $GITHUB_OUTPUT echo "::notice::PR_NUMBER is ${PR_NUMBER}" echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV - echo "::notice::event number is ${{ github.event.number }}" env: GITHUB_TOKEN: ${{ secrets.PAT }} @@ -50,10 +49,6 @@ jobs: if: ${{ env.RELEASE_KIND != 'no-release' }} shell: bash run: | - # PR_NUMBER=$(gh pr view --json number -q .number || echo "") - # echo "::notice::PR_NUMBER is: ${PR_NUMBER}" - # echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV - # export PR_NUMBER case "$RELEASE_KIND" in major|minor|patch) bump-my-version bump --allow-dirty --verbose "$RELEASE_KIND" @@ -70,18 +65,13 @@ jobs: # echo "Intentionally not bumping version for dev release" ;; esac - env: - GH_TOKEN: ${{ github.token }} - - name: show runid - run: | - echo "::notice::GitHub Run ID is ${{ github.run_id }}" - - name: Package and upload artifacts - if: ${{ env.PACKAGE == 'true' }} - uses: ./.github/actions/package-and-upload-artifacts - with: - tag-name: ${{ env.TAG_NAME }} +# - name: Package and upload artifacts +# if: ${{ env.PACKAGE == 'true' }} +# uses: ./.github/actions/package-and-upload-artifacts +# with: +# tag-name: ${{ env.TAG_NAME }} - - name: Create a GitHub release - if: ${{ env.PACKAGE == 'true' }} - uses: ./.github/actions/release +# - name: Create a GitHub release +# if: ${{ env.PACKAGE == 'true' }} +# uses: ./.github/actions/release From 690452e6164a529a48c1f2043c00fc315b50bb05 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 08:32:37 -0600 Subject: [PATCH 31/32] Fixes workflow triggers --- .github/workflows/test.yaml | 7 +++---- .github/workflows/version.yaml | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5a5188f9..ec8e0cbc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,10 +1,9 @@ name: CI on: - workflow_call: -# pull_request: -# types: [opened, synchronize] -# branches: [master] + pull_request: + types: [opened, synchronize] + branches: [master] defaults: run: diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index 6d78ec2c..4c2f8da4 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -1,7 +1,7 @@ name: Bump the version on merge on: pull_request: -# types: [closed] + types: [closed] branches: [master] jobs: From ad607d5b026f28fc9d9e79b5f67b62f1e65873aa Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Dec 2023 08:35:23 -0600 Subject: [PATCH 32/32] Generated changelog !wip --- CHANGELOG.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fde85063..36f07de2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,31 @@ # Changelog -## Unreleased (2023-12-16) +## Unreleased (2023-12-18) +[Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.15.0...HEAD) + +### Fixes + +- Fixes workflow triggers. [690452e](https://github.com/callowayproject/bump-my-version/commit/690452e6164a529a48c1f2043c00fc315b50bb05) + +- Fixes mismatched artifact up/downloading versions. [3f61742](https://github.com/callowayproject/bump-my-version/commit/3f6174297498b641e19ce6a02e48496e6e5c97c8) + +- Fixed PR_NUMBER retrieval. [85a8b48](https://github.com/callowayproject/bump-my-version/commit/85a8b4852e55c493f43db8a5e8dd2c2fcaa5fb14) + +- Fixes committing and download-artifact. [12ba54f](https://github.com/callowayproject/bump-my-version/commit/12ba54f21e5f1c1853fcd38fca5d23c7350b41a2) + +- Refactored workflows. [d2f30a8](https://github.com/callowayproject/bump-my-version/commit/d2f30a8e649d099668c03a3e292c2e5a41e3c670) + +### Other + +- Testing PR acquisition. [67ab83d](https://github.com/callowayproject/bump-my-version/commit/67ab83d79d67e729bec62a1d25c735ecad41afdb) + +- Put in temporary debugging steps. [6ac064e](https://github.com/callowayproject/bump-my-version/commit/6ac064eb9b1889981c32782a915cd25b86e4ffe9) + +### Updates + +- Changed the triggers to cause runs. [23e6c18](https://github.com/callowayproject/bump-my-version/commit/23e6c18cd941e7a8717a8ff62ee9e8aa23a5c242) + +## 0.15.0 (2023-12-16) [Compare the full difference.](https://github.com/callowayproject/bump-my-version/compare/0.14.0...0.15.0) ### Fixes