From 0ee53472379db3869f865245618b5a08c2e10ed1 Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Thu, 29 Feb 2024 11:34:31 +0100 Subject: [PATCH 1/3] Update setver task Properly implement fail_fast and the test parameters. Support PEP 440 Python versions. Test the majority of the functionality and logic in the setver task. --- ci_cd/tasks/setver.py | 195 ++++++++++-------- ci_cd/utils/versions.py | 34 +++ tests/tasks/test_setver.py | 409 +++++++++++++++++++++++++++++++++++++ 3 files changed, 556 insertions(+), 82 deletions(-) create mode 100644 tests/tasks/test_setver.py diff --git a/ci_cd/tasks/setver.py b/ci_cd/tasks/setver.py index 03f663b1..3859a450 100644 --- a/ci_cd/tasks/setver.py +++ b/ci_cd/tasks/setver.py @@ -14,7 +14,7 @@ from invoke import task -from ci_cd.utils import Emoji, SemanticVersion, update_file +from ci_cd.utils import Emoji, SemanticVersion, error_msg, update_file # Get logger LOGGER = logging.getLogger(__name__) @@ -22,7 +22,7 @@ @task( help={ - "version": "Version to set.", + "version": "Version to set. Must be either a SemVer or a PEP 440 version.", "package-dir": ( "Relative path to package dir from the repository root, " "e.g. 'src/my_package'." @@ -45,9 +45,13 @@ "The string separator to use for '--code-base-update' values." ), "fail_fast": ( - "Whether to exist the task immediately upon failure or wait until the end." + "Whether to exit the task immediately upon failure or wait until the end. " + "Note, no code changes will happen if an error occurs." + ), + "test": ( + "Whether to do a dry run or not. If set, the task will not make any " + "changes to the code base." ), - "test": "Whether to print extra debugging statements.", }, iterable=["code_base_update"], ) @@ -71,21 +75,13 @@ def setver( test: bool = test # type: ignore[no-redef] fail_fast: bool = fail_fast # type: ignore[no-redef] - match = re.fullmatch( - ( - r"v?(?P[0-9]+)\.(?P[0-9]+)\.(?P[0-9]+)" - r"(?:-(?P[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?" - r"(?:\+(?P[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?" - ), - version, - ) - if not match: + try: + semantic_version = SemanticVersion(version) + except ValueError: sys.exit( - "Error: Please specify version as " - "'Major.Minor.Patch(-Pre-Release+Build Metadata)' or " - "'vMajor.Minor.Patch(-Pre-Release+Build Metadata)'" + "Error: Please specify version as a semantic version (SemVer) or " + "PEP 440 version. The version may be prepended by a 'v'." ) - semantic_version = SemanticVersion(**match.groupdict()) if not code_base_update: init_file = Path(root_repo_path).resolve() / package_dir / "__init__.py" @@ -102,80 +98,115 @@ def setver( f'__version__ = "{semantic_version}"', ), ) - else: - errors: list[str] = [] - for code_update in code_base_update: - try: - filepath, pattern, replacement = code_update.split( - code_base_update_separator - ) - except ValueError: - msg = traceback.format_exc() - LOGGER.error(msg) - if test: - print(msg) - sys.exit( - f"{Emoji.CROSS_MARK.value} Error: Could not properly extract " - "'file path', 'pattern', 'replacement string' from the " - f"'--code-base-update'={code_update}" - ) - - filepath = Path( - filepath.format(package_dir=package_dir, version=semantic_version) - ).resolve() - if not filepath.exists(): - error_msg = ( - f"{Emoji.CROSS_MARK.value} Error: Could not find the " - f"user-provided file at: {filepath}" - ) - if fail_fast: - sys.exit(error_msg) - errors.append(error_msg) - continue - - LOGGER.debug( - """filepath: %s + + # Success, done + print( + f"{Emoji.PARTY_POPPER.value} Bumped version for {package_dir} to " + f"{semantic_version}." + ) + return + + # Code base updates were provided + # First, validate the inputs + validated_code_base_updates: list[tuple[Path, str, str, str]] = [] + error: bool = False + for code_update in code_base_update: + try: + filepath, pattern, replacement = code_update.split( + code_base_update_separator + ) + except ValueError as exc: + msg = ( + "Could not properly extract 'file path', 'pattern', " + f"'replacement string' from the '--code-base-update'={code_update}:" + f"\n{exc}" + ) + LOGGER.error(msg) + LOGGER.debug("Traceback: %s", traceback.format_exc()) + if fail_fast: + sys.exit(f"{Emoji.CROSS_MARK.value} {error_msg(msg)}") + print(error_msg(msg), file=sys.stderr, flush=True) + error = True + continue + + # Resolve file path + filepath = Path( + filepath.format(package_dir=package_dir, version=semantic_version) + ) + + if not filepath.is_absolute(): + filepath = Path(root_repo_path).resolve() / filepath + + if not filepath.exists(): + msg = f"Could not find the user-provided file at: {filepath}" + LOGGER.error(msg) + if fail_fast: + sys.exit(f"{Emoji.CROSS_MARK.value} {error_msg(msg)}") + print(error_msg(msg), file=sys.stderr, flush=True) + error = True + continue + + LOGGER.debug( + """filepath: %s pattern: %r replacement (input): %s replacement (handled): %s """, + filepath, + pattern, + replacement, + replacement.format(package_dir=package_dir, version=semantic_version), + ) + + validated_code_base_updates.append( + ( filepath, pattern, - replacement, replacement.format(package_dir=package_dir, version=semantic_version), + replacement, + ) + ) + + if error: + sys.exit( + f"{Emoji.CROSS_MARK.value} Errors occurred! See printed statements above." + ) + + for ( + filepath, + pattern, + replacement, + input_replacement, + ) in validated_code_base_updates: + if test: + print( + f"filepath: {filepath}\npattern: {pattern!r}\n" + f"replacement (input): {input_replacement}\n" + f"replacement (handled): {replacement}" + ) + continue + + try: + update_file(filepath, (pattern, replacement)) + except re.error as exc: + if validated_code_base_updates[0] != ( + filepath, + pattern, + replacement, + input_replacement, + ): + msg = "Some files have already been updated !\n\n " + + msg += ( + f"Could not update file {filepath} according to the given input:\n\n " + f"pattern: {pattern}\n replacement: {replacement}\n\nException: " + f"{exc}" ) - if test: - print( - f"filepath: {filepath}\npattern: {pattern!r}\n" - f"replacement (input): {replacement}" - ) - print( - "replacement (handled): " - f"{replacement.format(package_dir=package_dir, version=semantic_version)}" # noqa: E501 - ) - - try: - update_file( - filepath, - ( - pattern, - replacement.format( - package_dir=package_dir, version=semantic_version - ), - ), - ) - except re.error: - msg = traceback.format_exc() - LOGGER.error(msg) - if test: - print(msg) - sys.exit( - f"{Emoji.CROSS_MARK.value} Error: Could not update file {filepath}" - f" according to the given input:\n\n pattern: {pattern}\n " - "replacement: " - f"{replacement.format(package_dir=package_dir, version=semantic_version)}" # noqa: E501 - ) + LOGGER.error(msg) + LOGGER.debug("Traceback: %s", traceback.format_exc()) + sys.exit(f"{Emoji.CROSS_MARK.value} {error_msg(msg)}") + # Success, done print( f"{Emoji.PARTY_POPPER.value} Bumped version for {package_dir} to " f"{semantic_version}." diff --git a/ci_cd/utils/versions.py b/ci_cd/utils/versions.py index 07bd21e5..cc251601 100644 --- a/ci_cd/utils/versions.py +++ b/ci_cd/utils/versions.py @@ -295,6 +295,40 @@ def __repr__(self) -> str: """Return the string representation of the object.""" return f"{self.__class__.__name__}({self.__str__()!r})" + def __getattribute__(self, name: str) -> Any: + """Return the attribute value.""" + accepted_python_attributes = ( + "epoch", + "release", + "pre", + "post", + "dev", + "local", + "public", + "base_version", + "micro", + ) + + try: + return object.__getattribute__(self, name) + except AttributeError as exc: + # Try returning the attribute from the Python version, if it is in a list + # of accepted attributes + if name not in accepted_python_attributes: + raise AttributeError( + f"{self.__class__.__name__} object has no attribute {name!r}" + ) from exc + + python_version = object.__getattribute__(self, "as_python_version")( + shortened=False + ) + try: + return getattr(python_version, name) + except AttributeError as exc: + raise AttributeError( + f"{self.__class__.__name__} object has no attribute {name!r}" + ) from exc + def _validate_other_type(self, other: Any) -> SemanticVersion: """Initial check/validation of `other` before rich comparisons.""" not_implemented_exc = NotImplementedError( diff --git a/tests/tasks/test_setver.py b/tests/tasks/test_setver.py new file mode 100644 index 00000000..054256d1 --- /dev/null +++ b/tests/tasks/test_setver.py @@ -0,0 +1,409 @@ +"""Test `ci_cd.tasks.setver()`.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from pathlib import Path + + +compliant_python_version_schemes: list[tuple[str, str]] = [ + # Simple “major.minor” versioning: + ("0.1", "0.1.0"), + ("0.2", "0.2.0"), + ("0.3", "0.3.0"), + ("1.0", "1.0.0"), + ("1.1", "1.1.0"), + # Simple “major.minor.micro” versioning: + ("1.1.0", "1.1.0"), + ("1.1.1", "1.1.1"), + ("1.1.2", "1.1.2"), + ("1.2.0", "1.2.0"), + # “major.minor” versioning with alpha, beta and candidate pre-releases: + ("0.9", "0.9.0"), + ("1.0a1", "1.0.0a1"), + ("1.0a2", "1.0.0a2"), + ("1.0b1", "1.0.0b1"), + ("1.0rc1", "1.0.0rc1"), + ("1.0", "1.0.0"), + ("1.1a1", "1.1.0a1"), + # “major.minor” versioning with developmental releases, release candidates and + # post-releases for minor corrections: + ("0.9", "0.9.0"), + ("1.0.dev1", "1.0.0.dev1"), + ("1.0.dev2", "1.0.0.dev2"), + ("1.0.dev3", "1.0.0.dev3"), + ("1.0.dev4", "1.0.0.dev4"), + ("1.0c1", "1.0.0rc1"), # Note: “c” is a valid abbreviation for “rc” + ("1.0c2", "1.0.0rc2"), # Note: “c” is a valid abbreviation for “rc” + ("1.0", "1.0.0"), + ("1.0.post1", "1.0.0.post1"), + ("1.1.dev1", "1.1.0.dev1"), + # Date based releases, using an incrementing serial within each year, skipping zero: + ("2012.1", "2012.1.0"), + ("2012.2", "2012.2.0"), + ("2012.3", "2012.3.0"), + ("2012.15", "2012.15.0"), + ("2013.1", "2013.1.0"), + ("2013.2", "2013.2.0"), + # Another collection of valid versions from PEP 440: + ("1.dev0", "1.0.0.dev0"), + ("1.0.dev456", "1.0.0.dev456"), + ("1.0a1", "1.0.0a1"), + ("1.0a2.dev456", "1.0.0a2.dev456"), + ("1.0a12.dev456", "1.0.0a12.dev456"), + ("1.0a12", "1.0.0a12"), + ("1.0b1.dev456", "1.0.0b1.dev456"), + ("1.0b2", "1.0.0b2"), + ("1.0b2.post345.dev456", "1.0.0b2.post345.dev456"), + ("1.0b2.post345", "1.0.0b2.post345"), + ("1.0rc1.dev456", "1.0.0rc1.dev456"), + ("1.0rc1", "1.0.0rc1"), + ("1.0", "1.0.0"), + ("1.0+abc.5", "1.0.0+abc.5"), + ("1.0+abc.7", "1.0.0+abc.7"), + ("1.0+5", "1.0.0+5"), + ("1.0.post456.dev34", "1.0.0.post456.dev34"), + ("1.0.post456", "1.0.0.post456"), + ("1.0.15", "1.0.15"), + ("1.1.dev1", "1.1.0.dev1"), +] + + +def test_setver(tmp_path: Path) -> None: + """Test setver runs with defaults.""" + from invoke import MockContext + + from ci_cd.tasks.setver import setver + + # Create __init__.py file + package_dir = tmp_path / "src" / "my_package" + package_dir.mkdir(parents=True) + (package_dir / "__init__.py").write_text("__version__ = '0.0.0'\n") + + setver( + MockContext(), + package_dir=package_dir.relative_to(tmp_path), + version="0.1.0", + root_repo_path=tmp_path, + ) + + # Check __init__.py file + assert (package_dir / "__init__.py").read_text() == '__version__ = "0.1.0"\n' + + +@pytest.mark.parametrize( + ("version", "expected_version"), + compliant_python_version_schemes, + ids=[f"{v} -> {ev}" for v, ev in compliant_python_version_schemes], +) +def test_setver_python_version( + tmp_path: Path, version: str, expected_version: str +) -> None: + """Test setver runs with Python version.""" + from invoke import MockContext + + from ci_cd.tasks.setver import setver + + # Create __init__.py file + package_dir = tmp_path / "src" / "my_package" + package_dir.mkdir(parents=True) + (package_dir / "__init__.py").write_text("__version__ = '0.0.0'\n") + + setver( + MockContext(), + package_dir=package_dir.relative_to(tmp_path), + version=version, + root_repo_path=tmp_path, + ) + + # Check __init__.py file + assert ( + package_dir / "__init__.py" + ).read_text() == f'__version__ = "{expected_version}"\n' + + +def test_invalid_version() -> None: + """Test setver emits an error and stops when given an invalid version.""" + from invoke import MockContext + + from ci_cd.tasks.setver import setver + from ci_cd.utils.versions import SemanticVersion + + invalid_version = "invalid" + + # Ensure the version is invalid + with pytest.raises(ValueError, match="cannot be parsed"): + SemanticVersion(invalid_version) + + with pytest.raises( + SystemExit, + match=( + r"^Error: Please specify version as a semantic version \(SemVer\) or " + r"PEP 440 version\..*" + ), + ): + setver(MockContext(), package_dir="does not matter", version="invalid") + + +@pytest.mark.parametrize( + "variant", ["absolute path", "relative path", "package_dir variable"] +) +def test_setver_with_code_base_update_variants( + tmp_path: Path, caplog: pytest.LogCaptureFixture, variant: str +) -> None: + """Test setver runs with code_base_update.""" + from invoke import MockContext + + from ci_cd.tasks.setver import setver + + # Create __init__.py file + package_dir = tmp_path / "src" / "my_package" + package_dir.mkdir(parents=True) + (package_dir / "__init__.py").write_text("__version__ = '0.0.0'\n") + + # Create a file to update + file_to_update = package_dir / "file_to_update" + file_to_update.write_text("version = '0.0.0'\n") + + # Create a file outside package_dir to update + outside_file_to_update = tmp_path / "outside_file_to_update" + outside_file_to_update.write_text( + "https://example.com/something/0.0.0/update-previous-path-part\n" + ) + + # Prepare setver inputs + if variant == "absolute path": + filepaths: list[str | Path] = [ + file_to_update.resolve(), + outside_file_to_update.resolve(), + ] + elif variant == "relative path": + filepaths = [ + file_to_update.relative_to(tmp_path), + outside_file_to_update.relative_to(tmp_path), + ] + elif variant == "package_dir variable": + filepaths = [ + f"{{package_dir}}/{file_to_update.relative_to(package_dir)}", + outside_file_to_update.resolve(), + ] + + new_version = "0.1.0" + + # Run setver + setver( + MockContext(), + package_dir=package_dir.relative_to(tmp_path), + version=new_version, + root_repo_path=tmp_path, + code_base_update=[ + f"{filepaths[0]},version = '.*',version = '{{version}}'", + f"{filepaths[1]},something/.*/update-previous,something/{{version}}/update-previous", + ], + code_base_update_separator=",", + ) + + # Check logs + assert f"filepath: {(package_dir / '__init__.py').resolve()}" not in caplog.text + assert f"filepath: {file_to_update.resolve()}" in caplog.text + assert f"filepath: {outside_file_to_update.resolve()}" in caplog.text + + # Check __init__.py file was NOT updated (not in code_base_update) + assert (package_dir / "__init__.py").read_text() == "__version__ = '0.0.0'\n" + + # Check file_to_update WAS updated (in code_base_update) + assert file_to_update.read_text() == f"version = '{new_version}'\n" + + # Check outside_file_to_update WAS updated (in code_base_update) + assert ( + outside_file_to_update.read_text() + == f"https://example.com/something/{new_version}/update-previous-path-part\n" + ) + + +@pytest.mark.parametrize( + ("version", "expected_version"), + compliant_python_version_schemes, + ids=[f"{v} -> {ev}" for v, ev in compliant_python_version_schemes], +) +@pytest.mark.parametrize( + "version_part", + [ + "ALL", + "major", + "minor", + "patch", + "pre_release", + "build", + "epoch", + "release", + "pre", + "post", + "dev", + "local", + "public", + "base_version", + "micro", + ], +) +def test_setver_with_code_base_update_version_parts( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + version: str, + expected_version: str, + version_part: str, +) -> None: + """Test setver runs with code_base_update.""" + from invoke import MockContext + + from ci_cd.tasks.setver import setver + from ci_cd.utils.versions import SemanticVersion + + python_specific_version_parts = [ + "epoch", + "release", + "pre", + "post", + "dev", + "local", + "public", + "base_version", + "micro", + ] + + # Create __init__.py file + package_dir = tmp_path / "src" / "my_package" + package_dir.mkdir(parents=True) + (package_dir / "__init__.py").write_text("__version__ = '0.0.0'\n") + + # Create a file to update + file_to_update = package_dir / "file_to_update" + file_to_update.write_text("version = '0.0.0'\n") + + # Create a file outside package_dir to update + outside_file_to_update = tmp_path / "outside_file_to_update" + outside_file_to_update.write_text( + "https://example.com/something/0.0.0/update-previous-path-part\n" + ) + + replacements = ( + [ + "version = '{version}'", + "something/{version}/update-previous", + ] + if version_part == "ALL" + else [ + f"version = '{{version.{version_part}}}'", + f"something/{{version.{version_part}}}/update-previous", + ] + ) + + semantic_version = SemanticVersion(version) + + # Explicitly extract the expected version if Python version-specific parts are + # requested + if version_part != "ALL": + if version_part in python_specific_version_parts: + expected_version = getattr( + semantic_version.as_python_version(shortened=False), version_part + ) + else: + expected_version = getattr(semantic_version, version_part) + + print(f"Updated expected version: {expected_version}") + + # Run setver + setver( + MockContext(), + package_dir=package_dir.relative_to(tmp_path), + version=version, + root_repo_path=tmp_path, + code_base_update=[ + f"{file_to_update.resolve()},version = '.*',{replacements[0]}", + f"{outside_file_to_update.resolve()},something/.*/update-previous,{replacements[1]}", + ], + code_base_update_separator=",", + ) + + # Check logs + assert f"filepath: {(package_dir / '__init__.py').resolve()}" not in caplog.text + assert f"filepath: {file_to_update.resolve()}" in caplog.text + assert f"filepath: {outside_file_to_update.resolve()}" in caplog.text + + # Check __init__.py file was NOT updated (not in code_base_update) + assert (package_dir / "__init__.py").read_text() == "__version__ = '0.0.0'\n" + + # Check file_to_update WAS updated (in code_base_update) + assert file_to_update.read_text() == f"version = '{expected_version}'\n" + + # Check outside_file_to_update WAS updated (in code_base_update) + assert ( + outside_file_to_update.read_text() + == f"https://example.com/something/{expected_version}/update-previous-path-part\n" + ) + + +def test_init_file_not_found() -> None: + """Test setver emits an error and stops when the __init__.py file is not found.""" + from pathlib import Path + + from invoke import MockContext + + from ci_cd.tasks.setver import setver + + assert not (Path.cwd() / "does not matter" / "__init__.py").exists() + + with pytest.raises( + SystemExit, match="Could not find the Python package's root '__init__.py' file" + ): + setver(MockContext(), package_dir="does not matter", version="0.1.0") + + +@pytest.mark.parametrize("fail_fast", [True, False]) +def test_invalid_code_base_update(fail_fast: bool) -> None: + """Test setver emits an error and stops when given an invalid code_base_update.""" + from invoke import MockContext + + from ci_cd.tasks.setver import setver + + error_msg = ( + "Could not properly extract" + if fail_fast + else "Errors occurred! See printed statements above." + ) + + with pytest.raises(SystemExit, match=error_msg): + setver( + MockContext(), + package_dir="does not matter", + version="0.1.0", + code_base_update=["invalid"], + fail_fast=fail_fast, + ) + + +@pytest.mark.parametrize("fail_fast", [True, False]) +def test_invalid_code_base_filepaths(fail_fast: bool) -> None: + """Test setver emits an error and stops when given invalid file paths in + code_base_update.""" + from invoke import MockContext + + from ci_cd.tasks.setver import setver + + if fail_fast: + error_msg = "Could not find the user-provided file at:" + else: + error_msg = "Errors occurred! See printed statements above." + + with pytest.raises(SystemExit, match=error_msg): + setver( + MockContext(), + package_dir="does not matter", + version="0.1.0", + code_base_update=["invalid,invalid,invalid again"], + fail_fast=fail_fast, + ) From dd3b2245b9d354e641f026446fca719c478ea95c Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Thu, 29 Feb 2024 13:01:50 +0100 Subject: [PATCH 2/3] Move resolving root_repo_path outside loop Co-authored-by: Treesa Joseph --- ci_cd/tasks/setver.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/ci_cd/tasks/setver.py b/ci_cd/tasks/setver.py index 3859a450..fc52602f 100644 --- a/ci_cd/tasks/setver.py +++ b/ci_cd/tasks/setver.py @@ -75,21 +75,35 @@ def setver( test: bool = test # type: ignore[no-redef] fail_fast: bool = fail_fast # type: ignore[no-redef] + # Validate inputs + # Version try: semantic_version = SemanticVersion(version) except ValueError: - sys.exit( - "Error: Please specify version as a semantic version (SemVer) or " - "PEP 440 version. The version may be prepended by a 'v'." + msg = ( + "Please specify version as a semantic version (SemVer) or PEP 440 version. " + "The version may be prepended by a 'v'." + ) + sys.exit(f"{Emoji.CROSS_MARK.value} {error_msg(msg)}") + + # Root repo path + root_repo = Path(root_repo_path).resolve() + if not root_repo.exists(): + msg = ( + f"Could not find the repository root at: {root_repo} (user provided: " + f"{root_repo_path!r})" ) + sys.exit(f"{Emoji.CROSS_MARK.value} {error_msg(msg)}") + # Run the task with defaults if not code_base_update: - init_file = Path(root_repo_path).resolve() / package_dir / "__init__.py" + init_file = root_repo / package_dir / "__init__.py" if not init_file.exists(): - sys.exit( - f"{Emoji.CROSS_MARK.value} Error: Could not find the Python package's " - f"root '__init__.py' file at: {init_file}" + msg = ( + "Could not find the Python package's root '__init__.py' file at: " + f"{init_file}" ) + sys.exit(f"{Emoji.CROSS_MARK.value} {error_msg(msg)}") update_file( init_file, @@ -135,7 +149,7 @@ def setver( ) if not filepath.is_absolute(): - filepath = Path(root_repo_path).resolve() / filepath + filepath = root_repo / filepath if not filepath.exists(): msg = f"Could not find the user-provided file at: {filepath}" From 37d18de4af05d7a150af274522ff38830bb0016f Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Thu, 29 Feb 2024 13:03:36 +0100 Subject: [PATCH 3/3] Fix test relying on error message output --- tests/tasks/test_setver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tasks/test_setver.py b/tests/tasks/test_setver.py index 054256d1..3da13ab2 100644 --- a/tests/tasks/test_setver.py +++ b/tests/tasks/test_setver.py @@ -142,8 +142,8 @@ def test_invalid_version() -> None: with pytest.raises( SystemExit, match=( - r"^Error: Please specify version as a semantic version \(SemVer\) or " - r"PEP 440 version\..*" + r"Please specify version as a semantic version \(SemVer\) or PEP 440 " + r"version\..*" ), ): setver(MockContext(), package_dir="does not matter", version="invalid")