diff --git a/.gitignore b/.gitignore index 74d8416..e3de911 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ wtf .mypy_cache coverage.json site -wtf.py \ No newline at end of file +wtf.py +CODE_OF_CONDUCT.md \ No newline at end of file diff --git a/README.md b/README.md index 79ba1ef..bb5d321 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ assert main("1", 2) == 4.0 --- ### Features + Synchronous code is fully supported in this package: without any `async_to_sync`, `run_sync`, `syncify` or any other tricks. Also, *FastDepends* casts functions' return values the same way, it can be very helpful in building your own tools. @@ -104,6 +105,35 @@ These are two main defferences from native Fastapi DI System. --- +### Custom Fields + +If you wish to write your own FastAPI or another closely by architecture tool, you should define your own custom fields to specify application behavior. + +Custom fields can be used to adding something specific to a function arguments (like a BackgroundTask) or parsing incoming objects special way. You able decide by own, why and how you will use these tools. + +FastDepends grants you this opportunity a very intuitive and comfortable way. + +```python +from fast_depends import inject +from fast_depends.library import CustomField + +class Header(CustomField): + def use(self, **kwargs: AnyDict) -> AnyDict: + kwargs = super().use(**kwargs) + kwargs[self.param_name] = kwargs["headers"][self.param_name] + return kwargs + +@inject +def my_func(header_field: int = Header()): + return header_field + +assert my_func( + headers={ "header_field": "1" } +) == 1 +``` + +--- + ### Note Library was based on **0.95.0 FastAPI** version. diff --git a/docs/docs/advanced/index.md b/docs/docs/advanced/index.md new file mode 100644 index 0000000..bc05693 --- /dev/null +++ b/docs/docs/advanced/index.md @@ -0,0 +1,102 @@ +# CustomField + +!!! warning "Packages developers" + This is the part of documentation will talks you about some features, that can be helpfull to develop your own packages with `FastDepends` + +## Custom Arguments Field + +If you wish to write your own **FastAPI** or another closely by architecture tool, you +should define your own custom fields to specify application behavior. At **FastAPI** these fields are: + +* Body +* Path +* Query +* Header +* Cookie +* Form +* File +* Security + +Custom fields can be used to adding something specific to a function arguments (like a *BackgroundTask*) or +parsing incoming objects special way. You able decide by own, why and how you will use these tools. + +`FastDepends` grants you this opportunity a very intuitive and comfortable way. + +### Let's write *Header* + +As an example, will try to implement **FastAPI** *Header* field + +```python linenums="1" hl_lines="1 3-4" +{!> docs_src/advanced/custom/class_declaration.py !} +``` + +Just import `fast_depends.library.CustomField` and implements `use` (async or sync) method. +That's all. We already have own *Header* field to parse **kwargs** the special way. + +### *Header* usage + +Now we already can use the *Header* field + +```python linenums="1" hl_lines="4 8" +{!> docs_src/advanced/custom/usage.py !} +``` + +As we defined, *Header* parse incoming **headers kwargs field**, get a parameter by name and put it to +original function as an argument. + +### More details + +`CustomField` has some fields you should know about + +```python +class CustomField: + param_name: str + cast: bool +``` + +* `param_name` - an original function argument name to replace by your field instance. It was `header_field` at the example above. +* `cast` - specify the typecasting behavior. Use *False* to disable pydantic typecasting for fields using with your *CustomField* + +```python linenums="1" hl_lines="3 8 12-13" +{!> docs_src/advanced/custom/cast_arg.py !} +``` + +!!! note + Pydantic understands only python-native annotation or Pydantic classes. If users will annotate your fields by other classes, + you should set `cast=False` to avoid pydantic exeptions. + +```python +def use(self, **kwargs: AnyDict) -> AnyDict: ... +``` + +Your *CustimField* objects receive casted to *kwargs* an original function incoming arguments at `use` method. +Returning from the `use` method dict replace an original one. Original function will be executed **with a returned from your fields kwargs**. +Be accurate with. + +And one more time: + +```python linenums="1" hl_lines="6 9" +original_kwargs = { "headers": { "field": 1 }} + +new_kwargs = Header().set_param_name("field").use(**kwargs) +# new_kwargs = { +# "headers": { "field": 1 }, +# "field": 1 <-- new field from Header +# } + +original_function(**new_kwargs) +``` + +I hope it was clearly enough right now. + +Also, custom fields using according their definition: from left to right. +Next Custom Fields **kwargs** is a return of previous. + +An example: + +```python linenums="1" +@inject +def func(field1 = Header(), field2 = Header()): ... +``` + +**field2** incoming kwargs is an output of **field1.use()** \ No newline at end of file diff --git a/docs/docs/advanced/starlette.md b/docs/docs/advanced/starlette.md new file mode 100644 index 0000000..1423c75 --- /dev/null +++ b/docs/docs/advanced/starlette.md @@ -0,0 +1,60 @@ +# Let's write some code + +Now we take the *starlette example* from [usages](/FastDepends/usages/) and specify it to use *Path* now. + +## Handle *request* specific fields + +First of all, **Starlette** pass to a handler the only one argument - `request` +To use them with `FastDepends` we need unwrap `request` to kwargs. + +```python hl_lines="6-8" linenums="1" +{!> docs_src/advanced/custom/starlette.py [ln:1,16-21] !} +``` + +!!! note "" + Also, we wraps an original handler to `fast_depends.inject` too at *3* line + +## Declare *Custom Field* + +Next step, define *Path* custom field + +```python linenums="1" hl_lines="8" +{!> docs_src/advanced/custom/starlette.py [ln:2,8-13] !} +``` + +## Usage with the *Starlette* + +And use it at our *Starlette* application: +```python linenums="1" hl_lines="6 7 10" +{!> docs_src/advanced/custom/starlette.py [ln:4-6,23-30] !} +``` + +Depends is working as expected too + +```python linenums="1" hl_lines="1 4-5 9" +def get_user(user_id: int = Path()): + return f"user {user_id}" + +@wrap_starlette +async def hello(user: str = Depends(get_user)): + return PlainTextResponse(f"Hello, {user}!") + +app = Starlette(debug=True, routes=[ + Route("/{user_id}", hello) +]) +``` + +As an *Annotated* does +```python linenums="1" hl_lines="2" +@wrap_starlette +async def get_user(user: Annotated[int, Path()]): + return PlainTextResponse(f"Hello, {user}!") +``` + +## Full example + +```python linenums="1" +{!> docs_src/advanced/custom/starlette.py !} +``` + +The code above works "as it". You can copy it and declare other *Header*, *Cookie*, *Query* fields by yourself. Just try, it's fun! \ No newline at end of file diff --git a/docs/docs/index.md b/docs/docs/index.md index 98b0242..2905dd4 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -72,6 +72,35 @@ You can use this library without any frameworks in both **sync** and **async** c These are two main defferences from native Fastapi DI System. +## Custom Fields + +If you wish to write your own FastAPI or another closely by architecture tool, you should define your own custom fields to specify application behavior. + +Custom fields can be used to adding something specific to a function arguments (like a BackgroundTask) or parsing incoming objects special way. You able decide by own, why and how you will use these tools. + +FastDepends grants you this opportunity a very intuitive and comfortable way. + +```python +from fast_depends import inject +from fast_depends.library import CustomField + +class Header(CustomField): + def use(self, **kwargs: AnyDict) -> AnyDict: + kwargs = super().use(**kwargs) + kwargs[self.param_name] = kwargs["headers"][self.param_name] + return kwargs + +@inject +def my_func(header_field: int = Header()): + return header_field + +assert my_func( + headers={ "header_field": "1" } +) == 1 +``` + +More details you can find at [advanced](/FastDepends/advanced) tutorial + !!! warning "" Library was based on **0.95.0 FastAPI** version. diff --git a/docs/docs/tutorial/classes.md b/docs/docs/tutorial/classes.md index c8d552b..c9b72ac 100644 --- a/docs/docs/tutorial/classes.md +++ b/docs/docs/tutorial/classes.md @@ -3,7 +3,7 @@ ### "Callable", remember? ```python linenums="1" -{!> docs_src/tutorial_2/tutorial_1.py !} +{!> docs_src/tutorial_2_classes/tutorial_1.py !} ``` Yep, all of these examples can be used as a dependency! @@ -16,7 +16,7 @@ You can use class initializer as a dependency. This way, object of this class will be the type of your dependency: ```python linenums="1" hl_lines="5-6 9" -{!> docs_src/tutorial_2/tutorial_2.py !} +{!> docs_src/tutorial_2_classes/tutorial_2.py !} ``` !!! warning @@ -32,7 +32,7 @@ If you wish to specify your dependency behavior earlier, you can use `__call__` already inititalized class object. ```python linenums="1" hl_lines="7-8 11" -{!> docs_src/tutorial_2/tutorial_3.py !} +{!> docs_src/tutorial_2_classes/tutorial_3.py !} ``` --- @@ -43,7 +43,7 @@ Also, you can use classmethods or staticmethod as dependencies. It can be helpful with some OOP patterns (Strategy as an example). ```python linenums="1" hl_lines="4-6 9" -{!> docs_src/tutorial_2/tutorial_4.py !} +{!> docs_src/tutorial_2_classes/tutorial_4.py !} ``` --- @@ -51,7 +51,7 @@ It can be helpful with some OOP patterns (Strategy as an example). ### ANY METHOD (4-th call) ```python linenums="1" hl_lines="7-8 11" -{!> docs_src/tutorial_2/tutorial_5.py !} +{!> docs_src/tutorial_2_classes/tutorial_5.py !} ``` diff --git a/docs/docs/tutorial/index.md b/docs/docs/tutorial/index.md index e7745c3..33cea72 100644 --- a/docs/docs/tutorial/index.md +++ b/docs/docs/tutorial/index.md @@ -10,12 +10,12 @@ But, I can remember you, if it's nessesary. === "Sync" ```python hl_lines="3-4" linenums="1" - {!> docs_src/tutorial_1/1_sync.py !} + {!> docs_src/tutorial_1_quickstart/1_sync.py !} ``` === "Async" ```python hl_lines="4-5 7-8" linenums="1" - {!> docs_src/tutorial_1/1_async.py !} + {!> docs_src/tutorial_1_quickstart/1_async.py !} ``` !!! tip "Be accurate" @@ -31,24 +31,24 @@ But, I can remember you, if it's nessesary. === "Sync" ```python hl_lines="2" linenums="6" - {!> docs_src/tutorial_1/1_sync.py [ln:5-8]!} + {!> docs_src/tutorial_1_quickstart/1_sync.py [ln:5-8]!} ``` === "Async" ```python hl_lines="1 4 5" linenums="10" - {!> docs_src/tutorial_1/1_async.py [ln:10-16]!} + {!> docs_src/tutorial_1_quickstart/1_async.py [ln:10-16]!} ``` **Second step**: declare dependency required with `Depends` === "Sync" ```python hl_lines="3 5" linenums="6" - {!> docs_src/tutorial_1/1_sync.py [ln:5-10]!} + {!> docs_src/tutorial_1_quickstart/1_sync.py [ln:5-10]!} ``` === "Async" ```python hl_lines="7 9" linenums="10" - {!> docs_src/tutorial_1/1_async.py [ln:10-18]!} + {!> docs_src/tutorial_1_quickstart/1_async.py [ln:10-18]!} ``` **Last step**: just use the dependencies calling result! @@ -67,14 +67,14 @@ just declare `Depends` requirement at original dependency function. === "Sync" ```python linenums="1" hl_lines="3-4 6-7 12-13" - {!> docs_src/tutorial_1/2_sync.py !} + {!> docs_src/tutorial_1_quickstart/2_sync.py !} ``` 1. Call another_dependency here === "Async" ```python linenums="1" hl_lines="4-5 7-8 13-14" - {!> docs_src/tutorial_1/2_async.py !} + {!> docs_src/tutorial_1_quickstart/2_async.py !} ``` 1. Call another_dependency here diff --git a/docs/docs/tutorial/yield.md b/docs/docs/tutorial/yield.md index db5b6b1..44af617 100644 --- a/docs/docs/tutorial/yield.md +++ b/docs/docs/tutorial/yield.md @@ -10,17 +10,17 @@ For example, you could use this to create a database session and close it after Only the code prior `yield` statement is executed before sending a response ```python linenums="1" hl_lines="1-2" -{!> docs_src/tutorial_3/tutorial_1.py !} +{!> docs_src/tutorial_3_yield/tutorial_1.py !} ``` The *yielded* value is what is injected into original function ```python linenums="1" hl_lines="3" -{!> docs_src/tutorial_3/tutorial_1.py !} +{!> docs_src/tutorial_3_yield/tutorial_1.py !} ``` The code following the `yield` statement is executed after the original function has been called ```python linenums="1" hl_lines="4" -{!> docs_src/tutorial_3/tutorial_1.py !} +{!> docs_src/tutorial_3_yield/tutorial_1.py !} ``` !!! tip @@ -30,9 +30,9 @@ The code following the `yield` statement is executed after the original function !!! warning All errors occures at original function or another dependencies will be raised this place ```python linenums="1" hl_lines="3" -{!> docs_src/tutorial_3/tutorial_1.py !} +{!> docs_src/tutorial_3_yield/tutorial_1.py !} ``` To guarantee `db.close()` execution use the following code: ```python linenums="1" hl_lines="3 5" -{!> docs_src/tutorial_3/tutorial_2.py !} +{!> docs_src/tutorial_3_yield/tutorial_2.py !} ``` \ No newline at end of file diff --git a/docs/docs_src/advanced/custom/cast_arg.py b/docs/docs_src/advanced/custom/cast_arg.py new file mode 100644 index 0000000..abde913 --- /dev/null +++ b/docs/docs_src/advanced/custom/cast_arg.py @@ -0,0 +1,14 @@ +class Header(CustomField): + def __init__(self): + super().__init__(cast=True) + + +class NotCastHeader(CustomField): + def __init__(self): + super().__init__(cast=False) + + +def func( + h1: int = Header(), # <-- casts to int + h2: int = NotCastHeader() # <-- just an annotation +): ... \ No newline at end of file diff --git a/docs/docs_src/advanced/custom/class_declaration.py b/docs/docs_src/advanced/custom/class_declaration.py new file mode 100644 index 0000000..25ef104 --- /dev/null +++ b/docs/docs_src/advanced/custom/class_declaration.py @@ -0,0 +1,7 @@ +from fast_depends.library import CustomField + +class Header(CustomField): + def use(self, **kwargs: AnyDict) -> AnyDict: + kwargs = super().use(**kwargs) + kwargs[self.param_name] = kwargs["headers"][self.param_name] + return kwargs diff --git a/docs/docs_src/advanced/custom/starlette.py b/docs/docs_src/advanced/custom/starlette.py new file mode 100644 index 0000000..b484666 --- /dev/null +++ b/docs/docs_src/advanced/custom/starlette.py @@ -0,0 +1,28 @@ +from fast_depends import inject +from fast_depends.library import CustomField + +from starlette.applications import Starlette +from starlette.responses import PlainTextResponse +from starlette.routing import Route + +class Path(CustomField): + def use(self, *, request, **kwargs): + return { + **super().use(request=request, **kwargs), + self.param_name: request.path_params.get(self.param_name) + } + +def wrap_starlette(func): + async def wrapper(request): + return await inject(func)( + request=request + ) + return wrapper + +@wrap_starlette +async def hello(user: str = Path()): + return PlainTextResponse(f"Hello, {user}!") + +app = Starlette(debug=True, routes=[ + Route("/{user}", hello) +]) diff --git a/docs/docs_src/advanced/custom/usage.py b/docs/docs_src/advanced/custom/usage.py new file mode 100644 index 0000000..c08b9b3 --- /dev/null +++ b/docs/docs_src/advanced/custom/usage.py @@ -0,0 +1,9 @@ +from fast_depends import inject + +@inject +def my_func(header_field: int = Header()): + return header_field + +assert h( + headers={"header_field": "1"} +) == 1 \ No newline at end of file diff --git a/docs/docs_src/tutorial_1_quickstart/1_async.py b/docs/docs_src/tutorial_1_quickstart/1_async.py new file mode 100644 index 0000000..2e1fc2a --- /dev/null +++ b/docs/docs_src/tutorial_1_quickstart/1_async.py @@ -0,0 +1,18 @@ +import asyncio +from fast_depends import inject, Depends + +async def simple_dependency(a: int, b: int = 3): + return a + b + +def another_dependency(a: int): + return a + +@inject +async def method( + a: int, + b: int = Depends(simple_dependency), + c: int = Depends(another_dependency), +): + return a + b + c + +assert asyncio.run(method("1")) == 6 \ No newline at end of file diff --git a/docs/docs_src/tutorial_1_quickstart/1_sync.py b/docs/docs_src/tutorial_1_quickstart/1_sync.py new file mode 100644 index 0000000..f4d0d72 --- /dev/null +++ b/docs/docs_src/tutorial_1_quickstart/1_sync.py @@ -0,0 +1,10 @@ +from fast_depends import inject, Depends + +def simple_dependency(a: int, b: int = 3): + return a + b + +@inject +def method(a: int, d: int = Depends(simple_dependency)): + return a + d + +assert method("1") == 5 \ No newline at end of file diff --git a/docs/docs_src/tutorial_1_quickstart/2_async.py b/docs/docs_src/tutorial_1_quickstart/2_async.py new file mode 100644 index 0000000..5544f06 --- /dev/null +++ b/docs/docs_src/tutorial_1_quickstart/2_async.py @@ -0,0 +1,18 @@ +import asyncio +from fast_depends import inject, Depends + +def another_dependency(a: int): + return a * 2 + +async def simple_dependency(a: int, b: int = Depends(another_dependency)): # (1) + return a + b + +@inject +async def method( + a: int, + b: int = Depends(simple_dependency), + c: int = Depends(another_dependency), +): + return a + b + c + +assert asyncio.run(method("1")) == 6 \ No newline at end of file diff --git a/docs/docs_src/tutorial_1_quickstart/2_sync.py b/docs/docs_src/tutorial_1_quickstart/2_sync.py new file mode 100644 index 0000000..86ca467 --- /dev/null +++ b/docs/docs_src/tutorial_1_quickstart/2_sync.py @@ -0,0 +1,17 @@ +from fast_depends import inject, Depends + +def another_dependency(a: int): + return a * 2 + +def simple_dependency(a: int, b: int = Depends(another_dependency)): # (1) + return a + b + +@inject +def method( + a: int, + b: int = Depends(another_dependency), + c: int = Depends(simple_dependency) +): + return a + b + c + +assert method("1") == 6 \ No newline at end of file diff --git a/docs/docs_src/tutorial_2_classes/tutorial_1.py b/docs/docs_src/tutorial_2_classes/tutorial_1.py new file mode 100644 index 0000000..b37cbf7 --- /dev/null +++ b/docs/docs_src/tutorial_2_classes/tutorial_1.py @@ -0,0 +1,23 @@ +class MyClass: pass + +MyClass() # It is a "call"! 1-st call + + +class MyClass: + def __call__(): pass + +m = MyClass() +m() # It is a "call" too! 2-nd call + + +class MyClass: + @classmethod + def f(): pass + +MyClass.f() # Yet another "call"! 3-rd call + + +class MyClass + def f(self): pass + +MyClass().f() # "call"? 4-th call \ No newline at end of file diff --git a/docs/docs_src/tutorial_2_classes/tutorial_2.py b/docs/docs_src/tutorial_2_classes/tutorial_2.py new file mode 100644 index 0000000..6b3dd26 --- /dev/null +++ b/docs/docs_src/tutorial_2_classes/tutorial_2.py @@ -0,0 +1,12 @@ +from typing import Any +from fast_depends import inject, Depends + +class MyDependency: + def __init__(self, a: int): + self.field = a + +@inject +def func(d: Any = Depends(MyDependency)): + return d.field + +assert func(a=3) == 3 \ No newline at end of file diff --git a/docs/docs_src/tutorial_2_classes/tutorial_3.py b/docs/docs_src/tutorial_2_classes/tutorial_3.py new file mode 100644 index 0000000..a56f768 --- /dev/null +++ b/docs/docs_src/tutorial_2_classes/tutorial_3.py @@ -0,0 +1,14 @@ +from fast_depends import inject, Depends + +class MyDependency: + def __init__(self, a: int): + self.field = a + + def __call__(self, b: int): + return self.field + b + +@inject +def func(d: int = Depends(MyDependency(3))): + return d + +assert func(b=3) == 6 \ No newline at end of file diff --git a/docs/docs_src/tutorial_2_classes/tutorial_4.py b/docs/docs_src/tutorial_2_classes/tutorial_4.py new file mode 100644 index 0000000..6007bf6 --- /dev/null +++ b/docs/docs_src/tutorial_2_classes/tutorial_4.py @@ -0,0 +1,12 @@ +from fast_depends import inject, Depends + +class MyDependency: + @staticmethod + def dep(a: int): + return a ** 2 + +@inject +def func(d: int = Depends(MyDependency.dep)): + return d + +assert func(a=3) == 9 \ No newline at end of file diff --git a/docs/docs_src/tutorial_2_classes/tutorial_5.py b/docs/docs_src/tutorial_2_classes/tutorial_5.py new file mode 100644 index 0000000..966559e --- /dev/null +++ b/docs/docs_src/tutorial_2_classes/tutorial_5.py @@ -0,0 +1,14 @@ +from fast_depends import inject, Depends + +class MyDependency: + def __init__(self, a): + self.field = a + + def dep(self, a: int): + return self.field + a + +@inject +def func(d: int = Depends(MyDependency(3).dep)): + return d + +assert func(a=3) == 6 \ No newline at end of file diff --git a/docs/docs_src/tutorial_3_yield/tutorial_1.py b/docs/docs_src/tutorial_3_yield/tutorial_1.py new file mode 100644 index 0000000..2705380 --- /dev/null +++ b/docs/docs_src/tutorial_3_yield/tutorial_1.py @@ -0,0 +1,4 @@ +def dependency(): + db = DBSession() + yield db + db.close() diff --git a/docs/docs_src/tutorial_3_yield/tutorial_2.py b/docs/docs_src/tutorial_3_yield/tutorial_2.py new file mode 100644 index 0000000..144a161 --- /dev/null +++ b/docs/docs_src/tutorial_3_yield/tutorial_2.py @@ -0,0 +1,6 @@ +def dependency(): + db = DBSession() + try: + yield db + finally: + db.close() diff --git a/docs/docs_src/usages/starlette.py b/docs/docs_src/usages/starlette.py index 64aa1d2..15c7e71 100644 --- a/docs/docs_src/usages/starlette.py +++ b/docs/docs_src/usages/starlette.py @@ -1,4 +1,4 @@ -# Is that FastAPI??? +w# Is that FastAPI??? from starlette.applications import Starlette from starlette.responses import PlainTextResponse from starlette.routing import Route diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6cb6f27..ece48d9 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -89,5 +89,8 @@ nav: - Validations: tutorial/validations.md - Annotated: tutorial/annotated.md - Override Dependencies: tutorial/overrides.md + - Advanced: + - advanced/index.md + - More Complex Example: advanced/starlette.md - Alternatives: alternatives.md - Contributing: contributing.md diff --git a/fast_depends/__about__.py b/fast_depends/__about__.py index 9cf2ad1..3eed3b7 100644 --- a/fast_depends/__about__.py +++ b/fast_depends/__about__.py @@ -1,3 +1,3 @@ """FastDepends - extracted and cleared from HTTP domain Fastapi Dependency Injection System""" -__version__ = "1.0.3" +__version__ = "1.1.0" diff --git a/fast_depends/construct.py b/fast_depends/construct.py index 18e1ca3..8d45ee0 100644 --- a/fast_depends/construct.py +++ b/fast_depends/construct.py @@ -22,6 +22,8 @@ from fast_depends import model from fast_depends.types import AnyCallable, AnyDict +from fast_depends.library import CustomField + sequence_shapes = { SHAPE_LIST, @@ -53,7 +55,7 @@ def get_dependant( signature_params = endpoint_signature.parameters for param in signature_params.values(): - depends, param_field = analyze_param( + custom, depends, param_field = analyze_param( param_name=param.name, annotation=param.annotation, default=param.default, @@ -63,7 +65,10 @@ def get_dependant( dependant.return_field = param_field continue - if depends is not None: + elif custom is not None: + dependant.custom.append(custom) + + elif depends is not None: sub_dependant = get_param_sub_dependant( param_name=param.name, depends=depends, @@ -81,8 +86,9 @@ def analyze_param( param_name: str, annotation: Any, default: Any, -) -> Tuple[Any, Optional[model.Depends], Optional[ModelField]]: +) -> Tuple[Optional[CustomField], Optional[model.Depends], Optional[ModelField]]: depends = None + custom = None field_info = None if ( @@ -93,7 +99,7 @@ def analyze_param( custom_annotations = [ arg for arg in annotated_args[1:] - if isinstance(arg, (FieldInfo, model.Depends)) + if isinstance(arg, (FieldInfo, model.Depends, CustomField)) ] custom_annotations = next(iter(custom_annotations), None) @@ -105,9 +111,15 @@ def analyze_param( ) field_info.default = Required - elif isinstance(custom_annotations, model.Depends): # pragma: no branch + elif isinstance(custom_annotations, model.Depends): depends = custom_annotations + elif isinstance(custom_annotations, CustomField): # pragma: no branch + custom_annotations.set_param_name(param_name) + custom = custom_annotations + if custom.cast is False: + annotation = Any + if isinstance(default, model.Depends): assert depends is None, ( "Cannot specify `Depends` in `Annotated` and default value" @@ -119,6 +131,12 @@ def analyze_param( ) depends = default + elif isinstance(default, CustomField): + default.set_param_name(param_name) + custom = default + if custom.cast is False: + annotation = Any + elif isinstance(default, FieldInfo): assert field_info is None, ( "Cannot specify annotations in `Annotated` and default value" @@ -126,7 +144,7 @@ def analyze_param( ) field_info = default - if depends is not None: + if (depends or custom) is not None: field = None if field_info is not None: @@ -149,7 +167,7 @@ def analyze_param( field_info=field_info, ) - return depends, field + return custom, depends, field def get_typed_signature(call: AnyCallable) -> inspect.Signature: diff --git a/fast_depends/injector.py b/fast_depends/injector.py index 520c15a..d6da07a 100644 --- a/fast_depends/injector.py +++ b/fast_depends/injector.py @@ -115,6 +115,9 @@ async def solve_dependencies_async( if use_sub_dependant.cache_key not in dependency_cache: dependency_cache[use_sub_dependant.cache_key] = solved + for custom in dependant.custom: + body = await run_async(custom.use, **(body or {})) + params, main_errors = params_to_args(dependant.params, body or {}) errors.extend(main_errors) return params, errors, dependency_cache @@ -203,6 +206,11 @@ def solve_dependencies_sync( if use_sub_dependant.cache_key not in dependency_cache: dependency_cache[use_sub_dependant.cache_key] = solved + for custom in dependant.custom: + assert not is_coroutine_callable(custom.use) and not is_async_gen_callable(custom.use), \ + f"You can't use async `{type(custom).__name__}` at sync code" + body = custom.use(**(body or {})) + params, main_errors = params_to_args(dependant.params, body or {}) errors.extend(main_errors) return params, errors, dependency_cache diff --git a/fast_depends/library/__init__.py b/fast_depends/library/__init__.py new file mode 100644 index 0000000..67cc844 --- /dev/null +++ b/fast_depends/library/__init__.py @@ -0,0 +1,6 @@ +from fast_depends.library.model import CustomField + + +__all__ = ( + "CustomField", +) \ No newline at end of file diff --git a/fast_depends/library/model.py b/fast_depends/library/model.py new file mode 100644 index 0000000..1332d09 --- /dev/null +++ b/fast_depends/library/model.py @@ -0,0 +1,20 @@ +from abc import ABC +from typing import Optional + +from fast_depends.types import AnyDict + + +class CustomField(ABC): + param_name: Optional[str] + cast: bool + + def __init__(self, *, cast: bool = True) -> None: + self.cast = cast + self.param_name = None + + def set_param_name(self, name: str) -> "CustomField": + self.param_name = name + + def use(self, **kwargs: AnyDict) -> AnyDict: + assert self.param_name, "You should specify `param_name` before using" + return kwargs diff --git a/fast_depends/model.py b/fast_depends/model.py index 02b5d0d..be24cf3 100644 --- a/fast_depends/model.py +++ b/fast_depends/model.py @@ -5,6 +5,8 @@ from pydantic.fields import ModelField from fast_depends.types import AnyCallable +from fast_depends.library import CustomField + RETURN_FIELD = "custom_return" @@ -17,6 +19,7 @@ def __init__( params: Optional[List[ModelField]] = None, return_field: Optional[ModelField] = None, dependencies: Optional[List["Dependant"]] = None, + custom: Optional[List[CustomField]] = None, use_cache: bool = True, path: Optional[str] = None, name: Optional[str] = None, @@ -24,6 +27,7 @@ def __init__( self.params = params or [] self.return_field = return_field self.dependencies = dependencies or [] + self.custom = custom or [] self.call = call self.use_cache = use_cache # Parent argument name at subdependency diff --git a/pyproject.toml b/pyproject.toml index 479e48e..23ab5e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ Source = "https://github.com/Lancetnik/FastDepends" [project.optional-dependencies] test = [ + "toml", "coverage[toml]>=7.2", "pytest>=7", "pytest-asyncio>=0.21", @@ -67,8 +68,8 @@ doc = [ ] dev = [ - "FastDepends[test]", - "FastDepends[doc]", + "fast-depends[test]", + "fast-depends[doc]", "mypy>=1.1", "black>=23.3.0", diff --git a/tests/library/test_custom.py b/tests/library/test_custom.py new file mode 100644 index 0000000..305f993 --- /dev/null +++ b/tests/library/test_custom.py @@ -0,0 +1,88 @@ +from typing_extensions import Annotated +import logging + +import pytest + +from fast_depends.library import CustomField +from fast_depends.types import AnyDict +from fast_depends import inject, Depends + + +class Header(CustomField): + def use(self, **kwargs: AnyDict) -> AnyDict: + kwargs = super().use(**kwargs) + kwargs[self.param_name] = kwargs["headers"][self.param_name] + return kwargs + + +class AsyncHeader(Header): + async def use(self, **kwargs: AnyDict) -> AnyDict: + return super().use(**kwargs) + + +def test_header(): + @inject + def catch(key: str = Header()): + return key + + assert catch(headers={"key": 1}) == "1" + + +@pytest.mark.asyncio +async def test_header_async(): + @inject + async def catch(key: str = Header()): + return key + + assert (await catch(headers={"key": 1})) == "1" + + +@pytest.mark.asyncio +async def test_adync_header_async(): + @inject + async def catch(key: str = AsyncHeader()): + return key + + assert (await catch(headers={"key": 1})) == "1" + + +def test_adync_header_sync(): + @inject + def catch(key: str = AsyncHeader()): # pragma: no cover + return key + + with pytest.raises(AssertionError): + catch(headers={"key": 1}) == "1" + + +def test_header_annotated(): + @inject + def catch(key: Annotated[str, Header()]): + return key + + assert catch(headers={"key": 1}) == "1" + + +def test_depends(): + def dep(key: Annotated[str, Header()]): + return key + + @inject + def catch(k = Depends(dep)): + return k + + assert catch(headers={"key": 1}) == "1" + + +def test_not_cast(): + @inject + def catch(key: Annotated[str, Header(cast=False)]): + return key + + assert catch(headers={"key": 1}) == 1 + + @inject + def catch(key: logging.Logger = Header(cast=False)): + return key + + assert catch(headers={"key": 1}) == 1