Skip to content

Commit

Permalink
Implemented support for output type casting in flexible_listlike_input
Browse files Browse the repository at this point in the history
  • Loading branch information
timbernat committed Apr 24, 2024
1 parent f9b2490 commit cfe73d5
Showing 1 changed file with 24 additions and 16 deletions.
40 changes: 24 additions & 16 deletions polymerist/genutils/decorators/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

from typing import Callable, Iterable, Optional, Type, Union

import inspect
from functools import wraps
from inspect import signature, Parameter
from functools import wraps, partial

from copy import deepcopy
from pathlib import Path

from .meta import extend_to_methods
from . import signatures
from ..typetools.parametric import T, Args, KWArgs
from ..typetools.categorical import ListLike
from ..fileutils.pathutils import aspath, asstrpath


Expand All @@ -19,7 +20,7 @@ def optional_in_place(funct : Callable[[object, Args, KWArgs], None]) -> Callabl
'''Decorator function for allowing in-place (writeable) functions which modify object attributes
to be not performed in-place (i.e. read-only), specified by a boolean flag'''
# TODO : add assertion that the wrapped function has at least one arg AND that the first arg is of the desired (limited) type
old_sig = inspect.signature(funct)
old_sig = signature(funct)

@wraps(funct) # for preserving docstring and type annotations / signatures
def in_place_wrapper(obj : object, *args : Args, in_place : bool=False, **kwargs : KWArgs) -> Optional[object]: # read-only by default
Expand All @@ -34,11 +35,11 @@ def in_place_wrapper(obj : object, *args : Args, in_place : bool=False, **kwargs
# ADD IN-PLACE PARAMETER TO FUNCTION SIGNATURE
new_sig = signatures.insert_parameter_at_index(
old_sig,
new_param=inspect.Parameter(
new_param=Parameter(
name='in_place',
default=False,
annotation=bool,
kind=inspect.Parameter.KEYWORD_ONLY
kind=Parameter.KEYWORD_ONLY
),
index=signatures.get_index_after_positionals(old_sig)
)
Expand All @@ -49,29 +50,36 @@ def in_place_wrapper(obj : object, *args : Args, in_place : bool=False, **kwargs

return in_place_wrapper

@extend_to_methods
def flexible_tuple_input(func : Callable[[tuple], T], valid_types : Union[Type, tuple[Type]]=object) -> Callable[[Iterable], T]:
'''Wrapper which allows a function which expects a single tuple to accept Iterable or even star-unpacked arguments'''
@wraps(func)
# TODO : implement support for extend_to_methods (current mechanism is broken by additional deocrator parameters)
def flexible_listlike_input(funct : Callable[[ListLike], T]=None, CastType : Type[ListLike]=list, valid_member_types : Union[Type, tuple[Type]]=object) -> Callable[[Iterable], T]:
'''Wrapper which allows a function which expects a single list-initializable, Container-like object to accept any Iterable (or even star-unpacked arguments)'''
if not issubclass(CastType, ListLike):
raise TypeError(f'Cannot wrap listlike input with non-listlike type "{CastType.__name__}"')

@wraps(funct)
def wrapper(*args) -> T: # wrapper which accepts an arbitrary number of non-keyword argument
if (len(args) == 1) and isinstance(args[0], Iterable):
args = args[0]

inputs = []
for value in args:
if isinstance(value, valid_types): # works because isinstance() accepts either a single type or a tuple of types
inputs.append(value)
for member in args:
if isinstance(member, valid_member_types): # works because isinstance() accepts either a single type or a tuple of types
inputs.append(member)
else:
raise TypeError
return func(inputs) # TODO: modify input type signature of wrapper function
raise TypeError(f'Item {member!r} of type {type(member).__name__} is not an instance of any of the following valid wrapped types: {valid_member_types}')
inputs = CastType(inputs) # convert to the expected cast type (this is where the requirement of listlike cast types comes into play)

return funct(inputs) # TODO: modify input type signature of wrapper function

if funct is None:
return partial(flexible_listlike_input, CastType=CastType, valid_member_types=valid_member_types)
return wrapper

@extend_to_methods
def allow_string_paths(funct : Callable[[Path, Args, KWArgs], T]) -> Callable[[Union[Path, str], Args, KWArgs], T]:
'''Modifies a function which expects a Path as its first argument to also accept string-paths'''
# TODO : add assertion that the wrapped function has at least one arg AND that the first arg is of the desired (limited) type
old_sig = inspect.signature(funct) # lookup old type signature
old_sig = signature(funct) # lookup old type signature

@wraps(funct) # for preserving docstring and type annotations / signatures
def str_path_wrapper(flex_path : Union[str, Path], *args : Args, **kwargs : KWArgs) -> T:
Expand All @@ -91,7 +99,7 @@ def str_path_wrapper(flex_path : Union[str, Path], *args : Args, **kwargs : KWAr
def allow_pathlib_paths(funct : Callable[[str, Args, KWArgs], T]) -> Callable[[Union[Path, str], Args, KWArgs], T]:
'''Modifies a function which expects a string path as its first argument to also accept canonical pathlib Paths'''
# TODO : add assertion that the wrapped function has at least one arg AND that the first arg is of the desired (limited) type
old_sig = inspect.signature(funct) # lookup old type signature
old_sig = signature(funct) # lookup old type signature

@wraps(funct) # for preserving docstring and type annotations / signatures
def str_path_wrapper(flex_path : Union[str, Path], *args : Args, **kwargs : KWArgs) -> T:
Expand Down

0 comments on commit cfe73d5

Please sign in to comment.