Skip to content

Commit

Permalink
Refactored file resolution, inclusion, and exclusion
Browse files Browse the repository at this point in the history
- Fixes #61
- Config now includes `resolved_filemap` property
- resolved filemap exapands all globs
- Config now includes `files_to_modify` property
- files to modify resolves inclusions and exclutions
- Improved Config.add_files property
  • Loading branch information
coordt committed Sep 4, 2023
1 parent 557b4d8 commit 646af54
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 70 deletions.
2 changes: 1 addition & 1 deletion bumpversion/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def do_bump(

ctx = get_context(config, version, next_version)

configured_files = resolve_file_config(config.files, config.version_config)
configured_files = resolve_file_config(config.files_to_modify, config.version_config)
modify_files(configured_files, version, next_version, ctx, dry_run)
update_config_file(config_file, config.current_version, next_version_str, dry_run)

Expand Down
15 changes: 9 additions & 6 deletions bumpversion/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bumpversion.aliases import AliasedGroup
from bumpversion.bump import do_bump
from bumpversion.config import find_config_file, get_configuration
from bumpversion.files import modify_files, resolve_file_config
from bumpversion.files import ConfiguredFile, modify_files
from bumpversion.logging import setup_logging
from bumpversion.show import do_show, log_list
from bumpversion.ui import print_warning
Expand Down Expand Up @@ -300,10 +300,11 @@ def bump(
config.scm_info.tool.assert_nondirty()

if no_configured_files:
config.files = []
config.excluded_paths = list(config.resolved_filemap.keys())

if files:
config.add_files(files)
config.included_paths = files

do_bump(version_part, new_version, config, found_config_file, dry_run)

Expand Down Expand Up @@ -495,20 +496,22 @@ def replace(
config.scm_info.tool.assert_nondirty()

if no_configured_files:
config.files = []
config.excluded_paths = list(config.resolved_filemap.keys())

if files:
config.add_files(files)
config.included_paths = files

version = config.version_config.parse(config.current_version)
configured_files = [
ConfiguredFile(file_cfg, config.version_config, search, replace) for file_cfg in config.files_to_modify
]

version = config.version_config.parse(config.current_version)
if new_version:
next_version = config.version_config.parse(new_version)
else:
next_version = None

ctx = get_context(config, version, next_version)

configured_files = resolve_file_config(config.files, config.version_config, search, replace)

modify_files(configured_files, version, next_version, ctx, dry_run)
59 changes: 58 additions & 1 deletion bumpversion/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Configuration management."""
from __future__ import annotations

import glob
import itertools
import logging
import re
Expand Down Expand Up @@ -66,14 +67,51 @@ class Config(BaseSettings):
scm_info: Optional["SCMInfo"]
parts: Dict[str, VersionPartConfig]
files: List[FileConfig]
included_paths: List[str] = []
excluded_paths: List[str] = []

class Config:
env_prefix = "bumpversion_"

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
self.files.extend([FileConfig(filename=name) for name in filenames]) # type: ignore[call-arg]
for name in filenames:
if name in self.resolved_filemap:
continue
self.files.append(
FileConfig(
filename=name,
glob=None,
parse=self.parse,
serialize=self.serialize,
search=self.search,
replace=self.replace,
no_regex=self.no_regex,
ignore_missing_version=self.ignore_missing_version,
)
)

@property
def resolved_filemap(self) -> Dict[str, FileConfig]:
"""Return a map of filenames to file configs, expanding any globs."""
new_files = []
for file_cfg in self.files:
if file_cfg.glob:
new_files.extend(get_glob_files(file_cfg))

Check warning on line 101 in bumpversion/config.py

View check run for this annotation

Codecov / codecov/patch

bumpversion/config.py#L101

Added line #L101 was not covered by tests
else:
new_files.append(file_cfg)

return {file_cfg.filename: file_cfg for file_cfg in new_files}

@property
def files_to_modify(self) -> List[FileConfig]:
"""Return a list of files to modify."""
files_not_excluded = [
file_cfg.filename for file_cfg in self.files if file_cfg.filename not in self.excluded_paths
]
inclusion_set = set(self.included_paths) | set(files_not_excluded)
return [file_cfg for file_cfg in self.files if file_cfg.filename in inclusion_set]

@property
def version_config(self) -> "VersionConfig":
Expand Down Expand Up @@ -410,3 +448,22 @@ def update_config_file(

if not dry_run:
config_path.write_text(new_config)


def get_glob_files(file_cfg: FileConfig) -> List[FileConfig]:
"""
Return a list of files that match the glob pattern.
Args:
file_cfg: The file configuration containing the glob pattern
Returns:
A list of resolved file configurations according to the pattern.
"""
files = []
for filename_glob in glob.glob(file_cfg.glob, recursive=True):
new_file_cfg = file_cfg.copy()
new_file_cfg.filename = filename_glob
new_file_cfg.glob = None
files.append(new_file_cfg)
return files
33 changes: 1 addition & 32 deletions bumpversion/files.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Methods for changing files."""
import glob
import logging
import re
from copy import deepcopy
Expand Down Expand Up @@ -190,14 +189,7 @@ def resolve_file_config(
Returns:
A list of ConfiguredFiles
"""
configured_files = []
for file_cfg in files:
if file_cfg.glob:
configured_files.extend(get_glob_files(file_cfg, version_config))
else:
configured_files.append(ConfiguredFile(file_cfg, version_config, search, replace))

return configured_files
return [ConfiguredFile(file_cfg, version_config, search, replace) for file_cfg in files]


def modify_files(
Expand All @@ -222,29 +214,6 @@ def modify_files(
f.replace_version(current_version, new_version, context, dry_run)


def get_glob_files(
file_cfg: FileConfig, version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None
) -> List[ConfiguredFile]:
"""
Return a list of files that match the glob pattern.
Args:
file_cfg: The file configuration containing the glob pattern
version_config: The version configuration
search: The search pattern to use instead of any configured search pattern
replace: The replace pattern to use instead of any configured replace pattern
Returns:
A list of resolved files according to the pattern.
"""
files = []
for filename_glob in glob.glob(file_cfg.glob, recursive=True):
new_file_cfg = file_cfg.copy()
new_file_cfg.filename = filename_glob
files.append(ConfiguredFile(new_file_cfg, version_config, search, replace))
return files


def _check_files_contain_version(
files: List[ConfiguredFile], current_version: Version, context: MutableMapping
) -> None:
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/basic_cfg_expected.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
'commit': True,
'commit_args': None,
'current_version': '1.0.0',
'excluded_paths': [],
'files': [{'filename': 'setup.py',
'glob': None,
'ignore_missing_version': False,
Expand Down Expand Up @@ -30,6 +31,7 @@
'serialize': ['{major}.{minor}.{patch}-{release}',
'{major}.{minor}.{patch}']}],
'ignore_missing_version': False,
'included_paths': [],
'message': 'Bump version: {current_version} → {new_version}',
'no_regex': False,
'parse': '(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?',
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/basic_cfg_expected.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ allow_dirty: false
commit: true
commit_args: null
current_version: "1.0.0"
excluded_paths:

files:
- filename: "setup.py"
glob: null
Expand Down Expand Up @@ -34,6 +36,8 @@ files:
- "{major}.{minor}.{patch}-{release}"
- "{major}.{minor}.{patch}"
ignore_missing_version: false
included_paths:

message: "Bump version: {current_version} → {new_version}"
no_regex: false
parse: "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?"
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/basic_cfg_expected_full.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"commit": true,
"commit_args": null,
"current_version": "1.0.0",
"excluded_paths": [],
"files": [
{
"filename": "setup.py",
Expand Down Expand Up @@ -45,6 +46,7 @@
}
],
"ignore_missing_version": false,
"included_paths": [],
"message": "Bump version: {current_version} \u2192 {new_version}",
"no_regex": false,
"parse": "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
Expand Down
53 changes: 53 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,14 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path):
"DEPRECATED: The --list option is deprecated and will be removed in a future version.",
"new_version=1.0.1-dev",
"current_version=1.0.0",
"excluded_paths=[]",
"parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
"serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']",
"search={current_version}",
"replace={new_version}",
"no_regex=False",
"ignore_missing_version=False",
"included_paths=[]",
"tag=True",
"sign_tags=False",
"tag_name=v{new_version}",
Expand Down Expand Up @@ -254,12 +256,14 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path):
"",
"DEPRECATED: The --list option is deprecated and will be removed in a future version.",
"current_version=1.0.0",
"excluded_paths=[]",
"parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
"serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']",
"search={current_version}",
"replace={new_version}",
"no_regex=False",
"ignore_missing_version=False",
"included_paths=[]",
"tag=True",
"sign_tags=False",
"tag_name=v{new_version}",
Expand Down Expand Up @@ -517,3 +521,52 @@ def test_replace_search_with_plain_string(tmp_path, fixtures_path):
print(traceback.print_exception(result.exc_info[1]))

assert result.exit_code == 0


def test_valid_regex_not_ignoring_regex(tmp_path: Path, caplog) -> None:
"""A search string not meant to be a regex (but is) is still found and replaced correctly."""
# Arrange
search = "(unreleased)"
replace = "(2023-01-01)"

version_path = tmp_path / "VERSION"
version_path.write_text("# Changelog\n\n## [0.0.1 (unreleased)](https://cool.url)\n\n- Test unreleased package.\n")
config_file = tmp_path / ".bumpversion.toml"
config_file.write_text(
"[tool.bumpversion]\n"
'current_version = "0.0.1"\n'
"allow_dirty = true\n\n"
"[[tool.bumpversion.files]]\n"
'filename = "VERSION"\n'
"no_regex = true\n"
f'search = "{search}"\n'
f'replace = "{replace}"\n'
)

# Act
runner: CliRunner = CliRunner()
with inside_dir(tmp_path):
result: Result = runner.invoke(
cli.cli,
[
"replace",
"--verbose",
"--no-regex",
"--no-configured-files",
"--search",
search,
"--replace",
replace,
"VERSION",
],
)

# Assert
if result.exit_code != 0:
print(result.output)

assert result.exit_code == 0
assert (
version_path.read_text()
== "# Changelog\n\n## [0.0.1 (2023-01-01)](https://cool.url)\n\n- Test unreleased package.\n"
)
30 changes: 29 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pytest import param

from bumpversion import config
from tests.conftest import inside_dir
from tests.conftest import inside_dir, get_config_data


@pytest.fixture(params=[".bumpversion.cfg", "setup.cfg"])
Expand Down Expand Up @@ -246,3 +246,31 @@ def test_pep440_config(git_repo: Path, fixtures_path: Path):
# 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"],
[
param("*.txt", {Path("file1.txt"), Path("file2.txt")}, id="simple-glob"),
param("**/*.txt", {Path("file1.txt"), Path("file2.txt"), Path("directory/file3.txt")}, id="recursive-glob"),
],
)
def test_get_glob_files(glob_pattern: str, file_list: set, fixtures_path: Path):
"""Get glob files should return all the globbed files and nothing else."""
overrides = {
"current_version": "1.0.0",
"parse": r"(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<release>[a-z]+))?",
"serialize": ["{major}.{minor}.{release}", "{major}.{minor}"],
"files": [
{
"glob": glob_pattern,
}
],
}
conf, version_config, current_version = get_config_data(overrides)
with inside_dir(fixtures_path.joinpath("glob")):
result = config.get_glob_files(conf.files[0])

assert len(result) == len(file_list)
for f in result:
assert Path(f.filename) in file_list
Loading

0 comments on commit 646af54

Please sign in to comment.