Skip to content
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

Better Errors for Nested Traits #402

Merged
merged 2 commits into from
May 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 60 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,61 @@ 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, info=None):
"""Raise a TraitError

Parameters
----------
obj: HasTraits or None
The instance which owns the trait. If not
object is given, then an object agnostic
error will be raised.
value: any
The value that caused the error.
error: Exception (default: None)
An error that was raised by a child trait.
The arguments of this exception should be
of the form ``(value, info, *traits)``.
Where the ``value`` and ``info`` are the
problem value, and string describing the
expected value. The ``traits`` are a series
of :class:`TraitType` instances that are
"children" of this one (the first being
the deepest).
info: str (default: None)
A description of the expected value. By
default this is infered from this trait's
``info`` method.
"""
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 message
chain = " of ".join(describe("a", t) for t in error.args[2:])
if obj is not None:
error.args = ("The '%s' trait of %s instance contains %s which "
"expected %s, not %s." % (self.name, describe("an", obj),
chain, error.args[1], describe("the", error.args[0])),)
else:
error.args = ("The '%s' trait contains %s which "
"expected %s, not %s." % (self.name, chain,
error.args[1], 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, info or self.info(), self)
else:
# this is the root trait
if obj is not None:
e = "The '%s' trait of %s instance expected %s, not %s." % (
self.name, class_of(obj), self.info(), describe("the", value))
else:
e = "The '%s' trait expected %s, not %s." % (
self.name, self.info(), describe("the", value))
raise TraitError(e)

def get_metadata(self, key, default=None):
"""DEPRECATED: Get a metadata value.
Expand Down Expand Up @@ -1573,23 +1588,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 +2330,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 +2348,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 +2540,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 +2706,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