Skip to content

Commit 12a29cb

Browse files
committed
refactor: remove _isfunctionlike()
The `inspect` module (CPython) relies on structural checking to support non-native function objects (in particular, any user-defined function can be "cythonized", and for the resulting object, `inspect.isfunction()` will return `False`), and the same was used in `_isfunctionlike()`. Essentially, this is a check that the function is introspectable, as also indicated in python/cpython#131983. But should we really follow this approach? One of the main disadvantages of structural checks is that they are slow. Moreover, `_isfunctionlike()` also checks attribute types for compliance with "native" types (types from the `types` module), which is a rather narrow approach: will every possible introspectable function really provide `__code__` of a native type? And that is not to mention how ambiguous the checks themselves are: 1. They do not check the complete set of introspectable attributes (and it is quite possible that some implementations will not even provide all introspectable attributes, as can be seen in cython/cython#7448). 2. Even an incomplete set of attributes is redundant when we only need `__code__`, and it prevents support for partially introspectable objects. It follows that "function-like" objects do not actually exist, which means that it is impossible to define a protocol for them. The answer to how we should deal with non-native but introspectable objects can be found in the `functools.wraps()` implementation. It knows nothing about object types. We can wrap any callable object, and all known introspectable attributes will be assigned to the wrapper, even if the former is not a user-defined function. We can say that it assumes that the attribute values are consistent with the behavior of the object (and the meaning of the attributes themselves is already specified by their special names). And so, just the existence of the `__code__` attribute is enough to consider its flags relevant to the call result. If the assumption is violated, we will consider it a user error.
1 parent d51a225 commit 12a29cb

3 files changed

Lines changed: 12 additions & 73 deletions

File tree

‎src/aiologic/meta/_inspect.py‎

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
CO_ITERABLE_COROUTINE,
1818
isawaitable,
1919
isclass,
20-
iscode,
21-
isfunction,
2220
ismethod,
2321
)
2422
from types import (
@@ -32,8 +30,7 @@
3230
from ._markers import DEFAULT, MISSING
3331

3432
if TYPE_CHECKING:
35-
from types import CodeType
36-
from typing import Any, TypeVar, type_check_only
33+
from typing import Any, TypeVar
3734

3835
from ._markers import DefaultType, MissingType
3936

@@ -73,11 +70,6 @@ def ismethodwrapper(object: object) -> TypeIs[MethodWrapperType]:
7370
else: # typing-extensions>=3.10.0
7471
from typing_extensions import ParamSpec
7572

76-
if sys.version_info >= (3, 13): # various fixes and improvements
77-
from typing import Protocol
78-
else: # typing-extensions>=4.10.0
79-
from typing_extensions import Protocol
80-
8173
if sys.version_info >= (3, 10): # PEP 647
8274
from typing import TypeGuard
8375
else: # typing-extensions>=3.10.0
@@ -94,16 +86,6 @@ def ismethodwrapper(object: object) -> TypeIs[MethodWrapperType]:
9486
from typing_extensions import overload
9587

9688
if TYPE_CHECKING:
97-
98-
@type_check_only
99-
class _FunctionLike(Protocol):
100-
__code__: CodeType
101-
__name__: str
102-
__defaults__: tuple[Any, ...] | None
103-
__kwdefaults__: dict[str, Any] | None
104-
105-
def __call__(self, /, *args: Any, **kwargs: Any) -> Any: ...
106-
10789
_T = TypeVar("_T")
10890
_CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
10991
_P = ParamSpec("_P")
@@ -413,28 +395,6 @@ def _catch_asyncgenfunction_marker() -> _MarkerInfo:
413395
_partialmethod_attribute_name: str = "_partialmethod"
414396

415397

416-
def _isfunctionlike(obj: object, /) -> TypeIs[_FunctionLike]:
417-
return isfunction(obj) or (
418-
callable(obj)
419-
and not isclass(obj)
420-
and iscode(
421-
getattr(obj, "__code__", MISSING),
422-
)
423-
and isinstance(
424-
getattr(obj, "__name__", MISSING),
425-
str,
426-
)
427-
and isinstance(
428-
getattr(obj, "__defaults__", MISSING),
429-
(NoneType, tuple),
430-
)
431-
and isinstance(
432-
getattr(obj, "__kwdefaults__", MISSING),
433-
(NoneType, dict),
434-
)
435-
)
436-
437-
438398
def _iscallwrapper(obj: object, /) -> bool:
439399
return ismethodwrapper(obj) and (
440400
not ismethod(obj) # CPython
@@ -480,7 +440,12 @@ def _unwrap_and_check(
480440
return False
481441

482442
if _iscallwrapper(call) and call.__self__ is obj:
483-
return _isfunctionlike(obj) and bool(obj.__code__.co_flags & flag)
443+
code = getattr(obj, "__code__", None)
444+
445+
if code is None:
446+
return False
447+
448+
return bool(getattr(code, "co_flags", 0) & flag)
484449

485450
obj = call
486451

‎src/aiologic/meta/_inspect.pyi‎

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import sys
77

88
from dataclasses import dataclass, field
9-
from types import AsyncGeneratorType, CodeType, CoroutineType, GeneratorType
10-
from typing import Any, TypeVar, type_check_only
9+
from types import AsyncGeneratorType, CoroutineType, GeneratorType
10+
from typing import Any, TypeVar
1111

1212
from ._markers import DEFAULT, MISSING, DefaultType, MissingType
1313

@@ -38,11 +38,6 @@ if sys.version_info >= (3, 10): # PEP 612
3838
else: # typing-extensions>=3.10.0
3939
from typing_extensions import ParamSpec
4040

41-
if sys.version_info >= (3, 13): # various fixes and improvements
42-
from typing import Protocol
43-
else: # typing-extensions>=4.10.0
44-
from typing_extensions import Protocol
45-
4641
if sys.version_info >= (3, 10): # PEP 647
4742
from typing import TypeGuard
4843
else: # typing-extensions>=3.10.0
@@ -58,15 +53,6 @@ if sys.version_info >= (3, 11): # runtime introspection support
5853
else: # typing-extensions>=4.2.0
5954
from typing_extensions import overload
6055

61-
@type_check_only
62-
class _FunctionLike(Protocol):
63-
__code__: CodeType
64-
__name__: str
65-
__defaults__: tuple[Any, ...] | None
66-
__kwdefaults__: dict[str, Any] | None
67-
68-
def __call__(self, /, *args: Any, **kwargs: Any) -> Any: ...
69-
7056
_T = TypeVar("_T")
7157
_CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
7258
_P = ParamSpec("_P")
@@ -109,7 +95,6 @@ def _catch_asyncgenfunction_marker() -> _MarkerInfo: ...
10995

11096
_partialmethod_attribute_name: str
11197

112-
def _isfunctionlike(obj: object, /) -> TypeIs[_FunctionLike]: ...
11398
def _iscallwrapper(obj: object, /) -> bool: ...
11499
def _unwrap_and_check(
115100
obj: object,

‎src/aiologic/meta/_types.py‎

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@
1919
from typing import TYPE_CHECKING, Any, TypeVar, get_args, get_origin
2020

2121
from ._helpers import GeneratorCoroutineWrapper
22-
from ._inspect import (
23-
_isfunctionlike,
24-
isasyncgenfactory,
25-
iscoroutinefactory,
26-
isgeneratorfactory,
27-
)
22+
from ._inspect import isasyncgenfactory, iscoroutinefactory, isgeneratorfactory
2823

2924
if TYPE_CHECKING:
3025
from typing import Final
@@ -432,10 +427,7 @@ def generator(func, /):
432427

433428
genfunc = _generator(func)
434429

435-
if _isfunctionlike(func):
436-
flags = func.__code__.co_flags
437-
else:
438-
flags = 0
430+
flags = getattr(getattr(func, "__code__", None), "co_flags", 0)
439431

440432
if flags & CO_GENERATOR or isgeneratorfactory(func):
441433

@@ -507,10 +499,7 @@ def coroutine(func, /):
507499

508500
corofunc = _coroutine(func)
509501

510-
if _isfunctionlike(func):
511-
flags = func.__code__.co_flags
512-
else:
513-
flags = 0
502+
flags = getattr(getattr(func, "__code__", None), "co_flags", 0)
514503

515504
if flags & CO_GENERATOR or isgeneratorfactory(func):
516505

0 commit comments

Comments
 (0)