Skip to content

Commit

Permalink
Merge 028315e into cf6c204
Browse files Browse the repository at this point in the history
  • Loading branch information
jaysonsantos committed Jun 13, 2017
2 parents cf6c204 + 028315e commit 70f92ab
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 10 deletions.
11 changes: 10 additions & 1 deletion schematics/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sys


__all__ = ['PY2', 'PY3', 'string_type', 'iteritems', 'metaclass', 'py_native_string', 'str_compat']
__all__ = ['PY2', 'PY3', 'string_type', 'iteritems', 'metaclass', 'py_native_string', 'reraise', 'str_compat']


PY2 = sys.version_info[0] == 2
Expand All @@ -24,11 +24,20 @@
from itertools import izip as zip
iteritems = operator.methodcaller('iteritems')
itervalues = operator.methodcaller('itervalues')

# reraise code taken from werzeug BSD license at https://github.com/pallets/werkzeug/blob/master/LICENSE
exec('def reraise(tp, value, tb=None):\n raise tp, value, tb')
else:
string_type = str
iteritems = operator.methodcaller('items')
itervalues = operator.methodcaller('values')

# reraise code taken from werzeug BSD license at https://github.com/pallets/werkzeug/blob/master/LICENSE
def reraise(tp, value, tb=None):
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value


def metaclass(metaclass):
def make_class(cls):
Expand Down
28 changes: 19 additions & 9 deletions schematics/types/compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
export_loop,
get_import_context, get_export_context,
to_native_converter, to_primitive_converter)
from ..util import import_string

from .base import BaseType, get_value_in

Expand Down Expand Up @@ -83,13 +84,22 @@ def native_type(self):
def fields(self):
return self.model_class.fields

@property
def model_class(self):
if self._model_class:
return self._model_class

model_class = import_string(self.model_name)
self._model_class = model_class
return model_class

def __init__(self, model_spec, **kwargs):

if isinstance(model_spec, ModelMeta):
self.model_class = model_spec
self._model_class = model_spec
self.model_name = self.model_class.__name__
elif isinstance(model_spec, string_type):
self.model_class = None
self._model_class = None
self.model_name = model_spec
else:
raise TypeError("ModelType: Expected a model, got an argument "
Expand All @@ -105,11 +115,11 @@ def _mock(self, context=None):

def _setup(self, field_name, owner_model):
# Resolve possible name-based model reference.
if not self.model_class:
if not self._model_class:
if self.model_name == owner_model.__name__:
self.model_class = owner_model
self._model_class = owner_model
else:
raise Exception("ModelType: Unable to resolve model '{}'.".format(self.model_name))
pass # Intentionally left blank, it will be setup later.
super(ModelType, self)._setup(field_name, owner_model)

def pre_setattr(self, value):
Expand All @@ -121,14 +131,14 @@ def pre_setattr(self, value):
return value

def _convert(self, value, context):

if isinstance(value, self.model_class):
field_model_class = self.model_class
if isinstance(value, field_model_class):
model_class = type(value)
elif isinstance(value, dict):
model_class = self.model_class
model_class = field_model_class
else:
raise ConversionError(
"Input must be a mapping or '%s' instance" % self.model_class.__name__)
"Input must be a mapping or '%s' instance" % field_model_class.__name__)
if context.convert and context.oo:
return model_class(value, context=context)
else:
Expand Down
96 changes: 96 additions & 0 deletions schematics/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,101 @@ def package_exports(package_name):
]


class ImportStringError(ImportError):

"""Provides information about a failed :func:`import_string` attempt.
Code taken from werzeug BSD license at https://github.com/pallets/werkzeug/blob/master/LICENSE
"""

#: String in dotted notation that failed to be imported.
import_name = None
#: Wrapped exception.
exception = None

def __init__(self, import_name, exception):
self.import_name = import_name
self.exception = exception

msg = (
'import_string() failed for %r. Possible reasons are:\n\n'
'- missing __init__.py in a package;\n'
'- package or module path not included in sys.path;\n'
'- duplicated package or module name taking precedence in '
'sys.path;\n'
'- missing module, class, function or variable;\n\n'
'Debugged import:\n\n%s\n\n'
'Original exception:\n\n%s: %s')

name = ''
tracked = []
for part in import_name.replace(':', '.').split('.'):
name += (name and '.') + part
imported = import_string(name, silent=True)
if imported:
tracked.append((name, getattr(imported, '__file__', None)))
else:
track = ['- %r found in %r.' % (n, i) for n, i in tracked]
track.append('- %r not found.' % name)
msg = msg % (import_name, '\n'.join(track),
exception.__class__.__name__, str(exception))
break

ImportError.__init__(self, msg)

def __repr__(self):
return '<%s(%r, %r)>' % (self.__class__.__name__, self.import_name,
self.exception)


def import_string(import_name, silent=False):
"""Imports an object based on a string. This is useful if you want to
use import paths as endpoints or something similar. An import path can
be specified either in dotted notation (``xml.sax.saxutils.escape``)
or with a colon as object delimiter (``xml.sax.saxutils:escape``).
If `silent` is True the return value will be `None` if the import fails.
Code taken from werzeug BSD license at https://github.com/pallets/werkzeug/blob/master/LICENSE
:param import_name: the dotted name for the object to import.
:param silent: if set to `True` import errors are ignored and
`None` is returned instead.
:return: imported object
"""
# force the import name to automatically convert to strings
# __import__ is not able to handle unicode strings in the fromlist
# if the module is a package
import_name = str(import_name).replace(':', '.')
try:
try:
__import__(import_name)
except ImportError:
if '.' not in import_name:
raise
else:
return sys.modules[import_name]

module_name, obj_name = import_name.rsplit('.', 1)
try:
module = __import__(module_name, None, None, [obj_name])
except ImportError:
# support importing modules not yet set up by the parent module
# (or package for that matter)
module = import_string(module_name)

try:
return getattr(module, obj_name)
except AttributeError as e:
raise ImportError(e)

except ImportError as e:
if not silent:
reraise(
ImportStringError,
ImportStringError(import_name, e),
sys.exc_info()[2])


__all__ = module_exports(__name__)

19 changes: 19 additions & 0 deletions tests/test_model_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from schematics.types import IntType, StringType
from schematics.types.compound import ModelType, ListType
from schematics.exceptions import DataError
from schematics.util import ImportStringError


def test_simple_embedded_models():
Expand Down Expand Up @@ -263,3 +264,21 @@ class Thing(Model):
thing = Thing(input, app_data=app_data)
assert thing.x == 'thingiez'
assert thing.to_primitive(app_data=app_data) == {'x': 'thingie'}


class OuterModel:
class InnerModel(Model):
test = StringType()


def test_deep_string_search():
class TestModel(Model):
deep_model = ModelType('test_model_type.OuterModel.InnerModel')

test = TestModel(dict(deep_model=dict(test='Abc')))
assert test.validate() is None

class TestModel2(Model):
invalid_model = ModelType('a.c.d.e')
with pytest.raises(ImportStringError):
TestModel2(dict(invalid_model=dict(a='1')))

0 comments on commit 70f92ab

Please sign in to comment.