• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

beartype / plum / 30306158179

27 Jul 2026 09:16PM UTC coverage: 99.409% (-0.09%) from 99.494%
30306158179

Pull #289

github

web-flow
Merge fa35b0614 into 6840ebbd9
Pull Request #289: build(mypyc): make `_BoundFunction` native too

47 of 48 new or added lines in 1 file covered. (97.92%)

1009 of 1015 relevant lines covered (99.41%)

6.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.36
/src/plum/_function.py
1
__all__ = ("Function",)
7✔
2

3
import os
7✔
4
import textwrap
7✔
5
import threading
7✔
6
from collections.abc import Callable
7✔
7
from copy import copy
7✔
8
from functools import partial, wraps
7✔
9
from types import MethodType
7✔
10
from typing import Any, ClassVar, Protocol, TypeVar, overload
7✔
11
from typing_extensions import Self
7✔
12

13
from ._method import Method, MethodList
7✔
14
from ._mypyc import mypyc_attr
7✔
15
from ._resolver import AmbiguousLookupError, NotFoundLookupError, Resolver
7✔
16
from ._signature import Signature, append_default_args
7✔
17
from ._type import resolve_type_hint
7✔
18
from ._util import TypeHint
7✔
19

20
_MODULE = __name__
7✔
21
"""str: This module's name, returned as `Function.__module__` for class access."""
7✔
22

23
# Annotated (not left to inference as `None`) so a `mypyc`-compiled `_function` accepts
24
# the external assignment `plum._function._promised_convert = convert` in `_promotion`.
25
_promised_convert: Callable[..., Any] | None = None
26
"""function or None: This will be set to :func:`.parametric.convert`."""
7✔
27

28
SomeExceptionType = TypeVar("SomeExceptionType", bound=Exception)
7✔
29

30

31
def _convert(obj: Any, target_type: TypeHint, /) -> Any:
7✔
32
    """Convert an object to a particular type. Only converts if `target_type` is set.
33

34
    Args:
35
        obj (object): Object to convert.
36
        target_type (type): Type to convert to.
37

38
    Returns:
39
        object: `object_to_covert` converted to type of `obj_from_target`.
40
    """
41
    if target_type is Any:
7✔
42
        return obj
7✔
43
    else:
44
        assert _promised_convert is not None
7✔
45
        return _promised_convert(obj, target_type)
7✔
46

47

48
_owner_transfer: dict[type, type] = {}
7✔
49
"""dict[type, type]: When the keys of this dictionary are detected as the owner of
7✔
50
a function (see :meth:`Function.owner`), make the corresponding value the owner."""
51

52

53
@mypyc_attr(native_class=False)
7✔
54
class _InvokedMethod:
7✔
55
    """Callable returned by :meth:`Function.invoke`: it runs the resolved `method` and
56
    converts the result. A class (holding `method`/`return_type` as attributes) rather
57
    than a `self`-capturing closure, which `mypyc` cannot compile (mypyc/mypyc#1205).
58
    Non-native so :func:`functools.wraps` can copy `__name__`/`__doc__` onto instances.
59
    """
60

61
    def __init__(
62
        self, f: Callable[..., Any], method: Callable[..., Any], return_type: TypeHint
63
    ) -> None:
64
        self._method = method
65
        self._return_type = return_type
66
        wraps(f)(self)
67
        self.__wrapped_by_plum__ = method
68

69
    def __call__(self, *args: Any, **kw: Any) -> Any:
7✔
70
        return _convert(self._method(*args, **kw), self._return_type)
7✔
71

72

73
class _DocDescriptor:
7✔
74
    """Serves `__doc__` for both class access (`-> _class_doc`) and instance access
75
    (`-> _compute_doc()`, the computed/merged docstring), replacing the old metaclass.
76
    Shared by `Function` and `_BoundFunction`. Attached with `setattr` below because
77
    `mypyc` replaces a class-body `__doc__` with a filler. A descriptor (not a
78
    metaclass) lets the classes be native, which makes the hot `__call__` path faster.
79
    """
80

81
    def __get__(self, instance: Any, owner: type) -> str | None:
7✔
82
        if instance is None:
7✔
83
            return Function._class_doc
