Skip to content

Commit

Permalink
refactor: πŸ”¨ improve metadata decorators api
Browse files Browse the repository at this point in the history
  • Loading branch information
lucas-labs committed Jan 24, 2024
1 parent 8bb6d9d commit 6624d41
Show file tree
Hide file tree
Showing 18 changed files with 235 additions and 97 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
test.py

.todo
.github/act-test

# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
Expand Down
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files.exclude": {
// πŸ“™ sub-libs
// "tools": true,
"tools": true,

// βš™οΈ config
// "pyproject.toml": true,
Expand Down Expand Up @@ -32,7 +32,7 @@
".git": true,
".gitignore": true,
".pytest_cache": true,
// ".github": true,
".github": true,

// πŸ“ docs
// "**/**/README.md": true
Expand Down
2 changes: 1 addition & 1 deletion example/app/app_module.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pest.decorators.module import module
from pest.metadata.types.module_meta import ValueProvider
from pest.metadata.types.injectable_meta import ValueProvider

from .data.data import TodoRepo
from .modules.todo.module import TodoModule
Expand Down
1 change: 0 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def tests(session: Session) -> None:
'loguru',
'pytest',
'httpx',
'pytest',
'pytest-asyncio',
)

Expand Down
20 changes: 20 additions & 0 deletions pest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
from .decorators.handler import delete, get, head, options, patch, post, put, trace
from .decorators.module import dom, domain, mod, module
from .factory import Pest
from .metadata.types.injectable_meta import (
ClassProvider,
ExistingProvider,
FactoryProvider,
ProviderBase,
Scope,
SingletonProvider,
ValueProvider,
)
from .utils.decorators import meta

__all__ = [
'Pest',
Expand All @@ -25,4 +35,14 @@
'router',
'rtr',
'api',
# decorators - utils
'meta',
# meta - providers
'ProviderBase',
'ClassProvider',
'ValueProvider',
'SingletonProvider',
'FactoryProvider',
'ExistingProvider',
'Scope',
]
6 changes: 3 additions & 3 deletions pest/core/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __pest_object_type__(cls) -> PestType:

@classmethod
def __str__(cls) -> str:
meta = get_meta(cls, type=ControllerMeta, clean=True)
meta = get_meta(cls, ControllerMeta, clean=True)
return f'Controller {meta.prefix}'

@classmethod
Expand All @@ -76,7 +76,7 @@ def __repr__(cls) -> str:
@classmethod
def __setup_controller_class__(cls, module: Optional['Module']) -> None:
"""sets up a controller class"""
meta = get_meta(cls, type=dict, clean=True)
meta = get_meta(cls, dict, clean=True)
cls.__router__ = PestRouter(**meta)
cls.__parent_module__ = module
inject_metadata(cls.__router__, name=f'{cls.__name__} {meta.get("prefix", "")}')
Expand Down Expand Up @@ -113,6 +113,6 @@ def __handlers__(cls) -> List[HandlerTuple]:
for _, method in members:
meta_type = get_meta_value(method, key='meta_type', type=PestType, default=None)
if meta_type == PestType.HANDLER:
handlers.append((method, get_meta(method, type=HandlerMeta)))
handlers.append((method, get_meta(method, HandlerMeta)))

return handlers
8 changes: 3 additions & 5 deletions pest/core/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@

