Source code for meshed.scrap.annotations_to_meshes

"""
Code related to work on the "From annotated functions to meshes" discussion:

https://github.com/i2mint/meshed/discussions/55


"""


import typing
from typing import Dict, Protocol, Callable, TypeVar, Any
from collections.abc import Callable as CallableGenericAlias
from inspect import signature, Signature, Parameter
from functools import wraps

import builtins
import re

_camel_pattern = re.compile(r'(?<!^)(?=[A-Z])')
PK = Parameter.POSITIONAL_OR_KEYWORD


def _camel_to_snake(x):
    return _camel_pattern.sub('_', x)


_is_lower = lambda x: x == x.lower()
_builtin_lower_names = set(map(_camel_to_snake, filter(_is_lower, dir(builtins))))


Annotation, ArgPosition, MethodName, Argname = Any, int, str, str
MkArgname = Callable[[Annotation, ArgPosition, MethodName], Argname]


def try_annotation_name(
    arg_annotation: Annotation, arg_position: ArgPosition, method_name: MethodName
) -> Argname:
    argname = getattr(arg_annotation, '__name__', None)
    if argname is None or argname in _builtin_lower_names:
        argname = f'arg_{arg_position:02.0f}'
    return argname.lower()


def _is_callable_type_annot(x):
    return isinstance(x, CallableGenericAlias)


[docs] def callable_annots_to_signature( callable_annots: CallableGenericAlias, mk_argname: MkArgname = try_annotation_name ) -> Signature: """Produces a signature from a Callable type annotation >>> from typing import Callable, NewType >>> MyType = NewType('MyType', str) >>> sig = callable_annots_to_signature(Callable[[MyType, str], str]) >>> import inspect >>> isinstance(sig, inspect.Signature) True >>> list(sig.parameters.keys()) ['self', 'mytype', 'arg_01'] >>> sig.parameters['arg_01'].annotation <class 'str'> """ origin = typing.get_origin(callable_annots) if not _is_callable_type_annot(origin): raise ValueError('The provided type is not a Callable generic alias') input_annots, return_annot = typing.get_args(callable_annots) arg_params = [ Parameter(mk_argname(annot, i, None), PK, annotation=annot) for i, annot in enumerate(input_annots) ] self_param = Parameter('self', PK) return Signature([self_param] + arg_params, return_annotation=return_annot)
[docs] def func_types_to_protocol( func_types: Dict[str, CallableGenericAlias], name: str = None, *, mk_argname: MkArgname = try_annotation_name, ) -> typing.Protocol: """Produces a typing.Protocol based on a dictionary of `(method_name, Callable_type)` specification""" class TempProtocol(Protocol): pass if name: TempProtocol.__name__ = name for method_name, callable_type in func_types.items(): sig = callable_annots_to_signature(callable_type, mk_argname) def method_stub(*args, **kwargs): pass method_stub.__signature__ = sig setattr(TempProtocol, method_name, method_stub) return TempProtocol
def test_func_types_to_protocol(): from typing import Iterable, Any, NewType, Callable Group = NewType('Group', str) Item = NewType('Item', Any) class Groups: add_item_to_group: Callable[[Item, Group], Any] add_items_to_group: Callable[[Iterable[Item], Group], Any] list_groups: Callable[[], Iterable[Group]] items_for_group: Callable[[Group], Iterable[Item]] class ExpectedGroupsProtocol(Protocol): def add_items_to_group(self, item: Item, group: Group) -> Any: pass def add_items_to_group(self, items: Iterable[Item], group: Group) -> Any: pass def list_groups(self) -> Iterable[Group]: pass def items_for_group(self, group: Group) -> Iterable[Item]: pass ActualGroupsProtocol = func_types_to_protocol(Groups.__annotations__) # TODO: assert that ActualGroupsProtocol and ExpectedGroupsProtocol are equal in # some way (but == doesn't work, and is not meant to!)
[docs] def func_types_to_scaffold( func_types: Dict[str, CallableGenericAlias], name: str = None ) -> str: """Produces a scaffold class containing the said methods, with given annotations""" if name is None: name = 'GeneratedClass' methods = [] for method_name, callable_type in func_types.items(): sig = callable_annots_to_signature(callable_type) arg_str = ', '.join( f'{param.name}: {param.annotation.__name__}' if param.annotation != Parameter.empty else f'{param.name}' for param in sig.parameters.values() ) return_annotation = ( sig.return_annotation.__name__ if sig.return_annotation != Parameter.empty else 'None' ) method_str = ( f'def {method_name}({arg_str}) -> {return_annotation}:\n \tpass\n' ) methods.append(method_str) class_str = f'\nclass {name}:\n ' + '\n '.join(methods) return class_str
_expected_scaffold = ''' class GeneratedClass: def add_item_to_group(self, item: Item, group: Group) -> Any: pass def add_items_to_group(self, iterable: Iterable, group: Group) -> Any: pass def list_groups(self) -> Iterable: pass def items_for_group(self, group: Group) -> Iterable: pass ''' def test_func_types_to_scaffold(): from typing import Iterable, Any, NewType, Callable Group = NewType('Group', str) Item = NewType('Item', Any) class Groups: add_item_to_group: Callable[[Item, Group], Any] add_items_to_group: Callable[[Iterable[Item], Group], Any] list_groups: Callable[[], Iterable[Group]] items_for_group: Callable[[Group], Iterable[Item]] actual_scaffold = func_types_to_scaffold(Groups.__annotations__) assert actual_scaffold == _expected_scaffold