7✔
84
        doc: str | None = instance._compute_doc()
7✔
85
        return doc
7✔
86

87

88
class _ModuleDescriptor:
7✔
89
    """Serves `__module__` as the wrapped function's module (as `functools.wraps` did).
90
    Shared by `Function` and `_BoundFunction`. A descriptor because `__module__` is
91
    read-only on a native-class instance, so it cannot be assigned in `__init__`.
92
    """
93

94
    def __get__(self, instance: Any, owner: type) -> str:
7✔
95
        if instance is None:
7✔
NEW
96
            return _MODULE
×
97
        module: str = instance._f.__module__
7✔
98
        return module
7✔
99

100

101
class Function:
7✔
102
    #: The class-level docstring, served as `Function.__doc__` by `_DocDescriptor`.
103
    _class_doc: ClassVar[str] = """A function.
7✔
104

105
    Args:
106
        f (function): Function that is wrapped.
107
        owner (str, optional): Name of the class that owns the function.
108
        warn_redefinition (bool, optional): Throw a warning whenever a method is
109
            redefined. Defaults to `False`.
110
    """
111

112
    _instances: ClassVar[list["Function"]] = []
7✔
113

114
    # Instance attributes are declared so `Function` can be a `mypyc` native class.
115
    _f: Callable[..., Any]
116
    _cache: dict[tuple[TypeHint, ...], tuple[Callable[..., Any], TypeHint]]
117
    _doc: str
5✔
118
    _owner_name: str | None
5✔
119
    _owner: type | None
5✔
120
    _warn_redefinition: bool
5✔
121
    _pending: list[tuple[Callable[..., Any], "Signature | None", int | None]]
122
    _resolved: list[tuple[Callable[..., Any], "Signature | None", int | None]]
123
    _resolver: Resolver
5✔
124
    _lock: "threading.RLock"
5✔
125
    __name__: str
5✔
126
    __qualname__: str
5✔
127
    __wrapped__: Callable[..., Any]
128

129
    def __init__(
130
        self,
131
        f: Callable[..., Any],
132
        /,
133
        owner: str | None = None,
134
        warn_redefinition: bool = False,
135
    ) -> None:
136
        Function._instances.append(self)
137

138
        self._f = f
139
        # Cache maps type tuples to `(method, return_type)`. Keys can be either
140
        # actual types (from `__call__`) or `TypeHints` (from `invoke`).
141
        self._cache = {}
142

143
        # Guards the lazy resolution of pending registrations, which mutates each
144
        # registered function's `__annotations__` in place (via beartype's
145
        # `resolve_pep563`) and is otherwise not thread-safe. Reentrant because
146
        # `_resolve_pending_registrations` calls `clear_cache`, which also acquires this
147
        # lock. See GitHub issue #274.
148
        self._lock = threading.RLock()
149

150
        # Set explicitly instead of `functools.wraps`: `wraps` writes `__module__`
151
        # (read-only on a native instance) and updates `__dict__` (native classes have
152
        # none). `__doc__` is the `_DocDescriptor`, so store the raw docstring in
153
        # `self._doc`.
154
        self._doc = f.__doc__ if f.__doc__ else ""
155
        self.__wrapped__ = f
156

157
        self.__name__ = f.__name__
158
        self.__qualname__ = _generate_qualname(f)
159

160
        # `owner` is the name of the owner. We will later attempt to resolve to
161
        # which class it actually points.
162
        self._owner_name = owner
163
        self._owner = None
164

165
        self._warn_redefinition = warn_redefinition
166

167
        # Initialise pending and resolved methods.
168
        self._pending = []
169
        self._resolver = Resolver(
170
            self.__name__,
171
            warn_redefinition=self._warn_redefinition,
172
        )
173
        self._resolved = []
174

175
    @property
7✔
176
    def owner(self) -> type | None:
7✔
177
        """object or None: Owner of the function. If `None`, then there is no owner."""
178
        if self._owner is None and self._owner_name is not None:
7✔
179
            name = self._owner_name.split(".")[-1]
7✔
180
            self._owner = self._f.__globals__[name]
7✔
181
            # Check if the ownership needs to be transferred to another class. This
182
            # can be very important for preventing infinite loops.
