Skip to content

Commit e0beaa6

Browse files
[patch] Allow _FactoryMade to explicitly define where its reduce should import from (#27)
* Allow `_FactoryMade` to explicitly define where its reduce should import from * Give the same <locals> protection with the new syntax * Add comment * Extend tests to cover both syntaxes * Edit docstring * Format black --------- Co-authored-by: pyiron-runner <pyiron@mpie.de>
1 parent 5c2f87a commit e0beaa6

File tree

2 files changed

+69
-21
lines changed

2 files changed

+69
-21
lines changed

pyiron_snippets/factory.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,22 @@ class _FactoryMade(ABC):
237237
"""
238238
A mix-in to make class-factory-produced classes pickleable.
239239
240-
If the factory is used as a decorator for another function, it will conflict with
241-
this function (i.e. the owned function will be the true function, and will mismatch
242-
with imports from that location, which will return the post-decorator factory made
243-
class). This can be resolved by setting the
240+
If the factory is used as a decorator for another function (or class), it will
241+
conflict with this function (i.e. the owned function will be the true function,
242+
and will mismatch with imports from that location, which will return the
243+
post-decorator factory made class). This can be resolved by setting the
244+
:attr:`_reduce_imports_as` attribute to a tuple of the (module, qualname) obtained
245+
from the decorated definition in order to manually specify where it should be
246+
re-imported from. (DEPRECATED alternative: set
244247
:attr:`_class_returns_from_decorated_function` attribute to be the decorated
245-
function in the decorator definition.
248+
function in the decorator definition.)
246249
"""
247250

251+
# DEPRECATED: Use _reduce_imports_as instead
248252
_class_returns_from_decorated_function: ClassVar[callable | None] = None
249253

254+
_reduce_imports_as: ClassVar[tuple[str, str] | None] = None # Module and qualname
255+
250256
def __init_subclass__(cls, /, class_factory, class_factory_args, **kwargs):
251257
super().__init_subclass__(**kwargs)
252258
cls._class_factory = class_factory
@@ -271,6 +277,19 @@ def __reduce__(self):
271277
),
272278
self.__getstate__(),
273279
)
280+
elif (
281+
self._reduce_imports_as is not None
282+
and "<locals>" not in self._reduce_imports_as[1]
283+
):
284+
return (
285+
_instantiate_from_decorated,
286+
(
287+
self._reduce_imports_as[0],
288+
self._reduce_imports_as[1],
289+
self.__getnewargs_ex__(),
290+
),
291+
self.__getstate__(),
292+
)
274293
else:
275294
return (
276295
_instantiate_from_factory,

tests/unit/test_factory.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ def add_to_function(self, *args, **kwargs):
116116

117117
@classfactory
118118
def adder_factory(fnc, n, /):
119+
return (
120+
f"{AddsNandX.__name__}{fnc.__name__}",
121+
(AddsNandX,),
122+
{
123+
"fnc": staticmethod(fnc),
124+
"n": n,
125+
"_reduce_imports_as": (fnc.__module__, fnc.__qualname__)
126+
},
127+
{},
128+
)
129+
130+
131+
@classfactory
132+
def deprecated_adder_factory(fnc, n, /):
119133
return (
120134
f"{AddsNandX.__name__}{fnc.__name__}",
121135
(AddsNandX,),
@@ -131,7 +145,7 @@ def adder_factory(fnc, n, /):
131145
def add_to_this_decorator(n):
132146
def wrapped(fnc):
133147
factory_made = adder_factory(fnc, n)
134-
factory_made._class_returns_from_decorated_function = fnc
148+
factory_made._reduce_imports_as = (fnc.__module__, fnc.__qualname__)
135149
return factory_made
136150
return wrapped
137151

@@ -141,6 +155,19 @@ def adds_5_plus_x(y: int):
141155
return y
142156

143157

158+
def deprecated_add_to_this_decorator(n):
159+
def wrapped(fnc):
160+
factory_made = adder_factory(fnc, n)
161+
factory_made._class_returns_from_decorated_function = fnc
162+
return factory_made
163+
return wrapped
164+
165+
166+
@deprecated_add_to_this_decorator(5)
167+
def deprecated_adds_5_plus_x(y: int):
168+
return y
169+
170+
144171
class TestClassfactory(unittest.TestCase):
145172

146173
def test_factory_initialization(self):
@@ -474,21 +501,23 @@ def test_other_decorators(self):
474501
In case the factory-produced class itself comes from a decorator, we need to
475502
check that name conflicts between the class and decorated function are handled.
476503
"""
477-
a5 = adds_5_plus_x(2)
478-
self.assertIsInstance(a5, AddsNandX)
479-
self.assertIsInstance(a5, _FactoryMade)
480-
self.assertEqual(5, a5.n)
481-
self.assertEqual(2, a5.x)
482-
self.assertEqual(
483-
1 + 5 + 2, # y + n=5 + x=2
484-
a5.add_to_function(1),
485-
msg="Should execute the function as part of call"
486-
)
487-
488-
reloaded = pickle.loads(pickle.dumps(a5))
489-
self.assertEqual(a5.n, reloaded.n)
490-
self.assertIs(a5.fnc, reloaded.fnc)
491-
self.assertEqual(a5.x, reloaded.x)
504+
for fnc in [adds_5_plus_x, deprecated_adds_5_plus_x]:
505+
with self.subTest(fnc.__name__):
506+
a5 = fnc(2)
507+
self.assertIsInstance(a5, AddsNandX)
508+
self.assertIsInstance(a5, _FactoryMade)
509+
self.assertEqual(5, a5.n)
510+
self.assertEqual(2, a5.x)
511+
self.assertEqual(
512+
1 + 5 + 2, # y + n=5 + x=2
513+
a5.add_to_function(1),
514+
msg="Should execute the function as part of call"
515+
)
516+
517+
reloaded = pickle.loads(pickle.dumps(a5))
518+
self.assertEqual(a5.n, reloaded.n)
519+
self.assertIs(a5.fnc, reloaded.fnc)
520+
self.assertEqual(a5.x, reloaded.x)
492521

493522
def test_other_decorators_inside_locals(self):
494523
@add_to_this_decorator(6)

0 commit comments

Comments
 (0)