pyiron_snippets.factory module

Tools for making dynamically generated classes unique, and their instances pickleable.

Provides two main user-facing tools: classfactory(), which should be used _exclusively_ as a decorator (this restriction pertains to namespace requirements for re-importing), and ClassFactory, which can be used to instantiate a new factory from some existing factory function.

In both cases, the decorated function/input argument should be a pickleable function taking only positional arguments, and returning a tuple suitable for use in dynamic class creation via builtins.type() – i.e. taking a class name, a tuple of base classes, a dictionary of class attributes, and a dictionary of values to be expanded into kwargs for __subclass_init__.

The resulting factory produces classes that are (a) pickleable, and (b) the same object as any previously built class with the same name. (Note: avoiding class degeneracy with respect to class name is the responsibility of the person writing the factory function.)

These classes are then themselves pickleable, and produce instances which are in turn pickleable (so long as any data they’ve been fed as inputs or attributes is pickleable, i.e. here the only pickle-barrier we resolve is that of having come from a dynamically generated class).

Since users need to build their own class factories returning classes with sensible names, we also provide a helper function sanitize_callable_name(), which makes sure a string is compliant with use as a class name. This is run internally on user- provided names, and failure for the user name and sanitized name to match will give a clear error message.

Constructed classes can, in turn be used as bases in further class factories.

class pyiron_snippets.factory.ClassFactory(factory_function)[source]

Bases: object

A constructor for new class factories.

Use on existing class factory callables, _not_ as a decorator.

Cf. the classfactory() decorator for more info.

exception pyiron_snippets.factory.InvalidClassNameError[source]

Bases: ValueError

When a string isn’t a good class name

exception pyiron_snippets.factory.InvalidFactorySignature[source]

Bases: ValueError

When the factory function’s arguments are not purely positional

pyiron_snippets.factory.classfactory(factory_function: Callable[[...], tuple[str, tuple[type, ...], dict, dict]]) _ClassFactory[source]

A decorator for building dynamic class factories whose classes are unique and whose terminal instances can be pickled.

Under the hood, classes created by factories get dependence on _FactoryMade mixed in. This class leverages __reduce__() and __init_subclass__() and uses up the class namespace _class_factory and _class_factory_args to hold data (using up corresponding public variable names in the __init_subclass__() kwargs), so any interference with these fields may cause unexpected side effects. For un-pickling, the dynamic class gets recreated then its __new__() is called using __newargs_ex__; a default implementation returning no arguments is provided on _FactoryMade but can be overridden.

Parameters:

factory_function (Callable[..., tuple[str, tuple[type, ...], dict, dict]]) – A function returning arguments that would be passed to builtins.type to dynamically generate a class. The function must accept exclusively positional arguments

Returns:

A new callable that returns unique classes whose

instances can be pickled.

Return type:

(type[_ClassFactory])

Notes

If the :param:`factory_function` itself, or any data stored on instances of its resulting class(es) cannot be pickled, then the instances will not be able to be pickled. Here we only remove the trouble associated with pickling dynamically created classes.

If the __init_subclass__ kwargs are exploited, remember that these are subject to all the same “gotchas” as their regular non-factory use; namely, all child classes must specify _all_ parent class kwargs in order to avoid them getting overwritten by the parent class defaults!

Dynamically generated classes can, in turn, be used as base classes for further @classfactory decorated factory functions.

Warning

Use _exclusively_ as a decorator. For an inline constructor for an existing callable, use ClassFactory instead.

Examples

>>> import pickle
>>>
>>> from pyiron_snippets.factory import classfactory
>>> from abc import ABC
>>>
>>> class HasN(ABC):
...     '''Some class I want to make dynamically subclass.'''
...     def __init_subclass__(cls, /, n=0, s="foo", **kwargs):
...         super(HasN, cls).__init_subclass__(**kwargs)
...         cls.n = n
...         cls.s = s
...
...     def __init__(self, x, y=0):
...         self.x = x
...         self.y = y
>>>
>>> @classfactory
... def has_n_factory(n, s="wrapped_function", /):
...     return (
...         f"{HasN.__name__}{n}{s}",  # New class name
...         (HasN,),  # Base class(es)
...         {},  # Class attributes dictionary
...         {"n": n, "s": s}
...         # dict of `builtins.type` kwargs (passed to `__init_subclass__`)
...     )
>>>
>>> Has2 = has_n_factory(2, "my_dynamic_class")
>>> HasToo = has_n_factory(2, "my_dynamic_class")
>>> HasToo is Has2
True
>>> foo = Has2(42, y=-1)
>>> print(foo.n, foo.s, foo.x, foo.y)
2 my_dynamic_class 42 -1
>>> reloaded = pickle.loads(pickle.dumps(foo))
>>> print(reloaded.n, reloaded.s, reloaded.x, reloaded.y)
2 my_dynamic_class 42 -1
pyiron_snippets.factory.sanitize_callable_name(name: str)[source]

A helper class for sanitizing a string so it’s appropriate as a class/function name.