diff --git a/pyproject.toml b/pyproject.toml index 6678ca8a9..7a95986d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -212,7 +212,6 @@ ignore = [ "src/usethis/_version.py" ] reportAny = false reportExplicitAny = false reportImplicitStringConcatenation = false -reportMissingTypeArgument = false reportUnknownArgumentType = false reportUnknownMemberType = false reportUnknownVariableType = false @@ -232,6 +231,7 @@ reportUnusedParameter = false [[tool.basedpyright.executionEnvironments]] # Particularly interested in avoiding the auto-generated schema script root = "src/usethis/_integrations/pre_commit" +reportMissingTypeArgument = false reportUnannotatedClassAttribute = false [tool.sync-with-uv.repo-to-package] diff --git a/src/usethis/_backend/uv/version.py b/src/usethis/_backend/uv/version.py index 6bdaf7f8b..def34a246 100644 --- a/src/usethis/_backend/uv/version.py +++ b/src/usethis/_backend/uv/version.py @@ -16,7 +16,7 @@ def get_uv_version() -> str: except UVSubprocessFailedError: return FALLBACK_UV_VERSION - json_dict: dict = json.loads(json_str) + json_dict: dict[str, str] = json.loads(json_str) return json_dict.get("version", FALLBACK_UV_VERSION) diff --git a/src/usethis/_file/ini/io_.py b/src/usethis/_file/ini/io_.py index 1d6fa0c89..92131eeba 100644 --- a/src/usethis/_file/ini/io_.py +++ b/src/usethis/_file/ini/io_.py @@ -39,7 +39,7 @@ from usethis._file.types_ import Key -class INIFileManager(KeyValueFileManager, metaclass=ABCMeta): +class INIFileManager(KeyValueFileManager[INIDocument], metaclass=ABCMeta): _content_by_path: ClassVar[dict[Path, INIDocument | None]] = {} @override diff --git a/src/usethis/_file/manager.py b/src/usethis/_file/manager.py index c9755f2ce..1d2876687 100644 --- a/src/usethis/_file/manager.py +++ b/src/usethis/_file/manager.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar, cast from typing_extensions import override @@ -21,7 +21,13 @@ from usethis._file.types_ import Key -DocumentT = TypeVar("DocumentT") +class Document(Protocol): + """Protocol for the document type managed by FileManager.""" + + pass + + +DocumentT = TypeVar("DocumentT", covariant=True) class UnexpectedFileOpenError(UsethisError): @@ -107,7 +113,7 @@ def get(self) -> DocumentT: else: return self._content - def commit(self, document: DocumentT) -> None: + def commit(self, document: DocumentT) -> None: # pyright: ignore[reportGeneralTypeIssues] not modifying DocumentT so safe to use covariant type variable here """Store the given document in memory for deferred writing.""" self._validate_lock() self._content = document @@ -171,7 +177,7 @@ def _parse_content(self, content: str) -> DocumentT: @property def _content(self) -> DocumentT | None: - return self._content_by_path.get(self.path) + return cast("DocumentT | None", self._content_by_path.get(self.path)) @_content.setter def _content(self, value: DocumentT | None) -> None: @@ -197,7 +203,9 @@ def unlock(self) -> None: self._dirty_by_path.pop(self.path, None) -class KeyValueFileManager(FileManager, Generic[DocumentT], metaclass=ABCMeta): +class KeyValueFileManager( + FileManager[DocumentT], Generic[DocumentT], metaclass=ABCMeta +): """A manager for files which store (at least some) values in key-value mappings.""" @abstractmethod diff --git a/src/usethis/_file/toml/io_.py b/src/usethis/_file/toml/io_.py index 7c9dbb531..a7c71b379 100644 --- a/src/usethis/_file/toml/io_.py +++ b/src/usethis/_file/toml/io_.py @@ -45,7 +45,7 @@ from usethis._file.types_ import Key -class TOMLFileManager(KeyValueFileManager, metaclass=ABCMeta): +class TOMLFileManager(KeyValueFileManager[TOMLDocument], metaclass=ABCMeta): """An abstract class for managing TOML files.""" _content_by_path: ClassVar[dict[Path, TOMLDocument | None]] = {} diff --git a/src/usethis/_file/yaml/io_.py b/src/usethis/_file/yaml/io_.py index 79bb5c9fa..20af679c0 100644 --- a/src/usethis/_file/yaml/io_.py +++ b/src/usethis/_file/yaml/io_.py @@ -47,7 +47,19 @@ from usethis._file.yaml.typing_ import YAMLLiteral -class YAMLFileManager(KeyValueFileManager, metaclass=ABCMeta): +@dataclass +class YAMLDocument: + """A dataclass to represent a YAML document in memory. + + Attributes: + content: The content of the YAML document as a ruamel.yaml object. + """ + + content: YAMLLiteral + roundtripper: ruamel.yaml.YAML + + +class YAMLFileManager(KeyValueFileManager[YAMLDocument], metaclass=ABCMeta): """An abstract class for managing YAML files.""" _content_by_path: ClassVar[dict[Path, YAMLDocument | None]] = {} @@ -431,18 +443,6 @@ def _validate_keys(keys: Sequence[Key]) -> list[str]: return so_far_keys -@dataclass -class YAMLDocument: - """A dataclass to represent a YAML document in memory. - - Attributes: - content: The content of the YAML document as a ruamel.yaml object. - """ - - content: YAMLLiteral - roundtripper: ruamel.yaml.YAML - - @contextmanager def edit_yaml( yaml_path: Path, diff --git a/src/usethis/_tool/base.py b/src/usethis/_tool/base.py index 527c83091..d750f18a6 100644 --- a/src/usethis/_tool/base.py +++ b/src/usethis/_tool/base.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from collections.abc import Sequence - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager from usethis._file.types_ import Key from usethis._tool.config import ConfigItem from usethis._tool.rule import Rule @@ -256,7 +256,7 @@ def _add_config_item( self, config_item: ConfigItem, *, - file_managers: set[KeyValueFileManager[object]], + file_managers: set[KeyValueFileManager[Document]], ) -> bool: """Add a specific configuration item using specified file managers. @@ -416,7 +416,9 @@ def get_install_method(self) -> Literal["devdep", "pre-commit"] | None: return "pre-commit" return None - def _get_select_keys(self, file_manager: KeyValueFileManager[object]) -> list[str]: + def _get_select_keys( + self, file_manager: KeyValueFileManager[Document] + ) -> list[str]: """Get the configuration keys for selected rules. This is optional - tools that don't support rule selection can leave this @@ -482,7 +484,9 @@ def select_rules(self, rules: Sequence[Rule]) -> bool: return True - def _get_ignore_keys(self, file_manager: KeyValueFileManager[object]) -> list[str]: + def _get_ignore_keys( + self, file_manager: KeyValueFileManager[Document] + ) -> list[str]: """Get the configuration keys for ignored rules. Args: diff --git a/src/usethis/_tool/config.py b/src/usethis/_tool/config.py index ddc7f82c4..fb67c0590 100644 --- a/src/usethis/_tool/config.py +++ b/src/usethis/_tool/config.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, InstanceOf from usethis._config import usethis_config -from usethis._file.manager import KeyValueFileManager +from usethis._file.manager import Document, KeyValueFileManager from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._file.types_ import Key from usethis._init import ensure_pyproject_toml @@ -43,14 +43,14 @@ class ConfigSpec(BaseModel): config_items: A list of configuration items that can be managed by the tool. """ - file_manager_by_relative_path: dict[Path, InstanceOf[KeyValueFileManager]] + file_manager_by_relative_path: dict[Path, InstanceOf[KeyValueFileManager[Document]]] resolution: ResolutionT config_items: list[ConfigItem] @classmethod def from_flat( cls, - file_managers: list[KeyValueFileManager[object]], + file_managers: list[KeyValueFileManager[Document]], resolution: ResolutionT, config_items: list[ConfigItem], ) -> Self: @@ -149,7 +149,7 @@ def paths(self) -> set[Path]: return {(usethis_config.cpd() / path).resolve() for path in self.root} -def ensure_managed_file_exists(file_manager: FileManager[object]) -> None: +def ensure_managed_file_exists(file_manager: FileManager[Document]) -> None: """Ensure a file manager's managed file exists.""" if isinstance(file_manager, PyprojectTOMLManager): ensure_pyproject_toml() diff --git a/src/usethis/_tool/impl/base/deptry.py b/src/usethis/_tool/impl/base/deptry.py index 4ebc1debd..c4218244c 100644 --- a/src/usethis/_tool/impl/base/deptry.py +++ b/src/usethis/_tool/impl/base/deptry.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from collections.abc import Sequence - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager @final @@ -54,7 +54,9 @@ def ignored_rules(self) -> list[Rule]: return rules @override - def _get_ignore_keys(self, file_manager: KeyValueFileManager[object]) -> list[str]: + def _get_ignore_keys( + self, file_manager: KeyValueFileManager[Document] + ) -> list[str]: """Get the keys for the ignored rules in the given file manager.""" if isinstance(file_manager, PyprojectTOMLManager): return ["tool", "deptry", "ignore"] diff --git a/src/usethis/_tool/impl/base/pytest.py b/src/usethis/_tool/impl/base/pytest.py index 61b896a0a..20d57519e 100644 --- a/src/usethis/_tool/impl/base/pytest.py +++ b/src/usethis/_tool/impl/base/pytest.py @@ -16,7 +16,7 @@ from usethis._types.deps import Dependency if TYPE_CHECKING: - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager @final @@ -37,7 +37,9 @@ def print_how_to_use(self) -> None: how_print(f"Run '{self.how_to_use_cmd()}' to run the tests.") @override - def get_active_config_file_managers(self) -> set[KeyValueFileManager[object]]: + def get_active_config_file_managers( + self, + ) -> set[KeyValueFileManager[Document]]: # This is a variant of the "first" method config_spec = self.config_spec() if config_spec.resolution != "bespoke": diff --git a/src/usethis/_tool/impl/base/ruff.py b/src/usethis/_tool/impl/base/ruff.py index f27e4fa38..83a68c40f 100644 --- a/src/usethis/_tool/impl/base/ruff.py +++ b/src/usethis/_tool/impl/base/ruff.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from collections.abc import Sequence - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager from usethis._tool.rule import RuleConfig @@ -300,7 +300,9 @@ def is_pydocstyle_rule(rule: Rule) -> bool: return [d for d in rule if d.isalpha()] == ["D"] @override - def _get_select_keys(self, file_manager: KeyValueFileManager[object]) -> list[str]: + def _get_select_keys( + self, file_manager: KeyValueFileManager[Document] + ) -> list[str]: """Get the keys for the selected rules in the given file manager.""" if isinstance(file_manager, PyprojectTOMLManager): return ["tool", "ruff", "lint", "select"] @@ -310,7 +312,9 @@ def _get_select_keys(self, file_manager: KeyValueFileManager[object]) -> list[st return super()._get_select_keys(file_manager) @override - def _get_ignore_keys(self, file_manager: KeyValueFileManager[object]) -> list[str]: + def _get_ignore_keys( + self, file_manager: KeyValueFileManager[Document] + ) -> list[str]: """Get the keys for the ignored rules in the given file manager.""" if isinstance(file_manager, PyprojectTOMLManager): return ["tool", "ruff", "lint", "ignore"] @@ -320,7 +324,7 @@ def _get_ignore_keys(self, file_manager: KeyValueFileManager[object]) -> list[st return super()._get_ignore_keys(file_manager) def _get_per_file_ignore_keys( - self, file_manager: KeyValueFileManager[object], *, glob: str + self, file_manager: KeyValueFileManager[Document], *, glob: str ) -> list[str]: """Get the keys for the per-file ignored rules in the given file manager.""" if isinstance(file_manager, PyprojectTOMLManager): @@ -335,7 +339,7 @@ def _get_per_file_ignore_keys( raise NotImplementedError(msg) def _get_docstyle_keys( - self, file_manager: KeyValueFileManager[object] + self, file_manager: KeyValueFileManager[Document] ) -> list[str]: """Get the keys for the docstyle rules in the given file manager.""" if isinstance(file_manager, PyprojectTOMLManager): diff --git a/src/usethis/_tool/impl/spec/codespell.py b/src/usethis/_tool/impl/spec/codespell.py index 8c4f8595f..8a93444de 100644 --- a/src/usethis/_tool/impl/spec/codespell.py +++ b/src/usethis/_tool/impl/spec/codespell.py @@ -25,7 +25,7 @@ from usethis._types.deps import Dependency if TYPE_CHECKING: - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager class CodespellToolSpec(ToolSpec): @@ -41,7 +41,7 @@ def meta(self) -> ToolMeta: @override @final - def preferred_file_manager(self) -> KeyValueFileManager[object]: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: if (usethis_config.cpd() / "pyproject.toml").exists(): return PyprojectTOMLManager() return DotCodespellRCManager() diff --git a/src/usethis/_tool/impl/spec/coverage_py.py b/src/usethis/_tool/impl/spec/coverage_py.py index f8d745110..7e6f4793f 100644 --- a/src/usethis/_tool/impl/spec/coverage_py.py +++ b/src/usethis/_tool/impl/spec/coverage_py.py @@ -20,7 +20,7 @@ from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec if TYPE_CHECKING: - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager class CoveragePyToolSpec(ToolSpec): @@ -36,7 +36,7 @@ def meta(self) -> ToolMeta: @override @final - def preferred_file_manager(self) -> KeyValueFileManager[object]: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: if (usethis_config.cpd() / "pyproject.toml").exists(): return PyprojectTOMLManager() return DotCoverageRCManager() diff --git a/src/usethis/_tool/impl/spec/import_linter.py b/src/usethis/_tool/impl/spec/import_linter.py index 83daae10e..537af8b57 100644 --- a/src/usethis/_tool/impl/spec/import_linter.py +++ b/src/usethis/_tool/impl/spec/import_linter.py @@ -33,7 +33,7 @@ from usethis._types.deps import Dependency if TYPE_CHECKING: - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager from usethis._tool.config import ResolutionT IMPORT_LINTER_CONTRACT_MIN_MODULE_COUNT = 3 @@ -65,7 +65,7 @@ def dev_deps(self, *, unconditional: bool = False) -> list[Dependency]: @override @final - def preferred_file_manager(self) -> KeyValueFileManager[object]: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: if (usethis_config.cpd() / "pyproject.toml").exists(): return PyprojectTOMLManager() return DotImportLinterManager() @@ -304,7 +304,7 @@ def _get_resolution(self) -> ResolutionT: @final def _get_file_manager_by_relative_path( self, - ) -> dict[Path, KeyValueFileManager[object]]: + ) -> dict[Path, KeyValueFileManager[Document]]: return { Path("setup.cfg"): SetupCFGManager(), Path(".importlinter"): DotImportLinterManager(), diff --git a/src/usethis/_tool/impl/spec/mkdocs.py b/src/usethis/_tool/impl/spec/mkdocs.py index c99f418c7..4c97a862b 100644 --- a/src/usethis/_tool/impl/spec/mkdocs.py +++ b/src/usethis/_tool/impl/spec/mkdocs.py @@ -14,7 +14,7 @@ from usethis._types.deps import Dependency if TYPE_CHECKING: - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager class MkDocsToolSpec(ToolSpec): @@ -40,7 +40,7 @@ def doc_deps(self, *, unconditional: bool = False) -> list[Dependency]: @override @final - def preferred_file_manager(self) -> KeyValueFileManager[object]: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: """If there is no currently active config file, this is the preferred one.""" # Should set the mkdocs.yml file manager as the preferred one return MkDocsYMLManager() diff --git a/src/usethis/_tool/impl/spec/pytest.py b/src/usethis/_tool/impl/spec/pytest.py index 277f0007c..db8d038c9 100644 --- a/src/usethis/_tool/impl/spec/pytest.py +++ b/src/usethis/_tool/impl/spec/pytest.py @@ -19,7 +19,7 @@ from usethis._tool.rule import RuleConfig if TYPE_CHECKING: - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager class PytestToolSpec(ToolSpec): @@ -45,7 +45,7 @@ def raw_cmd(self) -> str: @override @final - def preferred_file_manager(self) -> KeyValueFileManager[object]: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: if (usethis_config.cpd() / "pyproject.toml").exists(): return PyprojectTOMLManager() return PytestINIManager() diff --git a/src/usethis/_tool/impl/spec/ruff.py b/src/usethis/_tool/impl/spec/ruff.py index 40ceffccc..65e687f4a 100644 --- a/src/usethis/_tool/impl/spec/ruff.py +++ b/src/usethis/_tool/impl/spec/ruff.py @@ -15,7 +15,7 @@ from usethis._types.deps import Dependency if TYPE_CHECKING: - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager class RuffToolSpec(ToolSpec): @@ -62,7 +62,7 @@ def dev_deps(self, *, unconditional: bool = False) -> list[Dependency]: @override @final - def preferred_file_manager(self) -> KeyValueFileManager[object]: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: if (usethis_config.cpd() / "pyproject.toml").exists(): return PyprojectTOMLManager() return RuffTOMLManager() diff --git a/src/usethis/_tool/impl/spec/ty.py b/src/usethis/_tool/impl/spec/ty.py index 4b7443398..196a1ad7f 100644 --- a/src/usethis/_tool/impl/spec/ty.py +++ b/src/usethis/_tool/impl/spec/ty.py @@ -22,7 +22,7 @@ from usethis._types.deps import Dependency if TYPE_CHECKING: - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager class TyToolSpec(ToolSpec): @@ -38,7 +38,7 @@ def meta(self) -> ToolMeta: @override @final - def preferred_file_manager(self) -> KeyValueFileManager[object]: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: if (usethis_config.cpd() / "pyproject.toml").exists(): return PyprojectTOMLManager() return TyTOMLManager() diff --git a/src/usethis/_tool/spec.py b/src/usethis/_tool/spec.py index a3b536e07..79b23a899 100644 --- a/src/usethis/_tool/spec.py +++ b/src/usethis/_tool/spec.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: from pathlib import Path - from usethis._file.manager import KeyValueFileManager + from usethis._file.manager import Document, KeyValueFileManager from usethis._integrations.pre_commit import schema as pre_commit_schema from usethis._tool.config import ResolutionT from usethis._tool.rule import Rule @@ -78,7 +78,7 @@ def rule_config(self) -> RuleConfig: """ return self.meta.rule_config - def preferred_file_manager(self) -> KeyValueFileManager[object]: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: """If there is no currently active config file, this is the preferred one. This can vary dynamically, since often we will prefer to respect an existing @@ -96,7 +96,9 @@ def config_spec(self) -> ConfigSpec: """ return ConfigSpec.empty() - def get_active_config_file_managers(self) -> set[KeyValueFileManager[object]]: + def get_active_config_file_managers( + self, + ) -> set[KeyValueFileManager[Document]]: """Get file managers for all active configuration files. Active configuration files are just those that we expect to use based on our @@ -118,8 +120,8 @@ def _get_active_config_file_managers_from_resolution( self, resolution: ResolutionT, *, - file_manager_by_relative_path: dict[Path, KeyValueFileManager[object]], - ) -> set[KeyValueFileManager[object]]: + file_manager_by_relative_path: dict[Path, KeyValueFileManager[Document]], + ) -> set[KeyValueFileManager[Document]]: if resolution == "first": # N.B. keep this roughly in sync with the bespoke logic for pytest # since that logic is based on this logic. diff --git a/tests/usethis/_backend/test_dispatch.py b/tests/usethis/_backend/test_dispatch.py index 5474e7c31..f6527ca38 100644 --- a/tests/usethis/_backend/test_dispatch.py +++ b/tests/usethis/_backend/test_dispatch.py @@ -66,7 +66,7 @@ def mock_call_uv_subprocess(*_: object, **__: object): # Assert assert result == BackendEnum.none - def test_poetry_used(self, tmp_path: Path, capfd: pytest.CaptureFixture): + def test_poetry_used(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]): # Arrange (tmp_path / "poetry.lock").touch() diff --git a/tests/usethis/_file/test_manager.py b/tests/usethis/_file/test_manager.py index 1ffb6758f..a5f6f23fc 100644 --- a/tests/usethis/_file/test_manager.py +++ b/tests/usethis/_file/test_manager.py @@ -2,7 +2,7 @@ from typing_extensions import override -from usethis._file.manager import FileManager +from usethis._file.manager import Document, FileManager from usethis._test import change_cwd @@ -10,7 +10,7 @@ class TestUsethisFileManager: class TestContent: def test_setter(self, tmp_path: Path) -> None: # Arrange - class MyUsethisFileManager(FileManager): + class MyUsethisFileManager(FileManager[Document]): @property @override def relative_path(self) -> Path: @@ -36,7 +36,7 @@ def _parse_content(self, content: str) -> None: class TestEq: def test_example(self) -> None: # Arrange - class MyUsethisFileManager(FileManager): + class MyUsethisFileManager(FileManager[Document]): @property @override def relative_path(self) -> Path: @@ -61,7 +61,7 @@ def _parse_content(self, content: str) -> None: def test_different_type(self) -> None: # Arrange - class MyUsethisFileManager(FileManager): + class MyUsethisFileManager(FileManager[Document]): @property @override def relative_path(self) -> Path: @@ -87,7 +87,7 @@ def _parse_content(self, content: str) -> None: class TestRepr: def test_example(self, tmp_path: Path) -> None: # Arrange - class MyUsethisFileManager(FileManager): + class MyUsethisFileManager(FileManager[Document]): @property @override def relative_path(self) -> Path: diff --git a/tests/usethis/_file/test_merge.py b/tests/usethis/_file/test_merge.py index f48c3a86b..813e7637d 100644 --- a/tests/usethis/_file/test_merge.py +++ b/tests/usethis/_file/test_merge.py @@ -6,77 +6,77 @@ class TestDeepMerge: class TestBasicMerge: def test_top_level_key_added(self) -> None: - target: dict = {"a": 1} - source: dict = {"b": 2} + target: dict[str, int] = {"a": 1} + source: dict[str, int] = {"b": 2} result = deep_merge(target, source) assert result == {"a": 1, "b": 2} class TestNestedDicts: def test_nested_merge(self) -> None: - target: dict = {"a": {"x": 1}} - source: dict = {"a": {"y": 2}} + target: dict[str, dict[str, int]] = {"a": {"x": 1}} + source: dict[str, dict[str, int]] = {"a": {"y": 2}} result = deep_merge(target, source) assert result == {"a": {"x": 1, "y": 2}} def test_deeply_nested(self) -> None: - target: dict = {"a": {"b": {"c": 1}}} - source: dict = {"a": {"b": {"d": 2}}} + target: dict[str, dict[str, dict[str, int]]] = {"a": {"b": {"c": 1}}} + source: dict[str, dict[str, dict[str, int]]] = {"a": {"b": {"d": 2}}} result = deep_merge(target, source) assert result == {"a": {"b": {"c": 1, "d": 2}}} class TestReplacementOfNonDictValues: def test_scalar_replaced_by_scalar(self) -> None: - target: dict = {"a": 1} - source: dict = {"a": 2} + target: dict[str, int] = {"a": 1} + source: dict[str, int] = {"a": 2} result = deep_merge(target, source) assert result == {"a": 2} def test_dict_replaced_by_scalar(self) -> None: - target: dict = {"a": {"x": 1}} - source: dict = {"a": 99} + target: dict[str, dict[str, int]] = {"a": {"x": 1}} + source: dict[str, int] = {"a": 99} result = deep_merge(target, source) assert result == {"a": 99} def test_scalar_replaced_by_dict(self) -> None: - target: dict = {"a": 99} - source: dict = {"a": {"x": 1}} + target: dict[str, int] = {"a": 99} + source: dict[str, dict[str, int]] = {"a": {"x": 1}} result = deep_merge(target, source) assert result == {"a": {"x": 1}} def test_list_replaced_by_list(self) -> None: - target: dict = {"a": [1, 2]} - source: dict = {"a": [3, 4]} + target: dict[str, list[int]] = {"a": [1, 2]} + source: dict[str, list[int]] = {"a": [3, 4]} result = deep_merge(target, source) assert result == {"a": [3, 4]} class TestInPlaceMutation: def test_returns_target(self) -> None: - target: dict = {"a": 1} - source: dict = {"b": 2} + target: dict[str, int] = {"a": 1} + source: dict[str, int] = {"b": 2} result = deep_merge(target, source) assert result is target def test_target_is_mutated(self) -> None: - target: dict = {"a": 1} - source: dict = {"b": 2} + target: dict[str, int] = {"a": 1} + source: dict[str, int] = {"b": 2} deep_merge(target, source) assert target == {"a": 1, "b": 2} class TestDisjointKeys: def test_disjoint_keys_merged(self) -> None: - target: dict = {"a": 1, "b": 2} - source: dict = {"c": 3, "d": 4} + target: dict[str, int] = {"a": 1, "b": 2} + source: dict[str, int] = {"c": 3, "d": 4} result = deep_merge(target, source) assert result == {"a": 1, "b": 2, "c": 3, "d": 4} def test_empty_source(self) -> None: - target: dict = {"a": 1} - source: dict = {} + target: dict[str, int] = {"a": 1} + source: dict[str, int] = {} result = deep_merge(target, source) assert result == {"a": 1} def test_empty_target(self) -> None: - target: dict = {} - source: dict = {"a": 1} + target: dict[str, int] = {} + source: dict[str, int] = {"a": 1} result = deep_merge(target, source) assert result == {"a": 1} diff --git a/tests/usethis/_file/yaml/test_yaml_io_.py b/tests/usethis/_file/yaml/test_yaml_io_.py index 0390b2769..5a548742d 100644 --- a/tests/usethis/_file/yaml/test_yaml_io_.py +++ b/tests/usethis/_file/yaml/test_yaml_io_.py @@ -1518,7 +1518,9 @@ def test_invalid_indentation(self, tmp_path: Path): ): pass - def test_incorrect_indentation(self, tmp_path: Path, capfd: pytest.CaptureFixture): + def test_incorrect_indentation( + self, tmp_path: Path, capfd: pytest.CaptureFixture[str] + ): # Arrange (tmp_path / "x.yml").write_text("""\ - path: / diff --git a/tests/usethis/_integrations/pre_commit/test_yaml.py b/tests/usethis/_integrations/pre_commit/test_yaml.py index ef5c4c425..53cbc594c 100644 --- a/tests/usethis/_integrations/pre_commit/test_yaml.py +++ b/tests/usethis/_integrations/pre_commit/test_yaml.py @@ -44,7 +44,7 @@ def test_start_with_empty_file(self, tmp_path: Path): mgr = PreCommitConfigYAMLManager() doc = mgr.get() mgr.model_validate() - content = cast("dict", doc.content) + content = cast("dict[str, list[dict[str, str]]]", doc.content) content["repos"] = [] mgr.commit(doc) @@ -129,7 +129,7 @@ def test_extra_config(self, tmp_path: Path): ): doc = mgr.get() mgr.model_validate() - content = cast("dict", doc.content) + content = cast("dict[str, list[str]]", doc.content) content["repos"] = ["something"] diff --git a/tests/usethis/_integrations/pydantic/test_dump.py b/tests/usethis/_integrations/pydantic/test_dump.py index 1a3e1ea04..8e42291a3 100644 --- a/tests/usethis/_integrations/pydantic/test_dump.py +++ b/tests/usethis/_integrations/pydantic/test_dump.py @@ -23,7 +23,7 @@ class MyBaseModel(BaseModel): def test_list_remove_element(self): # Arrange - class MyRootModel(RootModel): + class MyRootModel(RootModel[list[int]]): root: list[int] mrm = MyRootModel([1, 3]) @@ -51,7 +51,7 @@ class MyModel(BaseModel): class TestRootModel: def test_singleton_list(self): # Arrange - class MyRootModel(RootModel): + class MyRootModel(RootModel[list[int]]): root: list[int] mrm = MyRootModel([2]) @@ -68,7 +68,7 @@ class MySubModel(BaseModel): x: Literal[0, 1] = 1 y: Literal[0, 1] = 0 - class MyRootModel(RootModel): + class MyRootModel(RootModel[list[MySubModel]]): root: list[MySubModel] mrm = MyRootModel( @@ -93,7 +93,7 @@ class MyRootModel(RootModel): def test_mismatch_list_ref(self): # Arrange - class MyRootModel(RootModel): + class MyRootModel(RootModel[list[int]]): root: list[int] mrm = MyRootModel([2]) @@ -106,7 +106,7 @@ class MyRootModel(RootModel): def test_constant(self): # Arrange - class MyRootModel(RootModel): + class MyRootModel(RootModel[str]): root: str mrm = MyRootModel("yo") @@ -126,7 +126,7 @@ class MySubModel(BaseModel): z: int = 1 w: int - class MyRootModel(RootModel): + class MyRootModel(RootModel[MySubModel]): root: MySubModel mrm = MyRootModel(MySubModel(x=-1, y=1, z=1, w=2)) @@ -139,7 +139,7 @@ class MyRootModel(RootModel): def test_bool_type(self): # Arrange - class MyRootModel(RootModel): + class MyRootModel(RootModel[list[bool]]): root: list[bool] mrm = MyRootModel([False, True]) @@ -169,10 +169,10 @@ class MyOuterModel(BaseModel): def test_rootmodel(self): # Arrange - class MyInnerModel(RootModel): + class MyInnerModel(RootModel[list[str]]): root: list[str] - class MyOuterModel(RootModel): + class MyOuterModel(RootModel[list[MyInnerModel]]): root: list[MyInnerModel] mom = MyOuterModel([MyInnerModel(["hello"])]) diff --git a/tests/usethis/_tool/test_base.py b/tests/usethis/_tool/test_base.py index 9a533a035..601f7503c 100644 --- a/tests/usethis/_tool/test_base.py +++ b/tests/usethis/_tool/test_base.py @@ -8,7 +8,7 @@ from usethis._config_file import files_manager from usethis._console import how_print from usethis._deps import add_deps_to_group -from usethis._file.manager import KeyValueFileManager +from usethis._file.manager import Document, KeyValueFileManager from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._file.setup_cfg.io_ import SetupCFGManager from usethis._integrations.pre_commit import schema @@ -131,14 +131,16 @@ def __init__( # noqa: PLR0913 self, *, name: str = "mocktool", - file_manager: KeyValueFileManager | None = None, + file_manager: KeyValueFileManager[Document] | None = None, ignored_rules: list[Rule] | None = None, selected_rules: list[Rule] | None = None, ignore_keys: list[str] | None = None, select_keys: list[str] | None = None, ): self._name = name - self._file_manager = file_manager or MagicMock() + self._file_manager = file_manager or MagicMock( + spec=KeyValueFileManager[Document] + ) self._ignored_rules = ignored_rules or [] self._selected_rules = selected_rules or [] self._ignore_keys = ignore_keys or ["tool", "mocktool", "ignore"] @@ -150,15 +152,19 @@ def meta(self) -> ToolMeta: return ToolMeta(name=self._name) @override - def get_active_config_file_managers(self) -> set[KeyValueFileManager]: + def get_active_config_file_managers(self) -> set[KeyValueFileManager[Document]]: return {self._file_manager} @override - def _get_ignore_keys(self, file_manager: KeyValueFileManager) -> list[str]: + def _get_ignore_keys( + self, file_manager: KeyValueFileManager[Document] + ) -> list[str]: return self._ignore_keys @override - def _get_select_keys(self, file_manager: KeyValueFileManager) -> list[str]: + def _get_select_keys( + self, file_manager: KeyValueFileManager[Document] + ) -> list[str]: return self._select_keys @override @@ -447,7 +453,7 @@ def config_spec(self) -> ConfigSpec: ) @override - def preferred_file_manager(self) -> KeyValueFileManager: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: return SetupCFGManager() tool = ThisTool() @@ -1122,7 +1128,7 @@ def config_spec(self) -> ConfigSpec: ) @override - def preferred_file_manager(self) -> KeyValueFileManager: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: return SetupCFGManager() (tmp_path / "setup.cfg").touch() @@ -1166,7 +1172,7 @@ def config_spec(self) -> ConfigSpec: ) @override - def preferred_file_manager(self) -> KeyValueFileManager: + def preferred_file_manager(self) -> KeyValueFileManager[Document]: return SetupCFGManager() (tmp_path / "setup.cfg").write_text("""\ @@ -1538,7 +1544,7 @@ def test_tool_without_selection_support(self) -> None: class MyMock(MockToolForRuleTests): @override def _get_select_keys( - self, file_manager: KeyValueFileManager + self, file_manager: KeyValueFileManager[Document] ) -> list[str]: return super(MockToolForRuleTests, self)._get_select_keys( file_manager