183
            while self._owner in _owner_transfer:
7✔
184
                self._owner = _owner_transfer[self._owner]
7✔
185
        return self._owner
7✔
186

187
    def _compute_doc(self) -> str | None:
7✔
188
        """Compute the function's documentation: the documentation given at
189
        initialisation with the documentation of all other registered methods appended.
190

191
        Attached as the instance `__doc__` property by :class:`_FunctionMeta` (mypyc
192
        clobbers a class-body property named `__doc__`).
193
        """
194
        try:
7✔
195
            self._resolve_pending_registrations()
7✔
196
        except NameError:
7✔
197
            # When `staticmethod` is combined with `from __future__ import
198
            # annotations`, in Python 3.10 and higher `staticmethod` will
199
            # attempt to inherit `__doc__` (see
200
            # https://docs.python.org/3/library/functions.html#staticmethod).
201
            # Since we are still in class construction, forward references are
202
            # not yet defined, so attempting to resolve all pending methods
203
            # might fail with a `NameError`. This is fine, because later calling
204
            # `__doc__` on the `staticmethod` will again call this `__doc__`, at
205
            # which point all methods will resolve properly. For now, we just
206
            # ignore the error and undo the partially completed
207
            # :meth:`Function._resolve_pending_registrations` by clearing the
208
            # cache.
209
            self.clear_cache(reregister=False)
7✔
210

211
        # Don't do any fancy appending of docstrings when the environment variable
212
        # `PLUM_SIMPLE_DOC` is set to `1`.
213
        if "PLUM_SIMPLE_DOC" in os.environ and os.environ["PLUM_SIMPLE_DOC"] == "1":
7✔
214
            return self._doc
7✔
215

216
        # Derive the basis of the docstring from `self._f`, removing any indentation.
217
        doc = self._doc.strip()
7✔
218
        if doc:
7✔
219
            # Do not include the first line when removing the indentation.
220
            lines = doc.splitlines()
7✔
221
            doc = lines[0]
7✔
222
            # There might not be more than one line.
223
            if len(lines) > 1:
7✔
224
                doc += "\n" + textwrap.dedent("\n".join(lines[1:]))
7✔
225

226
        # Append the docstrings of all other implementations to it. Exclude the
227
        # docstring from `self._f`, because that one forms the basis (see boave).
228
        resolver_doc = self._resolver.doc(exclude=self._f)
7✔
229
        if resolver_doc:
7✔
230
            # Add a newline if the documentation is non-empty.
231
            if doc:
7✔
232
                doc = doc + "\n\n"
7✔
233
            doc += resolver_doc
7✔
234
            # Replace separators with horizontal lines of the right length.
235
            separator_length = max(map(len, doc.splitlines()))
7✔
236
            doc = doc.replace("<separator>", "-" * separator_length)
7✔
237

238
        # If the docstring is empty, return `None`, which is consistent with omitting
239
        # the docstring.
240
        return doc if doc else None
7✔
241

242
    @property
7✔
243
    def methods(self) -> MethodList:
7✔
244
        """list[:class:`.method.Method`]: All available methods."""
245
        self._resolve_pending_registrations()
7✔
246
        return self._resolver.methods
7✔
247

248
    def dispatch(
249
        self: Self, method: Callable[..., Any] | None = None, precedence: int = 0
250
    ) -> Self | Callable[[Callable[..., Any]], Self]:
251
        """Decorator to extend the function with another signature.
252

253
        Args:
254
            precedence (int, optional): Precedence of the signature. Defaults to `0`.
255

256
        Returns:
257
            function: Decorator.
258
        """
259
        if method is None:
260
            # `partial`, not a `self`-capturing closure: `mypyc` cannot compile the
261
            # latter on a non-native class (mypyc/mypyc#1205).
262
            return partial(self._dispatch_one, precedence=precedence)
263
        return self._dispatch_one(method, precedence=precedence)
264

265
    def dispatch_multi(
266
        self: Self, *signatures: Signature | tuple[TypeHint, ...]
267
    ) -> Callable[[Callable[..., Any]], Self]:
268
        """Decorator to extend the function with multiple signatures at once.
269

270
        Args:
271
            *signatures (tuple or :class:`.signature.Signature`): Signatures to
272
                register.
273

274
        Returns:
275
            function: Decorator.
276
        """
