11from __future__ import annotations
22
3+ import inspect
34import logging
45import os
56from collections .abc import Mapping , Sequence
1718PLUGIN_ENTRY_POINT_GROUP = "aws_durable_execution.plugins"
1819PLUGIN_ENVIRONMENT_VARIABLE = "DURABLE_EXECUTION_PLUGINS"
1920
21+ # Stands in for the InvocationStartInfo when a factory's signature is checked at
22+ # registration. Only the bind is performed, so nothing reads it.
23+ _ARGUMENT_PROBE = object ()
24+
2025
2126def _parse_configured_plugin_names (environment : Mapping [str , str ]) -> list [str ]:
2227 configured_plugins = environment .get (PLUGIN_ENVIRONMENT_VARIABLE )
@@ -58,21 +63,57 @@ def _is_plugin_factory(value: object) -> bool:
5863 """Report whether a value has the shape of a plugin factory.
5964
6065 :class:`DurableInstrumentationPluginFactory` declares one method, so the
61- shape is one member: a callable ``create_plugin``. The attribute is fetched
62- and tested for callability rather than merely for presence, because an
63- object carrying a non-callable ``create_plugin`` would otherwise pass here
64- and fail at invocation time.
66+ shape is one member: a ``create_plugin`` that can be called with one
67+ positional argument. The attribute is fetched and tested rather than merely
68+ checked for presence, because an object carrying a non-callable
69+ ``create_plugin`` would otherwise pass here and fail at invocation time.
70+
71+ Callability alone is not enough. ``plugins=[MyFactory]`` -- the factory
72+ *class* rather than an instance of it -- resolves ``create_plugin`` to a
73+ plain function whose first parameter is ``self``, which is callable. The
74+ per-invocation call supplies only the info, Python binds it to ``self``, and
75+ the resulting :exc:`TypeError` is contained like any other factory failure:
76+ telemetry is silently absent for the lifetime of the function. Binding one
77+ positional argument to the signature rejects that at registration instead.
78+ The bind is a signature operation, so no factory code runs.
79+
80+ A callable with no introspectable signature -- a C-implemented callable, for
81+ example -- is accepted on the member alone. ``inspect.signature`` raises for
82+ it, and refusing a factory because its signature could not be read would
83+ reject a usable factory over a missing description of it.
6584
6685 Structural rather than nominal, so a factory need not import the SDK
6786 protocol to satisfy it. The protocol is deliberately not
6887 ``@runtime_checkable``; see its docstring.
6988
70- The check cannot go further than one member . Whether ``create_plugin``
71- accepts the info , and whether it returns a plugin, is only knowable by
72- calling it, and calling it at load time is what the per- invocation factory
73- design avoids: there is no invocation yet .
89+ The check stops there . Whether ``create_plugin`` returns a plugin is only
90+ knowable by calling it , and calling it at load time is what the
91+ per-invocation factory design avoids: there is no invocation yet. That case
92+ is checked per invocation by :meth:`PluginExecutor._create_plugins` .
7493 """
75- return callable (getattr (value , "create_plugin" , None ))
94+ create_plugin = getattr (value , "create_plugin" , None )
95+ if not callable (create_plugin ):
96+ return False
97+ return _accepts_one_positional_argument (create_plugin )
98+
99+
100+ def _accepts_one_positional_argument (create_plugin : object ) -> bool :
101+ """Report whether one positional argument can be bound to a callable.
102+
103+ A bound method, a ``@classmethod`` or ``@staticmethod`` read off a class, and
104+ a ``__call__`` on an instance all present the signature the SDK calls, so all
105+ three bind. An instance method read off the class does not: its first
106+ parameter is ``self``, so one argument leaves the info unbound.
107+ """
108+ try :
109+ signature = inspect .signature (create_plugin ) # type: ignore[arg-type]
110+ except (TypeError , ValueError ):
111+ return True
112+ try :
113+ signature .bind (_ARGUMENT_PROBE )
114+ except TypeError :
115+ return False
116+ return True
76117
77118
78119def _load_factory (
@@ -103,7 +144,7 @@ def _load_factory(
103144 "create_plugin(info) method returning a "
104145 "DurableInstrumentationPlugin -- but resolved to "
105146 f"{ _qualified_type_name (factory )} . Name the factory instance, not a "
106- "plugin and not a plugin class."
147+ "plugin, not a plugin class, and not the factory class."
107148 )
108149
109150 return cast (DurableInstrumentationPluginFactory , factory )
@@ -126,11 +167,15 @@ def _validate_explicit_factories(
126167 A plugin *class* is rejected, and so is any bare callable. Both were
127168 accepted while the registration type was ``Callable``: a lambda satisfied it
128169 directly, and a class satisfied it because calling a class constructs an
129- instance. Neither carries ``create_plugin``, so ``plugins=[MyPlugin]`` and
130- ``plugins=[lambda info: MyPlugin()]`` now fail here. The replacement is a
131- small factory class, which is also where setup work that can fail belongs. A
132- class that declares ``create_plugin`` as a ``@classmethod`` is accepted,
133- because the requirement is the member and not the kind of object.
170+ instance. Neither carries a ``create_plugin`` the SDK can call, so
171+ ``plugins=[MyPlugin]`` and ``plugins=[lambda info: MyPlugin()]`` now fail
172+ here. A *factory* class passed instead of an instance of it fails here too:
173+ ``MyFactory.create_plugin`` is callable, but its first parameter is ``self``,
174+ so the per-invocation call binds the info to ``self``. The replacement is a
175+ small factory class, instantiated, which is also where setup work that can
176+ fail belongs. A class that declares ``create_plugin`` as a ``@classmethod``
177+ or a ``@staticmethod`` is accepted, because that member presents the
178+ signature the SDK calls.
134179 """
135180 factories = list (explicit_plugins or [])
136181 for index , factory in enumerate (factories ):
@@ -140,7 +185,8 @@ class that declares ``create_plugin`` as a ``@classmethod`` is accepted,
140185 "plugin factory -- an object with a create_plugin(info) method "
141186 "returning a DurableInstrumentationPlugin -- but is "
142187 f"{ _qualified_type_name (factory )} . Pass a factory rather than a "
143- "plugin, a plugin class, or a plain callable, for example "
188+ "plugin, a plugin class, or a plain callable, and pass a factory "
189+ "instance rather than the factory class, for example "
144190 "plugins=[MyPluginFactory(exporter)]."
145191 )
146192 return factories
0 commit comments