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

deepset-ai / haystack / 14877781483

07 May 2025 07:34AM UTC coverage: 90.408% (-0.007%) from 90.415%
14877781483

push

github

web-flow
feat: add py.typed; adjust `Component` protocol (#9329)

* experimenting with py.typed

* try changing run method in protocol

* Trigger Build

* better docstring + release note

* remove type:ignore where possible

* Removed a few more type: ignores

---------

Co-authored-by: Sebastian Husch Lee <sjrl423@gmail.com>

10914 of 12072 relevant lines covered (90.41%)

0.9 hits per line

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

98.9
haystack/core/component/component.py
1
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2
#
3
# SPDX-License-Identifier: Apache-2.0
4

5
"""
6
Attributes:
7

8
    component: Marks a class as a component. Any class decorated with `@component` can be used by a Pipeline.
9

10
All components must follow the contract below. This docstring is the source of truth for components contract.
11

12
<hr>
13

14
`@component` decorator
15

16
All component classes must be decorated with the `@component` decorator. This allows Haystack to discover them.
17

18
<hr>
19

20
`__init__(self, **kwargs)`
21

22
Optional method.
23

24
Components may have an `__init__` method where they define:
25

26
- `self.init_parameters = {same parameters that the __init__ method received}`:
27
    In this dictionary you can store any state the components wish to be persisted when they are saved.
28
    These values will be given to the `__init__` method of a new instance when the pipeline is loaded.
29
    Note that by default the `@component` decorator saves the arguments automatically.
30
    However, if a component sets their own `init_parameters` manually in `__init__()`, that will be used instead.
31
    Note: all of the values contained here **must be JSON serializable**. Serialize them manually if needed.
32

33
Components should take only "basic" Python types as parameters of their `__init__` function, or iterables and
34
dictionaries containing only such values. Anything else (objects, functions, etc) will raise an exception at init
35
time. If there's the need for such values, consider serializing them to a string.
36

37
_(TODO explain how to use classes and functions in init. In the meantime see `test/components/test_accumulate.py`)_
38

39
The `__init__` must be extremely lightweight, because it's a frequent operation during the construction and
40
validation of the pipeline. If a component has some heavy state to initialize (models, backends, etc...) refer to
41
the `warm_up()` method.
42

43
<hr>
44

45
`warm_up(self)`
46

47
Optional method.
48

49
This method is called by Pipeline before the graph execution. Make sure to avoid double-initializations,
50
because Pipeline will not keep track of which components it called `warm_up()` on.
51

52
<hr>
53

54
`run(self, data)`
55

56
Mandatory method.
57

58
This is the method where the main functionality of the component should be carried out. It's called by
59
`Pipeline.run()`.
60

61
When the component should run, Pipeline will call this method with an instance of the dataclass returned by the
62
method decorated with `@component.input`. This dataclass contains:
63

64
- all the input values coming from other components connected to it,
65
- if any is missing, the corresponding value defined in `self.defaults`, if it exists.
66

67
`run()` must return a single instance of the dataclass declared through the method decorated with
68
`@component.output`.
69

70
"""
71

72
import inspect
1✔
73
from collections.abc import Callable, Coroutine
1✔
74
from contextlib import contextmanager
1✔
75
from contextvars import ContextVar
1✔
76
from copy import deepcopy
1✔
77
from dataclasses import dataclass
1✔
78
from types import new_class
1✔
79
from typing import Any, Dict, Optional, Protocol, Type, TypeVar, Union, runtime_checkable
1✔
80

81
from typing_extensions import ParamSpec
1✔
82

83
from haystack import logging
1✔
84
from haystack.core.errors import ComponentError
1✔
85

86
from .sockets import Sockets
1✔
87
from .types import InputSocket, OutputSocket, _empty
1✔
88

89
logger = logging.getLogger(__name__)
1✔
90

91
RunParamsT = ParamSpec("RunParamsT")
1✔
92
SyncRunReturnT = TypeVar("SyncRunReturnT", bound=Dict[str, Any])
1✔
93
AsyncRunReturnT = TypeVar("AsyncRunReturnT", bound=Coroutine[Any, Any, Dict[str, Any]])
1✔
94
RunReturnT = Union[SyncRunReturnT, AsyncRunReturnT]
1✔
95

96
T = TypeVar("T")
1✔
97

98

99
@dataclass
1✔
100
class PreInitHookPayload:
1✔
101
    """
102
    Payload for the hook called before a component instance is initialized.
103

104
    :param callback:
105
        Receives the following inputs: component class and init parameter keyword args.
106
    :param in_progress:
107
        Flag to indicate if the hook is currently being executed.
108
        Used to prevent it from being called recursively (if the component's constructor
109
        instantiates another component).
110
    """
111

112
    callback: Callable
1✔
113
    in_progress: bool = False
1✔
114

115

116
_COMPONENT_PRE_INIT_HOOK: ContextVar[Optional[PreInitHookPayload]] = ContextVar("component_pre_init_hook", default=None)
1✔
117

118

119
@contextmanager
1✔
120
def _hook_component_init(callback: Callable):
1✔
121
    """
122
    Context manager to set a callback that will be invoked before a component's constructor is called.
123

124
    The callback receives the component class and the init parameters (as keyword arguments) and can modify the init
125
    parameters in place.
126

127
    :param callback:
128
        Callback function to invoke.
129
    """
130
    token = _COMPONENT_PRE_INIT_HOOK.set(PreInitHookPayload(callback))
1✔
131
    try:
1✔
132
        yield
1✔
133
    finally:
134
        _COMPONENT_PRE_INIT_HOOK.reset(token)
1✔
135

136

137
@runtime_checkable
1✔
138
class Component(Protocol):
1✔
139
    """
140
    Note this is only used by type checking tools.
141

142
    In order to implement the `Component` protocol, custom components need to
143
    have a `run` method. The signature of the method and its return value
144
    won't be checked, i.e. classes with the following methods:
145

146
        def run(self, param: str) -> Dict[str, Any]:
147
            ...
148

149
    and
150

151
        def run(self, **kwargs):
152
            ...
153

154
    will be both considered as respecting the protocol. This makes the type
155
    checking much weaker, but we have other places where we ensure code is
156
    dealing with actual Components.
157

158
    The protocol is runtime checkable so it'll be possible to assert:
159

160
        isinstance(MyComponent, Component)
161
    """
162

163
    # The following expression defines a run method compatible with any input signature.
164
    # Its type is equivalent to Callable[..., Dict[str, Any]].
165
    # See https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable.
166
    #
167
    # Using `run: Callable[..., Dict[str, Any]]` directly leads to type errors: the protocol would expect a settable
168
    # attribute `run`, while the actual implementation is a read-only method.
169
    # For example:
170
    # from haystack import Pipeline, component
171
    # @component
172
    # class MyComponent:
173
    #     @component.output_types(out=str)
174
    #     def run(self):
175
    #         return {"out": "Hello, world!"}
176
    # pipeline = Pipeline()
177
    # pipeline.add_component("my_component", MyComponent())
178
    #
179
    # mypy raises:
180
    # error: Argument 2 to "add_component" of "PipelineBase" has incompatible type "MyComponent"; expected "Component"
181
    # [arg-type]
182
    # note: Protocol member Component.run expected settable variable, got read-only attribute
183

184
    def run(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:  # pylint: disable=missing-function-docstring # noqa: D102
1✔
185
        ...
×
186

187

188
class ComponentMeta(type):
1✔
189
    @staticmethod
1✔
190
    def _positional_to_kwargs(cls_type, args) -> Dict[str, Any]:
1✔
191
        """
192
        Convert positional arguments to keyword arguments based on the signature of the `__init__` method.
193
        """
194
        init_signature = inspect.signature(cls_type.__init__)
1✔
195
        init_params = {name: info for name, info in init_signature.parameters.items() if name != "self"}
1✔
196

197
        out = {}
1✔
198
        for arg, (name, info) in zip(args, init_params.items()):
1✔
199
            if info.kind == inspect.Parameter.VAR_POSITIONAL:
1✔
200
                raise ComponentError(
1✔
201
                    "Pre-init hooks do not support components with variadic positional args in their init method"
202
                )
203

204
            assert info.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY)
1✔
205
            out[name] = arg
1✔
206
        return out
1✔
207

208
    @staticmethod
1✔
209
    def _parse_and_set_output_sockets(instance: Any):
1✔
210
        has_async_run = hasattr(instance, "run_async")
1✔
211

212
        # If `component.set_output_types()` was called in the component constructor,
213
        # `__haystack_output__` is already populated, no need to do anything.
214
        if not hasattr(instance, "__haystack_output__"):
1✔
215
            # If that's not the case, we need to populate `__haystack_output__`
216
            #
217
            # If either of the run methods were decorated, they'll have a field assigned that
218
            # stores the output specification. If both run methods were decorated, we ensure that
219
            # outputs are the same. We deepcopy the content of the cache to transfer ownership from
220
            # the class method to the actual instance, so that different instances of the same class
221
            # won't share this data.
222

223
            run_output_types = getattr(instance.run, "_output_types_cache", {})
1✔
224
            async_run_output_types = getattr(instance.run_async, "_output_types_cache", {}) if has_async_run else {}
1✔
225

226
            if has_async_run and run_output_types != async_run_output_types:
1✔
227
                raise ComponentError("Output type specifications of 'run' and 'run_async' methods must be the same")
1✔
228
            output_types_cache = run_output_types
1✔
229

230
            instance.__haystack_output__ = Sockets(instance, deepcopy(output_types_cache), OutputSocket)
1✔
231

232
    @staticmethod
1✔
233
    def _parse_and_set_input_sockets(component_cls: Type, instance: Any):
1✔
234
        def inner(method, sockets):
1✔
235
            from inspect import Parameter
1✔
236

237
            run_signature = inspect.signature(method)
1✔
238

239
            for param_name, param_info in run_signature.parameters.items():
1✔
240
                if param_name == "self" or param_info.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
1✔
241
                    continue
1✔
242

243
                socket_kwargs = {"name": param_name, "type": param_info.annotation}
1✔
244
                if param_info.default != Parameter.empty:
1✔
245
                    socket_kwargs["default_value"] = param_info.default
1✔
246

247
                new_socket = InputSocket(**socket_kwargs)
1✔
248

249
                # Also ensure that new sockets don't override existing ones.
250
                existing_socket = sockets.get(param_name)
1✔
251
                if existing_socket is not None and existing_socket != new_socket:
1✔
252
                    raise ComponentError(
1✔
253
                        "set_input_types()/set_input_type() cannot override the parameters of the 'run' method"
254
                    )
255

256
                sockets[param_name] = new_socket
1✔
257

258
            return run_signature
1✔
259

260
        # Create the sockets if set_input_types() wasn't called in the constructor.
261
        if not hasattr(instance, "__haystack_input__"):
1✔
262
            instance.__haystack_input__ = Sockets(instance, {}, InputSocket)
1✔
263

264
        inner(getattr(component_cls, "run"), instance.__haystack_input__)
1✔
265

266
        # Ensure that the sockets are the same for the async method, if it exists.
267
        async_run = getattr(component_cls, "run_async", None)
1✔
268
        if async_run is not None:
1✔
269
            run_sockets = Sockets(instance, {}, InputSocket)
1✔
270
            async_run_sockets = Sockets(instance, {}, InputSocket)
1✔
271

272
            # Can't use the sockets from above as they might contain
273
            # values set with set_input_types().
274
            run_sig = inner(getattr(component_cls, "run"), run_sockets)
1✔
275
            async_run_sig = inner(async_run, async_run_sockets)
1✔
276

277
            if async_run_sockets != run_sockets or run_sig != async_run_sig:
1✔
278
                sig_diff = _compare_run_methods_signatures(run_sig, async_run_sig)
1✔
279
                raise ComponentError(
1✔
280
                    f"Parameters of 'run' and 'run_async' methods must be the same.\nDifferences found:\n{sig_diff}"
281
                )
282

283
    def __call__(cls, *args, **kwargs):
1✔
284
        """
285
        This method is called when clients instantiate a Component and runs before __new__ and __init__.
286
        """
287
        # This will call __new__ then __init__, giving us back the Component instance
288
        pre_init_hook = _COMPONENT_PRE_INIT_HOOK.get()
1✔
289
        if pre_init_hook is None or pre_init_hook.in_progress:
1✔
290
            instance = super().__call__(*args, **kwargs)
1✔
291
        else:
292
            try:
1✔
293
                pre_init_hook.in_progress = True
1✔
294
                named_positional_args = ComponentMeta._positional_to_kwargs(cls, args)
1✔
295
                assert set(named_positional_args.keys()).intersection(kwargs.keys()) == set(), (
1✔
296
                    "positional and keyword arguments overlap"
297
                )
298
                kwargs.update(named_positional_args)
1✔
299
                pre_init_hook.callback(cls, kwargs)
1✔
300
                instance = super().__call__(**kwargs)
1✔
301
            finally:
302
                pre_init_hook.in_progress = False
1✔
303

304
        # Before returning, we have the chance to modify the newly created
305
        # Component instance, so we take the chance and set up the I/O sockets
306
        has_async_run = hasattr(instance, "run_async")
1✔
307
        if has_async_run and not inspect.iscoroutinefunction(instance.run_async):
1✔
308
            raise ComponentError(f"Method 'run_async' of component '{cls.__name__}' must be a coroutine")
1✔
309
        instance.__haystack_supports_async__ = has_async_run
1✔
310

311
        ComponentMeta._parse_and_set_input_sockets(cls, instance)
1✔
312
        ComponentMeta._parse_and_set_output_sockets(instance)
1✔
313

314
        # Since a Component can't be used in multiple Pipelines at the same time
315
        # we need to know if it's already owned by a Pipeline when adding it to one.
316
        # We use this flag to check that.
317
        instance.__haystack_added_to_pipeline__ = None
1✔
318

319
        return instance
1✔
320

321

322
def _component_repr(component: Component) -> str:
1✔
323
    """
324
    All Components override their __repr__ method with this one.
325

326
    It prints the component name and the input/output sockets.
327
    """
328
    result = object.__repr__(component)
1✔
329
    if pipeline := getattr(component, "__haystack_added_to_pipeline__", None):
1✔
330
        # This Component has been added in a Pipeline, let's get the name from there.
331
        result += f"\n{pipeline.get_component_name(component)}"
1✔
332

333
    # We're explicitly ignoring the type here because we're sure that the component
334
    # has the __haystack_input__ and __haystack_output__ attributes at this point
335
    return (
1✔
336
        f"{result}\n{getattr(component, '__haystack_input__', '<invalid_input_sockets>')}"
337
        f"\n{getattr(component, '__haystack_output__', '<invalid_output_sockets>')}"
338
    )
339

340

341
def _component_run_has_kwargs(component_cls: Type) -> bool:
1✔
342
    run_method = getattr(component_cls, "run", None)
1✔
343
    if run_method is None:
1✔
344
        return False
×
345
    else:
346
        return any(
1✔
347
            param.kind == inspect.Parameter.VAR_KEYWORD for param in inspect.signature(run_method).parameters.values()
348
        )
349

350

351
def _compare_run_methods_signatures(run_sig: inspect.Signature, async_run_sig: inspect.Signature) -> str:
1✔
352
    """
353
    Builds a detailed error message with the differences between the signatures of the run and run_async methods.
354

355
    :param run_sig: The signature of the run method
356
    :param async_run_sig: The signature of the run_async method
357

358
    :returns:
359
        A detailed error message if signatures don't match, empty string if they do
360
    """
361
    differences = []
1✔
362
    run_params = list(run_sig.parameters.items())
1✔
363
    async_params = list(async_run_sig.parameters.items())
1✔
364

365
    if len(run_params) != len(async_params):
1✔
366
        differences.append(
1✔
367
            f"Different number of parameters: run has {len(run_params)}, run_async has {len(async_params)}"
368
        )
369

370
    for (run_name, run_param), (async_name, async_param) in zip(run_params, async_params):
1✔
371
        if run_name != async_name:
1✔
372
            differences.append(f"Parameter name mismatch: {run_name} vs {async_name}")
1✔
373

374
        if run_param.annotation != async_param.annotation:
1✔
375
            differences.append(
1✔
376
                f"Parameter '{run_name}' type mismatch: {run_param.annotation} vs {async_param.annotation}"
377
            )
378

379
        if run_param.default != async_param.default:
1✔
380
            differences.append(
1✔
381
                f"Parameter '{run_name}' default value mismatch: {run_param.default} vs {async_param.default}"
382
            )
383

384
        if run_param.kind != async_param.kind:
1✔
385
            differences.append(
1✔
386
                f"Parameter '{run_name}' kind (POSITIONAL, KEYWORD, etc.) mismatch: "
387
                f"{run_param.kind} vs {async_param.kind}"
388
            )
389

390
    return "\n".join(differences)
1✔
391

392

393
class _Component:
1✔
394
    """
395
    See module's docstring.
396

397
    Args:
398
        cls: the class that should be used as a component.
399

400
    Returns:
401
        A class that can be recognized as a component.
402

403
    Raises:
404
        ComponentError: if the class provided has no `run()` method or otherwise doesn't respect the component contract.
405
    """
406

407
    def __init__(self):
1✔
408
        self.registry = {}
1✔
409

410
    def set_input_type(
1✔
411
        self,
412
        instance,
413
        name: str,
414
        type: Any,  # noqa: A002
415
        default: Any = _empty,
416
    ):
417
        """
418
        Add a single input socket to the component instance.
419

420
        Replaces any existing input socket with the same name.
421

422
        :param instance: Component instance where the input type will be added.
423
        :param name: name of the input socket.
424
        :param type: type of the input socket.
425
        :param default: default value of the input socket, defaults to _empty
426
        """
427
        if not _component_run_has_kwargs(instance.__class__):
1✔
428
            raise ComponentError(
1✔
429
                "Cannot set input types on a component that doesn't have a kwargs parameter in the 'run' method"
430
            )
431

432
        if not hasattr(instance, "__haystack_input__"):
1✔
433
            instance.__haystack_input__ = Sockets(instance, {}, InputSocket)
1✔
434
        instance.__haystack_input__[name] = InputSocket(name=name, type=type, default_value=default)
1✔
435

436
    def set_input_types(self, instance, **types):
1✔
437
        """
438
        Method that specifies the input types when 'kwargs' is passed to the run method.
439

440
        Use as:
441

442
        ```python
443
        @component
444
        class MyComponent:
445

446
            def __init__(self, value: int):
447
                component.set_input_types(self, value_1=str, value_2=str)
448
                ...
449

450
            @component.output_types(output_1=int, output_2=str)
451
            def run(self, **kwargs):
452
                return {"output_1": kwargs["value_1"], "output_2": ""}
453
        ```
454

455
        Note that if the `run()` method also specifies some parameters, those will take precedence.
456

457
        For example:
458

459
        ```python
460
        @component
461
        class MyComponent:
462

463
            def __init__(self, value: int):
464
                component.set_input_types(self, value_1=str, value_2=str)
465
                ...
466

467
            @component.output_types(output_1=int, output_2=str)
468
            def run(self, value_0: str, value_1: Optional[str] = None, **kwargs):
469
                return {"output_1": kwargs["value_1"], "output_2": ""}
470
        ```
471

472
        would add a mandatory `value_0` parameters, make the `value_1`
473
        parameter optional with a default None, and keep the `value_2`
474
        parameter mandatory as specified in `set_input_types`.
475

476
        """
477
        if not _component_run_has_kwargs(instance.__class__):
1✔
478
            raise ComponentError(
1✔
479
                "Cannot set input types on a component that doesn't have a kwargs parameter in the 'run' method"
480
            )
481

482
        instance.__haystack_input__ = Sockets(
1✔
483
            instance, {name: InputSocket(name=name, type=type_) for name, type_ in types.items()}, InputSocket
484
        )
485

486
    def set_output_types(self, instance, **types):
1✔
487
        """
488
        Method that specifies the output types when the 'run' method is not decorated with 'component.output_types'.
489

490
        Use as:
491

492
        ```python
493
        @component
494
        class MyComponent:
495

496
            def __init__(self, value: int):
497
                component.set_output_types(self, output_1=int, output_2=str)
498
                ...
499

500
            # no decorators here
501
            def run(self, value: int):
502
                return {"output_1": 1, "output_2": "2"}
503
        ```
504
        """
505
        has_decorator = hasattr(instance.run, "_output_types_cache")
1✔
506
        if has_decorator:
1✔
507
            raise ComponentError(
1✔
508
                "Cannot call `set_output_types` on a component that already has "
509
                "the 'output_types' decorator on its `run` method"
510
            )
511

512
        instance.__haystack_output__ = Sockets(
1✔
513
            instance, {name: OutputSocket(name=name, type=type_) for name, type_ in types.items()}, OutputSocket
514
        )
515

516
    def output_types(
1✔
517
        self, **types: Any
518
    ) -> Callable[[Callable[RunParamsT, RunReturnT]], Callable[RunParamsT, RunReturnT]]:
519
        """
520
        Decorator factory that specifies the output types of a component.
521

522
        Use as:
523
        ```python
524
        @component
525
        class MyComponent:
526
            @component.output_types(output_1=int, output_2=str)
527
            def run(self, value: int):
528
                return {"output_1": 1, "output_2": "2"}
529
        ```
530
        """
531

532
        def output_types_decorator(run_method: Callable[RunParamsT, RunReturnT]) -> Callable[RunParamsT, RunReturnT]:
1✔
533
            """
534
            Decorator that sets the output types of the decorated method.
535

536
            This happens at class creation time, and since we don't have the decorated
537
            class available here, we temporarily store the output types as an attribute of
538
            the decorated method. The ComponentMeta metaclass will use this data to create
539
            sockets at instance creation time.
540
            """
541
            method_name = run_method.__name__
1✔
542
            if method_name not in ("run", "run_async"):
1✔
543
                raise ComponentError("'output_types' decorator can only be used on 'run' and 'run_async' methods")
1✔
544

545
            setattr(
1✔
546
                run_method,
547
                "_output_types_cache",
548
                {name: OutputSocket(name=name, type=type_) for name, type_ in types.items()},
549
            )
550
            return run_method
1✔
551

552
        return output_types_decorator
1✔
553

554
    def _component(self, cls: Type[T]) -> Type[T]:
1✔
555
        """
556
        Decorator validating the structure of the component and registering it in the components registry.
557
        """
558
        logger.debug("Registering {component} as a component", component=cls)
1✔
559

560
        # Check for required methods and fail as soon as possible
561
        if not hasattr(cls, "run"):
1✔
562
            raise ComponentError(f"{cls.__name__} must have a 'run()' method. See the docs for more information.")
1✔
563

564
        def copy_class_namespace(namespace):
1✔
565
            """
566
            This is the callback that `typing.new_class` will use to populate the newly created class.
567

568
            Simply copy the whole namespace from the decorated class.
569
            """
570
            for key, val in dict(cls.__dict__).items():
1✔
571
                # __dict__ and __weakref__ are class-bound, we should let Python recreate them.
572
                if key in ("__dict__", "__weakref__"):
1✔
573
                    continue
1✔
574
                namespace[key] = val
1✔
575

576
        # Recreate the decorated component class so it uses our metaclass.
577
        # We must explicitly redefine the type of the class to make sure language servers
578
        # and type checkers understand that the class is of the correct type.
579
        new_cls: Type[T] = new_class(cls.__name__, cls.__bases__, {"metaclass": ComponentMeta}, copy_class_namespace)
1✔
580

581
        # Save the component in the class registry (for deserialization)
582
        class_path = f"{new_cls.__module__}.{new_cls.__name__}"
1✔
583
        if class_path in self.registry:
1✔
584
            # Corner case, but it may occur easily in notebooks when re-running cells.
585
            logger.debug(
1✔
586
                "Component {component} is already registered. Previous imported from '{module_name}', \
587
                new imported from '{new_module_name}'",
588
                component=class_path,
589
                module_name=self.registry[class_path],
590
                new_module_name=new_cls,
591
            )
592
        self.registry[class_path] = new_cls
1✔
593
        logger.debug("Registered Component {component}", component=new_cls)
1✔
594

595
        # Override the __repr__ method with a default one
596
        # mypy is not happy that:
597
        # 1) we are assigning a method to a class
598
        # 2) _component_repr has a different type (Callable[[Component], str]) than the expected
599
        # __repr__ method (Callable[[object], str])
600
        new_cls.__repr__ = _component_repr  # type: ignore[assignment]
1✔
601

602
        return new_cls
1✔
603

604
    def __call__(self, cls: Optional[type] = None):
1✔
605
        # We must wrap the call to the decorator in a function for it to work
606
        # correctly with or without parens
607
        def wrap(cls):
1✔
608
            return self._component(cls)
1✔
609

610
        if cls:
1✔
611
            # Decorator is called without parens
612
            return wrap(cls)
1✔
613

614
        # Decorator is called with parens
615
        return wrap
1✔
616

617

618
component = _Component()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc