diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 198fd2c3a..b0e966d16 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -451,10 +451,10 @@ jobs: ref: ${{ github.sha }} fetch-depth: 1 - - name: Setup | Install Python ${{ env.COMMON_PYTHON_VERSION }} + - name: Setup | Install Python ${{ env.LOWEST_PYTHON_VERSION }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: - python-version: ${{ env.COMMON_PYTHON_VERSION }} + python-version: ${{ env.LOWEST_PYTHON_VERSION }} cache: 'pip' - name: Setup | Install dependencies diff --git a/pyproject.toml b/pyproject.toml index 8f1071d59..d688ee0e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,13 +27,13 @@ authors = [ { name = "codejedi365", email = "codejedi365@gmail.com" }, ] dependencies = [ - "click ~= 8.1.0", + "click >= 8.1.0, < 9.0.0", "click-option-group ~= 0.5", "gitpython ~= 3.0", "requests ~= 2.25", "jinja2 ~= 3.1", "python-gitlab >= 4.0.0, < 9.0.0", - "tomlkit >= 0.13.0, < 0.16.0", + "tomlkit ~= 0.13.0", "dotty-dict ~= 1.3", "importlib-resources ~= 6.0", "pydantic ~= 2.0", @@ -57,7 +57,7 @@ repository = "http://github.com/python-semantic-release/python-semantic-release. [project.optional-dependencies] build = [ "build ~= 1.2", - "tomlkit >= 0.13.0, < 0.16.0", + "tomlkit ~= 0.13.0", ] docs = [ "Sphinx ~= 7.4", @@ -71,7 +71,7 @@ test = [ "flatdict ~= 4.0", "freezegun ~= 1.5", "pyyaml ~= 6.0", - "pytest ~= 8.3", + "pytest >= 8.3, < 10.0", "pytest-clarity ~= 1.0", "pytest-cov >= 5.0.0, < 8.0.0", "pytest-env ~= 1.0", @@ -84,12 +84,12 @@ test = [ "requests-mock ~= 1.10", ] dev = [ - "pre-commit ~= 4.3", + "pre-commit >= 3.5.0, < 5.0.0", "tox ~= 4.11", "ruff == 0.6.1" ] mypy = [ - "mypy == 1.16.1", + "mypy >= 1.14.1, < 2.0.0", "types-Deprecated ~= 1.2", "types-requests ~= 2.32.0", "types-pyyaml ~= 6.0", @@ -173,11 +173,11 @@ commands = [testenv:ruff] deps = .[dev] commands = - ruff check . --statistics --output-format=text + ruff check . --statistics """ + [tool.mypy] -python_version = "3.8" show_column_numbers = true show_error_context = true pretty = true @@ -213,6 +213,7 @@ ignore_missing_imports = true module = "dotty_dict" ignore_missing_imports = true + [tool.ruff] line-length = 88 target-version = "py38" diff --git a/src/semantic_release/cli/commands/main.py b/src/semantic_release/cli/commands/main.py index c10d3f647..387893974 100644 --- a/src/semantic_release/cli/commands/main.py +++ b/src/semantic_release/cli/commands/main.py @@ -30,8 +30,8 @@ ] -class Cli(click.MultiCommand): - """Root MultiCommand for the semantic-release CLI""" +class Cli(click.Group): + """Root Group for the semantic-release CLI""" class SubCmds(Enum): """Subcommand import definitions""" @@ -42,12 +42,12 @@ class SubCmds(Enum): VERSION = f"{__package__}.version" PUBLISH = f"{__package__}.publish" - def list_commands(self, _ctx: click.Context) -> list[str]: + def list_commands(self, ctx: click.Context) -> list[str]: # noqa: ARG002 # Used for shell-completion return [subcmd.lower().replace("_", "-") for subcmd in Cli.SubCmds.__members__] - def get_command(self, _ctx: click.Context, name: str) -> click.Command | None: - subcmd_name = name.lower().replace("-", "_") + def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: # noqa: ARG002 + subcmd_name = cmd_name.lower().replace("-", "_") try: subcmd_def: Cli.SubCmds = Cli.SubCmds.__dict__[subcmd_name.upper()] module_path = subcmd_def.value diff --git a/tests/conftest.py b/tests/conftest.py index 4e78b59b5..90f21339d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ from unittest import mock import pytest +from click import __version__ as click_version from click.testing import CliRunner from filelock import FileLock from git import Commit, Repo @@ -193,9 +194,17 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item item.add_marker(comprehensive_test_skip_marker) +def get_cli_runner() -> CliRunner: + return ( + CliRunner() + if tuple(map(int, click_version.split("."))) >= (8, 2, 0) + else CliRunner(mix_stderr=False) # type: ignore[call-arg] + ) + + @pytest.fixture def cli_runner() -> CliRunner: - return CliRunner(mix_stderr=False) + return get_cli_runner() @pytest.fixture(scope="session") @@ -211,7 +220,7 @@ def _run_cli( # Prevent logs from being propagated to the root logger (pytest) logger.propagate = False - cli_runner = CliRunner(mix_stderr=False) + cli_runner = get_cli_runner() env_vars = {**clean_os_environment, **(env or {})} args = ["-vv", *(argv or [])] diff --git a/tests/e2e/cmd_config/test_generate_config.py b/tests/e2e/cmd_config/test_generate_config.py index a9db934ea..fe3174d43 100644 --- a/tests/e2e/cmd_config/test_generate_config.py +++ b/tests/e2e/cmd_config/test_generate_config.py @@ -51,11 +51,11 @@ def test_generate_config_toml( # Evaluate: Check that the command ran successfully and that the output matches the expected configuration assert_successful_exit_code(result, cli_cmd) - assert expected_config_as_str == result.output.strip() + assert expected_config_as_str == result.stdout.strip() # Setup: Write the generated configuration to a file config_file = "releaserc.toml" - example_project_dir.joinpath(config_file).write_text(result.output) + example_project_dir.joinpath(config_file).write_text(result.stdout) # Act: Validate that the generated config is a valid configuration for PSR cli_cmd = [ @@ -93,11 +93,11 @@ def test_generate_config_json( # Evaluate: Check that the command ran successfully and that the output matches the expected configuration assert_successful_exit_code(result, cli_cmd) - assert expected_config_as_str == result.output.strip() + assert expected_config_as_str == result.stdout.strip() # Setup: Write the generated configuration to a file config_file = "releaserc.json" - example_project_dir.joinpath(config_file).write_text(result.output) + example_project_dir.joinpath(config_file).write_text(result.stdout) # Act: Validate that the generated config is a valid configuration for PSR cli_cmd = [ @@ -144,7 +144,7 @@ def test_generate_config_pyproject_toml( # Evaluate: Check that the command ran successfully and that the output matches the expected configuration assert_successful_exit_code(result, cli_cmd) - assert expected_config_as_str == result.output.strip() + assert expected_config_as_str == result.stdout.strip() # Setup: Write the generated configuration to a file example_pyproject_toml.write_text( @@ -152,7 +152,7 @@ def test_generate_config_pyproject_toml( "\n\n", [ example_pyproject_toml.read_text(encoding="utf-8").strip(), - result.output, + result.stdout, ], ) ) diff --git a/tests/e2e/cmd_version/test_version_build.py b/tests/e2e/cmd_version/test_version_build.py index 51022da87..40ac74388 100644 --- a/tests/e2e/cmd_version/test_version_build.py +++ b/tests/e2e/cmd_version/test_version_build.py @@ -26,27 +26,29 @@ @pytest.mark.skipif(sys.platform == "win32", reason="Unix only") @pytest.mark.parametrize( "shell", - filter( - None, - [ - # because we will actually run the build command in this shell, we must ensure it exists - "bash" - if list( - filter( - lambda sh_exe: Path(sh_exe).exists(), - ("/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash"), + list( + filter( + None, + [ + # because we will actually run the build command in this shell, we must ensure it exists + "bash" + if list( + filter( + lambda sh_exe: Path(sh_exe).exists(), + ("/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash"), + ) ) - ) - else "", - "zsh" - if list( - filter( - lambda sh_exe: Path(sh_exe).exists(), - ("/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"), + else "", + "zsh" + if list( + filter( + lambda sh_exe: Path(sh_exe).exists(), + ("/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"), + ) ) - ) - else "", - ], + else "", + ], + ) ) or ["sh"], ) diff --git a/tests/e2e/test_main.py b/tests/e2e/test_main.py index 4e1df87e0..86d60b57b 100644 --- a/tests/e2e/test_main.py +++ b/tests/e2e/test_main.py @@ -9,6 +9,7 @@ import git import pytest +from click import __version__ as click_version from click.testing import CliRunner from pytest_lazy_fixtures.lazy_fixture import lf as lazy_fixture @@ -65,7 +66,12 @@ def test_main_no_args_passes_w_help_text(): cli_cmd = [MAIN_PROG_NAME] result = CliRunner().invoke(main, prog_name=cli_cmd[0]) - assert_successful_exit_code(result, cli_cmd) + + if tuple(map(int, click_version.split("."))) >= (8, 2, 0): + assert_exit_code(2, result, cli_cmd) + else: + assert_successful_exit_code(result, cli_cmd) + assert "Usage: " in result.output diff --git a/tests/util.py b/tests/util.py index d97e04845..8ddff0a83 100644 --- a/tests/util.py +++ b/tests/util.py @@ -51,7 +51,7 @@ GitCommandWrapperType: TypeAlias = Git -def get_func_qual_name(func: Callable) -> str: +def get_func_qual_name(func: Callable[[Any], Any]) -> str: return str.join(".", filter(None, [func.__module__, func.__qualname__])) @@ -79,7 +79,7 @@ def assert_successful_exit_code(result: ClickInvokeResult, cli_cmd: list[str]) - return assert_exit_code(SUCCESS_EXIT_CODE, result, cli_cmd) -def get_full_qualname(callable_obj: Callable) -> str: +def get_full_qualname(callable_obj: Callable[[Any], Any]) -> str: parts = filter( None, [