Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update setver task #244

Merged
merged 3 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 132 additions & 87 deletions ci_cd/tasks/setver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@

CasperWA marked this conversation as resolved.
Show resolved Hide resolved
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__)


@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'."
Expand All @@ -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"],
)
Expand All @@ -71,29 +75,35 @@
test: bool = test # type: ignore[no-redef]
fail_fast: bool = fail_fast # type: ignore[no-redef]

match = re.fullmatch(
(
r"v?(?P<major>[0-9]+)\.(?P<minor>[0-9]+)\.(?P<patch>[0-9]+)"
r"(?:-(?P<pre_release>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
r"(?:\+(?P<build>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
),
version,
)
if not match:
sys.exit(
"Error: Please specify version as "
"'Major.Minor.Patch(-Pre-Release+Build Metadata)' or "
"'vMajor.Minor.Patch(-Pre-Release+Build Metadata)'"
# Validate inputs
# Version
try:
semantic_version = SemanticVersion(version)
except ValueError:
msg = (
"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())
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 = (

Check warning on line 92 in ci_cd/tasks/setver.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/tasks/setver.py#L92

Added line #L92 was not covered by tests
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)}")

Check warning on line 96 in ci_cd/tasks/setver.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/tasks/setver.py#L96

Added line #L96 was not covered by tests

# 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,
Expand All @@ -102,80 +112,115 @@
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 = root_repo / 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(

Check warning on line 196 in ci_cd/tasks/setver.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/tasks/setver.py#L196

Added line #L196 was not covered by tests
f"filepath: {filepath}\npattern: {pattern!r}\n"
f"replacement (input): {input_replacement}\n"
f"replacement (handled): {replacement}"
)
continue

Check warning on line 201 in ci_cd/tasks/setver.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/tasks/setver.py#L201

Added line #L201 was not covered by tests

try:
update_file(filepath, (pattern, replacement))
except re.error as exc:
if validated_code_base_updates[0] != (

Check warning on line 206 in ci_cd/tasks/setver.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/tasks/setver.py#L205-L206

Added lines #L205 - L206 were not covered by tests
filepath,
pattern,
replacement,
input_replacement,
):
msg = "Some files have already been updated !\n\n "

Check warning on line 212 in ci_cd/tasks/setver.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/tasks/setver.py#L212

Added line #L212 was not covered by tests

msg += (

Check warning on line 214 in ci_cd/tasks/setver.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/tasks/setver.py#L214

Added line #L214 was not covered by tests
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)}")

Check warning on line 221 in ci_cd/tasks/setver.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/tasks/setver.py#L219-L221

Added lines #L219 - L221 were not covered by tests

# Success, done
print(
f"{Emoji.PARTY_POPPER.value} Bumped version for {package_dir} to "
f"{semantic_version}."
Expand Down
34 changes: 34 additions & 0 deletions ci_cd/utils/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,40 @@
"""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(

Check warning on line 318 in ci_cd/utils/versions.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/utils/versions.py#L318

Added line #L318 was not covered by tests
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(

Check warning on line 328 in ci_cd/utils/versions.py

View check run for this annotation

Codecov / codecov/patch

ci_cd/utils/versions.py#L327-L328

Added lines #L327 - L328 were not covered by tests
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(
Expand Down
Loading
Loading