277
        resolved_signatures = []
278
        for signature in signatures:
279
            if isinstance(signature, Signature):
280
                resolved_signatures.append(signature)
281
            elif isinstance(signature, tuple):
282
                resolved_signatures.append(Signature(*signature))
283
            else:
284
                # `TypeError` (not `ValueError`) to match the compiled build, where the
285
                # typed vararg rejects bad input at the C boundary before this runs.
286
                raise TypeError(
287
                    f"Signature `{signature}` must be a tuple or of type "
288
                    f"`plum.signature.Signature`."
289
                )
290
        return partial(self._dispatch_one, signatures=resolved_signatures)
291

292
    def _dispatch_one(
293
        self: Self,
294
        method: Callable[..., Any],
295
        /,
296
        *,
297
        signatures: list[Signature] | None = None,
298
        precedence: int = 0,
299
    ) -> Self:
300
        """Register `method` and return the function itself.
301

302
        Shared registration path for :meth:`dispatch` (single method, by `precedence`)
303
        and :meth:`dispatch_multi` (explicit `signatures`). Kept a bound method so the
304
        decorator forms can be built with :func:`functools.partial` rather than a
305
        `self`-capturing closure, which `mypyc` cannot compile (mypyc/mypyc#1205).
306
        """
307
        if signatures is None:
308
            self.register(method, precedence=precedence)
309
        else:
310
            # `precedence` is derived from each signature, so it is left as `None`.
311
            for signature in signatures:
312
                self.register(method, signature=signature, precedence=None)
313
        return self
314

315
    def clear_cache(self, reregister: bool = True) -> None:
7✔
316
        """Clear cache.
317

318
        Args:
319
            reregister (bool, optional): Also reregister all methods. Defaults to
320
                `True`.
321
        """
322
        # Serialise against concurrent resolution: the `reregister` branch swaps
323
        # `_pending`/`_resolved`/`_resolver` in multiple steps. See GitHub issue #274.
324
        with self._lock:
7✔
325
            self._cache.clear()
7✔
326

327
            if reregister:
7✔
328
                # Add all resolved to pending.
329
                self._pending.extend(self._resolved)
7✔
330

331
                # Clear resolved.
332
                self._resolved = []
7✔
333
                self._resolver = Resolver(
7✔
334
                    self._resolver.function_name,
335
                    warn_redefinition=self._warn_redefinition,
336
                )
337

338
    def register(
339
        self,
340
        f: Callable[..., Any],
341
        signature: Signature | None = None,
342
        precedence: int | None = 0,
343
    ) -> None:
344
        """Register a method.
345

346
        Either `signature` or `precedence` must be given.
347

348
        Args:
349
            f (function): Function that implements the method.
350
            signature (:class:`.signature.Signature`, optional): Signature. If it is
351
                not given, it will be derived from `f`.
352
            precedence (int, optional): Precedence of the function. If `signature` is
353
                given, then this argument will not be used. Defaults to `0`.
354
        """
355
        self._pending.append((f, signature, precedence))
356

357
    def _resolve_pending_registrations(self) -> None:
7✔
358
        # Fast path: nothing pending. This unlocked check keeps the common
359
        # already-resolved case, which is the hot dispatch path, lock-free.
360
        if not self._pending:
7✔
361
            return
7✔
362

363
        # Resolution mutates each registered function's annotations in place and is not
364
        # thread-safe, so serialise it. See GitHub issue #274.
365
        with self._lock:
7✔
366
            # Re-check under the lock: another thread may have completed the resolution
367
            # while we were waiting to acquire it.
368
            if not self._pending:
7✔
369
                return
7✔
370

371
            # Keep track of whether anything registered.
372
            registered = False
7✔
373

374
            # Perform any pending registrations.
375
            for f, signature, precedence in self._pending:
7✔
376
                # Add to resolved registrations.
377
                self._resolved.append((f, signature, precedence))
7✔
378

379
                # Obtain the signature if it is not available.
380
                if signature is None:
7✔
381
                    # When signature is `None`, precedence should always be set.
382
                    assert precedence is not None
