Skip to content
Back to top

# Source code for semantic_release.version.declaration

    from __future__ import annotations

    # TODO: Remove v11
    from abc import ABC, abstractmethod
    from pathlib import Path
    from typing import TYPE_CHECKING

    from deprecated.sphinx import deprecated

    from semantic_release.globals import logger
    from semantic_release.version.declarations.enum import VersionStampType
    from semantic_release.version.declarations.i_version_replacer import IVersionReplacer
    from semantic_release.version.declarations.pattern import PatternVersionDeclaration
    from semantic_release.version.declarations.toml import TomlVersionDeclaration

    if TYPE_CHECKING:  # pragma: no cover
        from semantic_release.version.version import Version

    # Globals
    __all__ = [
        "IVersionReplacer",
        "VersionStampType",
        "PatternVersionDeclaration",
        "TomlVersionDeclaration",
        "VersionDeclarationABC",
    ]

    @deprecated(
        version="9.20.0",
        reason=str.join(
            " ",
            [
                "Refactored to composition paradigm using the new IVersionReplacer interface.",
                "This class will be removed in a future release",
            ],
        ),
    )
    class VersionDeclarationABC(ABC):
        """
        ABC for classes representing a location in which a version is declared somewhere
        within the source tree of the repository
        """

        def __init__(self, path: Path | str, search_text: str) -> None:
            self.path = Path(path)
            if not self.path.exists():
                raise FileNotFoundError(f"path {self.path.resolve()!r} does not exist")
            self.search_text = search_text
            self._content: str | None = None

        @property
        def content(self) -> str:
            """
            The content of the source file in which the version is stored. This property
            is cached in the instance variable _content
            """
            if self._content is None:
                logger.debug(
                    "No content stored, reading from source file %s", self.path.resolve()
                )
                self._content = self.path.read_text()
            return self._content

        @content.deleter
        def content(self) -> None:
            logger.debug("resetting instance-stored source file contents")
            self._content = None

        @abstractmethod
        def parse(self) -> set[Version]:
            """
            Return a set of the versions which can be parsed from the file.
            Because a source can match in multiple places, this method returns a
            set of matches. Generally, there should only be one element in this
            set (i.e. even if the version is specified in multiple places, it
            should be the same version in each place), but enforcing that condition
            is not mandatory or expected.
            """

        @abstractmethod
        def replace(self, new_version: Version) -> str:
            """
            Update the versions.
            This method reads the underlying file, replaces each occurrence of the
            matched pattern, then writes the updated file.
            :param new_version: The new version number as a `Version` instance
            """

        def write(self, content: str) -> None:
            r"""
            Write new content back to the source path.
            Use alongside .replace():
            >>> class MyVD(VersionDeclarationABC):
            ...     def parse(self): ...
            ...     def replace(self, new_version: Version): ...
            ...     def write(self, content: str): ...

            >>> new_version = Version.parse("1.2.3")
            >>> vd = MyVD("path", r"__version__ = (?P<version>\d+\d+\d+)")
            >>> vd.write(vd.replace(new_version))
            """
            logger.debug("writing content to %r", self.path.resolve())
            self.path.write_text(content)
            self._content = None