Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions effectful/internals/unification.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,18 +962,39 @@ def _(value: effectful.ops.types.Term):
@nested_type.register
def _(value: effectful.ops.types.Operation):
typ = nested_type.dispatch(collections.abc.Callable)(value).value
(arg_types, return_type) = typing.get_args(typ)
args = typing.get_args(typ)
if not args:
# Callable branch widened to `Box(type(value))` because
# introspection failed (#673); propagate the widening.
return Box(type(value))
(arg_types, return_type) = args
return Box(effectful.ops.types.Operation[arg_types, return_type]) # type: ignore


@nested_type.register
def _(value: collections.abc.Callable):
if typing.get_overloads(value):
# `typing.get_overloads(value)` reads `__qualname__`/`__module__` on
# the callable and raises `AttributeError` on values like
# `pytest.mark.parametrize` (a `MarkDecorator`) or `dict.get` (a
# `method_descriptor`). Treat that the same way as the
# no-signature fallback: widen to `Box(type(value))`. #673.
try:
if typing.get_overloads(value):
return Box(type(value))
except (AttributeError, TypeError):
return Box(type(value))

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

if sig.return_annotation is inspect.Signature.empty:
Expand Down
67 changes: 67 additions & 0 deletions tests/test_internals_unification.py
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,73 @@ def test_nested_type_term_error():
nested_type(mock_term)


def test_nested_type_marker_decorator_widens_to_class():
"""#673: ``pytest.mark.parametrize`` is a ``MarkDecorator`` instance --
callable, but lacks ``__qualname__``. The Callable branch's
``typing.get_overloads`` raises ``AttributeError``; per the
canonicalize-widening principle (PR #613) the fallback should be
``Box(type(value))``."""
val = pytest.mark.parametrize
assert nested_type(val).value is type(val)


def test_nested_type_method_descriptor_widens_to_class():
"""#673: ``dict.get`` is a ``method_descriptor`` -- callable but
missing ``__module__``. Same widening shape as MarkDecorator."""
val = dict.get
assert nested_type(val).value is type(val)


def test_nested_type_unresolvable_forward_ref_widens():
"""#673: an ``Operation`` whose default carries a stringified
annotation whose name does not resolve in the captured scope.
``inspect.signature`` consults ``Operation.__signature__`` which
calls ``typing.get_type_hints`` -- forward-ref evaluation raises
``NameError``. ``nested_type`` should widen to ``Box(type(value))``
rather than propagating."""
from effectful.ops.syntax import defop

def _hidden_module():
class ClientSession: # noqa: F841 -- intentionally not visible to op
pass

def real(x: "ClientSession") -> int: # noqa: F821 -- forward ref unresolved
Comment thread
eb8680 marked this conversation as resolved.
raise NotImplementedError

return real

op = defop(_hidden_module())
assert nested_type(op).value is type(op)


def test_nested_type_eager_annotation_produces_precise_type():
"""#673: when the annotation is NOT stringified, it resolves at
function-def time and ``typing.get_type_hints`` succeeds.
``nested_type`` then has enough information to build the precise
``Operation[[ClientSession], int]`` expression (rather than
widening to ``Box(type(value))``). Counterpart to the
forward-ref-widens test above."""
import effectful.ops.types
from effectful.ops.syntax import defop

def _eager_module():
class ClientSession:
pass

def real(x: ClientSession) -> int:
raise NotImplementedError

return real, ClientSession

real, ClientSession = _eager_module()
op = defop(real)
inferred = nested_type(op).value
assert typing.get_origin(inferred) is effectful.ops.types.Operation
arg_types, return_type = typing.get_args(inferred)
assert list(arg_types) == [ClientSession]
assert return_type is int


def sequence_getitem[T](seq: collections.abc.Sequence[T], index: int) -> T:
return seq[index]

Expand Down
Loading