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
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,9 @@ Tool implementations are defined in classes in the `usethis._tool.impl` module.
- Declare this new submodule in the `.importlinter` configuration to architecturally describe its dependency relationships with other tools' submodules. For example, does your tool integrate with pre-commit? It should be in a higher layer module than the `pre-commit` submodule.
- Define a `usethis._tool.base.ToolSpec` subclass, e.g. for a tool named Xyz, define a class `XyzToolSpec(ToolSpec)`.
- Start by implementing its `name` property method, then work through the other methods. Most method have default implementations, but even in those cases you will need to consider them individually and determine an appropriate implementation. For example, methods which specify the tool's dependencies default to empty dependencies, but you shouldn't rely on this.
- Mark all methods in your `ToolSpec` subclass with the `@typing.final` decorator. This prevents the methods from being accidentally overridden in the `Tool` subclass.
- Then, define a subclass of the `ToolSpec` subclass you just created, which also subclasses `usethis._tool.base.Tool`, e.g. for a tool named Xyz, define a class `XyzTool(XyzToolSpec, Tool)`. The only method this usually requires a non-default implementation for is `config_spec` to specify which configuration sections should be set up for the tool (and which sections the tool manages). However, you may find it helpful to provide custom implementations for other methods as well, e.g. `print_how_to_use`.
- Include a comment with a URL linking to the tool's source repo for reference.
- Mark all methods in your `Tool` subclass with `@final` as well, to prevent further subclassing from overriding them.

#### Register your `Tool` subclass

Expand Down
3 changes: 3 additions & 0 deletions src/usethis/_tool/impl/base/codespell.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

from typing import final

from usethis._console import how_print
from usethis._tool.base import Tool
from usethis._tool.impl.spec.codespell import CodespellToolSpec


class CodespellTool(CodespellToolSpec, Tool):
@final
def print_how_to_use(self) -> None:
how_print(f"Run '{self.how_to_use_cmd()}' to run the {self.name} spellchecker.")
4 changes: 4 additions & 0 deletions src/usethis/_tool/impl/base/coverage_py.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import final

from typing_extensions import assert_never

from usethis._backend.dispatch import get_backend
Expand All @@ -12,6 +14,7 @@


