Skip to content

Commit

Permalink
better errors for nested traits
Browse files Browse the repository at this point in the history
  • Loading branch information
rmorshea committed May 22, 2017
1 parent 7796cf1 commit 8d79c4f
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 67 deletions.
96 changes: 29 additions & 67 deletions traitlets/traitlets.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from .utils.importstring import import_item
from .utils.sentinel import Sentinel
from .utils.bunch import Bunch
from .utils.descriptions import describe, class_of, add_article, repr_type

SequenceTypes = (list, tuple, set, frozenset)

Expand Down Expand Up @@ -145,38 +146,6 @@ def _deprecated_method(method, cls, method_name, msg):
else:
warn_explicit(warn_msg, DeprecationWarning, fname, lineno)

def class_of(object):
""" Returns a string containing the class name of an object with the
correct indefinite article ('a' or 'an') preceding it (e.g., 'an Image',
'a PlotValue').
"""
if isinstance( object, six.string_types ):
return add_article( object )

return add_article( object.__class__.__name__ )


def add_article(name):
""" Returns a string containing the correct indefinite article ('a' or 'an')
prefixed to the specified string.
"""
if name[:1].lower() in 'aeiou':
return 'an ' + name

return 'a ' + name


def repr_type(obj):
""" Return a string representation of a value and its type for readable
error messages.
"""
the_type = type(obj)
if six.PY2 and the_type is InstanceType:
# Old-style class.
the_type = obj.__class__
msg = '%r %r' % (obj, the_type)
return msg


