Skip to content

Commit

Permalink
Merge c8cbd68 into 2079240
Browse files Browse the repository at this point in the history
  • Loading branch information
macisamuele committed Apr 2, 2020
2 parents 2079240 + c8cbd68 commit 901743d
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 6 deletions.
34 changes: 34 additions & 0 deletions bravado_core/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,3 +923,37 @@ def model_discovery(swagger_spec):
# this ensures that the generated models have no references
_run_post_processing(tmp_spec)
swagger_spec.definitions = tmp_spec.definitions


def _to_pickleable_representation(model_name, model_type):
# type: (typing.Text, typing.Type[Model]) -> typing.Dict[typing.Text, typing.Any]
"""
Extract a pickleable representation of the input Model type.
Model types are runtime created types and so they are not pickleable.
In order to workaround this limitation we extract a representation,
which is pickleable such that we can re-create the input Model type
(via ``_from_pickleable_representation``).
NOTE: This API should not be considered a public API and is meant
only to be used by bravado_core.spec.Spec.__getstate__ .
"""
return {
'swagger_spec': model_type._swagger_spec,
'model_name': model_name,
'model_spec': model_type._model_spec,
'bases': model_type.__bases__,
'json_reference': model_type._json_reference,
}


def _from_pickleable_representation(model_pickleable_representation):
# type: (typing.Dict[typing.Text, typing.Any]) -> typing.Type[Model]
"""
Re-Create Model type form its pickleable representation
``model_pickleable_representation`` is supposed to be the output of ``_to_pickleable_representation``.
NOTE: This API should not be considered a public API and is meant
only to be used by bravado_core.spec.Spec.__getstate__ .
"""
return create_model_type(**model_pickleable_representation)
9 changes: 9 additions & 0 deletions bravado_core/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ def __deepcopy__(self, memo=None):
ops=deepcopy(self.operations, memo=memo),
)

def __getstate__(self):
# type: () -> typing.Dict[str, typing.Any]
return self.__dict__

def __setstate__(self, state):
# type: (typing.Dict[str, typing.Any]) -> None
self.__dict__.clear()
self.__dict__.update(state)

def __repr__(self):
# type: () -> str
repr = u"{self.__class__.__name__}({self.name})".format(self=self)
Expand Down
69 changes: 63 additions & 6 deletions bravado_core/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
from swagger_spec_validator.ref_validators import in_scope

