From 8d79c4f996fabb226afcc929a40ebf693d922b6b Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 8 May 2017 13:31:06 -0700 Subject: [PATCH] better errors for nested traits --- traitlets/traitlets.py | 96 ++++++------------ traitlets/utils/descriptions.py | 175 ++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 67 deletions(-) create mode 100644 traitlets/utils/descriptions.py diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index d3aa1489..132de517 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -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) @@ -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. @@ -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. @@ -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.""" @@ -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) @@ -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) @@ -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) @@ -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: diff --git a/traitlets/utils/descriptions.py b/traitlets/utils/descriptions.py new file mode 100644 index 00000000..3d9bffec --- /dev/null +++ b/traitlets/utils/descriptions.py @@ -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 \ No newline at end of file