Skip to content

setver

setver task.

Set the specified version.

setver(_, package_dir, version, root_repo_path='.', code_base_update=None, code_base_update_separator=',', test=False, fail_fast=False)

Sets the specified version of specified Python package.

Source code in ci_cd/tasks/setver.py
@task(
    help={
        "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'."
        ),
        "root-repo-path": (
            "A resolvable path to the root directory of the repository folder."
        ),
        "code-base-update": (
            "'--code-base-update-separator'-separated string defining 'file path', "
            "'pattern', 'replacement string', in that order, for something to update "
            "in the code base. E.g., '{package_dir}/__init__.py,__version__ *= "
            "*('|\").*('|\"),__version__ = \"{version}\"', where '{package_dir}' "
            "and {version} will be exchanged with the given '--package-dir' value and "
            "given '--version' value, respectively. The 'file path' must always "
            "either be relative to the repository root directory or absolute. The "
            "'pattern' should be given as a 'raw' Python string. This input option "
            "can be supplied multiple times."
        ),
        "code-base-update-separator": (
            "The string separator to use for '--code-base-update' values."
        ),
        "fail_fast": (
            "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."
        ),
    },
    iterable=["code_base_update"],
)
def setver(
    _,
    package_dir,
    version,
    root_repo_path=".",
    code_base_update=None,
    code_base_update_separator=",",
    test=False,
    fail_fast=False,
):
    """Sets the specified version of specified Python package."""
    if TYPE_CHECKING:  # pragma: no cover
        package_dir: str = package_dir  # type: ignore[no-redef]
        version: str = version  # type: ignore[no-redef]
        root_repo_path: str = root_repo_path  # type: ignore[no-redef]
        code_base_update: list[str] = code_base_update  # type: ignore[no-redef]
        code_base_update_separator: str = code_base_update_separator  # type: ignore[no-redef]
        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:
        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 = root_repo / package_dir / "__init__.py"
        if not init_file.exists():
            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,
            (
                r'__version__ *= *(?:\'|").*(?:\'|")',
                f'__version__ = "{semantic_version}"',
            ),
        )

        # 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.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}"
            )
            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}."
    )