Skip to content
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
6 changes: 4 additions & 2 deletions src/usethis/_core/ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
16 changes: 12 additions & 4 deletions src/usethis/_tool/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,12 +570,16 @@ def get_install_method(self) -> Literal["pre-commit", "devdep"] | None:
return "pre-commit"
return None

def get_bitbucket_steps(self) -> list[BitbucketStep]:
def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketStep]:
"""Get the Bitbucket pipeline step associated with this tool.

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.
"""
try:
cmd = self.default_command()
Expand Down Expand Up @@ -627,23 +631,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) -> 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:
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():
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()
for step_ in self.get_bitbucket_steps(matrix_python=matrix_python)
):
remove_bitbucket_step_from_default(step)

Expand Down
2 changes: 1 addition & 1 deletion src/usethis/_tool/impl/pre_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 8 additions & 4 deletions src/usethis/_tool/impl/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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
Expand Down Expand Up @@ -222,8 +223,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()

Expand Down Expand Up @@ -272,7 +276,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
Expand All @@ -284,7 +288,7 @@ def update_bitbucket_steps(self) -> None:

# But otherwise if not early exiting, we are going to add steps so we might
# need to inform the user
super().update_bitbucket_steps()
super().update_bitbucket_steps(matrix_python=matrix_python)

backend = get_backend()

Expand Down
2 changes: 1 addition & 1 deletion src/usethis/_tool/impl/ruff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
12 changes: 10 additions & 2 deletions src/usethis/_ui/interface/ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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.",
),
offline: bool = offline_opt,
quiet: bool = quiet_opt,
frozen: bool = frozen_opt,
Expand All @@ -28,12 +33,15 @@ def bitbucket(

with (
usethis_config.set(
offline=offline, quiet=quiet, frozen=frozen, backend=backend
offline=offline,
quiet=quiet,
frozen=frozen,
backend=backend,
),
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
77 changes: 77 additions & 0 deletions tests/usethis/_core/test_core_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -584,3 +585,79 @@ 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(),
):
PyprojectTOMLManager()[["project"]]["requires-python"] = ">=3.12,<3.14"

# Act
use_ci_bitbucket(matrix_python=False)

# 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),
):
# Act
use_ci_bitbucket(matrix_python=False)

# 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
51 changes: 51 additions & 0 deletions tests/usethis/_ui/interface/test_interface_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -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