Skip to content

Commit 9305f2e

Browse files
Add @final to all methods in ToolSpec and Tool implementations (#1432)
* Initial plan * Add @Final to all methods in ToolSpec and Tool implementations Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/ac4205d5-03bc-4d8d-951a-bdf5271a13a7 * Document @Final requirement for Tool/ToolSpec methods in CONTRIBUTING.md Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/a307b5d3-0c33-4642-b872-8712d1c47ffc * Remove example code for @typing.final usage Removed example code for marking methods with `@typing.final` in `ToolSpec` subclass. Also remove outdated advice regarding URL (now in `ToolMeta` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Co-authored-by: Nathan McDougall <nathan.j.mcdougall@gmail.com>
1 parent 2ae04db commit 9305f2e

22 files changed

Lines changed: 128 additions & 12 deletions

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,9 @@ Tool implementations are defined in classes in the `usethis._tool.impl` module.
140140
- 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.
141141
- Define a `usethis._tool.base.ToolSpec` subclass, e.g. for a tool named Xyz, define a class `XyzToolSpec(ToolSpec)`.
142142
- 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.
143+
- Mark all methods in your `ToolSpec` subclass with the `@typing.final` decorator. This prevents the methods from being accidentally overridden in the `Tool` subclass.
143144
- 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`.
144-
- Include a comment with a URL linking to the tool's source repo for reference.
145+
- Mark all methods in your `Tool` subclass with `@final` as well, to prevent further subclassing from overriding them.
145146

146147
#### Register your `Tool` subclass
147148

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import annotations
22

3+
from typing import final
4+
35
from usethis._console import how_print
46
from usethis._tool.base import Tool
57
from usethis._tool.impl.spec.codespell import CodespellToolSpec
68

79

810
class CodespellTool(CodespellToolSpec, Tool):
11+
@final
912
def print_how_to_use(self) -> None:
1013
how_print(f"Run '{self.how_to_use_cmd()}' to run the {self.name} spellchecker.")

src/usethis/_tool/impl/base/coverage_py.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import final
4+
35
from typing_extensions import assert_never
46

57
from usethis._backend.dispatch import get_backend
@@ -12,6 +14,7 @@
1214

1315

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

28+
@final
2529
def print_how_to_use(self) -> None:
2630
from usethis._tool.impl.base.pytest import ( # to avoid circularity; # noqa: PLC0415
2731
PytestTool,

src/usethis/_tool/impl/base/deptry.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING
3+
from typing import TYPE_CHECKING, final
44

55
from usethis._console import info_print
66
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
@@ -13,12 +13,14 @@
1313

1414

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

23+
@final
2224
def selected_rules(self) -> list[Rule]:
2325
"""No notion of selection for deptry.
2426
@@ -27,10 +29,12 @@ def selected_rules(self) -> list[Rule]:
2729
"""
2830
return []
2931

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

37+
@final
3438
def ignored_rules(self) -> list[Rule]:
3539
(file_manager,) = self.get_active_config_file_managers()
3640
keys = self._get_ignore_keys(file_manager)
@@ -41,6 +45,7 @@ def ignored_rules(self) -> list[Rule]:
4145

4246
return rules
4347

48+
@final
4449
def _get_ignore_keys(self, file_manager: KeyValueFileManager[object]) -> list[str]:
4550
"""Get the keys for the ignored rules in the given file manager."""
4651
if isinstance(file_manager, PyprojectTOMLManager):

src/usethis/_tool/impl/base/import_linter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING
3+
from typing import TYPE_CHECKING, final
44

55
from usethis._config import usethis_config
66
from usethis._console import info_print
@@ -13,13 +13,15 @@
1313

1414

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

24+
@final
2325
def print_how_to_use(self) -> None:
2426
if not _is_inp_rule_selected():
2527
# If Ruff is used, we enable the INP rules instead.

src/usethis/_tool/impl/base/mkdocs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import final
4+
35
from typing_extensions import assert_never
46

57
from usethis._backend.dispatch import get_backend
@@ -11,6 +13,7 @@
1113

1214

1315
class MkDocsTool(MkDocsToolSpec, Tool):
16+
@final
1417
def print_how_to_use(self) -> None:
1518
backend = get_backend()
1619
if backend is BackendEnum.uv and is_uv_used():

src/usethis/_tool/impl/base/pre_commit.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING
3+
from typing import TYPE_CHECKING, final
44

55
from typing_extensions import assert_never
66

@@ -20,12 +20,15 @@
2020

2121

2222
class PreCommitTool(PreCommitToolSpec, Tool):
23+
@final
2324
def is_used(self) -> bool:
2425
return is_pre_commit_used()
2526

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

31+
@final
2932
def get_bitbucket_steps(
3033
self,
3134
*,
@@ -63,6 +66,7 @@ def get_bitbucket_steps(
6366
else:
6467
assert_never(backend)
6568

69+
@final
6670
def update_bitbucket_steps(self, *, matrix_python: bool = True) -> None:
6771
"""Add Bitbucket steps associated with pre-commit, and remove outdated ones.
6872
@@ -74,8 +78,10 @@ def update_bitbucket_steps(self, *, matrix_python: bool = True) -> None:
7478
"""
7579
self._unconditional_update_bitbucket_steps(matrix_python=matrix_python)
7680

81+
@final
7782
def migrate_config_to_pre_commit(self) -> None:
7883
pass
7984

85+
@final
8086
def migrate_config_from_pre_commit(self) -> None:
8187
pass

src/usethis/_tool/impl/base/pyproject_toml.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import final
4+
35
from usethis._console import how_print, info_print, instruct_print
46
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
57
from usethis._tool.base import Tool
@@ -30,12 +32,14 @@
3032

3133

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

42+
@final
3943
def remove_managed_files(self) -> None:
4044
# https://github.com/usethis-python/usethis-python/issues/416
4145
# We need to step through the tools and see if pyproject.toml is the active

src/usethis/_tool/impl/base/pytest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import re
4-
from typing import TYPE_CHECKING
4+
from typing import TYPE_CHECKING, final
55

66
from typing_extensions import assert_never
77

@@ -29,6 +29,7 @@
2929

3030

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

43+
@final
4244
def print_how_to_use(self) -> None:
4345
how_print(
4446
"Add test files to the '/tests' directory with the format 'test_*.py'."
4547
)
4648
how_print("Add test functions with the format 'test_*()'.")
4749
how_print(f"Run '{self.how_to_use_cmd()}' to run the tests.")
4850

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

111+
@final
108112
def get_bitbucket_steps(
109113
self, *, matrix_python: bool = True
110114
) -> list[bitbucket_schema.Step]:
@@ -150,6 +154,7 @@ def get_bitbucket_steps(
150154
steps.append(step)
151155
return steps
152156

157+
@final
153158
def get_managed_bitbucket_step_names(self) -> list[str]:
154159
names = set()
155160
for step in get_steps_in_default():
@@ -164,6 +169,7 @@ def get_managed_bitbucket_step_names(self) -> list[str]:
164169

165170
return sorted(names)
166171

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

src/usethis/_tool/impl/base/requirements_txt.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import final
4+
35
from typing_extensions import assert_never
46

57
from usethis._backend.dispatch import get_backend
@@ -11,6 +13,7 @@
1113

1214

1315
class RequirementsTxtTool(RequirementsTxtToolSpec, Tool):
16+
@final
1417
def print_how_to_use(self) -> None:
1518
install_method = self.get_install_method()
1619
backend = get_backend()

0 commit comments

Comments
 (0)