diff --git a/poetry.lock b/poetry.lock index 69c4d79..0bd8831 100644 --- a/poetry.lock +++ b/poetry.lock @@ -536,17 +536,6 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - [[package]] name = "virtualenv" version = "20.26.3" @@ -570,4 +559,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "334a7105baa770bfd69d0391602995e317ccc0faa3d34539acbf7e9aa5355fee" +content-hash = "c7e77528e9eca8213b1d2a0d7d353b7f47900832569b2396f9026300939dac14" diff --git a/pyproject.toml b/pyproject.toml index f975398..da0e17e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ packages = [{ include = "src" }] [tool.poetry.dependencies] python = "^3.11" -typing-extensions = "^4.12.2" [tool.poetry.group.dev.dependencies] poethepoet = "^0.25.0" @@ -42,6 +41,7 @@ ignore = [ "CPY", # flake8-copyright "EM", # flake8-errmsg "SLF", # flake8-self + "TCH", # flake8-type-checking "ARG", # flake8-unused-arguments "TD", # flake8-todos "FIX", # flake8-fixme @@ -85,7 +85,6 @@ ignore = [ "PGH003", # Using specific rule codes in type ignores "E731", # Don't asign a lambda expression, use a def "S311", # Use `secrets` for random number generation, not `random` - "TRY003", # Avoid specifying long messages outside the exception class # Redundant rules with ruff-format: "E111", # Indentation of a non-multiple of 4 spaces @@ -149,67 +148,3 @@ reportUnnecessaryContains = "error" reportUnnecessaryTypeIgnoreComment = "error" reportImplicitOverride = "error" reportShadowedImports = "error" - -[tool.pytest.ini_options] -minversion = "6.0" -asyncio_mode = "auto" -testpaths = ["tests"] -addopts = "--strict-markers --cov --no-cov-on-fail" - -[tool.coverage.report] -precision = 2 -fail_under = 20 -show_missing = true -skip_covered = false -skip_empty = false -sort = "cover" -exclude_lines = [ - "\\#\\s*pragma: no cover", - "^\\s*if (typing\\.)?TYPE_CHECKING:", - "^\\s*@(abc\\.)?abstractmethod", - "^\\s*@(typing\\.)?overload", - "^\\s*def __repr__\\(", - "^\\s*class .*\\bProtocol\\):", - "^\\s*raise NotImplementedError", - "^\\s*return NotImplemented", - "^\\s*\\.\\.\\.", -] - -[tool.coverage.run] -relative_files = true -parallel = true -branch = true -timid = false -source = ["src"] - -[tool.poe.tasks.precommit] -cmd = "pre-commit install" -help = "install pre-commit hooks" - -[tool.poe.tasks.lint] -cmd = "pre-commit run --all-files" -help = "Run all project linters (via pre-commit)" - -[tool.poe.tasks.ruff] -cmd = "ruff check --fix ." -help = "Run ruff linter, with automatic issue fixing" - -[tool.poe.tasks.ruff-format] -cmd = "ruff format ." -help = "Run ruff formatter" - -[tool.poe.tasks.test] -cmd = "pytest -v --failed-first" -help = "Run pytest tests" - -[tool.poe.tasks.retest] -cmd = "pytest -v --last-failed" -help = "Run previously failed tests using pytest" - -[tool.poe.tasks.test-nocov] -cmd = "pytest -v --no-cov --failed-first" -help = "Run pytest tests without coverage" - -[tool.poe.tasks.retest-nocov] -cmd = "pytest -v --no-cov --last-failed" -help = "Run previously failed tests using pytest without coverage" diff --git a/tests/helpers.py b/tests/helpers.py deleted file mode 100644 index d21d6c6..0000000 --- a/tests/helpers.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import annotations - -import asyncio -import unittest.mock -from typing import Any, Generic, TYPE_CHECKING, TypeVar - -from typing_extensions import ParamSpec - -if TYPE_CHECKING: - from collections.abc import Callable, Coroutine - -T = TypeVar("T") -P = ParamSpec("P") -T_Mock = TypeVar("T_Mock", bound=unittest.mock.Mock) - -__all__ = [ - "synchronize", - "UnpropagatingMockMixin", - "CustomMockMixin", -] - - -def synchronize(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: - """Take an asynchronous function, and return a synchronous alternative. - - This is needed because we sometimes want to test asynchronous behavior in a synchronous test function, - where we can't simply await something. This function uses `asyncio.run` and generates a wrapper - around the original asynchronous function, that awaits the result in a blocking synchronous way, - returning the obtained value. - """ - - def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - return asyncio.run(f(*args, **kwargs)) - - return wrapper - - -class UnpropagatingMockMixin(Generic[T_Mock]): - """Provides common functionality for our :class:`~unittest.mock.Mock` classes. - - By default, mock objects propagate themselves by returning a new instance of the same mock - class, with same initialization attributes. This is done whenever we're accessing new - attributes that mock class. - - This propagation makes sense for simple mocks without any additional restrictions, however when - dealing with limited mocks to some ``spec_set``, it doesn't usually make sense to propagate - those same ``spec_set`` restrictions, since we generally don't have attributes/methods of a - class be of/return the same class. - - This mixin class stops this propagation, and instead returns instances of specified mock class, - defined in :attr:`.child_mock_type` class variable, which is by default set to - :class:`~unittest.mock.MagicMock`, as it can safely represent most objects. - - .. note: - This propagation handling will only be done for the mock classes that inherited from this - mixin class. That means if the :attr:`.child_mock_type` is one of the regular mock classes, - and the mock is propagated, a regular mock class is returned as that new attribute. This - regular class then won't have the same overrides, and will therefore propagate itself, like - any other mock class would. - - If you wish to counteract this, you can set the :attr:`.child_mock_type` to a mock class - that also inherits from this mixin class, perhaps to your class itself, overriding any - propagation recursively. - """ - - child_mock_type: T_Mock = unittest.mock.MagicMock - - # Since this is a mixin class, we can access some attributes defined in mock classes safely. - # Define the types of these variables here, for proper static type analysis. - _mock_sealed: bool - _extract_mock_name: Callable[[], str] - - def _get_child_mock(self, **kwargs) -> T_Mock: - """Make :attr:`.child_mock_type`` instances instead of instances of the same class. - - By default, this method creates a new mock instance of the same original class, and passes - over the same initialization arguments. This overrides that behavior to instead create an - instance of :attr:`.child_mock_type` class. - """ - # Mocks can be sealed, in which case we wouldn't want to allow propagation of any kind - # and rather raise an AttributeError, informing that given attr isn't accessible - if self._mock_sealed: - mock_name = self._extract_mock_name() - obj_name = f"{mock_name}.{kwargs['name']}" if "name" in kwargs else f"{mock_name}()" - raise AttributeError(f"Can't access {obj_name}, mock is sealed.") - - # Propagate any other children as simple `unittest.mock.Mock` instances - # rather than `self.__class__` instances - return self.child_mock_type(**kwargs) - - -class CustomMockMixin(UnpropagatingMockMixin): - """Provides common functionality for our custom mock types. - - * Stops propagation of same ``spec_set`` restricted mock in child mocks - (see :class:`.UnpropagatingMockMixin` for more info) - * Allows using the ``spec_set`` attribute as class attribute - """ - - spec_set = None - - def __init__(self, **kwargs): - if "spec_set" in kwargs: - self.spec_set = kwargs.pop("spec_set") - super().__init__(spec_set=self.spec_set, **kwargs) # type: ignore # Mixin class, this __init__ is valid