Skip to content

Commit

Permalink
Beginning of internal refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
tarsil committed Oct 11, 2023
1 parent 48aaf03 commit 98e536e
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 60 deletions.
3 changes: 2 additions & 1 deletion polyforce/_internal/_construction.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import inspect
from abc import ABCMeta
from inspect import Parameter, Signature
from itertools import islice
from typing import TYPE_CHECKING, Any, Dict, List, Set, Type, cast
Expand Down Expand Up @@ -80,7 +81,7 @@ def generate_model_signature(
)


class PolyMetaclass(type):
class PolyMetaclass(ABCMeta):
"""
Base metaclass used for the PolyModel objects
and applies all static type checking needed
Expand Down
172 changes: 113 additions & 59 deletions polyforce/main.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
from inspect import Parameter, Signature
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Set, _SpecialForm
from inspect import Signature
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Set, Type, Union, _SpecialForm

from typing_extensions import get_args, get_origin
from typing_extensions import get_args

from ._internal import _construction
from .config import Config

# class _PolyModelBase(metaclass=_construction.PolyMetaclass):
# """
# Simple base class that uses the metaclass directly
# avoiding the mectaclass conflicts when used with something else.
# """


class PolyModel(metaclass=_construction.PolyMetaclass):
"""
The class object used to be subclassed and apply
the static checking on the top of any python object
and function across the codebase.
The base class for applying static type checking to attribute access.
This class is meant to be subclassed for adding static type checking to attributes and methods.
Example:
from polyforce import PolyModel
```
from polyforce import PolyModel
class MyObject(PolyModel):
...
class MyObject(PolyModel):
def __init__(self, value: int):
self.value = value
```
Attributes:
__signature__ (ClassVar[Dict[str, Signature]]): Dictionary containing method signatures.
"""

if TYPE_CHECKING:
Expand All @@ -27,73 +39,115 @@ class MyObject(PolyModel):
else:
poly_fields = {}

__slots__ = "__dict__"
__signature__: ClassVar[Dict[str, Signature]]
__signature__: ClassVar[Dict[str, Signature]] = {}
config = Config()

def __getattribute__(self, __name: str) -> Any:
def __getattribute__(self, name: str) -> Any:
"""
Special action where it adds the static check validation
for the data being passed.
Get an attribute with static type checking.
Args:
name (str): The name of the attribute to access.
It checks if the values are properly checked and validated with
the right types.
Returns:
Any: The value of the attribute.
The class version of the decorator `polyforce.decorator.polycheck`.
Raises:
AttributeError: If the attribute does not exist.
Example:
```
obj = MyObject(42)
value = obj.value # Accessing the 'value' attribute
```
"""
# breakpoint()
# if name in self.__dict__:
# return super().__getattribute__(name)

try:
func = object.__getattribute__(self, __name)
func = super().__getattribute__(name)
signatures = object.__getattribute__(self, "__signature__")
signature: Signature = signatures[__name]

def polycheck(*args: Any, **kwargs: Any) -> Any:
nonlocal signature
nonlocal func
params = dict(zip(signature.parameters.values(), args))
params_from_kwargs: Dict[Parameter, Any] = {}

for key, value in kwargs.items():
parameter = signature.parameters.get(key)
if parameter:
params_from_kwargs[parameter] = value
continue
signature: Signature = signatures[name]

params_from_kwargs[
Parameter(name=key, kind=Parameter.KEYWORD_ONLY, annotation=type(value))
] = value
if signature is not None:
return self._add_static_type_checking(func, signature)
else:
return func
except (KeyError, AttributeError):
return object.__getattribute__(self, name)

params.update(params_from_kwargs)
def _extract_type_hint(type_hint: Union[Type, tuple]) -> Union[Type, tuple]:
"""
Extracts the base type from a type hint, considering typing extensions.
for parameter, value in params.items():
type_hint = parameter.annotation
This function checks if the given type hint is a generic type hint and extracts
the base type. If not, it returns the original type hint.
if isinstance(type_hint, _SpecialForm) or type_hint == Any:
continue
Args:
type_hint (Union[Type, tuple]): The type hint to extract the base type from.
if hasattr(type_hint, "__origin__"):
actual_type = type_hint.__origin__
else:
actual_type = get_origin(type_hint)
Returns:
Union[Type, tuple]: The base type of the type hint or the original type hint.
actual_type = actual_type or type_hint
Example:
```
from typing import List, Union
# For versions prior to python 3.10
if isinstance(actual_type, _SpecialForm):
actual_type = (
get_args(value)
if not hasattr(type_hint, "__origin__")
else type_hint.__args__
)
# Extract the base type from a List hint
base_type = extract_type_hint(List[int]) # Returns int
if not isinstance(value, actual_type):
# If the hint is not a generic type, it returns the original hint
original_hint = extract_type_hint(Union[int, str]) # Returns Union[int, str]
```
"""
if hasattr(type_hint, "__origin__"):
return get_args(type_hint)
return type_hint

def _add_static_type_checking(self, func: Any, signature: Signature) -> Callable:
"""
Add static type checking to a method or function.
Args:
func (Any): The method or function to add type checking to.
signature (Signature): The method's signature for type checking.
Returns:
Callable: A wrapped function with type checking.
Example:
```
def my_method(self, value: int) -> str:
return str(value)
obj = MyObject(42)
obj.__signature__['my_method'] = inspect.signature(my_method)
# Accessing 'my_method' will now perform type checking
result = obj.my_method(42) # This is valid
result = obj.my_method("42") # This will raise a TypeError
```
"""

def polycheck(*args: Any, **kwargs: Any) -> Any:
bound = signature.bind(*args, **kwargs)
bound.apply_defaults()

for name, value in bound.arguments.items():
if name in signature.parameters:
expected_type = signature.parameters[name].annotation

if expected_type in (_SpecialForm, Any):
continue

expected_args = self._extract_type_hint(expected_type)
if not isinstance(value, expected_args):
raise TypeError(
f"Expected type '{type_hint}' for attribute '{parameter.name}'"
f" but received type '{type(value)}' instead."
f"Expected type '{expected_type}' for attribute '{name}', "
f"but received type '{type(value)}' instead."
)

return func(*args, **kwargs)

return polycheck
return func(*args, **kwargs)

except (KeyError, AttributeError):
return object.__getattribute__(self, __name)
return polycheck
4 changes: 4 additions & 0 deletions tests/models/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ def test_missing_return():

with pytest.raises(ReturnSignatureMissing):
movie.set_actor(actor=Dummy())


# class Film(BaseModel, PolyModel):
# ...

0 comments on commit 98e536e

Please sign in to comment.