from bravado_core import formatter
from bravado_core import version as _version
from bravado_core.exception import SwaggerSchemaError
from bravado_core.exception import SwaggerValidationError
from bravado_core.formatter import return_true_wrapper
from bravado_core.model import _from_pickleable_representation
from bravado_core.model import _to_pickleable_representation
from bravado_core.model import Model
from bravado_core.model import model_discovery
from bravado_core.resource import build_resources
Expand Down Expand Up @@ -140,16 +143,19 @@ def __init__(
self.user_defined_formats = {}
self.format_checker = FormatChecker()

self.resolver = RefResolver(
base_uri=origin_url or '',
referrer=self.spec_dict,
handlers=self.get_ref_handlers(),
)

# spec dict used to build resources, in case internally_dereference_refs config is enabled
# it will be overridden by the dereferenced specs (by build method). More context in PR#263
self._internal_spec_dict = spec_dict

@cached_property
def resolver(self):
# type: () -> RefResolver
return RefResolver(
base_uri=self.origin_url or '',
referrer=self.spec_dict,
handlers=self.get_ref_handlers(),
)

def is_equal(self, other):
# type: (typing.Any) -> bool
"""
Expand Down Expand Up @@ -241,6 +247,57 @@ def __deepcopy__(self, memo=None):

return copied_self

def __getstate__(self):
state = {
k: v
for k, v in iteritems(self.__dict__)
if k not in (
# Exclude resolver as it is not easily pickleable. As there are no real
# benefits on re-using the same Resolver respect to build a new one
# we're going to ignore the field and eventually re-create it if needed
# via cached_property
'resolver',
# Exclude definitions because it contain runtime defined type and those
# are not directly pickleable.
# Check bravado_core.model._to_pickleable_representation for details.
'definitions',
)
}

# A possible approach would be to re-execute model discovery on the newly Spec
# instance (in __setstate__) but it would be very slow.
# To avoid model discovery we store a pickleable representation of the Model types
# such that we can re-create them.
state['definitions'] = {
model_name: _to_pickleable_representation(model_name, model_type)
for model_name, model_type in iteritems(self.definitions)
}
# Store the bravado-core version used to create the Spec state
state['__bravado_core_version__'] = _version
return state

def __setstate__(self, state):
state_version = state.pop('__bravado_core_version__')
if state_version != _version:
warnings.warn(
'You are creating a Spec instance from a state created by a different '
'bravado-core version. We are not going to guarantee that the created '
'Spec instance will be correct. '
'State created by version {state_version}, current version {_version}'.format(
state_version=state_version,
_version=_version,
),
category=UserWarning,
)

# Re-create Model types, avoiding model discovery
state['definitions'] = {
model_name: _from_pickleable_representation(pickleable_representation)
for model_name, pickleable_representation in iteritems(state['definitions'])
}
self.__dict__.clear()
self.__dict__.update(state)

@cached_property
def client_spec_dict(self):
"""Return a copy of spec_dict with x-scope metadata removed so that it
Expand Down
45 changes: 45 additions & 0 deletions tests/model/pickling_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from six import iterkeys
from six.moves.cPickle import dumps

from bravado_core.model import _from_pickleable_representation
from bravado_core.model import _to_pickleable_representation
from bravado_core.model import ModelDocstring


def test_ensure_pickleable_representation_is_pickleable(cat_type):
pickleable_representation = _to_pickleable_representation('Cat', cat_type)
# Ensures that the pickle.dump of the pickleable representation is pickleable
# If the dumps call does not raise an exception then we were able to pickle
# the model type
dumps(pickleable_representation)


def test_ensure_that_get_model_type__from_pickleable_representation_returns_the_original_model(cat_type):
# Ensures that the pickle.dump of the pickleable representation is pickleable
# If the dumps call does not raise an exception then we were able to pickle
# the model type
reconstructed_model_type = _from_pickleable_representation(
model_pickleable_representation=_to_pickleable_representation('Cat', cat_type),
)
assert reconstructed_model_type.__name__ == 'Cat'

def is_the_same(attr_name):
if attr_name == '_swagger_spec':
return cat_type._swagger_spec.is_equal(reconstructed_model_type._swagger_spec)
elif attr_name == '__doc__':
return (
isinstance(cat_type.__dict__[attr_name], ModelDocstring) and
isinstance(reconstructed_model_type.__dict__[attr_name], ModelDocstring)
)
elif attr_name == '_abc_impl':
# _abc_impl is of type builtins._abc_data which is not really comparable. So we'll ignore it
return True
else:
return cat_type.__dict__[attr_name] == reconstructed_model_type.__dict__[attr_name]

assert [
attribute_name
for attribute_name in iterkeys(cat_type.__dict__)
if not is_the_same(attribute_name)
] == []
31 changes: 31 additions & 0 deletions tests/spec/pickling_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
import mock
import pytest
from six.moves.cPickle import dumps
from six.moves.cPickle import loads

from bravado_core.spec import Spec
from tests.conftest import get_url


@pytest.mark.parametrize('validate_swagger_spec', [True, False])
@pytest.mark.parametrize('internally_dereference_refs', [True, False])
def test_ensure_spec_is_pickleable(petstore_dict, petstore_abspath, internally_dereference_refs, validate_swagger_spec):
spec = Spec.from_dict(
spec_dict=petstore_dict,
origin_url=get_url(petstore_abspath),
config={
'validate_swagger_spec': validate_swagger_spec,
'internally_dereference_refs': internally_dereference_refs,
},
)
assert spec.is_equal(loads(dumps(spec)))


def test_ensure_warning_presence_in_case_of_version_mismatch(petstore_spec):
with mock.patch('bravado_core.spec._version', '0.0.0'):
petstore_pickle = dumps(petstore_spec)

with pytest.warns(UserWarning, match='different bravado-core version.*created by version 0.0.0, current version'):
restored_petstore_spec = loads(petstore_pickle)
assert petstore_spec.is_equal(restored_petstore_spec)

0 comments on commit 901743d

Please sign in to comment.