Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat: stamp version with tag format #1190

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

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
132 changes: 118 additions & 14 deletions 132 docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1201,17 +1201,61 @@ Tags which do not match this format will not be considered as versions of your p

**Type:** ``list[str]``

Similar to :ref:`config-version_variables`, but allows the version number to be
identified safely in a toml file like ``pyproject.toml``, with each entry using
dotted notation to indicate the key for which the value represents the version:
This configuration option is similar to :ref:`config-version_variables`, but it uses
a TOML parser to interpret the data structure before, inserting the version. This
allows users to use dot-notation to specify the version via the logical structure
within the TOML file, which is more accurate than a pattern replace.

The ``version_toml`` option is commonly used to update the version number in the project
definition file: ``pyproject.toml`` as seen in the example below.

As of ${NEW_RELEASE_TAG}, the ``version_toml`` option accepts a colon-separated definition
with either 2 or 3 parts. The 2-part definition includes the file path and the version
parameter (in dot-notation). Newly with ${NEW_RELEASE_TAG}, it also accepts an optional
3rd part to allow configuration of the format type.

**Available Format Types**

- ``nf``: Number format (ex. ``1.2.3``)
- ``tf``: :ref:`Tag Format <config-tag_format>` (ex. ``v1.2.3``)

If the format type is not specified, it will default to the number format.

**Example**

.. code-block:: toml

[semantic_release]
version_toml = [
"pyproject.toml:tool.poetry.version",
# "file:variable:[format_type]"
"pyproject.toml:tool.poetry.version", # Implied Default: Number format
"definition.toml:project.version:nf", # Number format
"definition.toml:project.release:tf", # Tag format
]

This configuration will result in the following changes:

.. code-block:: diff

diff a/pyproject.toml b/pyproject.toml

[tool.poetry]
- version = "0.1.0"
+ version = "0.2.0"

.. code-block:: diff

diff a/definition.toml b/definition.toml

[project]
name = "example"

- version = "0.1.0"
+ version = "0.1.0"

- release = "v0.1.0"
+ release = "v0.2.0"

**Default:** ``[]``

----
Expand All @@ -1223,17 +1267,74 @@ dotted notation to indicate the key for which the value represents the version:

**Type:** ``list[str]``

Each entry represents a location where the version is stored in the source code,
specified in ``file:variable`` format. For example:
The ``version_variables`` configuration option is a list of string definitions
that defines where the version number should be updated in the repository, when
a new version is released.

As of ${NEW_RELEASE_TAG}, the ``version_variables`` option accepts a
colon-separated definition with either 2 or 3 parts. The 2-part definition includes
the file path and the variable name. Newly with ${NEW_RELEASE_TAG}, it also accepts
an optional 3rd part to allow configuration of the format type.

**Available Format Types**

- ``nf``: Number format (ex. ``1.2.3``)
- ``tf``: :ref:`Tag Format <config-tag_format>` (ex. ``v1.2.3``)

If the format type is not specified, it will default to the number format.

Prior to ${NEW_RELEASE_TAG}, PSR only supports entries with the first 2-parts
as the tag format type was not available and would only replace numeric
version numbers.

**Example**

.. code-block:: toml

[semantic_release]
tag_format = "v{version}"
version_variables = [
"src/semantic_release/__init__.py:__version__",
"docs/conf.py:version",
# "file:variable:format_type"
"src/semantic_release/__init__.py:__version__", # Implied Default: Number format
"docs/conf.py:version:nf", # Number format for sphinx docs
"kustomization.yml:newTag:tf", # Tag format
]

First, the ``__version__`` variable in ``src/semantic_release/__init__.py`` will be updated
with the next version using the `SemVer`_ number format.

.. code-block:: diff

diff a/src/semantic_release/__init__.py b/src/semantic_release/__init__.py

- __version__ = "0.1.0"
+ __version__ = "0.2.0"

Then, the ``version`` variable in ``docs/conf.py`` will be updated with the next version
with the next version using the `SemVer`_ number format because of the explicit ``nf``.

.. code-block:: diff

diff a/docs/conf.py b/docs/conf.py

- version = "0.1.0"
+ version = "0.2.0"

