From 2f1df23e81c4fddb09acee62884c86687229067d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:43:57 +0000 Subject: [PATCH 01/10] Initial plan From 22b34a3dfa6763fb5c81e7b2d7941247d22cb8c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:56:24 +0000 Subject: [PATCH 02/10] Add --matrix-python/--no-matrix-python flag to usethis ci bitbucket Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> --- bitbucket-pipelines.yml | 56 +++++++++++++ src/usethis/_config.py | 10 +++ src/usethis/_integrations/environ/python.py | 7 +- src/usethis/_ui/interface/ci.py | 11 ++- tests/usethis/_core/test_core_ci.py | 82 +++++++++++++++++++ .../_ui/interface/test_interface_ci.py | 51 ++++++++++++ 6 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 bitbucket-pipelines.yml diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml new file mode 100644 index 00000000..9b9d4d1d --- /dev/null +++ b/bitbucket-pipelines.yml @@ -0,0 +1,56 @@ +image: atlassian/default-image:3 +definitions: + caches: + uv: ~/.cache/uv + pre-commit: ~/.cache/pre-commit + script_items: + - &install-uv | + curl -LsSf https://astral.sh/uv/install.sh | sh + source $HOME/.local/bin/env + export UV_LINK_MODE=copy + uv --version +pipelines: + default: + - step: + name: Run pre-commit + caches: + - uv + - pre-commit + script: + - *install-uv + - uv run pre-commit run --all-files + - step: + name: Test on 3.10 + caches: + - uv + script: + - *install-uv + - uv run --python 3.10 pytest -x --junitxml=test-reports/report.xml + - step: + name: Test on 3.11 + caches: + - uv + script: + - *install-uv + - uv run --python 3.11 pytest -x --junitxml=test-reports/report.xml + - step: + name: Test on 3.12 + caches: + - uv + script: + - *install-uv + - uv run --python 3.12 pytest -x --junitxml=test-reports/report.xml + - step: + name: Test on 3.13 + caches: + - uv + script: + - *install-uv + - uv run --python 3.13 pytest -x --junitxml=test-reports/report.xml + - step: + name: Test on 3.14 + caches: + - uv + script: + - *install-uv + - uv run --python 3.14 pytest -x --junitxml=test-reports/report.xml diff --git a/src/usethis/_config.py b/src/usethis/_config.py index 5b1fefb7..43dba889 100644 --- a/src/usethis/_config.py +++ b/src/usethis/_config.py @@ -17,6 +17,7 @@ OFFLINE_DEFAULT = False QUIET_DEFAULT = False BACKEND_DEFAULT = "auto" +MATRIX_PYTHON_DEFAULT = True @dataclass @@ -38,6 +39,8 @@ class UsethisConfig: subprocess_verbose: Verbose output for subprocesses. force_project_dir: Directory for the project. If None, defaults to the current working directory dynamically determined at runtime. + matrix_python: Whether to use a Python version matrix for CI tests. When False, + only the current development Python version is used. """ offline: bool = OFFLINE_DEFAULT @@ -49,6 +52,7 @@ class UsethisConfig: disable_pre_commit: bool = False subprocess_verbose: bool = False project_dir: Path | None = None + matrix_python: bool = MATRIX_PYTHON_DEFAULT @contextmanager def set( # noqa: PLR0913 @@ -63,6 +67,7 @@ def set( # noqa: PLR0913 disable_pre_commit: bool | None = None, subprocess_verbose: bool | None = None, project_dir: Path | str | None = None, + matrix_python: bool | None = None, ) -> Generator[None, None, None]: """Temporarily change command options.""" old_offline = self.offline @@ -74,6 +79,7 @@ def set( # noqa: PLR0913 old_disable_pre_commit = self.disable_pre_commit old_subprocess_verbose = self.subprocess_verbose old_project_dir = self.project_dir + old_matrix_python = self.matrix_python if offline is None: offline = old_offline @@ -93,6 +99,8 @@ def set( # noqa: PLR0913 subprocess_verbose = old_subprocess_verbose if project_dir is None: project_dir = old_project_dir + if matrix_python is None: + matrix_python = old_matrix_python self.offline = offline self.quiet = quiet @@ -105,6 +113,7 @@ def set( # noqa: PLR0913 if isinstance(project_dir, str): project_dir = Path(project_dir) self.project_dir = project_dir + self.matrix_python = matrix_python yield self.offline = old_offline self.quiet = old_quiet @@ -115,6 +124,7 @@ def set( # noqa: PLR0913 self.disable_pre_commit = old_disable_pre_commit self.subprocess_verbose = old_subprocess_verbose self.project_dir = old_project_dir + self.matrix_python = old_matrix_python def cpd(self) -> Path: """Return the current project directory.""" diff --git a/src/usethis/_integrations/environ/python.py b/src/usethis/_integrations/environ/python.py index e47fec9c..f451e746 100644 --- a/src/usethis/_integrations/environ/python.py +++ b/src/usethis/_integrations/environ/python.py @@ -1,7 +1,6 @@ from __future__ import annotations -from typing_extensions import assert_never - +from usethis._config import usethis_config from usethis._integrations.backend.dispatch import get_backend from usethis._integrations.backend.uv.python import ( get_supported_uv_major_python_versions, @@ -11,6 +10,10 @@ def get_supported_major_python_versions() -> list[int]: + # If matrix_python is disabled, return only the current development version + if not usethis_config.matrix_python: + return [get_python_major_version()] + backend = get_backend() if backend is BackendEnum.uv: diff --git a/src/usethis/_ui/interface/ci.py b/src/usethis/_ui/interface/ci.py index 51c14b32..b1bd0ef7 100644 --- a/src/usethis/_ui/interface/ci.py +++ b/src/usethis/_ui/interface/ci.py @@ -13,6 +13,11 @@ def bitbucket( remove: bool = typer.Option( False, "--remove", help="Remove Bitbucket Pipelines CI instead of adding it." ), + matrix_python: bool = typer.Option( + True, + "--matrix-python/--no-matrix-python", + help="Test against multiple Python versions (matrix) or just the current development version.", + ), offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, @@ -28,7 +33,11 @@ def bitbucket( with ( usethis_config.set( - offline=offline, quiet=quiet, frozen=frozen, backend=backend + offline=offline, + quiet=quiet, + frozen=frozen, + backend=backend, + matrix_python=matrix_python, ), files_manager(), ): diff --git a/tests/usethis/_core/test_core_ci.py b/tests/usethis/_core/test_core_ci.py index 1fd4f141..e84062a1 100644 --- a/tests/usethis/_core/test_core_ci.py +++ b/tests/usethis/_core/test_core_ci.py @@ -2,6 +2,7 @@ import pytest +import usethis._integrations.python.version from usethis._config import usethis_config from usethis._config_file import files_manager from usethis._core.ci import use_ci_bitbucket @@ -584,3 +585,84 @@ def test_message(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): "ℹ Consider `usethis tool pytest` to test your code for the pipeline.\n" # noqa: RUF001 "☐ Run your pipeline via the Bitbucket website.\n" ) + + class TestPythonMatrix: + def test_matrix_enabled_by_default(self, uv_init_dir: Path): + """Test that matrix is enabled by default and creates multiple test steps.""" + # Arrange + (uv_init_dir / "tests").mkdir() + (uv_init_dir / "tests" / "conftest.py").touch() + + with change_cwd(uv_init_dir), files_manager(): + PyprojectTOMLManager()[["project"]]["requires-python"] = ( + ">=3.12,<3.14" + ) + + # Act + use_ci_bitbucket() + + # Assert + contents = (uv_init_dir / "bitbucket-pipelines.yml").read_text() + assert "Test on 3.12" in contents + assert "Test on 3.13" in contents + + def test_matrix_disabled_creates_single_step( + self, uv_init_dir: Path, monkeypatch: pytest.MonkeyPatch + ): + """Test that --no-matrix-python creates only one test step for current version.""" + # Arrange + monkeypatch.setattr( + usethis._integrations.python.version, + "get_python_version", + lambda: "3.10.0", + ) + (uv_init_dir / "tests").mkdir() + (uv_init_dir / "tests" / "conftest.py").touch() + + with ( + change_cwd(uv_init_dir), + files_manager(), + usethis_config.set(matrix_python=False), + ): + PyprojectTOMLManager()[["project"]]["requires-python"] = ( + ">=3.12,<3.14" + ) + + # Act + use_ci_bitbucket() + + # Assert + contents = (uv_init_dir / "bitbucket-pipelines.yml").read_text() + # Should only have one test step for the current development version (3.10) + assert "Test on 3.10" in contents + # Should NOT have other versions + assert "Test on 3.12" not in contents + assert "Test on 3.13" not in contents + + def test_matrix_disabled_with_none_backend( + self, bare_dir: Path, monkeypatch: pytest.MonkeyPatch + ): + """Test that --no-matrix-python works with backend=none.""" + # Arrange + monkeypatch.setattr( + usethis._integrations.python.version, + "get_python_version", + lambda: "3.11.0", + ) + (bare_dir / "tests").mkdir() + (bare_dir / "tests" / "conftest.py").touch() + + with ( + change_cwd(bare_dir), + files_manager(), + usethis_config.set(backend=BackendEnum.none, matrix_python=False), + ): + # Act + use_ci_bitbucket() + + # Assert + contents = (bare_dir / "bitbucket-pipelines.yml").read_text() + # Should have one test step for Python 3.11 + assert "Test on 3.11" in contents + # Should use image instead of uv + assert "image: python:3.11" in contents diff --git a/tests/usethis/_ui/interface/test_interface_ci.py b/tests/usethis/_ui/interface/test_interface_ci.py index 47f29441..469344c8 100644 --- a/tests/usethis/_ui/interface/test_interface_ci.py +++ b/tests/usethis/_ui/interface/test_interface_ci.py @@ -158,3 +158,54 @@ def test_import_pipeline_error_handled(self, tmp_path: Path): # Assert - error should be caught and handled, not propagate as unhandled exception assert result.exit_code == 1, result.output assert "import pipeline" in result.output.lower() + + def test_no_matrix_python_flag( + self, uv_init_dir: Path, monkeypatch: pytest.MonkeyPatch + ): + """Test that --no-matrix-python flag creates single test step via CLI.""" + # Arrange + monkeypatch.setattr( + usethis._integrations.python.version, + "get_python_version", + lambda: "3.10.0", + ) + (uv_init_dir / "tests").mkdir() + (uv_init_dir / "tests" / "conftest.py").touch() + + with PyprojectTOMLManager() as mgr: + mgr[["project"]]["requires-python"] = ">=3.12,<3.14" + + # Act + runner = CliRunner() + with change_cwd(uv_init_dir): + result = runner.invoke_safe(app, ["--no-matrix-python"]) + + # Assert + assert result.exit_code == 0, result.output + contents = (uv_init_dir / "bitbucket-pipelines.yml").read_text() + # Should only have one test step for the current development version (3.10) + assert "Test on 3.10" in contents + # Should NOT have other versions + assert "Test on 3.12" not in contents + assert "Test on 3.13" not in contents + + def test_matrix_python_flag_enabled_default(self, uv_init_dir: Path): + """Test that --matrix-python (default) creates multiple test steps via CLI.""" + # Arrange + (uv_init_dir / "tests").mkdir() + (uv_init_dir / "tests" / "conftest.py").touch() + + with PyprojectTOMLManager() as mgr: + mgr[["project"]]["requires-python"] = ">=3.12,<3.14" + + # Act + runner = CliRunner() + with change_cwd(uv_init_dir): + result = runner.invoke_safe(app, ["--matrix-python"]) + + # Assert + assert result.exit_code == 0, result.output + contents = (uv_init_dir / "bitbucket-pipelines.yml").read_text() + # Should have multiple test steps + assert "Test on 3.12" in contents + assert "Test on 3.13" in contents From 552f7868b24854a105e83ed53ad7e8c295d283a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:57:27 +0000 Subject: [PATCH 03/10] Remove accidentally committed bitbucket-pipelines.yml and update .gitignore Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> --- .gitignore | 5 +++- bitbucket-pipelines.yml | 56 ----------------------------------------- 2 files changed, 4 insertions(+), 57 deletions(-) delete mode 100644 bitbucket-pipelines.yml diff --git a/.gitignore b/.gitignore index 3122d6c2..1cd62e09 100644 --- a/.gitignore +++ b/.gitignore @@ -162,4 +162,7 @@ cython_debug/ #.idea/ # Windsurf config -.windsurfrules \ No newline at end of file +.windsurfrules + +# Bitbucket Pipelines config (can be generated by usethis) +bitbucket-pipelines.yml diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml deleted file mode 100644 index 9b9d4d1d..00000000 --- a/bitbucket-pipelines.yml +++ /dev/null @@ -1,56 +0,0 @@ -image: atlassian/default-image:3 -definitions: - caches: - uv: ~/.cache/uv - pre-commit: ~/.cache/pre-commit - script_items: - - &install-uv | - curl -LsSf https://astral.sh/uv/install.sh | sh - source $HOME/.local/bin/env - export UV_LINK_MODE=copy - uv --version -pipelines: - default: - - step: - name: Run pre-commit - caches: - - uv - - pre-commit - script: - - *install-uv - - uv run pre-commit run --all-files - - step: - name: Test on 3.10 - caches: - - uv - script: - - *install-uv - - uv run --python 3.10 pytest -x --junitxml=test-reports/report.xml - - step: - name: Test on 3.11 - caches: - - uv - script: - - *install-uv - - uv run --python 3.11 pytest -x --junitxml=test-reports/report.xml - - step: - name: Test on 3.12 - caches: - - uv - script: - - *install-uv - - uv run --python 3.12 pytest -x --junitxml=test-reports/report.xml - - step: - name: Test on 3.13 - caches: - - uv - script: - - *install-uv - - uv run --python 3.13 pytest -x --junitxml=test-reports/report.xml - - step: - name: Test on 3.14 - caches: - - uv - script: - - *install-uv - - uv run --python 3.14 pytest -x --junitxml=test-reports/report.xml From 800216543056e17f0b10179ece686dc1347d66bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:59:23 +0000 Subject: [PATCH 04/10] Fix linting issues (add missing import, noqa annotations) Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> --- src/usethis/_config.py | 2 +- src/usethis/_integrations/environ/python.py | 2 ++ src/usethis/_ui/interface/ci.py | 2 +- tests/usethis/_core/test_core_ci.py | 8 ++------ 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/usethis/_config.py b/src/usethis/_config.py index 43dba889..a3125477 100644 --- a/src/usethis/_config.py +++ b/src/usethis/_config.py @@ -55,7 +55,7 @@ class UsethisConfig: matrix_python: bool = MATRIX_PYTHON_DEFAULT @contextmanager - def set( # noqa: PLR0913 + def set( # noqa: PLR0913, PLR0915 self, *, offline: bool | None = None, diff --git a/src/usethis/_integrations/environ/python.py b/src/usethis/_integrations/environ/python.py index f451e746..05ee9723 100644 --- a/src/usethis/_integrations/environ/python.py +++ b/src/usethis/_integrations/environ/python.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing_extensions import assert_never + from usethis._config import usethis_config from usethis._integrations.backend.dispatch import get_backend from usethis._integrations.backend.uv.python import ( diff --git a/src/usethis/_ui/interface/ci.py b/src/usethis/_ui/interface/ci.py index b1bd0ef7..4ad58e7a 100644 --- a/src/usethis/_ui/interface/ci.py +++ b/src/usethis/_ui/interface/ci.py @@ -9,7 +9,7 @@ @app.command(help="Use Bitbucket Pipelines for CI.") -def bitbucket( +def bitbucket( # noqa: PLR0913 remove: bool = typer.Option( False, "--remove", help="Remove Bitbucket Pipelines CI instead of adding it." ), diff --git a/tests/usethis/_core/test_core_ci.py b/tests/usethis/_core/test_core_ci.py index e84062a1..31be0545 100644 --- a/tests/usethis/_core/test_core_ci.py +++ b/tests/usethis/_core/test_core_ci.py @@ -594,9 +594,7 @@ def test_matrix_enabled_by_default(self, uv_init_dir: Path): (uv_init_dir / "tests" / "conftest.py").touch() with change_cwd(uv_init_dir), files_manager(): - PyprojectTOMLManager()[["project"]]["requires-python"] = ( - ">=3.12,<3.14" - ) + PyprojectTOMLManager()[["project"]]["requires-python"] = ">=3.12,<3.14" # Act use_ci_bitbucket() @@ -624,9 +622,7 @@ def test_matrix_disabled_creates_single_step( files_manager(), usethis_config.set(matrix_python=False), ): - PyprojectTOMLManager()[["project"]]["requires-python"] = ( - ">=3.12,<3.14" - ) + PyprojectTOMLManager()[["project"]]["requires-python"] = ">=3.12,<3.14" # Act use_ci_bitbucket() From e33c0720cc9a2fec136102e17e2b3faaf65c5fe3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 12:29:01 +0000 Subject: [PATCH 05/10] Refactor to pass matrix_python as parameter instead of using global config Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> --- .gitignore | 5 +--- src/usethis/_config.py | 12 +------- src/usethis/_core/ci.py | 6 ++-- src/usethis/_integrations/environ/python.py | 5 ---- src/usethis/_tool/impl/pytest.py | 31 ++++++++++++++++----- src/usethis/_ui/interface/ci.py | 5 ++-- tests/usethis/_core/test_core_ci.py | 7 ++--- 7 files changed, 35 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 1cd62e09..3122d6c2 100644 --- a/.gitignore +++ b/.gitignore @@ -162,7 +162,4 @@ cython_debug/ #.idea/ # Windsurf config -.windsurfrules - -# Bitbucket Pipelines config (can be generated by usethis) -bitbucket-pipelines.yml +.windsurfrules \ No newline at end of file diff --git a/src/usethis/_config.py b/src/usethis/_config.py index a3125477..5b1fefb7 100644 --- a/src/usethis/_config.py +++ b/src/usethis/_config.py @@ -17,7 +17,6 @@ OFFLINE_DEFAULT = False QUIET_DEFAULT = False BACKEND_DEFAULT = "auto" -MATRIX_PYTHON_DEFAULT = True @dataclass @@ -39,8 +38,6 @@ class UsethisConfig: subprocess_verbose: Verbose output for subprocesses. force_project_dir: Directory for the project. If None, defaults to the current working directory dynamically determined at runtime. - matrix_python: Whether to use a Python version matrix for CI tests. When False, - only the current development Python version is used. """ offline: bool = OFFLINE_DEFAULT @@ -52,10 +49,9 @@ class UsethisConfig: disable_pre_commit: bool = False subprocess_verbose: bool = False project_dir: Path | None = None - matrix_python: bool = MATRIX_PYTHON_DEFAULT @contextmanager - def set( # noqa: PLR0913, PLR0915 + def set( # noqa: PLR0913 self, *, offline: bool | None = None, @@ -67,7 +63,6 @@ def set( # noqa: PLR0913, PLR0915 disable_pre_commit: bool | None = None, subprocess_verbose: bool | None = None, project_dir: Path | str | None = None, - matrix_python: bool | None = None, ) -> Generator[None, None, None]: """Temporarily change command options.""" old_offline = self.offline @@ -79,7 +74,6 @@ def set( # noqa: PLR0913, PLR0915 old_disable_pre_commit = self.disable_pre_commit old_subprocess_verbose = self.subprocess_verbose old_project_dir = self.project_dir - old_matrix_python = self.matrix_python if offline is None: offline = old_offline @@ -99,8 +93,6 @@ def set( # noqa: PLR0913, PLR0915 subprocess_verbose = old_subprocess_verbose if project_dir is None: project_dir = old_project_dir - if matrix_python is None: - matrix_python = old_matrix_python self.offline = offline self.quiet = quiet @@ -113,7 +105,6 @@ def set( # noqa: PLR0913, PLR0915 if isinstance(project_dir, str): project_dir = Path(project_dir) self.project_dir = project_dir - self.matrix_python = matrix_python yield self.offline = old_offline self.quiet = old_quiet @@ -124,7 +115,6 @@ def set( # noqa: PLR0913, PLR0915 self.disable_pre_commit = old_disable_pre_commit self.subprocess_verbose = old_subprocess_verbose self.project_dir = old_project_dir - self.matrix_python = old_matrix_python def cpd(self) -> Path: """Return the current project directory.""" diff --git a/src/usethis/_core/ci.py b/src/usethis/_core/ci.py index b3a28ad9..bb20ca2c 100644 --- a/src/usethis/_core/ci.py +++ b/src/usethis/_core/ci.py @@ -30,7 +30,9 @@ ] -def use_ci_bitbucket(*, remove: bool = False, how: bool = False) -> None: +def use_ci_bitbucket( + *, remove: bool = False, how: bool = False, matrix_python: bool = True +) -> None: if how: print_how_to_use_ci_bitbucket() return @@ -49,7 +51,7 @@ def use_ci_bitbucket(*, remove: bool = False, how: bool = False) -> None: for tool in _CI_QA_TOOLS: tool().update_bitbucket_steps() - PytestTool().update_bitbucket_steps() + PytestTool().update_bitbucket_steps(matrix_python=matrix_python) print_how_to_use_ci_bitbucket() else: diff --git a/src/usethis/_integrations/environ/python.py b/src/usethis/_integrations/environ/python.py index 05ee9723..e47fec9c 100644 --- a/src/usethis/_integrations/environ/python.py +++ b/src/usethis/_integrations/environ/python.py @@ -2,7 +2,6 @@ from typing_extensions import assert_never -from usethis._config import usethis_config from usethis._integrations.backend.dispatch import get_backend from usethis._integrations.backend.uv.python import ( get_supported_uv_major_python_versions, @@ -12,10 +11,6 @@ def get_supported_major_python_versions() -> list[int]: - # If matrix_python is disabled, return only the current development version - if not usethis_config.matrix_python: - return [get_python_major_version()] - backend = get_backend() if backend is BackendEnum.uv: diff --git a/src/usethis/_tool/impl/pytest.py b/src/usethis/_tool/impl/pytest.py index 040578d2..c267a759 100644 --- a/src/usethis/_tool/impl/pytest.py +++ b/src/usethis/_tool/impl/pytest.py @@ -17,9 +17,15 @@ from usethis._integrations.ci.bitbucket.schema import Image, ImageName from usethis._integrations.ci.bitbucket.schema import Script as BitbucketScript from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep -from usethis._integrations.ci.bitbucket.steps import get_steps_in_default +from usethis._integrations.ci.bitbucket.steps import ( + add_bitbucket_step_in_default, + bitbucket_steps_are_equivalent, + get_steps_in_default, + remove_bitbucket_step_from_default, +) from usethis._integrations.ci.bitbucket.used import is_bitbucket_used from usethis._integrations.environ.python import get_supported_major_python_versions +from usethis._integrations.python.version import get_python_major_version from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager from usethis._integrations.project.build import has_pyproject_toml_declared_build_system @@ -214,8 +220,11 @@ def get_active_config_file_managers(self) -> set[KeyValueFileManager]: raise NotImplementedError(msg) return {preferred_file_manager} - def get_bitbucket_steps(self) -> list[BitbucketStep]: - versions = get_supported_major_python_versions() + def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketStep]: + if matrix_python: + versions = get_supported_major_python_versions() + else: + versions = [get_python_major_version()] backend = get_backend() @@ -264,7 +273,7 @@ def get_managed_bitbucket_step_names(self) -> list[str]: return sorted(names) - def update_bitbucket_steps(self) -> None: + def update_bitbucket_steps(self, *, matrix_python: bool = True) -> None: """Update the pytest-related Bitbucket Pipelines steps. A bespoke function is needed here to ensure we inform the user about the need @@ -274,9 +283,17 @@ def update_bitbucket_steps(self) -> None: if not is_bitbucket_used() or not self.is_used(): return - # But otherwise if not early exiting, we are going to add steps so we might - # need to inform the user - super().update_bitbucket_steps() + # Add the new steps + for step in self.get_bitbucket_steps(matrix_python=matrix_python): + add_bitbucket_step_in_default(step) + + # Remove any old steps that are not active managed by this tool + for step in get_steps_in_default(): + if step.name in self.get_managed_bitbucket_step_names() and not any( + bitbucket_steps_are_equivalent(step, step_) + for step_ in self.get_bitbucket_steps(matrix_python=matrix_python) + ): + remove_bitbucket_step_from_default(step) backend = get_backend() diff --git a/src/usethis/_ui/interface/ci.py b/src/usethis/_ui/interface/ci.py index 4ad58e7a..0484ba22 100644 --- a/src/usethis/_ui/interface/ci.py +++ b/src/usethis/_ui/interface/ci.py @@ -16,7 +16,7 @@ def bitbucket( # noqa: PLR0913 matrix_python: bool = typer.Option( True, "--matrix-python/--no-matrix-python", - help="Test against multiple Python versions (matrix) or just the current development version.", + help="Test against multiple Python versions.", ), offline: bool = offline_opt, quiet: bool = quiet_opt, @@ -37,12 +37,11 @@ def bitbucket( # noqa: PLR0913 quiet=quiet, frozen=frozen, backend=backend, - matrix_python=matrix_python, ), files_manager(), ): try: - use_ci_bitbucket(remove=remove) + use_ci_bitbucket(remove=remove, matrix_python=matrix_python) except UsethisError as err: err_print(err) raise typer.Exit(code=1) from None diff --git a/tests/usethis/_core/test_core_ci.py b/tests/usethis/_core/test_core_ci.py index 31be0545..356c2741 100644 --- a/tests/usethis/_core/test_core_ci.py +++ b/tests/usethis/_core/test_core_ci.py @@ -620,12 +620,11 @@ def test_matrix_disabled_creates_single_step( with ( change_cwd(uv_init_dir), files_manager(), - usethis_config.set(matrix_python=False), ): PyprojectTOMLManager()[["project"]]["requires-python"] = ">=3.12,<3.14" # Act - use_ci_bitbucket() + use_ci_bitbucket(matrix_python=False) # Assert contents = (uv_init_dir / "bitbucket-pipelines.yml").read_text() @@ -651,10 +650,10 @@ def test_matrix_disabled_with_none_backend( with ( change_cwd(bare_dir), files_manager(), - usethis_config.set(backend=BackendEnum.none, matrix_python=False), + usethis_config.set(backend=BackendEnum.none), ): # Act - use_ci_bitbucket() + use_ci_bitbucket(matrix_python=False) # Assert contents = (bare_dir / "bitbucket-pipelines.yml").read_text() From 53f862055bf9c92da22670d96c8e6186381c8daa Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Thu, 6 Nov 2025 08:35:05 +1300 Subject: [PATCH 06/10] Run Ruff formatter --- src/usethis/_tool/impl/pytest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usethis/_tool/impl/pytest.py b/src/usethis/_tool/impl/pytest.py index c267a759..d06077cb 100644 --- a/src/usethis/_tool/impl/pytest.py +++ b/src/usethis/_tool/impl/pytest.py @@ -25,11 +25,11 @@ ) from usethis._integrations.ci.bitbucket.used import is_bitbucket_used from usethis._integrations.environ.python import get_supported_major_python_versions -from usethis._integrations.python.version import get_python_major_version from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager from usethis._integrations.project.build import has_pyproject_toml_declared_build_system from usethis._integrations.project.layout import get_source_dir_str +from usethis._integrations.python.version import get_python_major_version from usethis._tool.base import Tool from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec from usethis._tool.rule import RuleConfig From f5d4b71862dbfcbc9caf0662a72d1496f7e16e79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 19:55:37 +0000 Subject: [PATCH 07/10] Refactor: use super().update_bitbucket_steps() with kwargs in base class Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> --- src/usethis/_tool/base.py | 9 ++++++--- src/usethis/_tool/impl/pytest.py | 21 ++++----------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/usethis/_tool/base.py b/src/usethis/_tool/base.py index 8ec45669..f3b68bdd 100644 --- a/src/usethis/_tool/base.py +++ b/src/usethis/_tool/base.py @@ -564,23 +564,26 @@ def remove_bitbucket_steps(self) -> None: if step.name in self.get_managed_bitbucket_step_names(): remove_bitbucket_step_from_default(step) - def update_bitbucket_steps(self) -> None: + def update_bitbucket_steps(self, **kwargs) -> None: """Add Bitbucket steps associated with this tool, and remove outdated ones. Only runs if Bitbucket is used in the project. + + Args: + **kwargs: Additional keyword arguments to pass to get_bitbucket_steps(). """ if not is_bitbucket_used() or not self.is_used(): return # Add the new steps - for step in self.get_bitbucket_steps(): + for step in self.get_bitbucket_steps(**kwargs): add_bitbucket_step_in_default(step) # Remove any old steps that are not active managed by this tool for step in get_steps_in_default(): if step.name in self.get_managed_bitbucket_step_names() and not any( bitbucket_steps_are_equivalent(step, step_) - for step_ in self.get_bitbucket_steps() + for step_ in self.get_bitbucket_steps(**kwargs) ): remove_bitbucket_step_from_default(step) diff --git a/src/usethis/_tool/impl/pytest.py b/src/usethis/_tool/impl/pytest.py index d06077cb..fb77faae 100644 --- a/src/usethis/_tool/impl/pytest.py +++ b/src/usethis/_tool/impl/pytest.py @@ -17,12 +17,7 @@ from usethis._integrations.ci.bitbucket.schema import Image, ImageName from usethis._integrations.ci.bitbucket.schema import Script as BitbucketScript from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep -from usethis._integrations.ci.bitbucket.steps import ( - add_bitbucket_step_in_default, - bitbucket_steps_are_equivalent, - get_steps_in_default, - remove_bitbucket_step_from_default, -) +from usethis._integrations.ci.bitbucket.steps import get_steps_in_default from usethis._integrations.ci.bitbucket.used import is_bitbucket_used from usethis._integrations.environ.python import get_supported_major_python_versions from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager @@ -283,17 +278,9 @@ def update_bitbucket_steps(self, *, matrix_python: bool = True) -> None: if not is_bitbucket_used() or not self.is_used(): return - # Add the new steps - for step in self.get_bitbucket_steps(matrix_python=matrix_python): - add_bitbucket_step_in_default(step) - - # Remove any old steps that are not active managed by this tool - for step in get_steps_in_default(): - if step.name in self.get_managed_bitbucket_step_names() and not any( - bitbucket_steps_are_equivalent(step, step_) - for step_ in self.get_bitbucket_steps(matrix_python=matrix_python) - ): - remove_bitbucket_step_from_default(step) + # But otherwise if not early exiting, we are going to add steps so we might + # need to inform the user + super().update_bitbucket_steps(matrix_python=matrix_python) backend = get_backend() From 89ef4d4e72cc4306d1971db80de7e62ea9a7e624 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:15:10 +0000 Subject: [PATCH 08/10] Use explicit matrix_python parameter instead of **kwargs Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> --- src/usethis/_tool/base.py | 18 ++++++++++++------ src/usethis/_tool/impl/codespell.py | 2 +- src/usethis/_tool/impl/deptry.py | 2 +- src/usethis/_tool/impl/import_linter.py | 2 +- src/usethis/_tool/impl/pre_commit.py | 2 +- src/usethis/_tool/impl/pyproject_fmt.py | 2 +- src/usethis/_tool/impl/ruff.py | 2 +- 7 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/usethis/_tool/base.py b/src/usethis/_tool/base.py index f3b68bdd..574112a1 100644 --- a/src/usethis/_tool/base.py +++ b/src/usethis/_tool/base.py @@ -544,8 +544,13 @@ def get_install_method(self) -> Literal["pre-commit", "devdep"] | None: return "pre-commit" return None - def get_bitbucket_steps(self) -> list[BitbucketStep]: - """Get the Bitbucket pipeline step associated with this tool.""" + def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketStep]: + """Get the Bitbucket pipeline step associated with this tool. + + Args: + matrix_python: Whether to use a Python version matrix. When False, + only the current development version is used. + """ return [] def get_managed_bitbucket_step_names(self) -> list[str]: @@ -564,26 +569,27 @@ def remove_bitbucket_steps(self) -> None: if step.name in self.get_managed_bitbucket_step_names(): remove_bitbucket_step_from_default(step) - def update_bitbucket_steps(self, **kwargs) -> None: + def update_bitbucket_steps(self, *, matrix_python: bool = True) -> None: """Add Bitbucket steps associated with this tool, and remove outdated ones. Only runs if Bitbucket is used in the project. Args: - **kwargs: Additional keyword arguments to pass to get_bitbucket_steps(). + matrix_python: Whether to use a Python version matrix. When False, + only the current development version is used. """ if not is_bitbucket_used() or not self.is_used(): return # Add the new steps - for step in self.get_bitbucket_steps(**kwargs): + for step in self.get_bitbucket_steps(matrix_python=matrix_python): add_bitbucket_step_in_default(step) # Remove any old steps that are not active managed by this tool for step in get_steps_in_default(): if step.name in self.get_managed_bitbucket_step_names() and not any( bitbucket_steps_are_equivalent(step, step_) - for step_ in self.get_bitbucket_steps(**kwargs) + for step_ in self.get_bitbucket_steps(matrix_python=matrix_python) ): remove_bitbucket_step_from_default(step) diff --git a/src/usethis/_tool/impl/codespell.py b/src/usethis/_tool/impl/codespell.py index 045880fa..2d603b1f 100644 --- a/src/usethis/_tool/impl/codespell.py +++ b/src/usethis/_tool/impl/codespell.py @@ -137,7 +137,7 @@ def get_pre_commit_config(self) -> PreCommitConfig: requires_venv=False, ) - def get_bitbucket_steps(self) -> list[BitbucketStep]: + def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketStep]: backend = get_backend() if backend is BackendEnum.uv: diff --git a/src/usethis/_tool/impl/deptry.py b/src/usethis/_tool/impl/deptry.py index 0fbf6d4b..a81e299c 100644 --- a/src/usethis/_tool/impl/deptry.py +++ b/src/usethis/_tool/impl/deptry.py @@ -129,7 +129,7 @@ def get_pre_commit_config(self) -> PreCommitConfig: else: assert_never(backend) - def get_bitbucket_steps(self) -> list[BitbucketStep]: + def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketStep]: backend = get_backend() _dir = get_source_dir_str() diff --git a/src/usethis/_tool/impl/import_linter.py b/src/usethis/_tool/impl/import_linter.py index 9a1612af..c8552182 100644 --- a/src/usethis/_tool/impl/import_linter.py +++ b/src/usethis/_tool/impl/import_linter.py @@ -365,7 +365,7 @@ def get_pre_commit_config(self) -> PreCommitConfig: def get_managed_files(self) -> list[Path]: return [Path(".importlinter")] - def get_bitbucket_steps(self) -> list[BitbucketStep]: + def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketStep]: backend = get_backend() if backend is BackendEnum.uv: diff --git a/src/usethis/_tool/impl/pre_commit.py b/src/usethis/_tool/impl/pre_commit.py index 490772d4..c0658900 100644 --- a/src/usethis/_tool/impl/pre_commit.py +++ b/src/usethis/_tool/impl/pre_commit.py @@ -68,7 +68,7 @@ def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]: def get_managed_files(self) -> list[Path]: return [Path(".pre-commit-config.yaml")] - def get_bitbucket_steps(self) -> list[BitbucketStep]: + def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketStep]: backend = get_backend() if backend is BackendEnum.uv: diff --git a/src/usethis/_tool/impl/pyproject_fmt.py b/src/usethis/_tool/impl/pyproject_fmt.py index c621dbfa..224d43d1 100644 --- a/src/usethis/_tool/impl/pyproject_fmt.py +++ b/src/usethis/_tool/impl/pyproject_fmt.py @@ -92,7 +92,7 @@ def get_pre_commit_config(self) -> PreCommitConfig: requires_venv=False, ) - def get_bitbucket_steps(self) -> list[BitbucketStep]: + def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketStep]: backend = get_backend() if backend is BackendEnum.uv: diff --git a/src/usethis/_tool/impl/ruff.py b/src/usethis/_tool/impl/ruff.py index 6f479800..55ecf151 100644 --- a/src/usethis/_tool/impl/ruff.py +++ b/src/usethis/_tool/impl/ruff.py @@ -266,7 +266,7 @@ def get_pre_commit_config(self) -> PreCommitConfig: inform_how_to_use_on_migrate=True, # The pre-commit commands are not simpler than the venv-based commands ) - def get_bitbucket_steps(self) -> list[BitbucketStep]: + def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketStep]: backend = get_backend() steps = [] From 5a71451e7f260826bb68c8ddaad4e119bf87598e Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Thu, 6 Nov 2025 09:23:22 +1300 Subject: [PATCH 09/10] Format indentation in docstring --- src/usethis/_tool/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usethis/_tool/base.py b/src/usethis/_tool/base.py index 292b547f..46432a23 100644 --- a/src/usethis/_tool/base.py +++ b/src/usethis/_tool/base.py @@ -579,7 +579,7 @@ def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketSt Args: matrix_python: Whether to use a Python version matrix. When False, - only the current development version is used. + only the current development version is used. """ try: cmd = self.default_command() From ceeecdf98ffca5a708a01581bb2fa2275d821bd4 Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Thu, 6 Nov 2025 09:26:32 +1300 Subject: [PATCH 10/10] Ruf ruff formatter --- src/usethis/_tool/base.py | 4 ++-- src/usethis/_ui/interface/ci.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/usethis/_tool/base.py b/src/usethis/_tool/base.py index 46432a23..2ffa9640 100644 --- a/src/usethis/_tool/base.py +++ b/src/usethis/_tool/base.py @@ -576,7 +576,7 @@ def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketSt By default, this creates a single step using the tool's default_command(). Tools can override this method for more complex step requirements (e.g., pytest with multiple Python versions, or Ruff with separate linter/formatter steps). - + Args: matrix_python: Whether to use a Python version matrix. When False, only the current development version is used. @@ -635,7 +635,7 @@ def update_bitbucket_steps(self, *, matrix_python: bool = True) -> None: """Add Bitbucket steps associated with this tool, and remove outdated ones. Only runs if Bitbucket is used in the project. - + Args: matrix_python: Whether to use a Python version matrix. When False, only the current development version is used. diff --git a/src/usethis/_ui/interface/ci.py b/src/usethis/_ui/interface/ci.py index f4de2673..d3943403 100644 --- a/src/usethis/_ui/interface/ci.py +++ b/src/usethis/_ui/interface/ci.py @@ -10,7 +10,7 @@ @app.command(help="Use Bitbucket Pipelines for CI.") -def bitbucket( # noqa: PLR0913 +def bitbucket( remove: bool = typer.Option( False, "--remove", help="Remove Bitbucket Pipelines CI instead of adding it." ),