from ..exceptions.base.pest import PestException
from ..metadata.meta import get_meta
from ..metadata.types.module_meta import (
from ..metadata.types.injectable_meta import (
ClassProvider,
ExistingProvider,
FactoryProvider,
InjectionToken,
ModuleMeta,
Provider,
ValueProvider,
)
from ..metadata.types.module_meta import InjectionToken, ModuleMeta, Provider
from ..utils.functions import classproperty
from .common import PestPrimitive
from .controller import Controller, router_of, setup_controller
Expand Down Expand Up @@ -115,7 +113,7 @@ def __setup_module__(self, parent: Optional['Module']) -> None:
self.__parent_module__ = parent

# get module metadata
meta: ModuleMeta = get_meta(self.__class__, type=ModuleMeta)
meta: ModuleMeta = get_meta(self.__class__, ModuleMeta)

# set internal properties
self.providers = meta.providers if meta.providers else []
Expand Down
6 changes: 4 additions & 2 deletions pest/factory/app_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ def make_app(
log.info(f'Setting up {get_meta_value(router, "name")}')
for route in cast(List[APIRoute], router.routes):
for method in route.methods:
if not route.path.endswith('/'):
log.debug(f'{method: <7} {prefix}{route.path}')
full_route = f'{prefix}{route.path}'

if full_route != '/' and not full_route.endswith('/'):
log.debug(f'{method: <7} {full_route}')

# add the router
app.include_router(router, prefix=prefix)
Expand Down
42 changes: 30 additions & 12 deletions pest/metadata/meta.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
from dataclasses import asdict, is_dataclass
from typing import Any, Callable, Dict, List, Type, TypeVar, Union, cast
from dataclasses import asdict, fields, is_dataclass
from typing import Any, Callable, Dict, List, Mapping, Type, TypeVar, Union, cast

from dacite import Config, from_dict

from ..exceptions.base.pest import PestException
from ..utils.functions import drop_keys, keep_keys
from ..utils.protocols import DataclassInstance
from .types._meta import Meta

META_KEY = '__pest__'


DataType = TypeVar('DataType', bound=Union[Dict[str, Any], dict, Meta])
DataType = TypeVar('DataType', bound=Union[Dict[str, Any], dict, Meta, DataclassInstance])
GenericValue = TypeVar('GenericValue')


def get_meta(
target: Union[Callable[..., Any], type, object],
output_type: Type[DataType] = Dict[str, Any],
*,
type: Type[DataType] = Dict[str, Any],
raise_error: bool = True,
clean: bool = False,
keep: Union[List[str], None] = None,
drop: Union[List[str], None] = None,
) -> DataType:
"""πŸ€ ⇝ get pest `metadata` from a `callable`
"""πŸ€ ⇝ get `metadata` from a `callable`
#### Params
- target: target object, type or function
- type: return type (will create an instance if it's a `dataclass`)
Expand Down Expand Up @@ -51,10 +52,10 @@ def get_meta(
if not keep and not drop:
meta = drop_keys(meta, ['meta_type'])

if is_dataclass(type):
return cast(type, from_dict(type, meta, config=Config(check_types=False)))
if is_dataclass(output_type):
return cast(output_type, from_dict(output_type, meta, config=Config(check_types=False)))

return cast(type, meta)
return cast(output_type, meta)


def get_meta_value(
Expand All @@ -67,7 +68,9 @@ def get_meta_value(


def inject_metadata(
callable: Callable[..., Any], metadata: Union[Meta, None] = None, **kwargs: Any
callable: Callable[..., Any],
metadata: Union[Meta, Mapping[Any, Any], None] = None,
**kwargs: Any,
) -> None:
"""πŸ€ ⇝ initialize pest `metadata` for a `callable`
Expand All @@ -80,9 +83,24 @@ def inject_metadata(

dict_meta = {}
if metadata is not None:
if not is_dataclass(metadata):
raise PestException('metadata must be a dataclass')
dict_meta = asdict(metadata)
if not is_dataclass(metadata) and not isinstance(metadata, dict):
raise PestException('metadata must be a dataclass or a dict')

try:
dict_meta: dict = (
asdict(metadata)
if is_dataclass(metadata) and not isinstance(metadata, type)
else metadata
if isinstance(metadata, dict)
else {}
)
except Exception as e:
if is_dataclass(metadata) and not isinstance(metadata, type):
dict_meta = {}
for field in fields(metadata):
dict_meta[field.name] = getattr(metadata, field.name)
else:
raise e

meta = get_meta(callable)
meta.update({**dict_meta, **kwargs})
69 changes: 69 additions & 0 deletions pest/metadata/types/injectable_meta.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,77 @@
from dataclasses import dataclass, field
from typing import Callable, Generic, Type, TypeVar, Union

from rodi import ServiceLifeStyle

from ._meta import Meta, PestType

try:
from typing import TypeAlias
except ImportError:
from typing_extensions import TypeAlias

T = TypeVar('T')


Factory: TypeAlias = Callable[..., T]
Scope: TypeAlias = ServiceLifeStyle
InjectionToken: TypeAlias = Union[type, Type[T]]
Class: TypeAlias = type


@dataclass
class InjectableMeta(Meta):
meta_type: PestType = field(default=PestType.INJECTABLE, init=False)


@dataclass
class ProviderBase:
"""πŸ€ ⇝ base class for all providers."""

provide: InjectionToken
'''πŸ€ ⇝ unique injection token'''


@dataclass
class ClassProvider(ProviderBase):
"""πŸ€ ⇝ defines a `class` type provider"""

use_class: Class
'''πŸ€ ⇝ type (class) of provider (type of the instance to be injected πŸ’‰)'''
scope: Union[Scope, None] = None
'''πŸ€ ⇝ scope of the provider''' ''


@dataclass
class ValueProvider(ProviderBase, Generic[T]):
"""πŸ€ ⇝ defines a `value` (singleton) type provider"""

use_value: T
'''πŸ€ ⇝ instance to be injected πŸ’‰'''


@dataclass
class SingletonProvider(ValueProvider, Generic[T]):
"""πŸ€ ⇝ defines a `singleton` (value) type provider"""

pass


@dataclass
class FactoryProvider(ProviderBase, Generic[T]):
"""πŸ€ ⇝ defines a `factory` type provider"""

use_factory: Factory[T]
'''πŸ€ ⇝ factory function that returns an instance of the provider'''
scope: Union[Scope, None] = None
'''πŸ€ ⇝ scope of the provider'''


@dataclass
class ExistingProvider(ProviderBase, Generic[T]):
"""πŸ€ ⇝ defines an `existing` (aliased) type provider"""

provide: str
'''πŸ€ ⇝ unique injection token of the existing provider'''
use_existing: InjectionToken
'''πŸ€ ⇝ provider to be aliased by the injection token '''
74 changes: 11 additions & 63 deletions pest/metadata/types/module_meta.py
Original file line number Diff line number Diff line change
@@ -1,80 +1,28 @@
from dataclasses import dataclass, field
from typing import Callable, Generic, List, Type, TypeVar, Union
from typing import List, Type, Union

from ...core.controller import Controller
from ._meta import Meta, PestType
from .injectable_meta import (
ClassProvider,
ExistingProvider,
FactoryProvider,
InjectionToken,
ValueProvider,
)

try:
from typing import TypeAlias
except ImportError:
from typing_extensions import TypeAlias

from rodi import ServiceLifeStyle

from ...core.controller import Controller
from ._meta import Meta, PestType

T = TypeVar('T')

InjectionToken: TypeAlias = Union[type, Type[T]]
Class: TypeAlias = type
Factory: TypeAlias = Callable[..., T]
Scope: TypeAlias = ServiceLifeStyle
Provider: TypeAlias = Union[
Class, 'ValueProvider', 'FactoryProvider', 'ExistingProvider', 'ClassProvider'
]


@dataclass
class ProviderBase:
"""πŸ€ ⇝ base class for all providers."""

provide: InjectionToken
'''πŸ€ ⇝ unique injection token'''


@dataclass
class ClassProvider(ProviderBase):
"""πŸ€ ⇝ defines a `class` type provider"""

use_class: Class
'''πŸ€ ⇝ type (class) of provider (type of the instance to be injected πŸ’‰)'''
scope: Union[Scope, None] = None
'''πŸ€ ⇝ scope of the provider''' ''


@dataclass
class ValueProvider(ProviderBase, Generic[T]):
"""πŸ€ ⇝ defines a `value` (singleton) type provider"""

use_value: T
'''πŸ€ ⇝ instance to be injected πŸ’‰'''


@dataclass
class SingletonProvider(ValueProvider, Generic[T]):
"""πŸ€ ⇝ defines a `singleton` (value) type provider"""

pass


@dataclass
class FactoryProvider(ProviderBase, Generic[T]):
"""πŸ€ ⇝ defines a `factory` type provider"""

use_factory: Factory[T]
'''πŸ€ ⇝ factory function that returns an instance of the provider'''
scope: Union[Scope, None] = None
'''πŸ€ ⇝ scope of the provider'''


@dataclass
class ExistingProvider(ProviderBase, Generic[T]):
"""πŸ€ ⇝ defines an `existing` (aliased) type provider"""

provide: str
'''πŸ€ ⇝ unique injection token of the existing provider'''
use_existing: InjectionToken
'''πŸ€ ⇝ provider to be aliased by the injection token '''


@dataclass
class ModuleMeta(Meta):
meta_type: PestType = field(default=PestType.MODULE, init=False, metadata={'expose': False})
Expand Down
Loading

0 comments on commit 6624d41

Please sign in to comment.