Factory
coola.factory ¶
Provide utilities to import and instantiate objects from configurations.
A configuration is a plain dict (for example loaded from JSON or YAML)
that names an object by its fully qualified name, under the key
OBJECT_TARGET ("_target_"), and gives its keyword arguments. The
functions of the package:
import_object: import a class or a function from its fully qualified name.instantiate_object: instantiate an object from a target and its arguments.factory: import and instantiate an object from a fully qualified name, without any registration.resolve_object: return an object as is, or build it from a configuration.is_object_config: check if a configuration targets a given class.
Unlike coola.registry, an object does not need to be registered: any
importable object can be built.
coola.factory.factory ¶
factory(
_target_: str,
*args: Any,
_init_: str = "__init__",
_allowed_prefixes_: Sequence[str] | None = None,
**kwargs: Any,
) -> Any
Instantiate dynamically an object given its configuration.
This function provides a universal factory that can instantiate any class or call any function by its fully qualified name. Unlike the AbstractFactory or Registry approaches, this function does not require prior registration of classes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
_target_
|
str
|
The fully qualified name of the object (class or function) to instantiate, e.g., "collections.Counter" or "math.isclose". |
required |
*args
|
Any
|
Positional arguments to pass to the class constructor or function. |
()
|
_init_
|
str
|
The function or method used to create the object.
If |
'__init__'
|
_allowed_prefixes_
|
Sequence[str] | None
|
An optional allow-list of module prefixes
that |
None
|
**kwargs
|
Any
|
Keyword arguments to pass to the class constructor
or function. Any name is accepted, including |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
The instantiated object with the given parameters. |
Raises:
| Type | Description |
|---|---|
ImportError
|
if the target cannot be found, is not allowed by
|
TypeError
|
if |
ValueError
|
if |
Note
_target_ is resolved at the top level only: a nested
dictionary with a _target_ key is passed to the target
unchanged, it is not instantiated. Resolve the nested
configurations with resolve_object and pass the results.
Security
_target_ is imported and called with no allowlist or
restriction on which module or class it may reference: this
function will import arbitrary module code and instantiate or
call arbitrary objects from a dotted path string. Only pass a
_target_ that comes from a trusted source (e.g. code you
wrote, or a configuration file you control). Never resolve a
_target_ derived from untrusted input, such as a
third-party-uploaded config file or a network payload, as this
is a remote-code-execution vector. _allowed_prefixes_ is a
filter on a path, not a sandbox: it limits what is called,
not the arguments. A prefix that contains a function that
imports or resolves objects from its arguments (any
resolve_*, import_object, importlib, pickle,
eval, os.system, ...) makes the list worthless: never
allow "coola" or "coola.factory". The path is checked,
not the module that defines the object, so a re-export is
allowed by the prefix of the re-exporting module.
Example
>>> from coola.factory import factory
>>> factory("collections.Counter", [1, 2, 1, 3])
Counter({1: 2, 2: 1, 3: 1})
coola.factory.import_object ¶
import_object(
object_path: object,
*,
allowed_prefixes: Sequence[str] | None = None,
) -> Any
Import an object given its path.
This function dynamically imports a class, function, or other
Python object using its fully qualified name. The object path
should have the structure module_path.object_name (e.g.,
"collections.Counter" or "math.isclose"). The object can also be
an attribute of a class ("collections.Counter.fromkeys"): the
longest prefix that is a module is imported and the rest of the path
is read with getattr.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
object_path
|
object
|
The fully qualified path of the object to import. Must be a string in the format "module.path.ObjectName". |
required |
allowed_prefixes
|
Sequence[str] | None
|
An optional allow-list of module prefixes.
If provided, |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The imported object. |
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
ImportError
|
if |
Note
pkgutil.resolve_name is a different function:
it takes a path with a colon ("package.module:name") and is
used by the lazy registries. This function takes a dotted path
("package.module.name"), checks allowed_prefixes and
reports a missing target with one ImportError.
Security
By default, this function imports arbitrary module code with
no restriction on which module or attribute it may reference:
importing a module executes its top-level code. Use
allowed_prefixes to restrict it. Only pass an
object_path that comes from a trusted source. Never
resolve an object_path derived from untrusted input, such
as a third-party-uploaded config file or a network payload, as
this is a remote-code-execution vector.
Example
>>> from coola.factory import import_object
>>> cls = import_object("collections.Counter")
>>> cls()
Counter()
>>> fn = import_object("math.isclose")
>>> fn(1, 1)
True
>>> pi = import_object("math.pi")
>>> pi
3.141592653589793
>>> pkg = import_object("math")
>>> pkg
<module 'math' (built-in)>
coola.factory.instantiate_object ¶
instantiate_object(
obj: Callable[..., Any] | type,
/,
*args: Any,
_init_: str = "__init__",
**kwargs: Any,
) -> Any
Instantiate dynamically an object from its configuration.
This function creates an instance of a class or calls a callable
with the provided arguments. For classes, it supports different
instantiation methods (constructor or public class methods).
For any other callable (function, builtin, functools.partial,
or an object implementing __call__), it simply calls it with
the given arguments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
Callable[..., Any] | type
|
The class to instantiate or the callable to call. Must be a class or a callable object. |
required |
*args
|
Any
|
Positional arguments to pass to the class constructor or callable. |
()
|
_init_
|
str
|
The function or method to use to create the object.
This parameter is ignored if |
'__init__'
|
**kwargs
|
Any
|
Keyword arguments to pass to the class constructor
or callable. Any name is accepted, including |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
The instantiated object if |
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
Security
obj is instantiated or called with the given arguments with
no allowlist or restriction on what it may do: an arbitrary
class or callable will be invoked as-is. Only pass an obj
(and _init_) that come from a trusted source. Never
instantiate an object whose class/callable or arguments are
derived from untrusted input, as this is a remote-code-execution
vector.
Example
>>> from collections import Counter
>>> from coola.factory import instantiate_object
>>> instantiate_object(Counter, [1, 2, 1])
Counter({1: 2, 2: 1})
>>> instantiate_object(list, [1, 2, 1])
[1, 2, 1]
coola.factory.is_object_config ¶
is_object_config(
config: dict[str, Any],
cls: type,
*,
allowed_prefixes: Sequence[str] | None = None,
import_target: bool = False,
) -> bool
Indicate if the input configuration is a configuration for a given class.
This function only checks if the value of the key _target_
is valid. It does not check the other values. If _target_
indicates a function, the returned type hint is used to check
the class. A function whose return type hint cannot be resolved
(e.g. a forward reference to an undefined name, or a string
annotation that is not valid Python) is not a configuration for
cls. A config that is not a dict, or whose _target_
is not a string, is not a configuration for cls.
The module of _target_ is not imported to answer: if it is not
imported yet, the configuration is not a configuration for cls
(importing a module runs its top-level code). Use
allowed_prefixes to restrict the modules that can be imported,
or import_target=True to import the module of any _target_.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
dict[str, Any]
|
The configuration to check. |
required |
cls
|
type
|
The object class. |
required |
allowed_prefixes
|
Sequence[str] | None
|
An optional allow-list of module prefixes
that |
None
|
import_target
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
ImportError
|
if |
Security
By default, the module of the _target_ is not imported,
so a configuration from an untrusted source cannot run the
top-level code of an arbitrary module. With allowed_prefixes
or import_target=True, the module is imported to check
the class, and importing a module runs its top-level code: do
not use them with a _target_ that comes from an untrusted
source, unless allowed_prefixes restricts the modules that
can be imported.
Example
>>> from coola.factory import is_object_config
>>> from collections import Counter
>>> is_object_config({"_target_": "collections.Counter", "iterable": [1, 2, 1, 3]}, Counter)
True
coola.factory.resolve_object ¶
resolve_object(
obj: T | dict[str, Any],
*,
allowed_prefixes: Sequence[str] | None = None,
strict: bool = False,
) -> T
resolve_object(
obj: T | dict[str, Any],
cls: type[T],
*,
allowed_prefixes: Sequence[str] | None = None,
strict: bool = False,
) -> T
resolve_object(
obj: Any,
cls: tuple[type, ...],
*,
allowed_prefixes: Sequence[str] | None = None,
strict: bool = False,
) -> Any
resolve_object(
obj: Any,
cls: type | tuple[type, ...] = object,
*,
allowed_prefixes: Sequence[str] | None = None,
strict: bool = False,
) -> Any
Resolve an instance of cls from an existing object or a
configuration dictionary.
A dict with the key "_target_" is always a factory
configuration: it is built, and the result is checked against
cls. Any other object, including a dict without
"_target_", is returned as-is if it is an instance of cls.
The key "_target_" is reserved: a dict that has it (for
example Counter({"_target_": 2})) cannot be passed through as
data.
Note
"_target_" is resolved at the top level only: a nested
dictionary with a "_target_" key is passed to the target
unchanged, it is not instantiated. With allowed_prefixes
it is refused with a ValueError. Resolve the nested
configurations with resolve_object (with the same
allowed_prefixes) and pass the results.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
Any
|
Either a fully configured instance of |
required |
cls
|
type | tuple[type, ...]
|
The expected type, or a tuple of types (as accepted by
|
object
|
allowed_prefixes
|
Sequence[str] | None
|
An optional allow-list of module prefixes
that the |
None
|
strict
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
Any
|
A configured instance of |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the |
ImportError
|
If the |
ValueError
|
If |
Security
When obj is a dict with a "_target_" key, its
value is imported and the target is called: this imports and
instantiates arbitrary objects from a dotted path string with
no allowlist by default. Only call this function with a
configuration dictionary that comes from a trusted source;
never resolve a _target_ derived from untrusted input, as
this is a remote-code-execution vector. If the configuration
cannot be fully trusted, pass allowed_prefixes to restrict
the "_target_" to the modules you expect, and check the
arguments of the allowed objects too: an allow-list limits
what can be called, not the arguments it is called with, and
a prefix that contains a function that resolves its arguments
(any resolve_*, import_object, pickle, eval,
...) makes it worthless: never allow "coola" or
"coola.factory".
Example
>>> from datetime import date
>>> from coola.factory import resolve_object
>>> # From an existing instance:
>>> d = resolve_object(date(2020, 1, 1), cls=date)
>>> # From a configuration dictionary:
>>> d = resolve_object(
... {"_target_": "datetime.date", "year": 2020, "month": 1, "day": 1}, cls=date
... )