Skip to content

Commit 7b3f2f3

Browse files
committed
Widen nested_type Callable branch on introspection failure
Closes #673. The Callable branch of `nested_type` could raise on three real values where the documented `Box(type(value))` fallback was expected: - `pytest.mark.parametrize` (a `MarkDecorator`): `typing.get_overloads` raises `AttributeError` because `MarkDecorator` lacks `__qualname__`. - `dict.get` (a `method_descriptor`): same shape — missing `__module__`. - An `Operation` wrapping a callable with a stringified annotation that does not resolve in scope: `inspect.signature` consults `Operation.__signature__`, which calls `typing.get_type_hints`, which raises `NameError`. Per the canonicalize-widening principle (eb8680 on PR #613): when introspection cannot construct a more precise `TypeExpression`, the right move is to widen rather than propagate the failure. - Wrap `typing.get_overloads(value)` and `inspect.signature(value)` with broader `except` clauses (adding `AttributeError`, `TypeError`, `NameError`) that fall back to `Box(type(value))`. - The `Operation` branch propagates the widening when its Callable delegate returns a bare type rather than a parameterised one. Adds three regression tests pinning each failure mode.
1 parent d226d10 commit 7b3f2f3

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

effectful/internals/unification.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -962,18 +962,39 @@ def _(value: effectful.ops.types.Term):
962962
@nested_type.register
963963
def _(value: effectful.ops.types.Operation):
964964
typ = nested_type.dispatch(collections.abc.Callable)(value).value
965-
(arg_types, return_type) = typing.get_args(typ)
965+
args = typing.get_args(typ)
966+
if not args:
967+
# Callable branch widened to `Box(type(value))` because
968+
# introspection failed (#673); propagate the widening.
969+
return Box(type(value))
970+
(arg_types, return_type) = args
966971
return Box(effectful.ops.types.Operation[arg_types, return_type]) # type: ignore
967972

968973

969974
@nested_type.register
970975
def _(value: collections.abc.Callable):
971-
if typing.get_overloads(value):
976+
# `typing.get_overloads(value)` reads `__qualname__`/`__module__` on
977+
# the callable and raises `AttributeError` on values like
978+
# `pytest.mark.parametrize` (a `MarkDecorator`) or `dict.get` (a
979+
# `method_descriptor`). Treat that the same way as the
980+
# no-signature fallback: widen to `Box(type(value))`. #673.
981+
try:
982+
if typing.get_overloads(value):
983+
return Box(type(value))
984+
except (AttributeError, TypeError):
972985
return Box(type(value))
973986

987+
# `inspect.signature(value)` may consult a custom `__signature__`
988+
# property (e.g. `Operation.__signature__` calls
989+
# `typing.get_type_hints`) which raises `NameError` when an
990+
# annotation forward-ref cannot be resolved. Per canonicalize's
991+
# widening principle (see eb8680 on PR #613), an unresolvable
992+
# annotation does not mean the value isn't callable -- widen to
993+
# `Box(type(value))` rather than propagating the introspection
994+
# failure. #673.
974995
try:
975996
sig = inspect.signature(value)
976-
except ValueError:
997+
except (ValueError, TypeError, NameError, AttributeError):
977998
return Box(type(value))
978999

9791000
if sig.return_annotation is inspect.Signature.empty:

tests/test_internals_unification.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,45 @@ def test_nested_type_term_error():
867867
nested_type(mock_term)
868868

869869

870+
def test_nested_type_marker_decorator_widens_to_class():
871+
"""#673: ``pytest.mark.parametrize`` is a ``MarkDecorator`` instance --
872+
callable, but lacks ``__qualname__``. The Callable branch's
873+
``typing.get_overloads`` raises ``AttributeError``; per the
874+
canonicalize-widening principle (PR #613) the fallback should be
875+
``Box(type(value))``."""
876+
val = pytest.mark.parametrize
877+
assert nested_type(val).value is type(val)
878+
879+
880+
def test_nested_type_method_descriptor_widens_to_class():
881+
"""#673: ``dict.get`` is a ``method_descriptor`` -- callable but
882+
missing ``__module__``. Same widening shape as MarkDecorator."""
883+
val = dict.get
884+
assert nested_type(val).value is type(val)
885+
886+
887+
def test_nested_type_unresolvable_forward_ref_widens():
888+
"""#673: an ``Operation`` whose default carries a stringified
889+
annotation whose name does not resolve in the captured scope.
890+
``inspect.signature`` consults ``Operation.__signature__`` which
891+
calls ``typing.get_type_hints`` -- forward-ref evaluation raises
892+
``NameError``. ``nested_type`` should widen to ``Box(type(value))``
893+
rather than propagating."""
894+
from effectful.ops.syntax import defop
895+
896+
def _hidden_module():
897+
class ClientSession: # noqa: F841 -- intentionally not visible to op
898+
pass
899+
900+
def real(x: "ClientSession") -> int: # noqa: F821 -- forward ref unresolved
901+
raise NotImplementedError
902+
903+
return real
904+
905+
op = defop(_hidden_module())
906+
assert nested_type(op).value is type(op)
907+
908+
870909
def sequence_getitem[T](seq: collections.abc.Sequence[T], index: int) -> T:
871910
return seq[index]
872911

0 commit comments

Comments
 (0)