def is_trait(t):
""" Returns whether the given value is an instance or subclass of TraitType.
Expand Down Expand Up @@ -614,15 +583,30 @@ def __or__(self, other):
def info(self):
return self.info_text

def error(self, obj, value):
if obj is not None:
e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
% (self.name, class_of(obj),
self.info(), repr_type(value))
def error(self, obj, value, error=None):
if error is not None:
# handle nested error
error.args += (self,)
if self.name is not None:
# this is the root trait that must format the final nested error message
error.args = ("The '%s' trait of %s instance contains %s which expected %s, not %s." % (
self.name, describe("an", obj), " of ".join(describe("a", t) for t in error.args[1:]),
error.args[1].info(), describe("the", error.args[0])),)
raise error
else:
e = "The '%s' trait must be %s, but a value of %r was specified." \
% (self.name, self.info(), repr_type(value))
raise TraitError(e)
# this trait caused an error
if self.name is None:
# this is not the root trait
raise TraitError(value, self)
else:
# this is the root trait
if obj is not None:
e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
% (self.name, class_of(obj), self.info(), repr_type(value))
else:
e = "The '%s' trait must be %s, but a value of %r was specified." \
% (self.name, self.info(), repr_type(value))
raise TraitError(e)

def get_metadata(self, key, default=None):
"""DEPRECATED: Get a metadata value.
Expand Down Expand Up @@ -1573,23 +1557,6 @@ def _resolve_string(self, string):
"""
return import_item(string)

def error(self, obj, value):
kind = type(value)
if six.PY2 and kind is InstanceType:
msg = 'class %s' % value.__class__.__name__
else:
msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) )

if obj is not None:
e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
% (self.name, class_of(obj),
self.info(), msg)
else:
e = "The '%s' trait must be %s, but a value of %r was specified." \
% (self.name, self.info(), msg)

raise TraitError(e)


class Type(ClassBasedTraitType):
"""A trait whose value must be a subclass of a specified class."""
Expand Down Expand Up @@ -2332,11 +2299,6 @@ def __init__(self, trait=None, default_value=None, **kwargs):

super(Container,self).__init__(klass=self.klass, args=args, **kwargs)

def element_error(self, obj, element, validator):
e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
% (self.name, class_of(obj), validator.info(), repr_type(element))
raise TraitError(e)

def validate(self, obj, value):
if isinstance(value, self._cast_types):
value = self.klass(value)
Expand All @@ -2355,8 +2317,8 @@ def validate_elements(self, obj, value):
for v in value:
try:
v = self._trait._validate(obj, v)
except TraitError:
self.element_error(obj, v, self._trait)
except TraitError as error:
self.error(obj, v, error)
else:
validated.append(v)
return self.klass(validated)
Expand Down Expand Up @@ -2547,8 +2509,8 @@ def validate_elements(self, obj, value):
for t, v in zip(self._traits, value):
try:
v = t._validate(obj, v)
except TraitError:
self.element_error(obj, v, t)
except TraitError as error:
self.error(obj, v, error)
else:
validated.append(v)
return tuple(validated)
Expand Down Expand Up @@ -2713,7 +2675,7 @@ def validate_elements(self, obj, value):
if key_trait:
try:
key = key_trait._validate(obj, key)
except TraitError:
except TraitError as error:
self.element_error(obj, key, key_trait, 'Keys')
active_value_trait = per_key_override.get(key, value_trait)
if active_value_trait:
Expand Down
175 changes: 175 additions & 0 deletions traitlets/utils/descriptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import re
import six
import types
import inspect


def describe(article, value, name=None, verbose=False, capital=False):
"""Return string that describes a value
Parameters
----------
article: str or None
A definite or indefinite article. If the article is
indefinite (i.e. "a" or "an") the appropriate one
will be infered. Thus, the arguments of ``describe``
can themselves represent what the resulting string
will actually look like. If None, then no article
will be prepended to the result. For non-articled
description, values that are instances are treated
definitely, while classes are handled indefinitely.
value: any
The value which will be named.
name: str or None (default: None)
Only applies when ``article`` is "the" - this
``name`` is a definite reference to the value.
By default one will be infered from the value's
type and repr methods.
verbose: bool (default: False)
Whether the name should be concise or verbose. When
possible, verbose names include the module, and/or
class name where an object was defined.
capital: bool (default: False)
Whether the first letter of the article should
be capitalized or not. By default it is not.
Examples
--------
Indefinite description:
>>> describe("a", object())
'an object'
>>> describe("a", object)
'an object'
>>> describe("a", type(object))
'a type'
Definite description:
>>> describe("the", object())
"the object at '0x10741f1b0'"
>>> describe("the", object)
"the type 'object'"
>>> describe("the", type(object))
"the type 'type'"
Definitely named description:
>>> describe("the", object(), "I made")
'the object I made'
>>> describe("the", object, "I will use")
'the object I will use'
"""
if isinstance(article, str):
article = article.lower()

if not inspect.isclass(value):
typename = type(value).__name__
else:
typename = value.__name__
if verbose:
typename = _prefix(value) + typename

if article == "the" or (article is None and not inspect.isclass(value)):
if name is not None:
result = "%s %s" % (typename, name)
if article is not None:
return add_article(result, True, capital)
else:
return result
else:
tick_wrap = False
if inspect.isclass(value):
name = value.__name__
elif isinstance(value, types.FunctionType):
name = value.__name__
tick_wrap = True
elif isinstance(value, types.MethodType):
name = value.__func__.__name__
tick_wrap = True
elif type(value).__repr__ in (object.__repr__, type.__repr__):
name = "at '%s'" % hex(id(value))
verbose = False
else:
name = repr(value)
verbose = False
if verbose:
name = _prefix(value) + name
if tick_wrap:
name = name.join("''")
return describe(article, value, name=name,
verbose=verbose, capital=capital)
elif article in ("a", "an") or article is None:
if article is None:
return typename
return add_article(typename, False, capital)
else:
raise ValueError("The 'article' argument should "
"be 'the', 'a', 'an', or None not %r" % article)


def _prefix(value):
if isinstance(value, types.MethodType):
name = describe(None, value.__self__, verbose=True) + '.'
else:
module = inspect.getmodule(value)
if module is not None and module.__name__ != "builtins":
name = module.__name__ + '.'
else:
name = ""
return name


def class_of(value):
"""Returns a string of the value's type with an indefinite article.
For example 'an Image' or 'a PlotValue'.
"""
if inspect.isclass(value):
return add_article(value.__name__)
else:
return class_of(type(value))


def add_article(name, definite=False, capital=False):
"""Returns the string with a prepended article.
The input does not need to begin with a charater.
Parameters
----------
definite: bool (default: False)
Whether the article is definite or not.
Indefinite articles being 'a' and 'an',
while 'the' is definite.
capital: bool (default: False)
Whether the added article should have
its first letter capitalized or not.
"""
if definite:
result = "the " + name
else:
first_letters = re.compile(r'[\W_]+').sub('', name)
if first_letters[:1].lower() in 'aeiou':
result = 'an ' + name
else:
result = 'a ' + name
if capital:
return result[0].upper() + result[1:]
else:
return result
return result


def repr_type(obj):
"""Return a string representation of a value and its type for readable
error messages.
"""
the_type = type(obj)
if six.PY2 and the_type is types.InstanceType:
# Old-style class.
the_type = obj.__class__
msg = '%r %r' % (obj, the_type)
return msg

0 comments on commit 8d79c4f

Please sign in to comment.