Lastly, the ``newTag`` variable in ``kustomization.yml`` will be updated with the next version
with the next version using the configured :ref:`config-tag_format` because the definition
included ``tf``.

.. code-block:: diff

diff a/kustomization.yml b/kustomization.yml

images:
- name: repo/image
- newTag: v0.1.0
+ newTag: v0.2.0

**How It works**

Each version variable will be transformed into a Regular Expression that will be used
to substitute the version number in the file. The replacement algorithm is **ONLY** a
pattern match and replace. It will **NOT** evaluate the code nor will PSR understand
Expand All @@ -1242,16 +1343,17 @@ any internal object structures (ie. ``file:object.version`` will not work).
.. important::
The Regular Expression expects a version value to exist in the file to be replaced.
It cannot be an empty string or a non-semver compliant string. If this is the very
first time you are using PSR, we recommend you set the version to ``0.0.0``. This
may become more flexible in the future with resolution of issue `#941`_.
first time you are using PSR, we recommend you set the version to ``0.0.0``.

This may become more flexible in the future with resolution of issue `#941`_.

.. _#941: https://github.com/python-semantic-release/python-semantic-release/issues/941

Given the pattern matching nature of this feature, the Regular Expression is able to
support most file formats as a variable declaration in most languages is very similar.
We specifically support Python, YAML, and JSON as these have been the most common
requests. This configuration option will also work regardless of file extension
because its only a pattern match.
support most file formats because of the similarity of variable declaration across
programming languages. PSR specifically supports Python, YAML, and JSON as these have
been the most commonly requested formats. This configuration option will also work
regardless of file extension because it looks for a matching pattern string.

.. note::
This will also work for TOML but we recommend using :ref:`config-version_toml` for
Expand All @@ -1264,3 +1366,5 @@ because its only a pattern match.
both. This is a limitation of the pattern matching and not a bug.

**Default:** ``[]``

.. _SemVer: https://semver.org/
6 changes: 6 additions & 0 deletions 6 pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies = [
"pydantic ~= 2.0",
"rich ~= 13.0",
"shellingham ~= 1.5",
"Deprecated ~= 1.2", # Backport of deprecated decorator for python 3.8
]

[project.scripts]
Expand Down Expand Up @@ -83,6 +84,7 @@ dev = [
]
mypy = [
"mypy == 1.15.0",
"types-Deprecated ~= 1.2",
"types-requests ~= 2.32.0",
"types-pyyaml ~= 6.0",
]
Expand Down Expand Up @@ -201,6 +203,10 @@ ignore_missing_imports = true
module = "shellingham"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "dotty_dict"
ignore_missing_imports = true

[tool.ruff]
line-length = 88
target-version = "py38"
Expand Down
45 changes: 30 additions & 15 deletions 45 src/semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@

if TYPE_CHECKING: # pragma: no cover
from pathlib import Path
from typing import Iterable, Mapping
from typing import Mapping, Sequence

from git.refs.tag import Tag

from semantic_release.cli.cli_context import CliContextObj
from semantic_release.version.declaration import VersionDeclarationABC
from semantic_release.version.declaration import IVersionReplacer
from semantic_release.version.version import Version


Expand Down Expand Up @@ -135,28 +135,43 @@ def version_from_forced_level(

def apply_version_to_source_files(
repo_dir: Path,
version_declarations: Iterable[VersionDeclarationABC],
version_declarations: Sequence[IVersionReplacer],
version: Version,
noop: bool = False,
) -> list[str]:
paths = [
str(declaration.path.resolve().relative_to(repo_dir))
for declaration in version_declarations
if len(version_declarations) < 1:
return []

if not noop:
log.debug("Updating version %s in repository files...", version)

paths = list(
map(
lambda decl, new_version=version, noop=noop: ( # type: ignore[misc]
decl.update_file_w_version(new_version=new_version, noop=noop)
),
version_declarations,
)
)

repo_filepaths = [
str(updated_file.relative_to(repo_dir))
for updated_file in paths
if updated_file is not None
]

if noop:
noop_report(
"would have updated versions in the following paths:"
+ "".join(f"\n {path}" for path in paths)
str.join(
"",
[
"would have updated versions in the following paths:",
*[f"\n {filepath}" for filepath in repo_filepaths],
],
)
)
return paths

log.debug("writing version %s to source paths %s", version, paths)
for declaration in version_declarations:
new_content = declaration.replace(new_version=version)
declaration.path.write_text(new_content)

return paths
return repo_filepaths


def shell(
Expand Down
79 changes: 37 additions & 42 deletions 79 src/semantic_release/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
ScipyCommitParser,
TagCommitParser,
)
from semantic_release.const import COMMIT_MESSAGE, DEFAULT_COMMIT_AUTHOR, SEMVER_REGEX
from semantic_release.const import COMMIT_MESSAGE, DEFAULT_COMMIT_AUTHOR
from semantic_release.errors import (
DetachedHeadGitError,
InvalidConfiguration,
Expand All @@ -55,11 +55,9 @@
ParserLoadError,
)
from semantic_release.helpers import dynamic_import
from semantic_release.version.declaration import (
PatternVersionDeclaration,
TomlVersionDeclaration,
VersionDeclarationABC,
)
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
from semantic_release.version.translator import VersionTranslator

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -555,7 +553,7 @@ class RuntimeContext:
commit_author: Actor
commit_message: str
changelog_excluded_commit_patterns: Tuple[Pattern[str], ...]
version_declarations: Tuple[VersionDeclarationABC, ...]
version_declarations: Tuple[IVersionReplacer, ...]
hvcs_client: hvcs.HvcsBase
changelog_insertion_flag: str
changelog_mask_initial_release: bool
Expand Down Expand Up @@ -738,44 +736,41 @@ def from_raw_config( # noqa: C901

commit_author = Actor(*_commit_author_valid.groups())

version_declarations: list[VersionDeclarationABC] = []
for decl in () if raw.version_toml is None else raw.version_toml:
try:
path, search_text = decl.split(":", maxsplit=1)
# VersionDeclarationABC handles path existence check
vd = TomlVersionDeclaration(path, search_text)
except ValueError as exc:
log.exception("Invalid TOML declaration %r", decl)
raise InvalidConfiguration(
f"Invalid TOML declaration {decl!r}"
) from exc

version_declarations.append(vd)

for decl in () if raw.version_variables is None else raw.version_variables:
try:
path, variable = decl.split(":", maxsplit=1)
# VersionDeclarationABC handles path existence check
search_text = str.join(
"",
version_declarations: list[IVersionReplacer] = []

try:
version_declarations.extend(
TomlVersionDeclaration.from_string_definition(definition)
for definition in iter(raw.version_toml or ())
)
except ValueError as err:
raise InvalidConfiguration(
str.join(
"\n",
[
# Supports optional matching quotations around variable name
# Negative lookbehind to ensure we don't match part of a variable name
f"""(?x)(?P<quote1>['"])?(?<![\\w.-]){variable}(?P=quote1)?""",
# Supports walrus, equals sign, or colon as assignment operator ignoring whitespace separation
r"\s*(:=|[:=])\s*",
# Supports optional matching quotations around version number of a SEMVER pattern
f"""(?P<quote2>['"])?(?P<version>{SEMVER_REGEX.pattern})(?P=quote2)?""",
"Invalid 'version_toml' configuration",
str(err),
],
)
pd = PatternVersionDeclaration(path, search_text)
except ValueError as exc:
log.exception("Invalid variable declaration %r", decl)
raise InvalidConfiguration(
f"Invalid variable declaration {decl!r}"
) from exc

version_declarations.append(pd)
) from err

try:
version_declarations.extend(
PatternVersionDeclaration.from_string_definition(
definition, raw.tag_format
)
for definition in iter(raw.version_variables or ())
)
except ValueError as err:
raise InvalidConfiguration(
str.join(
"\n",
[
"Invalid 'version_variables' configuration",
str(err),
],
)
) from err

# Provide warnings if the token is missing
if not raw.remote.token:
Expand Down
2 changes: 1 addition & 1 deletion 2 src/semantic_release/cli/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def noop_report(msg: str) -> None:
Rich-prints a msg with a standard prefix to report when an action is not being
taken due to a "noop" flag
"""
fullmsg = "[bold cyan]:shield: semantic-release 'noop' mode is enabled! " + msg
fullmsg = "[bold cyan][:shield: NOP] " + msg
rprint(fullmsg)


Expand Down
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.