diff --git a/docs/configuration.rst b/docs/configuration.rst index e2bfe3bbc..ee63a99b0 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1102,4 +1102,33 @@ specified in ``file:variable`` format. For example: "docs/conf.py:version", ] +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 +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`_. + +.. _#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. + +.. note:: + This will also work for TOML but we recommend using :ref:`config-version_toml` for + TOML files as it actually will interpret the TOML file and replace the version + number before writing the file back to disk. + +.. warning:: + If the file (ex. JSON) you are replacing has two of the same variable name in it, + this pattern match will not be able to differentiate between the two and will replace + both. This is a limitation of the pattern matching and not a bug. + **Default:** ``[]`` diff --git a/pyproject.toml b/pyproject.toml index 242021dfe..36be341bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ docs = [ ] test = [ "coverage[toml] ~= 7.0", + "pyyaml ~= 6.0", "pytest ~= 8.3", "pytest-env ~= 1.0", "pytest-xdist ~= 3.0", @@ -86,8 +87,8 @@ env = [ ] addopts = [ # TO DEBUG in single process, swap auto to 0 - "-nauto", - # "-n0", + # "-nauto", + "-n0", "-ra", "--diff-symbols", "--cache-clear", diff --git a/semantic_release/cli/config.py b/semantic_release/cli/config.py index ba12f4ce4..5c9f31ef7 100644 --- a/semantic_release/cli/config.py +++ b/semantic_release/cli/config.py @@ -540,7 +540,18 @@ def from_raw_config( # noqa: C901 try: path, variable = decl.split(":", maxsplit=1) # VersionDeclarationABC handles path existence check - search_text = rf"(?x){variable}\s*(:=|[:=])\s*(?P['\"])(?P{SEMVER_REGEX.pattern})(?P=quote)" # noqa: E501 + search_text = str.join( + "", + [ + # Supports optional matching quotations around variable name + # Negative lookbehind to ensure we don't match part of a variable name + f"""(?x)(?P['"])?(?['"])?(?P{SEMVER_REGEX.pattern})(?P=quote2)?""", + ], + ) pd = PatternVersionDeclaration(path, search_text) except ValueError as exc: log.exception("Invalid variable declaration %r", decl) diff --git a/tests/command_line/conftest.py b/tests/command_line/conftest.py index b8160373d..46d68d46e 100644 --- a/tests/command_line/conftest.py +++ b/tests/command_line/conftest.py @@ -6,7 +6,6 @@ from unittest.mock import MagicMock import pytest -from click.testing import CliRunner from requests_mock import ANY from semantic_release.cli import config as cli_config_module @@ -40,11 +39,6 @@ class RetrieveRuntimeContextFn(Protocol): def __call__(self, repo: Repo) -> RuntimeContext: ... -@pytest.fixture -def cli_runner() -> CliRunner: - return CliRunner(mix_stderr=False) - - @pytest.fixture def post_mocker(requests_mock: Mocker) -> Mocker: """Patch all POST requests, mocking a response body for VCS release creation.""" diff --git a/tests/conftest.py b/tests/conftest.py index 3465b236a..911906a7d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING import pytest +from click.testing import CliRunner from git import Commit, Repo from tests.fixtures import * @@ -28,6 +29,11 @@ class TeardownCachedDirFn(Protocol): def __call__(self, directory: Path) -> Path: ... +@pytest.fixture +def cli_runner() -> CliRunner: + return CliRunner(mix_stderr=False) + + @pytest.fixture(scope="session") def default_netrc_username() -> str: return "username" diff --git a/tests/scenario/test_version_stamp.py b/tests/scenario/test_version_stamp.py new file mode 100644 index 000000000..60a408e67 --- /dev/null +++ b/tests/scenario/test_version_stamp.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING + +import pytest +import yaml + +from semantic_release.cli.commands.main import main + +from tests.const import MAIN_PROG_NAME, VERSION_SUBCMD +from tests.fixtures.repos.trunk_based_dev.repo_w_no_tags import ( + repo_with_no_tags_angular_commits, +) +from tests.util import assert_successful_exit_code + +if TYPE_CHECKING: + from click.testing import CliRunner + + from tests.fixtures.example_project import UpdatePyprojectTomlFn + + +@pytest.mark.usefixtures(repo_with_no_tags_angular_commits.__name__) +def test_stamp_version_variables_python( + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, +) -> None: + new_version = "0.1.0" + target_file = Path("src/example/_version.py") + + # Set configuration to modify the python file + update_pyproject_toml( + "tool.semantic_release.version_variables", [f"{target_file}:__version__"] + ) + + # Use the version command and prevent any action besides stamping the version + cli_cmd = [ + MAIN_PROG_NAME, + VERSION_SUBCMD, + "--no-changelog", + "--no-commit", + "--no-tag", + ] + + # Act + result = cli_runner.invoke(main, cli_cmd[1:]) + + # Check the result + assert_successful_exit_code(result, cli_cmd) + + # Load python module for reading the version (ensures the file is valid) + spec = importlib.util.spec_from_file_location("example._version", str(target_file)) + module = importlib.util.module_from_spec(spec) # type: ignore + spec.loader.exec_module(module) # type: ignore + + # Check the version was updated + assert new_version == module.__version__ + + +@pytest.mark.usefixtures(repo_with_no_tags_angular_commits.__name__) +def test_stamp_version_variables_yaml( + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, +) -> None: + orig_version = "0.0.0" + new_version = "0.1.0" + target_file = Path("example.yml") + orig_yaml = dedent( + f"""\ + --- + package: example + version: {orig_version} + date-released: 1970-01-01 + """ + ) + # Write initial text in file + target_file.write_text(orig_yaml) + + # Set configuration to modify the yaml file + update_pyproject_toml( + "tool.semantic_release.version_variables", [f"{target_file}:version"] + ) + + # Use the version command and prevent any action besides stamping the version + cli_cmd = [ + MAIN_PROG_NAME, + VERSION_SUBCMD, + "--no-changelog", + "--no-commit", + "--no-tag", + ] + + # Act + result = cli_runner.invoke(main, cli_cmd[1:]) + + # Check the result + assert_successful_exit_code(result, cli_cmd) + + # Read content + resulting_yaml_obj = yaml.safe_load(target_file.read_text()) + + # Check the version was updated + assert new_version == resulting_yaml_obj["version"] + + # Check the rest of the content is the same (by reseting the version & comparing) + resulting_yaml_obj["version"] = orig_version + + assert yaml.safe_load(orig_yaml) == resulting_yaml_obj + + +@pytest.mark.usefixtures(repo_with_no_tags_angular_commits.__name__) +def test_stamp_version_variables_yaml_cff( + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, +) -> None: + orig_version = "0.0.0" + new_version = "0.1.0" + target_file = Path("CITATION.cff") + # Derived format from python-semantic-release/python-semantic-release#962 + orig_yaml = dedent( + f"""\ + --- + cff-version: 1.2.0 + message: "If you use this software, please cite it as below." + authors: + - family-names: Doe + given-names: Jon + orcid: https://orcid.org/1234-6666-2222-5555 + title: "My Research Software" + version: {orig_version} + date-released: 1970-01-01 + """ + ) + # Write initial text in file + target_file.write_text(orig_yaml) + + # Set configuration to modify the yaml file + update_pyproject_toml( + "tool.semantic_release.version_variables", [f"{target_file}:version"] + ) + + # Use the version command and prevent any action besides stamping the version + cli_cmd = [ + MAIN_PROG_NAME, + VERSION_SUBCMD, + "--no-changelog", + "--no-commit", + "--no-tag", + ] + + # Act + result = cli_runner.invoke(main, cli_cmd[1:]) + + # Check the result + assert_successful_exit_code(result, cli_cmd) + + # Read content + resulting_yaml_obj = yaml.safe_load(target_file.read_text()) + + # Check the version was updated + assert new_version == resulting_yaml_obj["version"] + + # Check the rest of the content is the same (by reseting the version & comparing) + resulting_yaml_obj["version"] = orig_version + + assert yaml.safe_load(orig_yaml) == resulting_yaml_obj + + +@pytest.mark.usefixtures(repo_with_no_tags_angular_commits.__name__) +def test_stamp_version_variables_json( + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, +) -> None: + orig_version = "0.0.0" + new_version = "0.1.0" + target_file = Path("plugins.json") + orig_json = { + "id": "test-plugin", + "version": orig_version, + "meta": { + "description": "Test plugin", + }, + } + # Write initial text in file + target_file.write_text(json.dumps(orig_json, indent=4)) + + # Set configuration to modify the json file + update_pyproject_toml( + "tool.semantic_release.version_variables", [f"{target_file}:version"] + ) + + # Use the version command and prevent any action besides stamping the version + cli_cmd = [ + MAIN_PROG_NAME, + VERSION_SUBCMD, + "--no-changelog", + "--no-commit", + "--no-tag", + ] + + # Act + result = cli_runner.invoke(main, cli_cmd[1:]) + + # Check the result + assert_successful_exit_code(result, cli_cmd) + + # Read content + resulting_json_obj = json.loads(target_file.read_text()) + + # Check the version was updated + assert new_version == resulting_json_obj["version"] + + # Check the rest of the content is the same (by reseting the version & comparing) + resulting_json_obj["version"] = orig_version + + assert orig_json == resulting_json_obj pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy