Repository: seandstewart/typical (language: python)

Issue:
AttributeError when the annotation contains Callable 
* typical version:  2.3.0
* Python version: 3.8
* Operating System:  CentOS

### Description
Hi @seandstewart , If the function type hints has Callable, it will raise AttributeError.

```py
import typic, typing


@typic.al
def foo(f: typing.Callable): ...


def f(): ...


foo(f)  # AttributeError: 'getset_descriptor' object has no attribute 'items'

```



Discussion / hints:

Fix patch:
diff --git a/typic/constraints/factory.py b/typic/constraints/factory.py
index 48098dbb..6890d432 100644
--- a/typic/constraints/factory.py
+++ b/typic/constraints/factory.py
@@ -8,7 +8,7 @@
 import pathlib
 import re
 import uuid
-from collections import deque
+from collections import deque, abc
 from decimal import Decimal
 from typing import (
     Mapping,
@@ -125,11 +125,11 @@ def get_constraints(
         handler = _from_class
     else:
         ot = origin(t)
-        if ot is type:
+        if ot in {type, abc.Callable}:
             handler = _from_strict_type  # type: ignore
             t = ot
         else:
-            handler = _CONSTRAINT_BUILDER_HANDLERS.get_by_parent(origin(t), _from_class)  # type: ignore
+            handler = _CONSTRAINT_BUILDER_HANDLERS.get_by_parent(ot, _from_class)  # type: ignore
 
     __stack.add(t)
     c = handler(t, nullable=nullable, name=name, cls=cls)
@@ -414,6 +414,6 @@ def _from_class(
         ipaddress.IPv4Address: _from_strict_type,
         ipaddress.IPv6Address: _from_strict_type,
         Union: _from_union,  # type: ignore
-        Literal: _from_literal,
+        Literal: _from_literal,  # type: ignore
     }
 )
diff --git a/typic/ext/schema/field.py b/typic/ext/schema/field.py
index 604e96c3..c88bbfc6 100644
--- a/typic/ext/schema/field.py
+++ b/typic/ext/schema/field.py
@@ -124,7 +124,7 @@ class StringFormat(str, enum.Enum):
     IPV6 = "ipv6"
 
 
-@slotted
+@slotted(dict=True)
 @dataclasses.dataclass(frozen=True, repr=False)
 class BaseSchemaField(_Serializable):
     """The base JSON Schema Field."""
@@ -384,7 +384,7 @@ def get_field_type(type: Optional[Union[SchemaType, Any]]) -> Type[SchemaFieldT]
         ipaddress.IPv4Address: StrSchemaField(format=StringFormat.IPV4),
         ipaddress.IPv6Address: StrSchemaField(format=StringFormat.IPV6),
         str: StrSchemaField(),
-        AnyStr: StrSchemaField(),
+        AnyStr: StrSchemaField(),  # type: ignore
         Text: StrSchemaField(),
         bytes: StrSchemaField(),
         bool: BooleanSchemaField(),
@@ -396,6 +396,6 @@ def get_field_type(type: Optional[Union[SchemaType, Any]]) -> Type[SchemaFieldT]
         frozenset: ArraySchemaField(uniqueItems=True, additionalItems=False),
         dict: ObjectSchemaField(),
         type(None): NullSchemaField(),
-        Literal: BaseSchemaField(),
+        Literal: BaseSchemaField(),  # type: ignore
     }
 )
diff --git a/typic/serde/common.py b/typic/serde/common.py
index 0e1c5413..c4ac215e 100644
--- a/typic/serde/common.py
+++ b/typic/serde/common.py
@@ -456,31 +456,41 @@ def name(self) -> str:
 class DelayedSerdeProtocol(SerdeProtocol):
     __slots__ = (
         "delayed",
-        "_protocol",
-    ) + tuple(SerdeProtocol.__slots__)
+        "_resolved",
+    )
 
     def __init__(
         self,
         delayed: Union[ForwardDelayedAnnotation, DelayedAnnotation],
     ):
         self.delayed = delayed
-        self._protocol: Optional[SerdeProtocol] = None
-        self.transmute = lambda val: self.proto.transmute(val)  # type: ignore
+        self.transmute = lambda val: self.deserialize(val)  # type: ignore
+        self._resolved = False
 
     def __repr__(self):
-        return f"{self.__class__.__name__}(delayed={self.delayed}, protocol={self._protocol})"
-
-    @property
-    def proto(self) -> SerdeProtocol:
-        if self._protocol is None:
-            _protocol = self.delayed.resolved
-            for name in SerdeProtocol.__slots__:
-                object.__setattr__(self, name, getattr(_protocol, name))
-            self._protocol = _protocol
-        return self._protocol
+        return f"{self.__class__.__name__}(delayed={self.delayed}, resolved={self._resolved})"
+
+    def __delayed_init__(self):
+        if self._resolved:
+            return
+        protocol = self.delayed.resolved
+        super().__init__(
+            annotation=protocol.annotation,
+            constraints=protocol.constraints,
+            deserialize=protocol.deserialize,
+            decode=protocol.decode,
+            serialize=protocol.serialize,
+            encode=protocol.encode,
+            validate=protocol.validate,
+            translate=protocol.translate,
+            iterate=protocol.iterate,
+            tojson=protocol.tojson,
+        )
+        self._resolved = True
 
     def __getattr__(self, item):
-        return self.proto.__getattribute__(item)
+        self.__delayed_init__()
+        return super().__getattribute__(item)
 
     def __call__(self, val: Any) -> ObjectT:
         return self.transmute(val)  # type: ignore
diff --git a/typic/serde/des.py b/typic/serde/des.py
index 00f81507..4d907739 100644
--- a/typic/serde/des.py
+++ b/typic/serde/des.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import dataclasses
 import datetime
 import functools
 import inspect
@@ -23,6 +24,7 @@
     TYPE_CHECKING,
     Optional,
     Set,
+    NoReturn,
 )
 
 from pendulum import parse as dateparse, DateTime, instance
@@ -37,8 +39,10 @@
     get_tag_for_types,
     get_unique_name,
     get_name,
+    slotted,
 )
 from typic.common import DEFAULT_ENCODING, VAR_POSITIONAL, VAR_KEYWORD, ObjectT
+from typic.compat import TypeGuard, Literal
 from .common import (
     DeserializerT,
     DeserializerRegistryT,
@@ -103,6 +107,8 @@ class DesFactory:
             re.Match,  # type: ignore
             type(None),
             _empty,
+            Callable,
+            abc.Callable,
         )
     )
     VNAME = "val"
@@ -170,7 +176,12 @@ def _get_name(
     ) -> str:
         return get_defname("deserializer", (annotation, constr))
 
-    def _build_date_des(self, func: gen.Block, anno_name: str, annotation: Annotation):
+    def _build_date_des(self, context: BuildContext):
+        func, annotation, anno_name = (
+            context.func,
+            context.annotation,
+            context.anno_name,
+        )
         origin = annotation.resolved_origin
         # From an int
         with func.b(f"if isinstance({self.VNAME}, (int, float)):") as b:
@@ -222,7 +233,8 @@ def _build_date_des(self, func: gen.Block, anno_name: str, annotation: Annotatio
             line = f"{self.VNAME} = dateparse({self.VNAME}, exact=True)"
             b.l(line, dateparse=dateparse)
 
-    def _build_time_des(self, func: gen.Block, anno_name: str, annotation: Annotation):
+    def _build_time_des(self, context: BuildContext):
+        func, anno_name = context.func, context.anno_name
         # From an int
         with func.b(f"if isinstance({self.VNAME}, (int, float)):") as b:
             b.l(f"{self.VNAME} = {anno_name}(int({self.VNAME}))")
@@ -239,9 +251,8 @@ def _build_time_des(self, func: gen.Block, anno_name: str, annotation: Annotatio
         with func.b(f"if isinstance({self.VNAME}, date):", date=datetime.date) as b:
             b.l(f"{self.VNAME} = {anno_name}(0)")
 
-    def _build_timedelta_des(
-        self, func: gen.Block, anno_name: str, annotation: Annotation
-    ):
+    def _build_timedelta_des(self, context: BuildContext):
+        func, anno_name = context.func, context.anno_name
         # From an int
         with func.b(f"if isinstance({self.VNAME}, (int, float)):") as b:
             b.l(f"{self.VNAME} = {anno_name}(int({self.VNAME}))")
@@ -250,7 +261,8 @@ def _build_timedelta_des(
             line = f"{self.VNAME} = dateparse({self.VNAME}, exact=True)"
             b.l(line, dateparse=dateparse)
 
-    def _build_uuid_des(self, func: gen.Block, anno_name: str, annotation: Annotation):
+    def _build_uuid_des(self, context: BuildContext):
+        func, anno_name = context.func, context.anno_name
         self._add_type_check(func, anno_name)
         with func.b(f"if issubclass({self.VTYPE}, UUID):", UUID=uuid.UUID) as b:
             b.l(f"{self.VNAME} = {anno_name}(int={self.VNAME}.int)")
@@ -319,12 +331,12 @@ def factory(*, __origin=use):
 
         return factory
 
-    def _build_text_des(
-        self,
-        func: gen.Block,
-        anno_name: str,
-        annotation: Annotation,
-    ):
+    def _build_text_des(self, context: BuildContext):
+        func, annotation, anno_name = (
+            context.func,
+            context.annotation,
+            context.anno_name,
+        )
         origin = annotation.resolved_origin
         # Encode for bytes
         if issubclass(origin, bytes):
@@ -339,24 +351,25 @@ def _build_text_des(
                 b.l(f"{self.VNAME} = {self.VNAME}.decode({DEFAULT_ENCODING!r})")
         func.l(f"{self.VNAME} = {anno_name}({self.VNAME})")
 
-    def _build_builtin_des(
-        self,
-        func: gen.Block,
-        anno_name: str,
-        annotation: Annotation,
-    ):
+    def _build_builtin_des(self, context: BuildContext):
+        func, annotation, anno_name = (
+            context.func,
+            context.annotation,
+            context.anno_name,
+        )
         origin = annotation.resolved_origin
         if issubclass(origin, (str, bytes)):
-            self._build_text_des(func, anno_name, annotation)
+            self._build_text_des(context)
         elif checks.ismappingtype(origin):
-            self._build_mapping_des(func, anno_name, annotation)
+            self._build_mapping_des(context)
         elif checks.iscollectiontype(origin):
-            self._build_collection_des(func, anno_name, annotation)
+            self._build_collection_des(context)
         # bool, int, float...
         else:
             func.l(f"{self.VNAME} = {anno_name}({self.VNAME})")
 
-    def _build_pattern_des(self, func: gen.Block, anno_name: str):
+    def _build_pattern_des(self, context: BuildContext):
+        func, anno_name = context.func, context.anno_name
         func.l(
             f"{self.VNAME} = {self.VNAME} "
             f"if issubclass({self.VTYPE}, {anno_name}) "
@@ -368,16 +381,14 @@ def _build_fromdict_des(self, func: gen.Block, anno_name: str):
         self._add_type_check(func, anno_name)
         func.l(f"{self.VNAME} = {anno_name}.from_dict({self.VNAME})")
 
-    def _build_typeddict_des(
-        self,
-        func: gen.Block,
-        anno_name: str,
-        annotation: Annotation,
-        *,
-        total: bool = True,
-        namespace: Type = None,
-    ):
-
+    def _build_typeddict_des(self, context: BuildContext):
+        func, annotation, namespace, anno_name = (
+            context.func,
+            context.annotation,
+            context.namespace,
+            context.anno_name,
+        )
+        total = getattr(context.annotation.resolved_origin, "__total__", True)
         with func.b(f"if issubclass({self.VTYPE}, Mapping):", Mapping=abc.Mapping) as b:
             fields_deser = {
                 x: self.resolver._resolve_from_annotation(
@@ -402,16 +413,16 @@ def _build_typeddict_des(
                 translate=self.resolver.translate,
             )
 
-    def _build_typedtuple_des(
-        self,
-        func: gen.Block,
-        anno_name: str,
-        annotation: Annotation,
-        namespace: Type = None,
-    ):
+    def _build_typedtuple_des(self, context: BuildContext):
+        func, annotation, anno_name = (
+            context.func,
+            context.annotation,
+            context.anno_name,
+        )
         with func.b(f"if issubclass({self.VTYPE}, Mapping):", Mapping=abc.Mapping) as b:
             if annotation.serde.fields:
-                self._build_typeddict_des(b, anno_name, annotation, namespace=namespace)
+                ctx = dataclasses.replace(context, func=b)
+                self._build_typeddict_des(ctx)
             else:
                 b.l(
                     f"{self.VNAME} = {anno_name}(**{self.VNAME})",
@@ -434,13 +445,13 @@ def _build_typedtuple_des(
                 translate=self.resolver.translate,
             )
 
-    def _build_mapping_des(
-        self,
-        func: gen.Block,
-        anno_name: str,
-        annotation: Annotation,
-        namespace: Type = None,
-    ):
+    def _build_mapping_des(self, context: BuildContext):
+        func, annotation, namespace, anno_name = (
+            context.func,
+            context.annotation,
+            context.namespace,
+            context.anno_name,
+        )
         key_des, item_des = None, None
         args = annotation.args
         if args:
@@ -488,13 +499,13 @@ def _build_mapping_des(
             },
         )
 
-    def _build_tuple_des(
-        self,
-        func: gen.Block,
-        anno_name: str,
-        annotation: Annotation,
-        namespace: Type = None,
-    ):
+    def _build_tuple_des(self, context: BuildContext):
+        func, annotation, namespace, anno_name = (
+            context.func,
+            context.annotation,
+            context.namespace,
+            context.anno_name,
+        )
         if annotation.args and annotation.args[-1] is not ...:
             item_des = {
                 ix: self.resolver.resolve(
@@ -518,20 +529,15 @@ def _build_tuple_des(
                 },
             )
         else:
-            self._build_collection_des(
-                func=func,
-                anno_name=anno_name,
-                annotation=annotation,
-                namespace=namespace,
-            )
-
-    def _build_collection_des(
-        self,
-        func: gen.Block,
-        anno_name: str,
-        annotation: Annotation,
-        namespace: Type = None,
-    ):
+            self._build_collection_des(context)
+
+    def _build_collection_des(self, context: BuildContext):
+        func, annotation, namespace, anno_name = (
+            context.func,
+            context.annotation,
+            context.namespace,
+            context.anno_name,
+        )
         item_des = None
         it_name = f"{anno_name}_item_des"
         iterate = f"iterate({self.VNAME}, values=True)"
@@ -557,17 +563,18 @@ def _build_collection_des(
             },
         )
 
-    def _build_path_des(self, func: gen.Block, anno_name: str):
+    def _build_path_des(self, context: BuildContext):
+        func, anno_name = context.func, context.anno_name
         self._add_type_check(func, anno_name)
         func.l(f"{self.VNAME} = {anno_name}({self.VNAME})")
 
-    def _build_generic_des(
-        self,
-        func: gen.Block,
-        anno_name: str,
-        annotation: Annotation,
-        namespace: Type = None,
-    ):
+    def _build_user_type_des(self, context: BuildContext):
+        func, annotation, namespace, anno_name = (
+            context.func,
+            context.annotation,
+            context.namespace,
+            context.anno_name,
+        )
         serde = annotation.serde
         resolved = annotation.resolved
         self._add_type_check(func, anno_name)
@@ -654,9 +661,12 @@ def _build_literal_des(
         )
         return self._build_des(t_anno, func_name, namespace)
 
-    def _build_tagged_union_des(
-        self, func: gen.Block, annotation: Annotation, namespace
-    ):
+    def _build_union_des(self, context: BuildContext):
+        func, annotation, namespace = (
+            context.func,
+            context.annotation,
+            context.namespace,
+        )
         # Get all types which we may coerce to.
         args = (*(a for a in annotation.args if a not in {None, Ellipsis, type(None)}),)
         if not args:
@@ -665,14 +675,14 @@ def _build_tagged_union_des(
         targets = (*(a for a in args if not checks.isstdlibtype(a)),)
         # We can only build a tagged union deserializer if all args are valid
         if args != targets:
-            return self._build_generic_union_des(func, annotation, namespace)
+            return self._build_generic_union_des(context)
 
         # Try to collect the field which will be the discriminator.
         # First, get a mapping of Type -> Proto & Type -> Fields
         tagged = get_tag_for_types(targets)
         # Just bail out if we can't find a key.
         if not tagged:
-            return self._build_generic_union_des(func, annotation, namespace)
+            return self._build_generic_union_des(context)
         # If we got a key, re-map the protocols to the value for each type.
         deserializers = {
             value: self.resolver.resolve(t, namespace=namespace).transmute
@@ -698,9 +708,12 @@ def _build_tagged_union_des(
                 ")"
             )
 
-    def _build_generic_union_des(
-        self, func: gen.Block, annotation: Annotation, namespace
-    ):
+    def _build_generic_union_des(self, context: BuildContext):
+        annotation, namespace, func = (
+            context.annotation,
+            context.namespace,
+            context.func,
+        )
         args = [a for a in annotation.args if a not in {None, Ellipsis, type(None)}]
         if args:
             ctx = {
@@ -721,6 +734,7 @@ def _build_generic_union_des(
                 ")",
                 names=names,
             )
+            return False
 
     def _build_des(  # noqa: C901
         self,
@@ -745,62 +759,56 @@ def _build_des(  # noqa: C901
             return self._build_literal_des(annotation, func_name, namespace)
         with gen.Block(ns) as main:
             with main.f(func_name, main.param(f"{self.VNAME}")) as func:
+                needs_return = None
+                context = BuildContext(annotation, ns, anno_name, func, namespace)
                 if origin not in self.UNRESOLVABLE:
+                    # Set our top-level sanity checks.
                     self._set_checks(func, anno_name, annotation)
-                    if checks.isuniontype(origin):
-                        self._build_tagged_union_des(func, annotation, namespace)
-                    elif checks.isdatetype(origin):
-                        self._build_date_des(func, anno_name, annotation)
-                    elif checks.istimetype(origin):
-                        self._build_time_des(func, anno_name, annotation)
-                    elif checks.istimedeltatype(origin):
-                        self._build_timedelta_des(func, anno_name, annotation)
-                    elif checks.isuuidtype(origin):
-                        self._build_uuid_des(func, anno_name, annotation)
-                    elif origin in {Pattern, re.Pattern}:  # type: ignore
-                        self._build_pattern_des(func, anno_name)
-                    elif issubclass(origin, pathlib.Path):
-                        self._build_path_des(func, anno_name)
-                    elif not args and checks.isbuiltintype(origin):
-                        self._build_builtin_des(func, anno_name, annotation)
-                    elif checks.isfromdictclass(origin):
-                        self._build_fromdict_des(func, anno_name)
-                    elif checks.isenumtype(origin):
-                        self._build_builtin_des(func, anno_name, annotation)
-                    elif checks.istypeddict(origin):
-                        self._build_typeddict_des(
-                            func,
-                            anno_name,
-                            annotation,
-                            total=origin.__total__,  # type: ignore
-                            namespace=namespace,
-                        )
-                    elif checks.istypedtuple(origin) or checks.isnamedtuple(origin):
-                        self._build_typedtuple_des(
-                            func, anno_name, annotation, namespace=namespace
-                        )
-                    elif not args and checks.isbuiltinsubtype(origin):
-                        self._build_builtin_des(func, anno_name, annotation)
-                    elif checks.ismappingtype(origin):
-                        self._build_mapping_des(
-                            func, anno_name, annotation, namespace=namespace
-                        )
-                    elif checks.istupletype(origin):
-                        self._build_tuple_des(
-                            func, anno_name, annotation, namespace=namespace
-                        )
-                    elif checks.iscollectiontype(origin):
-                        self._build_collection_des(
-                            func, anno_name, annotation, namespace=namespace
-                        )
-                    else:
-                        self._build_generic_des(
-                            func, anno_name, annotation, namespace=namespace
-                        )
-                func.l(f"{gen.Keyword.RET} {self.VNAME}")
+                    # Move through our queue.
+                    for check, handler in self._HANDLERS.items():
+                        # If this is a valid type for this handler,
+                        #   write the deserializer.
+                        if check(origin, args):
+                            needs_return = handler(self, context)
+                            break
+                # If the deserializer doesn't contain a return statement, add one.
+                if needs_return is not False:
+                    func.l(f"{gen.Keyword.RET} {self.VNAME}")
         deserializer = main.compile(ns=ns, name=func_name)
         return deserializer
 
+    # Order is IMPORTANT! This is a FIFO queue.
+    _HANDLERS: Mapping[HandlerCheckT, BuildHandlerT] = {
+        # Special handler for Unions...
+        lambda origin, args: checks.isuniontype(origin): _build_union_des,
+        # Non-intersecting types (order doesn't matter here.
+        lambda origin, args: checks.isdatetype(origin): _build_date_des,
+        lambda origin, args: checks.istimetype(origin): _build_time_des,
+        lambda origin, args: checks.istimedeltatype(origin): _build_timedelta_des,
+        lambda origin, args: checks.isuuidtype(origin): _build_uuid_des,
+        lambda origin, args: origin in {Pattern, re.Pattern}: _build_pattern_des,
+        lambda origin, args: issubclass(origin, pathlib.Path): _build_path_des,
+        # MUST come before subtype check.
+        lambda origin, args: (
+            not args and checks.isbuiltintype(origin)
+        ): _build_builtin_des,
+        # Psuedo-structured containers, should check before generics.
+        lambda origin, args: checks.istypeddict(origin): _build_typeddict_des,
+        lambda origin, args: checks.istypedtuple(origin): _build_typedtuple_des,
+        lambda origin, args: checks.isnamedtuple(origin): _build_typedtuple_des,
+        lambda origin, args: (
+            not args and checks.isbuiltinsubtype(origin)
+        ): _build_builtin_des,
+        # A mapping is a collection so must come before that check.
+        lambda origin, args: checks.ismappingtype(origin): _build_mapping_des,
+        # A tuple is a collection so must come before that check.
+        lambda origin, args: checks.istupletype(origin): _build_tuple_des,
+        # Generic collection handler
+        lambda origin, args: checks.iscollectiontype(origin): _build_collection_des,
+        # Catch-all for custom user types (user-defined classes).
+        lambda origin, args: True: _build_user_type_des,
+    }
+
     def _check_varargs(
         self,
         anno: Annotation[Type[ObjectT]],
@@ -899,3 +907,17 @@ def factory(
         self.__DES_CACHE[key] = (deserializer, validator)
 
         return deserializer, validator
+
+
+@slotted(dict=False, weakref=True)
+@dataclasses.dataclass
+class BuildContext:
+    annotation: Annotation
+    ns: Mapping[str, Any]
+    anno_name: str
+    func: gen.Block
+    namespace: Optional[Type] = None
+
+
+HandlerCheckT = Callable[[Type[ObjectT], Tuple[Any, ...]], TypeGuard[Type[ObjectT]]]
+BuildHandlerT = Callable[[DesFactory, BuildContext], Union[NoReturn, Literal[False]]]
diff --git a/typic/serde/ser.py b/typic/serde/ser.py
index 7b2d8593..69a928f0 100644
--- a/typic/serde/ser.py
+++ b/typic/serde/ser.py
@@ -9,6 +9,7 @@
 import pathlib
 import re
 import uuid
+from collections import abc
 from collections.abc import (
     Mapping as Mapping_abc,
     Collection as Collection_abc,
@@ -273,12 +274,7 @@ def _compile_defined_serializer(
         ser_name = "ser"
         ns = {ser_name: ser}
         with gen.Block(ns) as main:
-            with main.f(
-                func_name,
-                main.param("o"),
-                main.param("lazy", default=False),
-                main.param("name", default=None),
-            ) as func:
+            with self._define(main, func_name) as func:
                 self._check_add_null_check(func, annotation)
                 self._add_type_check(func, annotation)
                 line = f"{ser_name}(o)"
@@ -317,6 +313,17 @@ def _compile_serializer(self, annotation: Annotation[Type[_T]]) -> SerializerT[_
         # Lazy shortcut for messy paths (Union, Any, ...)
         if origin in self._DYNAMIC or not annotation.static:
             serializer = cast(SerializerT, self.resolver.primitive)
+        # Routines (functions or methods) can't be serialized...
+        elif issubclass(origin, abc.Callable) or inspect.isroutine(origin):  # type: ignore
+            name = util.get_qualname(origin)
+            with gen.Block() as main:
+                with self._define(main, func_name) as func:
+                    func.l(
+                        f'raise TypeError("Routines are not serializable. ({name!r}).")'
+                    )
+
+            serializer = main.compile(name=func_name)
+            self._serializer_cache[func_name] = serializer
         # Enums are special
         elif checks.isenumtype(annotation.resolved):
             serializer = self._compile_enum_serializer(annotation)
@@ -325,12 +332,7 @@ def _compile_serializer(self, annotation: Annotation[Type[_T]]) -> SerializerT[_
         elif origin in self._PRIMITIVES:
             ns: dict = {}
             with gen.Block(ns) as main:
-                with main.f(
-                    func_name,
-                    main.param("o"),
-                    main.param("lazy", default=False),
-                    main.param("name", default=None),
-                ) as func:
+                with self._define(main, func_name) as func:
                     self._check_add_null_check(func, annotation)
                     self._add_type_check(func, annotation)
                     line = "o"
@@ -355,12 +357,7 @@ def _compile_serializer(self, annotation: Annotation[Type[_T]]) -> SerializerT[_
             anno_name = f"{func_name}_anno"
             ns = {anno_name: origin, **annotation.serde.asdict()}
             with gen.Block(ns) as main:
-                with main.f(
-                    func_name,
-                    main.param("o"),
-                    main.param("lazy", default=False),
-                    main.param("name", default=None),
-                ) as func:
+                with self._define(main, func_name) as func:
                     # Mapping types need special nested processing as well
                     istypeddict = checks.istypeddict(origin)
                     istypedtuple = checks.istypedtuple(origin)
@@ -380,6 +377,15 @@ def _compile_serializer(self, annotation: Annotation[Type[_T]]) -> SerializerT[_
             self._serializer_cache[func_name] = serializer
         return serializer
 
+    @staticmethod
+    def _define(main: gen.Block, name: str) -> gen.Function:
+        return main.func(
+            name,
+            main.param("o"),
+            main.param("lazy", default=False, kind=gen.ParameterKind.KEYWORD_ONLY),
+            main.param("name", default=None, kind=gen.ParameterKind.KEYWORD_ONLY),
+        )
+
     def factory(self, annotation: Annotation[Type[_T]]) -> SerializerT[_T]:
         if isinstance(annotation, (DelayedAnnotation, ForwardDelayedAnnotation)):
             return cast(SerializerT, DelayedSerializer(annotation, self))
diff --git a/typic/types/dsn.py b/typic/types/dsn.py
index 000465d3..2a85c3f8 100644
--- a/typic/types/dsn.py
+++ b/typic/types/dsn.py
@@ -29,7 +29,7 @@ class DSNValueError(NetworkAddressValueError):
     pass
 
 
-@slotted
+@slotted(dict=True)
 @dataclasses.dataclass(frozen=True)
 class DSNInfo:
     """Detailed information about a D(ata)S(ource)N(ame).
diff --git a/typic/types/email.py b/typic/types/email.py
index 2462da8e..126fcc1c 100644
--- a/typic/types/email.py
+++ b/typic/types/email.py
@@ -60,7 +60,7 @@ class EmailValueError(NetworkAddressValueError):
 )
 
 
-@slotted
+@slotted(dict=True)
 @dataclasses.dataclass(frozen=True)
 class EmailAddrInfo:
     """Detailed information about an email address.
diff --git a/typic/types/url.py b/typic/types/url.py
index c9049c24..03007533 100644
--- a/typic/types/url.py
+++ b/typic/types/url.py
@@ -102,7 +102,7 @@ class NetworkAddressValueError(ValueError):
     """A generic error indicating the value is not a valid network address."""
 
 
-@slotted
+@slotted(dict=True)
 @dataclasses.dataclass(frozen=True)
 class NetAddrInfo:
     """Detailed information about a network address.
diff --git a/typic/util.py b/typic/util.py
index 59496eca..71e36e63 100644
--- a/typic/util.py
+++ b/typic/util.py
@@ -1,9 +1,8 @@
 from __future__ import annotations
 
 import ast
-import bdb
 import collections
-import contextlib
+import collections.abc
 import dataclasses
 import functools
 import inspect
@@ -53,7 +52,6 @@
     "extract",
     "fastcachedmethod",
     "filtered_repr",
-    "guard_recursion",
     "get_args",
     "get_name",
     "get_defname",
@@ -186,6 +184,9 @@ def origin(annotation: Any) -> Any:
     if not checks.isbuiltintype(actual):
         actual = _check_generics(actual)
 
+    if inspect.isroutine(actual):
+        actual = collections.abc.Callable
+
     return actual
 
 
@@ -567,7 +568,7 @@ def safe_get_params(obj: Type) -> Mapping[str, inspect.Parameter]:
 VT = TypeVar("VT")
 
 
-class TypeMap(Dict[Union[Any], VT]):
+class TypeMap(Dict[Type, VT]):
     """A mapping of Type -> value."""
 
     def get_by_parent(self, t: Type, default: VT = None) -> Optional[VT]:
@@ -590,53 +591,6 @@ def get_by_parent(self, t: Type, default: VT = None) -> Optional[VT]:
         return default
 
 
-class RecursionDetected(RuntimeError):
-    ...
-
-
-class RecursionDetector(bdb.Bdb):  # pragma: nocover
-    """Prevent recursion from even starting.
-
-    https://stackoverflow.com/a/36663046
-
-    Warnings
-    --------
-    While the detector is tracing, no other debug tracers (i.e., codecov!) can trace.
-    """
-
-    def do_clear(self, arg):
-        pass
-
-    def __init__(self, *args):
-        bdb.Bdb.__init__(self, *args)
-        self.stack = set()
-
-    def user_call(self, frame, argument_list):
-        code = frame.f_code
-        if code in self.stack:
-            self.stack.clear()
-            raise RecursionDetected(f"Caught recursion in: {frame}")
-        self.stack.add(code)
-
-    def user_return(self, frame, return_value):
-        if frame.f_code in self.stack:
-            self.stack.remove(frame.f_code)
-
-
-_detector = RecursionDetector()
-
-
-@contextlib.contextmanager
-def guard_recursion():  # pragma: nocover
-    curtrace = sys.gettrace()
-    _detector.set_trace()
-    try:
-        yield
-    finally:
-        _detector.stack.clear()
-        sys.settrace(curtrace)
-
-
 def slotted(
     _cls: Type = None,
     *,


Test patch:
diff --git a/tests/test_serdes.py b/tests/test_serdes.py
index a14dd87e..a333c556 100644
--- a/tests/test_serdes.py
+++ b/tests/test_serdes.py
@@ -455,3 +455,16 @@ class Bar:
         excluded: bool = False
 
     assert typic.transmute(Bar, Foo()) == Bar()
+
+
+def test_routine_protocol():
+    def foo():
+        ...
+
+    proto = typic.protocol(foo)
+    assert proto.transmute(foo) is foo
+    assert proto.validate(foo) is foo
+    with pytest.raises(TypeError):
+        proto.serialize(foo)
+
+    assert list(proto.iterate(foo)) == [None]