-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add pickle support to dynamically created models and generics #1686
Add pickle support to dynamically created models and generics #1686
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1686 +/- ##
===========================================
- Coverage 100.00% 99.94% -0.06%
===========================================
Files 21 21
Lines 3925 3948 +23
Branches 788 790 +2
===========================================
+ Hits 3925 3946 +21
- Misses 0 1 +1
- Partials 0 1 +1
Continue to review full report at Codecov.
|
@MrMrRobat I reckon your PR also fixes #1734 so #1757 can probably be closed. I let it open in the meantime but maybe you could add my test in your PR and add #1734 in closed issues ;) |
8201b6e
to
3daa8df
Compare
Co-authored-by: PrettyWood <em.jolibois@gmail.com>
@PrettyWood, test I found in #1757 seems to work regardless of changes I made 🤔. |
@MrMrRobat weird 🤔 I just tried with the test on > globalns = sys.modules[cls.__module__].__dict__.copy()
E KeyError: None
pydantic/main.py:684: KeyError |
Ah, sorry, now I get it. I just removed setting model attribute to a module, but kept setting of a module to a model |
So, pickling can't be done if model has no global reference somewhere in code. Pickle threats path in Dynamically created models don't have global references therefore can not be pickled, unless referenced manually. We can't set global references to each dynamically created model, as it will either override previous created model, or/and lead to memory leaks. But turns out, it's not trivial task to determine if model was created globally or locally. It can be done like this in normal conditions: def is_call_from_module():
import inspect
return inspect.stack()[2].function == '<module>' However, if code is executed in debugger, value of a So, we either need a stable way to detect such differences, or think about other implementations. |
|
I finally found the way to determine call from module, but it won't work with compiled code, but it mostly needed in generics, and since they won't work in compiled code anyway, it's not a problem. However The question is: do we need that functionality for non-compiled code, or we better stick with the way it will work the same on both versions? What will work in compiled and non-compiled code:
import pickle
from pydantic import create_model
# from pydantic.utils import ensure_picklable
# this should not be done this way. Reference should be set globally, not dynamically,
# otherwise there is no guarantee that reference will be created by the time model will be unpickled
# model = ensure_picklable(create_model('FooModel'))
# pickle.dumps(model) # works
BarModel = create_model('BarModel', __module__=__name__)
pickle.dumps(BarModel) # works Additional things that will work in non-compiled code:
BarModel = create_model('BarModel')
pickle.dumps(BarModel) |
Hi! Is this blocked? Can we help? Thank you |
@remidebette, I'm trying to figure out consistent way of solving the problem acroas all platforms. Hacky staff with frames seems to not do the job. Hope, I'll have a success with implementing this with Until issue is resolved, you can still fix such issues by using from pydantic import create_model
MyModel = create_model('MyModel', __module__=__name__) Such models won't have any problems with pickling/forward refs/etc. For generics you have to manually set reference to base model module, like so: from typing import Generic, TypeVar
from pydantic.generics import GenericModel
T = TypeVar('T')
class MyGenericModel(Generic[T]. GenericModel):
t: T
IntModel = MyGenericModel[int]
setattr(MyGenericModel.__module__, IntModel.__name__, IntModel) Code above might be implemented in your base classe's class MyAutoGenericModel(Generic[T]. GenericModel):
t: T
def __class_getitem__(cls, *args, **kwargs):
created_model = super().__class_getitem__(*args, **kwargs)
setattr(cls.__module__, created_model.__name__, created_model)
return created_model |
Thanks for the detailed explanation! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good, let me know if I can do anything to help fix it.
I assume it's not as simple as moving is_call_from_module
to genetics.py
which is never compiled?
pydantic/main.py
Outdated
@@ -796,12 +796,12 @@ def __values__(self) -> 'DictStrAny': | |||
_is_base_model_class_defined = True | |||
|
|||
|
|||
def create_model( | |||
def create_model( # noqa: C901 (ignore complexity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this needed if create_model
hasn't been modified?
pydantic/utils.py
Outdated
@@ -241,7 +243,8 @@ def unique_list(input_list: Union[List[T], Tuple[T, ...]]) -> List[T]: | |||
|
|||
|
|||
def update_normalized_all( | |||
item: Union['AbstractSetIntStr', 'MappingIntStrAny'], all_items: Union['AbstractSetIntStr', 'MappingIntStrAny'], | |||
item: Union['AbstractSetIntStr', 'MappingIntStrAny'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anything changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like inconsistency between black versions. Which one should I use?
pydantic/utils.py
Outdated
except IndexError: | ||
raise RuntimeError('This function must be used inside another function') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
except IndexError: | |
raise RuntimeError('This function must be used inside another function') | |
except IndexError as e: | |
raise RuntimeError('This function must be used inside another function') from e |
pydantic/utils.py
Outdated
except IndexError: | ||
raise RuntimeError('This function must be used inside another function') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
except IndexError: | |
raise RuntimeError('This function must be used inside another function') | |
except IndexError as e: | |
raise RuntimeError('This function must be used inside another function') from e |
tests/test_utils.py
Outdated
|
||
|
||
@pytest.mark.skipif( | ||
compiled, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this skip required? Surely the code below in create_module
is never compiled so should always work.
If is_call_from_module
itself needs to be not compiled, I guess you need to move it into genetics.py
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, somehow I never thought about this. I did so and CI just passed!
Just to be clear: compiled code may not contain any use of GenericModel
, right?
tests/test_utils.py
Outdated
def get_current_module_name(): | ||
return get_caller_module_name() | ||
|
||
assert get_current_module_name() == __name__ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assert get_current_module_name() == __name__ | |
module_name = get_current_module_name() | |
assert module_name == __name__, (module_name, __name__) |
so we can better understand the error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't pytest provide such introspection in case of error? 🤔
upd. it doesn't :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot to apply suggestions before moving functions and tests to generics, so have to make changes manually
this looks great, please could you:
|
@samuelcolvin, master CI is failing again 😕. I guess PR fromatted with different black version was merged and broke CI. |
master should now be fixed. |
@samuelcolvin, FYI: def test_assert_in_validators(test_module):
test_module("try_assert.py") Upd. Actually, in case of As for current tests, I decided to rewrite them and enhanced |
this will allow run modules in subprocess and define module code in functions-containers
…el with module definition in function
Change Summary
__module__
to created class (try to find the actual one, in case of failure fallback topydantic.main
)Addpydantic.utils.ensure_picklable
which will allow to make model, created inside a functions or/and referenced with a different name from class name pickableRelated issue number
Closes #1667
Closes #1734
Checklist
changes/<pull request or issue id>-<github username>.md
file added describing change(see changes/README.md for details)