7✔
383
                    signature = Signature.from_callable(f, precedence=precedence)
7✔
384
                else:
385
                    # Ensure that the implementation is `f`, but make a copy before
386
                    # mutating.
387
                    signature = copy(signature)
7✔
388

389
                # Process default values.
390
                for subsignature in append_default_args(signature, f):
7✔
391
                    submethod = Method(f, subsignature, function_name=self.__name__)
7✔
392
                    self._resolver.register(submethod)
7✔
393
                    registered = True
7✔
394

395
            if registered:
7✔
396
                self._pending = []
7✔
397

398
                # Clear cache. Reenters `self._lock`, which is why it is an `RLock`.
399
                self.clear_cache(reregister=False)
7✔
400

401
    def resolve_method(
402
        self, target: tuple[object, ...] | Signature
403
    ) -> tuple[Callable[..., Any], TypeHint]:
404
        """Find the method and return type for arguments.
405

406
        Args:
407
            target (object): Target.
408

409
        Returns:
410
            `tuple[function, type]`:
411
                * Method.
412
                * Return type.
413
        """
414
        self._resolve_pending_registrations()
415

416
        try:
417
            # Attempt to find the method using the resolver.
418
            method = self._resolver.resolve(target)
419
            impl = method.implementation
420
            return_type = method.return_type
421

422
        # The two `except` clauses use distinct variable names (`e_ambiguous` /
423
        # `e_not_found`) rather than a shared `e`: `mypyc` gives a reused exception
424
        # variable a single type, so binding the second exception type to it fails a
425
        # runtime type check.
426
        except AmbiguousLookupError as e_ambiguous:
427
            __tracebackhide__ = True
428

429
            # Change the function name if this is a method.
430
            if self.owner:
431
                e_ambiguous.f_name = self.__qualname__
432
            raise e_ambiguous from None
433

434
        except NotFoundLookupError as e_not_found:
435
            __tracebackhide__ = True
436

437
            # Change the function name if this is a method.
438
            if self.owner:
439
                e_not_found.f_name = self.__qualname__
440
            impl, return_type = self._handle_not_found_lookup_error(e_not_found)
441

442
        return impl, return_type
443

444
    def _handle_not_found_lookup_error(
445
        self, ex: NotFoundLookupError, /
446
    ) -> tuple[Callable[..., Any], TypeHint]:
447
        if not self.owner:
448
            # Not in a class. Nothing we can do.
449
            raise ex from None
450

451
        # In a class. Walk through the classes in the class's MRO, except for this
452
        # class, and try to get the method.
453
        method: Callable[..., Any] | None = None
454
        return_type: TypeHint = object
455

456
        for c in self.owner.__mro__[1:]:
457
            # Skip the top of the type hierarchy given by `object` and `type`. We do
458
            # not suddenly want to fall back to any unexpected default behaviour.
459
            if c in {object, type}:
460
                continue
461

462
            # We need to check `c.__dict__` here instead of using `hasattr` since e.g.
463
            # `c.__le__` will return  even if `c` does not implement `__le__`!
464
            if self._f.__name__ in c.__dict__:
465
                method = getattr(c, self._f.__name__)
466
            else:
467
                # For some reason, coverage fails to catch the `continue` below. Add
468
                # the do-nothing `_ = None` fixes this.
469
                # TODO: Remove this once coverage properly catches this.
470
                _ = None
471
                continue
472

473
            # Ignore abstract methods.
474
            if getattr(method, "__isabstractmethod__", False):
475
                method = None
476
                continue
477

478
            # We found a good candidate. Break.
479
            break
480

481
        if not method:
482
            # If no method has been found after walking through the MRO, raise the
483
            # original exception.
484
            raise ex from None
485
        return method, return_type
486

487
    def __call__(self, *args: object, **kw: object) -> object:
7✔
488
        __tracebackhide__ = True
7✔
489
        method, return_type = self._resolve_method_with_cache(args=args)
7✔
490
        return _convert(method(*args, **kw), return_type)
7✔
491

492
    def _resolve_method_with_cache(
493
        self,
494
        args: tuple[object, ...] | Signature | None = None,
495
        types: tuple[TypeHint, ...] | None = None,
496
    ) -> tuple[Callable[..., Any], TypeHint]:
497
        if args is None and types is None:
498
            raise ValueError(
499
                "Arguments `args` and `types` cannot both be `None`. "
500
                "This should never happen!"
501
            )
502

503
        # Before attempting to use the cache, resolve any unresolved registrations. Use
504
        # an `if`-statement to speed up the common case.
505
        if self._pending:
506
            self._resolve_pending_registrations()
507

508
        # Compute cache key. When called from `__call__`, types will be actual
509
        # runtime types from `map(type, args)`. When called from `invoke`, types
510
        # may be `TypeHints` like `Union[int, str]`. Both are hashable and work
511
        # as cache keys.
512
        if types is None:
513
            # Attempt to use the cache based on the types of the arguments.
514
            # At this point, `args` must be a tuple (not `Signature` or `None`).
515
            assert isinstance(args, tuple)
516
            types = tuple(map(type, args))
517
        try:
518
            return self._cache[types]
519
        except KeyError:
520
            __tracebackhide__ = True
521

522
            if args is None:
523
                args = Signature(*(resolve_type_hint(t) for t in types))
524

525
            # Cache miss. Run the resolver based on the arguments.
526
            method, return_type = self.resolve_method(args)
527
            # If the resolver is faithful, then we can perform caching using the types
528
            # of the arguments. If the resolver is not faithful, then we cannot.
529
            if self._resolver.is_faithful:
530
                self._cache[types] = method, return_type
531
            return method, return_type
532

533
    def invoke(self, *types: TypeHint) -> Callable[..., Any]:
534
        """Invoke a particular method.
535

536
        Args:
537
            *types: Types to resolve.
538

539
        Returns:
540
            function: Method.
541
        """
542
        method, return_type = self._resolve_method_with_cache(types=types)
543
        return _InvokedMethod(self._f, method, return_type)
544

545
    @overload
546
    def __get__(self, instance: None, owner: type, /) -> "Function": ...
547
    @overload
548
    def __get__(self, instance: object, owner: type, /) -> MethodType: ...
549

550
    def __get__(
7✔
551
        self, instance: object | None, owner: type, /
552
    ) -> "Function | MethodType":
553
        if instance is None:
7✔
554
            return self
7✔
555
        return MethodType(_BoundFunction(self, instance), instance)
7✔
556

557
    def __repr__(self) -> str:
7✔
558
        return (
7✔
559
            f"<multiple-dispatch function {self.__qualname__} (with"
560
            f" {len(self._resolver)} registered and {len(self._pending)}"
561
            f" pending method(s))>"
562
        )
563

564

565
# Attach `__doc__`/`__module__` here, not in the class body: `mypyc` replaces a class
566
# `__doc__` with a filler, and `__module__` is read-only on a native instance. These
567
# descriptors serve instance access (`f.__doc__`, `f.__module__`). `setattr` also stops
568
# `mypy` treating these as class variables.
569
setattr(Function, "__doc__", _DocDescriptor())  # noqa: B010
7✔
570
setattr(Function, "__module__", _ModuleDescriptor())  # noqa: B010
7✔
571

572

573
def _generate_qualname(f: Callable[..., Any], /) -> str:
574
    """Generate a qualified name for a function.
575

576
    This function can be interpreted as an improved version of `f.__qualname__`
577
    and can be run regardless of whether `f.__qualname__` exists.
578

579
    Args:
580
        f (Callable): Function.
581

582
    Returns:
583
        str: Qualified name.
584
    """
585
    qualname = getattr(f, "__qualname__", f.__name__)
586

587
    # TODO: If we ever want to scope functions, we can uncomment this.
588
    # if hasattr(f, "__module__"):
589
    #     qualname = f"{f.__module__}.{qualname}"
590
    # `__main__` would be part of `f.__name__` in e.g. the REPL.
591
    # qualname = qualname.replace("__main__.", """)
592

593
    return qualname
594

595

596
class _DispatchFunction(Protocol):
7✔
597
    """Protocol for the `dispatch` method of a function."""
598

599
    def __call__(
600
        self, method: Callable[..., Any] | None, precedence: int
601
    ) -> Self | Callable[[Callable[..., Any]], Self]: ...
602