class CoveragePyTool(CoveragePyToolSpec, Tool):
@final
def test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
from usethis._tool.impl.base.pytest import ( # to avoid circularity; # noqa: PLC0415
PytestTool,
Expand All @@ -22,6 +25,7 @@ def test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
deps += [Dependency(name="pytest-cov")]
return deps

@final
def print_how_to_use(self) -> None:
from usethis._tool.impl.base.pytest import ( # to avoid circularity; # noqa: PLC0415
PytestTool,
Expand Down
7 changes: 6 additions & 1 deletion src/usethis/_tool/impl/base/deptry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, final

from usethis._console import info_print
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
Expand All @@ -13,12 +13,14 @@


class DeptryTool(DeptryToolSpec, Tool):
@final
def select_rules(self, rules: list[Rule]) -> bool:
"""Does nothing for deptry - all rules are automatically enabled by default."""
if rules:
info_print(f"All {self.name} rules are always implicitly selected.")
return False

@final
def selected_rules(self) -> list[Rule]:
"""No notion of selection for deptry.

Expand All @@ -27,10 +29,12 @@ def selected_rules(self) -> list[Rule]:
"""
return []

@final
def deselect_rules(self, rules: list[Rule]) -> bool:
"""Does nothing for deptry - all rules are automatically enabled by default."""
return False

@final
def ignored_rules(self) -> list[Rule]:
(file_manager,) = self.get_active_config_file_managers()
keys = self._get_ignore_keys(file_manager)
Expand All @@ -41,6 +45,7 @@ def ignored_rules(self) -> list[Rule]:

return rules

@final
def _get_ignore_keys(self, file_manager: KeyValueFileManager[object]) -> list[str]:
"""Get the keys for the ignored rules in the given file manager."""
if isinstance(file_manager, PyprojectTOMLManager):
Expand Down
4 changes: 3 additions & 1 deletion src/usethis/_tool/impl/base/import_linter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, final

from usethis._config import usethis_config
from usethis._console import info_print
Expand All @@ -13,13 +13,15 @@


class ImportLinterTool(ImportLinterToolSpec, Tool):
@final
def is_used(self) -> bool:
"""Check if the Import Linter tool is used in the project."""
# We suppress the warning about assumptions regarding the package name.
# See _importlinter_warn_no_packages_found
with usethis_config.set(quiet=True):
return super().is_used()

@final
def print_how_to_use(self) -> None:
if not _is_inp_rule_selected():
# If Ruff is used, we enable the INP rules instead.
Expand Down
3 changes: 3 additions & 0 deletions src/usethis/_tool/impl/base/mkdocs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import final

from typing_extensions import assert_never

from usethis._backend.dispatch import get_backend
Expand All @@ -11,6 +13,7 @@


class MkDocsTool(MkDocsToolSpec, Tool):
@final
def print_how_to_use(self) -> None:
backend = get_backend()
if backend is BackendEnum.uv and is_uv_used():
Expand Down
8 changes: 7 additions & 1 deletion src/usethis/_tool/impl/base/pre_commit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, final

from typing_extensions import assert_never

Expand All @@ -20,12 +20,15 @@


class PreCommitTool(PreCommitToolSpec, Tool):
@final
def is_used(self) -> bool:
return is_pre_commit_used()

@final
def print_how_to_use(self) -> None:
how_print(f"Run '{self.how_to_use_cmd()}' to run the hooks manually.")

@final
def get_bitbucket_steps(
self,
*,
Expand Down Expand Up @@ -63,6 +66,7 @@ def get_bitbucket_steps(
else:
assert_never(backend)

@final
def update_bitbucket_steps(self, *, matrix_python: bool = True) -> None:
"""Add Bitbucket steps associated with pre-commit, and remove outdated ones.

Expand All @@ -74,8 +78,10 @@ def update_bitbucket_steps(self, *, matrix_python: bool = True) -> None:
"""
self._unconditional_update_bitbucket_steps(matrix_python=matrix_python)

@final
def migrate_config_to_pre_commit(self) -> None:
pass

@final
def migrate_config_from_pre_commit(self) -> None:
pass
4 changes: 4 additions & 0 deletions src/usethis/_tool/impl/base/pyproject_toml.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import final

from usethis._console import how_print, info_print, instruct_print
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._tool.base import Tool
Expand Down Expand Up @@ -30,12 +32,14 @@


class PyprojectTOMLTool(PyprojectTOMLToolSpec, Tool):
@final
def print_how_to_use(self) -> None:
how_print("Populate 'pyproject.toml' with the project configuration.")
info_print(
"Learn more at https://packaging.python.org/en/latest/guides/writing-pyproject-toml/"
)

@final
def remove_managed_files(self) -> None:
# https://github.com/usethis-python/usethis-python/issues/416
# We need to step through the tools and see if pyproject.toml is the active
Expand Down
8 changes: 7 additions & 1 deletion src/usethis/_tool/impl/base/pytest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import re
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, final

from typing_extensions import assert_never

Expand Down Expand Up @@ -29,6 +29,7 @@


class PytestTool(PytestToolSpec, Tool):
@final
def test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
from usethis._tool.impl.base.coverage_py import ( # to avoid circularity; # noqa: PLC0415
CoveragePyTool,
Expand All @@ -39,13 +40,15 @@ def test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
deps += [Dependency(name="pytest-cov")]
return deps

@final
def print_how_to_use(self) -> None:
how_print(
"Add test files to the '/tests' directory with the format 'test_*.py'."
)
how_print("Add test functions with the format 'test_*()'.")
how_print(f"Run '{self.how_to_use_cmd()}' to run the tests.")

@final
def get_active_config_file_managers(self) -> set[KeyValueFileManager[object]]:
# This is a variant of the "first" method
config_spec = self.config_spec()
Expand Down Expand Up @@ -105,6 +108,7 @@ def get_active_config_file_managers(self) -> set[KeyValueFileManager[object]]:
raise NotImplementedError(msg)
return {preferred_file_manager}

@final
def get_bitbucket_steps(
self, *, matrix_python: bool = True
) -> list[bitbucket_schema.Step]:
Expand Down Expand Up @@ -150,6 +154,7 @@ def get_bitbucket_steps(
steps.append(step)
return steps

@final
def get_managed_bitbucket_step_names(self) -> list[str]:
names = set()
for step in get_steps_in_default():
Expand All @@ -164,6 +169,7 @@ def get_managed_bitbucket_step_names(self) -> list[str]:

return sorted(names)

@final
def update_bitbucket_steps(self, *, matrix_python: bool = True) -> None:
"""Update the pytest-related Bitbucket Pipelines steps.

Expand Down
3 changes: 3 additions & 0 deletions src/usethis/_tool/impl/base/requirements_txt.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import final

from typing_extensions import assert_never

from usethis._backend.dispatch import get_backend
Expand All @@ -11,6 +13,7 @@


class RequirementsTxtTool(RequirementsTxtToolSpec, Tool):
@final
def print_how_to_use(self) -> None:
install_method = self.get_install_method()
backend = get_backend()
Expand Down
Loading
Loading