diff --git a/README.md b/README.md new file mode 100644 index 0000000..d355237 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +![CDNJS](https://img.shields.io/badge/Python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-2334D058) +![CDNJS](https://shields.io/badge/FastAPI-%3E=0.7.0-009485) + +# FastAPI depends extension + +## Introduction + +Sometimes your FastAPI dependencies have to get value from functions cannot be available on initialization. The problem is particularly acute to use class dependencies with inheritance. This project try to solve problem of modify `Depends` after application initialization. + +## Installation + +``` +pip install fastapi-depends-ext +``` + +## Tutorial + +#### DependsAttr + +```python +from typing import List + +from fastapi import Depends +from fastapi import FastAPI +from fastapi import Query +from pydantic import conint + +from fastapi_depends_ext import DependsAttr +from fastapi_depends_ext import DependsAttrBinder + + +class ItemsPaginated(DependsAttrBinder): + _items = list(range(100)) + + async def get_page(self, page: conint(ge=1) = Query(1)): + return page + + async def items(self, page: int = DependsAttr("get_page")): + _slice = slice(page * 10, (page + 1) * 10) + return self._items[_slice] + + +class ItemsSquarePaginated(ItemsPaginated): + async def items(self, items: List[int] = DependsAttr("items", from_super=True)): + return [i**2 for i in items] + + +app = FastAPI() + + +@app.get("/") +def items_list(items: List[int] = Depends(ItemsPaginated().items)) -> List[int]: + return items + + +@app.get("/square") +def items_list_square(items: List[int] = Depends(ItemsSquarePaginated().items)) -> List[int]: + return items +``` + +Use `DependsAttr` to `Depends` from current instance attributes. All examples use `asyncio`, but you can write all methods synchronous. + +`DependsAttr` support next properties: +- class variables (must contains `callable` object) +- class methods +- static methods +- instance methods +- `property` returning `callable` + +Your class must inherit from `DependsAttrBinder` and attributes must be `DependsAttr`. `DependsAttrBinder` automatically patch all methods with `DependsAttr` by instance attributes. + +`DependsAttr` arguments: +- `method_name` - `str`, name of instance attribute to use as dependency +- `from_super` - `bool`, on true, will use attribute `method_name` from super class like `super().method_name()` +- `use_cache` - `bool`, allow to cache depends result for the same dependencies in request + +#### DependsExt + +Useless(?) class created to proof of concept of patching methods and correct work `FastAPI` applications. + +`DependsExt` allow you define default values of method arguments after `FastAPI` endpoint has been created. + +Example: +``` +from fastapi import FastAPI +from fastapi import Query + +from fastapi_depends_ext import DependsExt + + +def pagination(page: int = Query()): + return page + + +depends = DependsExt(pagination) + + +app = FastAPI() + + +@app.on_event("startup") +def setup_depends(): + depends.bind(page=Query(1)) + + +@app.get("/") +def get_method(value: int = depends) -> int: + return value + +``` + +Is equivalent for +``` +from fastapi import Depends +from fastapi import FastAPI +from fastapi import Query + + +def pagination(page: int = Query(1)): + return page + + +app = FastAPI() + + +@app.get("/") +def get_method(value: int = Depends(pagination)) -> int: + return value + +``` \ No newline at end of file diff --git a/fastapi_depends_ext/__init__.py b/fastapi_depends_ext/__init__.py index 2685a48..5a236f2 100644 --- a/fastapi_depends_ext/__init__.py +++ b/fastapi_depends_ext/__init__.py @@ -1,3 +1,3 @@ from .depends import DependsExt -from .depends import DependsMethod -from .depends import DependsMethodBinder +from .depends import DependsAttr +from .depends import DependsAttrBinder diff --git a/fastapi_depends_ext/depends.py b/fastapi_depends_ext/depends.py index ea49507..47381e3 100644 --- a/fastapi_depends_ext/depends.py +++ b/fastapi_depends_ext/depends.py @@ -1,3 +1,4 @@ +import functools import inspect from typing import Any from typing import Callable @@ -14,55 +15,63 @@ SUPPORTED_DEPENDS = Union[Callable[..., Any], FieldInfo, params.Depends] -SPECIAL_METHODS: Final = ("__call__", "__init__", "__new__") +SPECIAL_METHODS_ERROR: Final = ("__call__",) +SPECIAL_METHODS_IGNORE: Final = ("__init__", "__new__") -class DependsMethodBinder: +class DependsAttrBinder: def __init__(self, *args, **kwargs): - super(DependsMethodBinder, self).__init__(*args, **kwargs) + super(DependsAttrBinder, self).__init__(*args, **kwargs) methods = inspect.getmembers(self, predicate=inspect.ismethod) functions = inspect.getmembers(self, predicate=inspect.isfunction) for method_name, method in methods + functions: + if method_name in SPECIAL_METHODS_IGNORE: + continue + signature = get_typed_signature(method) - values_is_depends_method = [ - param.default for param in signature.parameters.values() if isinstance(param.default, DependsMethod) + values_is_depends_attr = [ + param.default for param in signature.parameters.values() if isinstance(param.default, DependsAttr) ] - if not values_is_depends_method: + if not values_is_depends_attr: continue - if method_name in SPECIAL_METHODS: + if method_name in SPECIAL_METHODS_ERROR: class_method = f"{type(self).__name__}.{method.__name__}" - raise AttributeError(f"`{class_method}` can't have `DependsMethod` as default value for arguments") + raise AttributeError(f"`{class_method}` can't have `DependsAttr` as default value for arguments") self.bind(method) def bind(self, method: Callable) -> Callable: - def depends_method_bind(depends: DependsMethod, _base_class: type, instance) -> DependsMethod: + def depends_attr_bind(depends: DependsAttr, _base_class: type, instance) -> DependsAttr: - # todo: DependsMethod.__copy__ - depends_copy = DependsMethod( + # todo: DependsAttr.__copy__ + depends_copy = DependsAttr( method_name=depends.method_name, from_super=depends.from_super, use_cache=depends.use_cache, ) - dependency = depends_method_get_method(depends, _base_class, instance) - depends_copy.dependency = self.bind(dependency) + method_definition = getattr(_base_class, depends.method_name) + if isinstance(method_definition, property): + depends_copy.dependency = functools.partial(method_definition.fget, instance) + else: + dependency = depends_attr_get_method(depends, _base_class, instance) + depends_copy.dependency = self.bind(dependency) return depends_copy - def depends_method_get_method(depends: DependsMethod, _base_class: type, instance) -> Callable: - # todo: DependsMethod.get_method + def depends_attr_get_method(depends: DependsAttr, _base_class: type, instance) -> Callable: + # todo: DependsAttr.get_method obj = super(_base_class, instance) if depends.from_super else instance return getattr(obj, depends.method_name) - base_class = get_base_class(method) + base_class = get_base_class(self, method.__name__, method) signature = get_typed_signature(method) - parameters = (param for param in signature.parameters.values() if isinstance(param.default, DependsMethod)) + parameters = (param for param in signature.parameters.values() if isinstance(param.default, DependsAttr)) instance_method_params = { - parameter.name: depends_method_bind(parameter.default, base_class, self) for parameter in parameters + parameter.name: depends_attr_bind(parameter.default, base_class, self) for parameter in parameters } if instance_method_params: @@ -91,9 +100,9 @@ def bind(self, **kwargs: SUPPORTED_DEPENDS) -> "DependsExt": return DependsExt(patched, use_cache=self.use_cache) -class DependsMethod(DependsExt): +class DependsAttr(DependsExt): def __init__(self, method_name: str, *, from_super: bool = False, use_cache=True): - super(DependsMethod, self).__init__(use_cache=use_cache) + super(DependsAttr, self).__init__(use_cache=use_cache) self.from_super = from_super self.method_name = method_name @@ -119,12 +128,12 @@ def bind(self, instance, super_from: type = None): cls_name = f"super({cls_name}, instance)" raise AttributeError(f"{cls_name} has not method `{self.method_name}`") - cls = get_base_class(method) + cls = get_base_class(instance, self.method_name, method) signature = get_typed_signature(method) for parameter in signature.parameters.values(): - depends: DependsMethod = parameter.default - if not isinstance(depends, DependsMethod) or depends.is_bound: + depends: DependsAttr = parameter.default + if not isinstance(depends, DependsAttr) or depends.is_bound: continue elif depends.method_name != self.method_name or depends.from_super: diff --git a/fastapi_depends_ext/utils.py b/fastapi_depends_ext/utils.py index 199ee77..5cf014b 100644 --- a/fastapi_depends_ext/utils.py +++ b/fastapi_depends_ext/utils.py @@ -4,22 +4,32 @@ from types import FunctionType from types import MethodType from typing import Callable +from typing import Optional from fastapi.dependencies.utils import get_typed_signature -def get_base_class(method: Callable) -> type: - if inspect.ismethod(method): - for cls in inspect.getmro(method.__self__.__class__): - if cls.__dict__.get(method.__name__) is method.__func__: - return cls - method = method.__func__ # fallback to __qualname__ parsing +def _get_func(instance, func) -> callable: + if type(func) is property: + return _get_func(instance, func.fget(instance)) + elif type(func) in (classmethod, staticmethod) or inspect.ismethod(func): + return func.__func__ + elif callable(func): + return func + else: + raise TypeError(f"Incorrect type of `{func}`") - if inspect.isfunction(method): - cls = getattr(inspect.getmodule(method), method.__qualname__.split(".", 1)[0].rsplit(".", 1)[0]) - if isinstance(cls, type): + +def get_base_class(instance: object, method_name: str, method_target: [type, Callable]) -> Optional[type]: + for cls in inspect.getmro(type(instance)): + method_cls = cls.__dict__.get(method_name) + if method_cls is None: # check to None cause can be not callable like property object (not property value) + continue + + _func_class = _get_func(instance, method_cls) + _func_target = _get_func(instance, method_target) + if _func_class is _func_target: return cls - return getattr(method, "__objclass__", None) # handle special descriptor objects def get_super_for_method(base_class: type, method_name: str, super_from: type = None) -> type: diff --git a/poetry.lock b/poetry.lock index 9b17549..489a84d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -72,6 +72,20 @@ category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +[[package]] +name = "coverage" +version = "7.0.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "exceptiongroup" version = "1.1.0" @@ -85,21 +99,21 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.70.0" +version = "0.89.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.16.0" +starlette = "0.22.0" [package.extras] -all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] -dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] -test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] +all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.8.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.6.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] [[package]] name = "idna" @@ -111,11 +125,11 @@ python-versions = ">=3.5" [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [[package]] name = "mypy-extensions" @@ -135,7 +149,7 @@ python-versions = ">=3.5" [[package]] name = "packaging" -version = "22.0" +version = "23.0" description = "Core utilities for Python packages" category = "dev" optional = false @@ -151,15 +165,15 @@ python-versions = ">=3.7" [[package]] name = "platformdirs" -version = "2.6.0" +version = "2.6.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.4)", "sphinx (>=5.3)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx (>=5.3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"] [[package]] name = "pluggy" @@ -175,14 +189,14 @@ dev = ["tox", "pre-commit"] [[package]] name = "pydantic" -version = "1.10.2" +version = "1.10.4" description = "Data validation and settings management using python type hints" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.2.0" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] @@ -190,7 +204,7 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pytest" -version = "7.2.0" +version = "7.2.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -208,6 +222,21 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "4.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + [[package]] name = "pytest-mock" version = "3.10.0" @@ -232,17 +261,18 @@ python-versions = ">=3.7" [[package]] name = "starlette" -version = "0.16.0" +version = "0.22.0" description = "The little ASGI library that shines." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -anyio = ">=3.0.0,<4" +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] -full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "graphene"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] [[package]] name = "tomli" @@ -262,8 +292,8 @@ python-versions = ">=3.7" [metadata] lock-version = "1.1" -python-versions = "^3.8" -content-hash = "81744edbcc857df83d65aac2875494cd760c42e64ece1bdb3fbafb302726612d" +python-versions = ">=3.8,<4.0" +content-hash = "37124fc3a128355cb68b26c84f4a07e91769c43dfdd9aa2355fed497a3000ac0" [metadata.files] anyio = [] @@ -274,6 +304,7 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [] +coverage = [] exceptiongroup = [] fastapi = [] idna = [] @@ -289,6 +320,7 @@ platformdirs = [] pluggy = [] pydantic = [] pytest = [] +pytest-cov = [] pytest-mock = [] sniffio = [] starlette = [] diff --git a/pyproject.toml b/pyproject.toml index 1c6c26e..a945418 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fastapi-depends-ext" -version = "0.1.4" +version = "0.2.0" description = "Extends FastAPI Depends classes to simple way of modifying them after creating" authors = ["Nikakto "] @@ -13,6 +13,7 @@ pytest = "^7.2.0" black = "^22.12.0" nest-asyncio = "^1.5.6" pytest-mock = "^3.10.0" +pytest-cov = "^4.0.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/depends/depends_method/__init__.py b/tests/depends/depends_attr/__init__.py similarity index 100% rename from tests/depends/depends_method/__init__.py rename to tests/depends/depends_attr/__init__.py diff --git a/tests/depends/depends_method/test_bind.py b/tests/depends/depends_attr/test_bind.py similarity index 60% rename from tests/depends/depends_method/test_bind.py rename to tests/depends/depends_attr/test_bind.py index 7a1b03e..cff131d 100644 --- a/tests/depends/depends_method/test_bind.py +++ b/tests/depends/depends_attr/test_bind.py @@ -1,22 +1,38 @@ import re from typing import Any +from typing import Callable import pytest -from fastapi_depends_ext.depends import DependsMethod +from fastapi_depends_ext.depends import DependsAttr from tests.utils_for_tests import SimpleDependency def test_bind__attribute_not_exist__error(): instance = object() - depends = DependsMethod("method") + depends = DependsAttr("method") with pytest.raises(AttributeError): depends.bind(instance) +def test_bind__dependence_depends_already_bounded__do_nothing(mocker): + depends = DependsAttr("dependency") + depends.dependency = object + assert depends.is_bound + + class TestClass(SimpleDependency): + def method(self, depends_method: Any = depends): + pass + + instance = TestClass() + depends.bind(instance) + + assert depends.dependency is object + + def test_bind__dependence_depends_instance_method__dependency_has_been_set(): - depends = DependsMethod("dependency") + depends = DependsAttr("dependency") class TestClass(SimpleDependency): def method(self, depends_method: Any = depends): @@ -28,8 +44,101 @@ def method(self, depends_method: Any = depends): assert depends.dependency.__func__ is TestClass.dependency +def test_bind__dependence_depends_static_method__dependency_has_been_set(): + depends = DependsAttr("static_method") + + class TestClass(SimpleDependency): + @staticmethod + def static_method() -> int: + pass + + def method(self, depends_method: Any = depends): + pass + + instance = TestClass() + depends.bind(instance) + + assert depends.dependency is TestClass.static_method + + +def test_bind__dependence_depends_class_method__dependency_has_been_set(): + depends = DependsAttr("class_method") + + class TestClass(SimpleDependency): + @classmethod + def class_method(cls) -> int: + pass + + def method(self, depends_method: Any = depends): + pass + + instance = TestClass() + depends.bind(instance) + + assert depends.dependency.__func__ is TestClass.class_method.__func__ + + +def test_bind__dependence_depends_property__dependency_has_been_set(): + depends = DependsAttr("property_callable") + + def function(): + return 1 + + class TestClass(SimpleDependency): + @property + def property_callable(self) -> Callable: + return function + + def method(self, depends_method: Any = depends): + pass + + instance = TestClass() + depends.bind(instance) + + assert depends.dependency is function + + +def test_bind__dependence_depends_property_type__dependency_has_been_set(): + depends = DependsAttr("dependency") + + class Dependency: + def __init__(self): + pass + + class TestClass: + dependency = Dependency + + def method(self, depends_method: Any = depends): + pass + + instance = TestClass() + depends.bind(instance) + + assert depends.dependency is Dependency + + +def test_bind__dependence_depends_property_instance__dependency_has_been_set(): + depends = DependsAttr("property") + + class CallableClass: + def __call__(self): + pass + + class TestClass(SimpleDependency): + property = CallableClass() + + def method(self, depends_method: Any = depends): + pass + + instance = TestClass() + depends.bind(instance) + + assert isinstance(depends.dependency, CallableClass) + assert depends.dependency.__call__.__func__ is CallableClass.__call__ + + def test_bind__dependence_recursive__error(): - depends = DependsMethod("method") + depends = DependsAttr("method") class TestClass: def method(self, depends_method: Any = depends): @@ -43,7 +152,7 @@ def method(self, depends_method: Any = depends): def test_bind__dependence_depends_method__dependency_has_been_set(): - depends_bounded = DependsMethod("depends_bounded") + depends_bounded = DependsAttr("depends_bounded") depends_bounded.dependency = lambda x: None class TestClass: @@ -51,7 +160,7 @@ def method_bounded(self, depends_method: int = depends_bounded): pass instance = TestClass() - depends = DependsMethod("method_bounded") + depends = DependsAttr("method_bounded") depends.bind(instance) assert depends.dependency.__func__ is TestClass.method_bounded @@ -66,7 +175,7 @@ def dependency(self): pass instance = TestClass() - depends = DependsMethod("method", from_super=True) + depends = DependsAttr("method", from_super=True) message = f"super(TestClass, instance) has not method `method`" with pytest.raises(AttributeError, match=re.escape(message)): @@ -74,7 +183,7 @@ def dependency(self): def test_bind__dependence_depends_from_super__dependency_has_been_set(): - depends = DependsMethod("dependency", from_super=True) + depends = DependsAttr("dependency", from_super=True) class TestClass(SimpleDependency): def dependency(self, depends_method: Any = depends): @@ -87,8 +196,8 @@ def dependency(self, depends_method: Any = depends): def test_bind__dependence_depends_from_super_deep_method__dependency_has_been_set(): - depends = DependsMethod("dependency", from_super=True) - depends_mixin = DependsMethod("dependency", from_super=True) + depends = DependsAttr("dependency", from_super=True) + depends_mixin = DependsAttr("dependency", from_super=True) class MixinClass: def dependency(self, depends_method: Any = depends_mixin): @@ -106,18 +215,18 @@ def dependency(self, depends_method: Any = depends): def test_bind__dependence_depends_from_super_another_method__dependency_has_been_set(): - depends = DependsMethod("method", from_super=True) - depends_mixin = DependsMethod("dependency", from_super=True) + depends = DependsAttr("method", from_super=True) + depends_mixin = DependsAttr("dependency", from_super=True) class MixinClass(SimpleDependency): def dependency(self): pass - def method(self, depends_method: Any = depends_mixin): # DependsMethod("dependency", from_super=True) + def method(self, depends_method: Any = depends_mixin): # DependsAttr("dependency", from_super=True) pass class TestClass(MixinClass, SimpleDependency): - def method(self, depends_method: Any = depends): # DependsMethod("method", from_super=True) + def method(self, depends_method: Any = depends): # DependsAttr("method", from_super=True) pass instance = TestClass() @@ -128,15 +237,15 @@ def method(self, depends_method: Any = depends): # DependsMethod("method", from def test_bind__dependence_depends_another_method_with_depends_super__dependency_has_been_set(): - depends = DependsMethod("dependency") - depends_mixin = DependsMethod("dependency", from_super=True) + depends = DependsAttr("dependency") + depends_mixin = DependsAttr("dependency", from_super=True) class MixinClass(SimpleDependency): - def dependency(self, depends_method: Any = depends_mixin): # DependsMethod("dependency", from_super=True) + def dependency(self, depends_method: Any = depends_mixin): # DependsAttr("dependency", from_super=True) pass class TestClass(MixinClass): - def method(self, depends_method: Any = depends): # DependsMethod("dependency") + def method(self, depends_method: Any = depends): # DependsAttr("dependency") pass instance = TestClass() @@ -147,14 +256,14 @@ def method(self, depends_method: Any = depends): # DependsMethod("dependency") def test_bind__dependence_recursive_deep__error(): - depends = DependsMethod("method", from_super=True) + depends = DependsAttr("method", from_super=True) class BaseClass: - def method(self, depends_method: Any = DependsMethod("method")): + def method(self, depends_method: Any = DependsAttr("method")): pass class TestClass(BaseClass): - def method(self, depends_method: Any = depends): # DependsMethod("method", from_super=True) + def method(self, depends_method: Any = depends): # DependsAttr("method", from_super=True) pass instance = TestClass() @@ -165,8 +274,8 @@ def method(self, depends_method: Any = depends): # DependsMethod("method", from def test_bind__dependence_has_unbounded_depends_method__bind_all(): - depends = DependsMethod("method_unbounded") - depends_unbounded = DependsMethod("dependency") + depends = DependsAttr("method_unbounded") + depends_unbounded = DependsAttr("dependency") class TestClass(SimpleDependency): def method_unbounded(self, depends_method: int = depends_unbounded): @@ -181,7 +290,7 @@ def method_unbounded(self, depends_method: int = depends_unbounded): def test_bind__dependence_has_unbounded_chained__bind_all(): - depends = [DependsMethod("method_2"), DependsMethod("method_3"), DependsMethod("dependency")] + depends = [DependsAttr("method_2"), DependsAttr("method_3"), DependsAttr("dependency")] class TestClass(SimpleDependency): def method_1(self, depends_method: Any = depends[0]): @@ -203,7 +312,7 @@ def method_3(self, depends_method: Any = depends[2]): def test_bind__methods_chained_recursive__error(): - depends = [DependsMethod("method_2"), DependsMethod("method_1")] + depends = [DependsAttr("method_2"), DependsAttr("method_1")] class TestClass(SimpleDependency): def method_1(self, depends_method: Any = depends[0]): @@ -219,7 +328,7 @@ def method_2(self, depends_method: Any = depends[1]): def test_bind__methods_chained_deep_recursive__error(): - depends = [DependsMethod("method_2"), DependsMethod("method_3"), DependsMethod("method_1")] + depends = [DependsAttr("method_2"), DependsAttr("method_3"), DependsAttr("method_1")] class TestClass(SimpleDependency): def method_1(self, depends_method: Any = depends[0]): @@ -238,13 +347,13 @@ def method_3(self, depends_method: Any = depends[2]): def test_bind__has_method_depends_super__bind_all(): - depends = [DependsMethod("dependency"), DependsMethod("dependency", from_super=True)] + depends = [DependsAttr("dependency"), DependsAttr("dependency", from_super=True)] class TestClass(SimpleDependency): - def method(self, depends_method: Any = depends[0]): # DependsMethod("dependency") + def method(self, depends_method: Any = depends[0]): # DependsAttr("dependency") pass - def dependency(self, depends_method: Any = depends[1]): # DependsMethod("dependency", from_super=True) + def dependency(self, depends_method: Any = depends[1]): # DependsAttr("dependency", from_super=True) pass instance = TestClass() @@ -256,18 +365,18 @@ def dependency(self, depends_method: Any = depends[1]): # DependsMethod("depend def test_bind__has_method_depends_super_chained__bind_all(): - depends = [DependsMethod("method_1", from_super=True), DependsMethod("dependency")] - depends_super = DependsMethod("method_2") + depends = [DependsAttr("method_1", from_super=True), DependsAttr("dependency")] + depends_super = DependsAttr("method_2") class BaseClass: - def method_1(self, depends_method: Any = depends_super): # DependsMethod("method_2") + def method_1(self, depends_method: Any = depends_super): # DependsAttr("method_2") pass class TestClass(SimpleDependency, BaseClass): - def method_1(self, depends_method: Any = depends[0]): # DependsMethod("method_1", from_super=True) + def method_1(self, depends_method: Any = depends[0]): # DependsAttr("method_1", from_super=True) pass - def method_2(self, depends_method: Any = depends[1]): # DependsMethod("dependency") + def method_2(self, depends_method: Any = depends[1]): # DependsAttr("dependency") pass instance = TestClass() diff --git a/tests/depends/depends_method_binder/__init__.py b/tests/depends/depends_attr_binder/__init__.py similarity index 100% rename from tests/depends/depends_method_binder/__init__.py rename to tests/depends/depends_attr_binder/__init__.py diff --git a/tests/depends/depends_attr_binder/test_bind.py b/tests/depends/depends_attr_binder/test_bind.py new file mode 100644 index 0000000..5fc6313 --- /dev/null +++ b/tests/depends/depends_attr_binder/test_bind.py @@ -0,0 +1,288 @@ +import unittest.mock +from typing import Any + +from fastapi import Depends +from fastapi.dependencies.utils import get_typed_signature + +from fastapi_depends_ext.depends import DependsAttr +from fastapi_depends_ext.depends import DependsAttrBinder +from tests.utils_for_tests import SimpleDependency + + +def test_bind__method_has_no_depends__method_not_change(mocker): + class TestClass(DependsAttrBinder): + def method(self, arg=1, *args, kwarg=2, **kwargs): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.method) + + assert instance.method.__self__ is instance + assert instance.method.__func__ is TestClass.method + + +def test_bind__method_has_depends__method_not_change(mocker): + depends = Depends(SimpleDependency.dependency) + + class TestClass(SimpleDependency, DependsAttrBinder): + def method(self, depends: Any = depends): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.method) + + assert instance.method.__self__ is instance + assert instance.method.__func__ is TestClass.method + assert instance.method.__func__.__code__ is TestClass.method.__code__ + + signature = get_typed_signature(instance.method) + assert signature.parameters["depends"].default is depends + + +def test_bind__method_has_depends_method__method_changed(mocker): + depends = DependsAttr("dependency") + + class TestClass(SimpleDependency, DependsAttrBinder): + def method(self, depends_attr: Any = depends): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.method) + + assert instance.method.__self__ is instance + assert instance.method.__func__ is not TestClass.method + assert instance.method.__func__.__code__ is TestClass.method.__code__ + + signature = get_typed_signature(instance.method) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends + assert depends_attr.default.is_bound + assert depends_attr.default.dependency.__func__ is TestClass.dependency + + +def test_bind__method_has_depends_class_method__method_change(mocker): + depends = DependsAttr("dependency") + + class TestClass(DependsAttrBinder): + @classmethod + def dependency(cls): + pass + + def method(self, depends_attr: Any = depends): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.method) + + assert instance.method.__self__ is instance + assert instance.method.__func__ is not TestClass.method + assert instance.method.__func__.__code__ is TestClass.method.__code__ + + signature = get_typed_signature(instance.method) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends + assert depends_attr.default.is_bound + assert depends_attr.default.dependency.__func__ is TestClass.dependency.__func__ + + +def test_bind__method_has_depends_static_method__method_change(mocker): + depends = DependsAttr("dependency") + + class TestClass(DependsAttrBinder): + @staticmethod + def dependency(): + pass + + def method(self, depends_attr: Any = depends): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.method) + + assert instance.method.__self__ is instance + assert instance.method.__func__ is not TestClass.method + assert instance.method.__func__.__code__ is TestClass.method.__code__ + + signature = get_typed_signature(instance.method) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends + assert depends_attr.default.is_bound + assert depends_attr.default.dependency is TestClass.dependency + + +def test_bind__method_has_depends_property__method_change(mocker): + depends = DependsAttr("dependency") + + def real_dependency(): + return 1 + + class TestClass(DependsAttrBinder): + @property + def dependency(self): + return real_dependency + + def method(self, depends_attr: Any = depends): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.method) + + assert instance.method.__self__ is instance + assert instance.method.__func__ is not TestClass.method + assert instance.method.__func__.__code__ is TestClass.method.__code__ + + signature = get_typed_signature(instance.method) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends + assert depends_attr.default.is_bound + assert depends_attr.default.dependency is not real_dependency + assert depends_attr.default.dependency() is real_dependency + + +def test_bind__method_has_depends_class__method_change(mocker): + depends = DependsAttr("dependency") + + class Dependency: + pass + + class TestClass(DependsAttrBinder): + dependency = Dependency + + def method(self, depends_attr: Any = depends): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.method) + + assert instance.method.__self__ is instance + assert instance.method.__func__ is not TestClass.method + assert instance.method.__func__.__code__ is TestClass.method.__code__ + + signature = get_typed_signature(instance.method) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends + assert depends_attr.default.is_bound + assert depends_attr.default.dependency is Dependency + + +def test_bind__method_has_depends_method_chained__method_changed_all(mocker): + depends = [DependsAttr("dependency"), DependsAttr("method_1")] + + class TestClass(SimpleDependency, DependsAttrBinder): + def method_1(self, depends_attr: Any = depends[0]): + pass + + def method_2(self, depends_attr: Any = depends[1]): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.method_2) + + # method_2 + assert instance.method_2.__self__ is instance + assert instance.method_2.__func__ is not TestClass.method_2 + assert instance.method_2.__func__.__code__ is TestClass.method_2.__code__ + + signature = get_typed_signature(instance.method_2) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends[1] + assert depends_attr.default.is_bound + assert depends_attr.default.dependency.__self__ is instance + assert depends_attr.default.dependency.__func__ is instance.method_1.__func__ + assert depends_attr.default.dependency.__func__.__code__ is instance.method_1.__func__.__code__ + + # method_1 + signature = get_typed_signature(instance.method_1) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends[0] + assert depends_attr.default.is_bound + assert depends_attr.default.dependency.__self__ is instance + assert depends_attr.default.dependency.__func__ is TestClass.dependency + + +def test_bind__method_has_depends_method_chained_to_super__method_changed_all(mocker): + depends = DependsAttr("dependency", from_super=True) + + class BaseClass(SimpleDependency): + pass + + class TestClass(BaseClass, DependsAttrBinder): + def dependency(self, depends_attr: Any = depends): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.dependency) + + # TestClass.dependency + assert instance.dependency.__self__ is instance + assert instance.dependency.__func__ is not TestClass.dependency + assert instance.dependency.__func__.__code__ is TestClass.dependency.__code__ + + signature = get_typed_signature(instance.dependency) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends + assert depends_attr.default.is_bound + assert depends_attr.default.dependency.__self__ is instance + assert depends_attr.default.dependency.__func__ is BaseClass.dependency + + +def test_bind__method_has_depends_method_chained_to_super_deep__method_changed_all(mocker): + depends = DependsAttr("dependency", from_super=True) + depends_mixin = DependsAttr("dependency", from_super=True) + + class BaseClass(SimpleDependency): + def dependency(self, depends_attr: Any = depends_mixin): + pass + + class TestClass(BaseClass, DependsAttrBinder): + def dependency(self, depends_attr: Any = depends): + pass + + with mocker.patch.object(DependsAttrBinder, "__init__", unittest.mock.MagicMock(return_value=None)): + instance = TestClass() + instance.bind(instance.dependency) + + # TestClass.dependency + assert instance.dependency.__self__ is instance + assert instance.dependency.__func__ is not TestClass.dependency + assert instance.dependency.__func__.__code__ is TestClass.dependency.__code__ + + signature = get_typed_signature(instance.dependency) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends + assert depends_attr.default.is_bound + assert depends_attr.default.dependency.__self__ is instance + assert depends_attr.default.dependency.__func__ is not BaseClass.dependency + assert depends_attr.default.dependency.__func__.__code__ is BaseClass.dependency.__code__ + + # Mixin.dependency + signature = get_typed_signature(depends_attr.default.dependency) + depends_attr = signature.parameters["depends_attr"] + assert isinstance(depends_attr.default, DependsAttr) + assert depends_attr.default is not depends + assert depends_attr.default.is_bound + assert depends_attr.default.dependency.__self__ is instance + assert depends_attr.default.dependency.__func__ is SimpleDependency.dependency + + +# todo +# def test_bind__multiple_method_depends_one_method__depends_is_one_object(): +# raise ValueError() diff --git a/tests/depends/depends_attr_binder/test_init.py b/tests/depends/depends_attr_binder/test_init.py new file mode 100644 index 0000000..fd96570 --- /dev/null +++ b/tests/depends/depends_attr_binder/test_init.py @@ -0,0 +1,141 @@ +import re + +import pytest +from fastapi import Depends + +from fastapi_depends_ext import DependsExt +from fastapi_depends_ext import DependsAttr +from fastapi_depends_ext import DependsAttrBinder +from tests.utils_for_tests import SimpleDependency + + +def function_dependency(): + return 1 + + +def test_init__no_methods_with_depends_attrs__do_nothing(mocker): + class TestClass(DependsAttrBinder): + def method_no_depends(self): + pass + + spy_bind = mocker.spy(TestClass, "bind") + TestClass() + + assert not spy_bind.called + + +@pytest.mark.parametrize("dependency", [Depends(function_dependency), DependsExt(function_dependency)]) +def test_init__method_with_depends__do_nothing(mocker, dependency): + class TestClass(DependsAttrBinder): + def method_with_depends(self, arg: int = dependency): + pass + + spy_bind = mocker.spy(TestClass, "bind") + TestClass() + + assert not spy_bind.called + + +def test_init__methods_with_depends_attr__all_have_been_patched(mocker): + class TestClass(SimpleDependency, DependsAttrBinder): + def method_with_depends(self, arg: int = Depends(function_dependency)): + pass + + def method_with_depends_attr_0(self, arg: int = DependsAttr("dependency")): + pass + + def method_with_depends_attr_1(self, arg: int = DependsAttr("dependency")): + pass + + spy_bind = mocker.spy(TestClass, "bind") + instance = TestClass() + + call_args_list_actual = [(_call.args[0], _call.args[1].__func__) for _call in spy_bind.call_args_list] + call_args_list_expected = [ + (instance, TestClass.method_with_depends_attr_0), + (instance, TestClass.dependency), # recursive call from TestClass.method_with_depends_attr_0 + (instance, TestClass.method_with_depends_attr_1), + (instance, TestClass.dependency), # recursive call from TestClass.method_with_depends_attr_1 + ] + + assert call_args_list_actual == call_args_list_expected + + +def test_init__class_methods_with_depends_attr__all_have_been_patched(mocker): + class TestClass(SimpleDependency, DependsAttrBinder): + def method_with_depends(self, arg: int = Depends(function_dependency)): + pass + + @classmethod + def method_with_depends_attr_0(cls, arg: int = DependsAttr("dependency")): + pass + + @classmethod + def method_with_depends_attr_1(cls, arg: int = DependsAttr("dependency")): + pass + + spy_bind = mocker.spy(TestClass, "bind") + instance = TestClass() + + call_args_list_expected = [ + mocker.call(instance, TestClass.method_with_depends_attr_0), + mocker.call(instance, instance.dependency), # recursive call from TestClass.method_with_depends_attr_0 + mocker.call(instance, TestClass.method_with_depends_attr_1), + mocker.call(instance, instance.dependency), # recursive call from TestClass.method_with_depends_attr_1 + ] + + assert spy_bind.call_args_list == call_args_list_expected + assert instance.method_with_depends_attr_0.__func__.__defaults__[0].dependency.__self__ is instance + assert instance.method_with_depends_attr_1.__func__.__defaults__[0].dependency.__self__ is instance + + +def test_init__static_methods_with_depends_attr__all_have_been_patched(mocker): + class TestClass(SimpleDependency, DependsAttrBinder): + def method_with_depends(self, arg: int = Depends(function_dependency)): + pass + + @staticmethod + def method_with_depends_attr_0(arg: int = DependsAttr("dependency")): + pass + + @staticmethod + def method_with_depends_attr_1(arg: int = DependsAttr("dependency")): + pass + + spy_bind = mocker.spy(TestClass, "bind") + instance = TestClass() + + call_args_list_expected = [ + mocker.call(instance, TestClass.method_with_depends_attr_0), + mocker.call(instance, instance.dependency), # recursive call from TestClass.method_with_depends_attr_0 + mocker.call(instance, TestClass.method_with_depends_attr_1), + mocker.call(instance, instance.dependency), # recursive call from TestClass.method_with_depends_attr_1 + ] + + assert spy_bind.call_args_list == call_args_list_expected + assert instance.method_with_depends_attr_0.__defaults__[0].dependency.__self__ is instance + assert instance.method_with_depends_attr_1.__defaults__[0].dependency.__self__ is instance + + +def test_init__new_init_depends_attr__ignored(mocker): + class TestClass(SimpleDependency, DependsAttrBinder): + def __init__(self, *args, arg: int = DependsAttr("dependency"), **kwargs): + super(TestClass, self).__init__(*args, **kwargs) + + def __new__(cls, *args, arg: int = DependsAttr("dependency"), **kwargs): + return super().__new__(cls, *args, **kwargs) + + spy_bind = mocker.spy(TestClass, "bind") + TestClass() + + assert not spy_bind.called + + +def test_init__call_depends_attr__error(): + class TestClass(SimpleDependency, DependsAttrBinder): + def __call__(self, *args, arg: int = DependsAttr("dependency"), **kwargs): + super(TestClass, self).__init__(*args, **kwargs) + + message = f"`TestClass.__call__` can't have `DependsAttr` as default value for arguments" + with pytest.raises(AttributeError, match=re.escape(message)): + TestClass() diff --git a/tests/depends/depends_method_binder/test_bind.py b/tests/depends/depends_method_binder/test_bind.py deleted file mode 100644 index ad37970..0000000 --- a/tests/depends/depends_method_binder/test_bind.py +++ /dev/null @@ -1,233 +0,0 @@ -import unittest.mock -from typing import Any - -from fastapi import Depends -from fastapi.dependencies.utils import get_typed_signature - -from fastapi_depends_ext.depends import DependsMethod -from fastapi_depends_ext.depends import DependsMethodBinder - - -class SimpleDependency: - def dependency(self) -> int: - return 2 - - -def test_bind__method_has_no_depends__method_not_change(mocker): - class TestClass(DependsMethodBinder): - def method(self, arg=1, *args, kwarg=2, **kwargs): - pass - - with mocker.patch.object(DependsMethodBinder, "__init__", unittest.mock.MagicMock(return_value=None)): - instance = TestClass() - instance.bind(instance.method) - - assert instance.method.__self__ is instance - assert instance.method.__func__ is TestClass.method - - -def test_bind__method_has_depends__method_not_change(mocker): - depends = Depends(SimpleDependency.dependency) - - class TestClass(SimpleDependency, DependsMethodBinder): - def method(self, depends: Any = depends): - pass - - with mocker.patch.object(DependsMethodBinder, "__init__", unittest.mock.MagicMock(return_value=None)): - instance = TestClass() - instance.bind(instance.method) - - assert instance.method.__self__ is instance - assert instance.method.__func__ is TestClass.method - assert instance.method.__func__.__code__ is TestClass.method.__code__ - - signature = get_typed_signature(instance.method) - assert signature.parameters["depends"].default is depends - - -def test_bind__method_has_depends_method__method_changed(mocker): - depends = DependsMethod("dependency") - - class TestClass(SimpleDependency, DependsMethodBinder): - def method(self, depends_method: Any = depends): - pass - - with mocker.patch.object(DependsMethodBinder, "__init__", unittest.mock.MagicMock(return_value=None)): - instance = TestClass() - instance.bind(instance.method) - - assert instance.method.__self__ is instance - assert instance.method.__func__ is not TestClass.method - assert instance.method.__func__.__code__ is TestClass.method.__code__ - - signature = get_typed_signature(instance.method) - depends_method = signature.parameters["depends_method"] - assert isinstance(depends_method.default, DependsMethod) - assert depends_method.default is not depends - assert depends_method.default.is_bound - assert depends_method.default.dependency.__func__ is TestClass.dependency - - -def test_bind__method_has_depends_class_method__method_change(mocker): - depends = DependsMethod("dependency") - - class TestClass(DependsMethodBinder): - @classmethod - def dependency(cls): - pass - - def method(self, depends_method: Any = depends): - pass - - with mocker.patch.object(DependsMethodBinder, "__init__", unittest.mock.MagicMock(return_value=None)): - instance = TestClass() - instance.bind(instance.method) - - assert instance.method.__self__ is instance - assert instance.method.__func__ is not TestClass.method - assert instance.method.__func__.__code__ is TestClass.method.__code__ - - signature = get_typed_signature(instance.method) - depends_method = signature.parameters["depends_method"] - assert isinstance(depends_method.default, DependsMethod) - assert depends_method.default is not depends - assert depends_method.default.is_bound - assert depends_method.default.dependency.__func__ is TestClass.dependency.__func__ - - -def test_bind__method_has_depends_static_method__method_change(mocker): - depends = DependsMethod("dependency") - - class TestClass(DependsMethodBinder): - @staticmethod - def dependency(): - pass - - def method(self, depends_method: Any = depends): - pass - - with mocker.patch.object(DependsMethodBinder, "__init__", unittest.mock.MagicMock(return_value=None)): - instance = TestClass() - instance.bind(instance.method) - - assert instance.method.__self__ is instance - assert instance.method.__func__ is not TestClass.method - assert instance.method.__func__.__code__ is TestClass.method.__code__ - - signature = get_typed_signature(instance.method) - depends_method = signature.parameters["depends_method"] - assert isinstance(depends_method.default, DependsMethod) - assert depends_method.default is not depends - assert depends_method.default.is_bound - assert depends_method.default.dependency is TestClass.dependency - - -def test_bind__method_has_depends_method_chained__method_changed_all(mocker): - depends = [DependsMethod("dependency"), DependsMethod("method_1")] - - class TestClass(SimpleDependency, DependsMethodBinder): - def method_1(self, depends_method: Any = depends[0]): - pass - - def method_2(self, depends_method: Any = depends[1]): - pass - - with mocker.patch.object(DependsMethodBinder, "__init__", unittest.mock.MagicMock(return_value=None)): - instance = TestClass() - instance.bind(instance.method_2) - - # method_2 - assert instance.method_2.__self__ is instance - assert instance.method_2.__func__ is not TestClass.method_2 - assert instance.method_2.__func__.__code__ is TestClass.method_2.__code__ - - signature = get_typed_signature(instance.method_2) - depends_method = signature.parameters["depends_method"] - assert isinstance(depends_method.default, DependsMethod) - assert depends_method.default is not depends[1] - assert depends_method.default.is_bound - assert depends_method.default.dependency.__self__ is instance - assert depends_method.default.dependency.__func__ is instance.method_1.__func__ - assert depends_method.default.dependency.__func__.__code__ is instance.method_1.__func__.__code__ - - # method_1 - signature = get_typed_signature(instance.method_1) - depends_method = signature.parameters["depends_method"] - assert isinstance(depends_method.default, DependsMethod) - assert depends_method.default is not depends[0] - assert depends_method.default.is_bound - assert depends_method.default.dependency.__self__ is instance - assert depends_method.default.dependency.__func__ is TestClass.dependency - - -def test_bind__method_has_depends_method_chained_to_super__method_changed_all(mocker): - depends = DependsMethod("dependency", from_super=True) - - class BaseClass(SimpleDependency): - pass - - class TestClass(BaseClass, DependsMethodBinder): - def dependency(self, depends_method: Any = depends): - pass - - with mocker.patch.object(DependsMethodBinder, "__init__", unittest.mock.MagicMock(return_value=None)): - instance = TestClass() - instance.bind(instance.dependency) - - # TestClass.dependency - assert instance.dependency.__self__ is instance - assert instance.dependency.__func__ is not TestClass.dependency - assert instance.dependency.__func__.__code__ is TestClass.dependency.__code__ - - signature = get_typed_signature(instance.dependency) - depends_method = signature.parameters["depends_method"] - assert isinstance(depends_method.default, DependsMethod) - assert depends_method.default is not depends - assert depends_method.default.is_bound - assert depends_method.default.dependency.__self__ is instance - assert depends_method.default.dependency.__func__ is BaseClass.dependency - - -def test_bind__method_has_depends_method_chained_to_super_deep__method_changed_all(mocker): - depends = DependsMethod("dependency", from_super=True) - depends_mixin = DependsMethod("dependency", from_super=True) - - class BaseClass(SimpleDependency): - def dependency(self, depends_method: Any = depends_mixin): - pass - - class TestClass(BaseClass, DependsMethodBinder): - def dependency(self, depends_method: Any = depends): - pass - - with mocker.patch.object(DependsMethodBinder, "__init__", unittest.mock.MagicMock(return_value=None)): - instance = TestClass() - instance.bind(instance.dependency) - - # TestClass.dependency - assert instance.dependency.__self__ is instance - assert instance.dependency.__func__ is not TestClass.dependency - assert instance.dependency.__func__.__code__ is TestClass.dependency.__code__ - - signature = get_typed_signature(instance.dependency) - depends_method = signature.parameters["depends_method"] - assert isinstance(depends_method.default, DependsMethod) - assert depends_method.default is not depends - assert depends_method.default.is_bound - assert depends_method.default.dependency.__self__ is instance - assert depends_method.default.dependency.__func__ is not BaseClass.dependency - assert depends_method.default.dependency.__func__.__code__ is BaseClass.dependency.__code__ - - # Mixin.dependency - signature = get_typed_signature(depends_method.default.dependency) - depends_method = signature.parameters["depends_method"] - assert isinstance(depends_method.default, DependsMethod) - assert depends_method.default is not depends - assert depends_method.default.is_bound - assert depends_method.default.dependency.__self__ is instance - assert depends_method.default.dependency.__func__ is SimpleDependency.dependency - - -# todo -# def test_bind__multiple_method_depends_one_method__depends_is_one_object(): -# raise ValueError() diff --git a/tests/utils/test_get_base_class.py b/tests/utils/test_get_base_class.py new file mode 100644 index 0000000..852dc18 --- /dev/null +++ b/tests/utils/test_get_base_class.py @@ -0,0 +1,152 @@ +import re + +import pytest + +from fastapi_depends_ext.utils import get_base_class +from tests.utils_for_tests import SimpleDependency + + +def dependency(): + return 1 + + +def method(self): + return self + + +class Dependency(SimpleDependency): + var = dependency + + def __call__(self, *args, **kwargs): + return self + + @classmethod + def class_method(cls): + return cls + + @staticmethod + def static_method(): + return 1 + + @property + def property(self): + return dependency + + +class IncorrectDependency: + var = "str" + + @property + def property(self): + return 1 + + +# Simpler tests. Property should return identical object to tst `A` is `B` +dependency_instance = Dependency() +dependency_method = dependency_instance.dependency +dependency_class_method = dependency_instance.class_method +dependency_static_method = dependency_instance.static_method +dependency_property = dependency_instance.property + + +supported_callable = [ + lambda self: self, # method + classmethod(lambda cls: cls), # classmethod + staticmethod(lambda: None), # staticmethod + property(lambda self: method), # property return method + property(lambda self: method), # property return type + property(lambda self: dependency_instance), # property return callable instance + property(lambda self: dependency_method), # property return bound_method + property(lambda self: dependency_class_method), # property return class_method + property(lambda self: dependency_static_method), # property return staticmethod + property(lambda self: dependency_instance.property), # property return staticmethod + property(lambda self: dependency_instance.var), # property return instance property + property(lambda self: dependency_property), # property return calculable property + Dependency, # type + Dependency(), # callable instance + Dependency().dependency, # instance method + Dependency.class_method, # instance classmethod + Dependency.static_method, # instance staticmethod + Dependency().var, # instance property + Dependency().property, # instance calculable property +] + + +incorrect_dependency = IncorrectDependency() +incorrect_dependency_property = incorrect_dependency.property +incorrect_dependency_var = incorrect_dependency.var + + +unsupported = [ + 1, + "str", + dict(), + incorrect_dependency, + incorrect_dependency_var, + incorrect_dependency_property, + property(lambda self: incorrect_dependency), # property return type + property(lambda self: incorrect_dependency_var), # property return instance property + property(lambda self: incorrect_dependency_property), # property return calculable property +] + + +@pytest.mark.parametrize("_method", supported_callable) +def test_get_base_class__class_defined__class(_method): + TestClass = type("TestClass", (object,), {"method": _method}) + + instance = TestClass() + + assert get_base_class(instance, "method", instance.method) is TestClass + + +@pytest.mark.parametrize("_method", supported_callable) +def test_get_base_class__parent_class_defined__class(_method): + class ParentClass: + method = _method + + class TestClass(ParentClass): + pass + + instance = TestClass() + + assert get_base_class(instance, "method", instance.method) is ParentClass + + +@pytest.mark.parametrize("_method", supported_callable) +def test_get_base_class__deep_parent_class_defined__deep_parent_class(_method): + class DeepParentClass: + method = _method + + class ParentClass(DeepParentClass): + pass + + class TestClass(ParentClass): + pass + + instance = TestClass() + + assert get_base_class(instance, "method", instance.method) is DeepParentClass + + +@pytest.mark.parametrize("_method", supported_callable) +def test_get_base_class__class_redefined__correct_class(_method): + class ParentClass: + def method(self): + return 1 + + TestClass = type("TestClass", (ParentClass,), {"method": _method}) + + instance = TestClass() + + assert get_base_class(instance, "method", instance.method) is TestClass + assert get_base_class(instance, "method", super(TestClass, instance).method) is ParentClass + + +@pytest.mark.parametrize("_method", unsupported) +def test_get_base_class__class_defined_not_callable__AttributeError(_method): + TestClass = type("TestClass", (object,), {"method": _method}) + + instance = TestClass() + + with pytest.raises(TypeError, match=re.escape(f"Incorrect type of `{instance.method}`")): + get_base_class(instance, "method", instance.method)