603

604
class _BoundFunctionProto(Protocol):
7✔
605
    """Subset of :class:`Function`'s interface required by :class:`_BoundFunction`.
606

607
    Declaring `_BoundFunction._f` with this Protocol rather than :class:`Function`
608
    directly prevents `mypy` from applying `Function.__get__`'s descriptor protocol
609
    when resolving instance-attribute accesses of `_f`.
610
    """
611

612
    _f: Callable[..., Any]
613

614
    def __call__(self, *args: object, **kw: object) -> object: ...
615

616
    def invoke(self, *types: TypeHint) -> Callable[..., Any]: ...
617

618
    @property
619
    def methods(self) -> MethodList: ...
620

621
    def dispatch(
622
        self,
623
        method: Callable[..., Any] | None = None,
624
        precedence: int = 0,
625
    ) -> Any: ...
626

627

628
class _BoundFunction:
7✔
629
    """A bound instance of `.function.Function`.
630

631
    Args:
632
        f (:class:`.function.Function`): Bound function.
633
        instance (object): Instance to which the function is bound.
634
    """
635

636
    # Declared so `_BoundFunction` is a `mypyc` native class (like `Function`), which
637
    # speeds up bound (class-method) dispatch. `_f` holds a `Function` (typed as proto).
638
    _f: "_BoundFunctionProto"
5✔
639
    _instance: object
5✔
640
    __name__: str
5✔
641
    __qualname__: str
5✔
642
    __wrapped__: Callable[..., Any]
643

644
    def __init__(self, f: "Function", instance: object) -> None:
7✔
645
        self._f = f
7✔
646
        self._instance = instance
7✔
647
        # Set explicitly instead of `functools.wraps` (native class); `__doc__`/
648
        # `__module__` are the descriptors attached below.
649
        self.__name__ = f.__name__
7✔
650
        self.__qualname__ = f.__qualname__
7✔
651
        self.__wrapped__ = f._f
7✔
652

653
    def _compute_doc(self) -> str | None:
7✔
654
        return self._f.__doc__
7✔
655

656
    def __call__(self, _: object, *args: object, **kw: object) -> object:
7✔
657
        return self._f(self._instance, *args, **kw)
7✔
658

659
    def invoke(self, *types: TypeHint) -> Callable[..., Any]:
660
        """See :meth:`.Function.invoke`."""
661
        # A callable object, not a `self`-capturing closure, which mypyc cannot compile
662
        # (mypyc/mypyc#1205). Unlike `Function.invoke`, we set no `__wrapped_by_plum__`
663
        # here: this callable prepends `self._instance`, so there is no directly-
664
        # extendable "wrapped method" to unwrap to.
665
        return _BoundInvokedMethod(self, types)
666

667
    @property
7✔
668
    def methods(self) -> MethodList:
7✔
669
        """list[:class:`.method.Method`]: All available methods."""
670
        return self._f.methods
7✔
671

672
    @property
7✔
673
    def dispatch(self) -> _DispatchFunction:
7✔
674
        """See :meth:`.Function.dispatch`."""
675
        return self._f.dispatch
7✔
676

677

678
# See `Function` above: the descriptors serve `__doc__`/`__module__` on instances.
679
setattr(_BoundFunction, "__doc__", _DocDescriptor())  # noqa: B010
7✔
680
setattr(_BoundFunction, "__module__", _ModuleDescriptor())  # noqa: B010
7✔
681

682

683
@mypyc_attr(native_class=False)
7✔
684
class _BoundInvokedMethod:
7✔
685
    """Callable returned by :meth:`_BoundFunction.invoke` (see there)."""
686

687
    def __init__(self, bound: "_BoundFunction", types: tuple[TypeHint, ...]) -> None:
688
        self._bound = bound
689
        self._types = types
690
        # `bound.__wrapped__` is the underlying function (`f._f`), set in
691
        # `_BoundFunction.__init__`.
692
        wraps(bound.__wrapped__)(self)
693

694
    def __call__(self, *args: Any, **kw: Any) -> Any:
7✔
695
        method = self._bound._f.invoke(type(self._bound._instance), *self._types)
7✔
696
        return method(self._bound._instance, *args, **kw)
7✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc