diff options
author | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:24:06 +0300 |
---|---|---|
committer | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:41:34 +0300 |
commit | e0e3e1717e3d33762ce61950504f9637a6e669ed (patch) | |
tree | bca3ff6939b10ed60c3d5c12439963a1146b9711 /contrib/python/traitlets/py2 | |
parent | 38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff) | |
download | ydb-e0e3e1717e3d33762ce61950504f9637a6e669ed.tar.gz |
add ydb deps
Diffstat (limited to 'contrib/python/traitlets/py2')
25 files changed, 8045 insertions, 0 deletions
diff --git a/contrib/python/traitlets/py2/.dist-info/METADATA b/contrib/python/traitlets/py2/.dist-info/METADATA new file mode 100644 index 0000000000..bd031a5c30 --- /dev/null +++ b/contrib/python/traitlets/py2/.dist-info/METADATA @@ -0,0 +1,34 @@ +Metadata-Version: 2.1 +Name: traitlets +Version: 4.3.3 +Summary: Traitlets Python config system +Home-page: http://ipython.org +Author: IPython Development Team +Author-email: ipython-dev@scipy.org +License: BSD +Keywords: Interactive,Interpreter,Shell,Web +Platform: Linux +Platform: Mac OS X +Platform: Windows +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Requires-Dist: ipython-genutils +Requires-Dist: six +Requires-Dist: decorator +Requires-Dist: enum34 ; python_version=="2.7" +Requires-Dist: enum34 ; python_version=="3.3" +Provides-Extra: test +Requires-Dist: pytest ; extra == 'test' +Requires-Dist: mock ; (python_version=="2.7") and extra == 'test' + +A configuration system for Python applications. + + diff --git a/contrib/python/traitlets/py2/.dist-info/top_level.txt b/contrib/python/traitlets/py2/.dist-info/top_level.txt new file mode 100644 index 0000000000..adfea9c6eb --- /dev/null +++ b/contrib/python/traitlets/py2/.dist-info/top_level.txt @@ -0,0 +1 @@ +traitlets diff --git a/contrib/python/traitlets/py2/COPYING.md b/contrib/python/traitlets/py2/COPYING.md new file mode 100644 index 0000000000..39ca730a63 --- /dev/null +++ b/contrib/python/traitlets/py2/COPYING.md @@ -0,0 +1,62 @@ +# Licensing terms + +Traitlets is adapted from enthought.traits, Copyright (c) Enthought, Inc., +under the terms of the Modified BSD License. + +This project is licensed under the terms of the Modified BSD License +(also known as New or Revised or 3-Clause BSD), as follows: + +- Copyright (c) 2001-, IPython Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the IPython Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## About the IPython Development Team + +The IPython Development Team is the set of all contributors to the IPython project. +This includes all of the IPython subprojects. + +The core team that coordinates development on GitHub can be found here: +https://github.com/jupyter/. + +## Our Copyright Policy + +IPython uses a shared copyright model. Each contributor maintains copyright +over their contributions to IPython. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, the IPython +source code, in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire IPython +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the IPython repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + + # Copyright (c) IPython Development Team. + # Distributed under the terms of the Modified BSD License. diff --git a/contrib/python/traitlets/py2/README.md b/contrib/python/traitlets/py2/README.md new file mode 100644 index 0000000000..aa288947ef --- /dev/null +++ b/contrib/python/traitlets/py2/README.md @@ -0,0 +1,143 @@ +# Traitlets + +[![Build Status](https://travis-ci.org/ipython/traitlets.svg?branch=master)](https://travis-ci.org/ipython/traitlets) +[![Documentation Status](https://readthedocs.org/projects/traitlets/badge/?version=latest)](http://traitlets.readthedocs.org/en/latest/?badge=latest) + +Traitlets is a pure Python library enabling: + + - the enforcement of strong typing for attributes of Python objects + (typed attributes are called "traits"), + - notifications on changes of trait attributes, + - automatic validation and coercion of trait attributes when attempting a + change. + +Its implementation relies on the [descriptor](https://docs.python.org/howto/descriptor.html) +pattern. + +Traitlets powers the configuration system of IPython and Jupyter +and the declarative API of IPython interactive widgets. + +## Installation + +For a local installation, make sure you have +[pip installed](https://pip.pypa.io/en/stable/installing/) and run: + +```bash +pip install traitlets +``` + +For a **development installation**, clone this repository, change into the +`traitlets` root directory, and run pip: + +```bash +git clone https://github.com/ipython/traitlets.git +cd traitlets +pip install -e . +``` + +## Running the tests + +```bash +pip install "traitlets[test]" +py.test traitlets +``` + +## Usage + +Any class with trait attributes must inherit from `HasTraits`. +For the list of available trait types and their properties, see the +[Trait Types](https://traitlets.readthedocs.io/en/latest/trait_types.html) +section of the documentation. + +### Dynamic default values + +To calculate a default value dynamically, decorate a method of your class with +`@default({traitname})`. This method will be called on the instance, and +should return the default value. In this example, the `_username_default` +method is decorated with `@default('username')`: + +```Python +import getpass +from traitlets import HasTraits, Unicode, default + +class Identity(HasTraits): + username = Unicode() + + @default('username') + def _username_default(self): + return getpass.getuser() +``` + +### Callbacks when a trait attribute changes + +When a trait changes, an application can follow this trait change with +additional actions. + +To do something when a trait attribute is changed, decorate a method with +[`traitlets.observe()`](https://traitlets.readthedocs.io/en/latest/api.html?highlight=observe#traitlets.observe). +The method will be called with a single argument, a dictionary which contains +an owner, new value, old value, name of the changed trait, and the event type. + +In this example, the `_num_changed` method is decorated with ``@observe(`num`)``: + +```Python +from traitlets import HasTraits, Integer, observe + +class TraitletsExample(HasTraits): + num = Integer(5, help="a number").tag(config=True) + + @observe('num') + def _num_changed(self, change): + print("{name} changed from {old} to {new}".format(**change)) +``` + +and is passed the following dictionary when called: + +```Python +{ + 'owner': object, # The HasTraits instance + 'new': 6, # The new value + 'old': 5, # The old value + 'name': "foo", # The name of the changed trait + 'type': 'change', # The event type of the notification, usually 'change' +} +``` + +### Validation and coercion + +Each trait type (`Int`, `Unicode`, `Dict` etc.) may have its own validation or +coercion logic. In addition, we can register custom cross-validators +that may depend on the state of other attributes. For example: + +```Python +from traitlets import HasTraits, TraitError, Int, Bool, validate + +class Parity(HasTraits): + value = Int() + parity = Int() + + @validate('value') + def _valid_value(self, proposal): + if proposal['value'] % 2 != self.parity: + raise TraitError('value and parity should be consistent') + return proposal['value'] + + @validate('parity') + def _valid_parity(self, proposal): + parity = proposal['value'] + if parity not in [0, 1]: + raise TraitError('parity should be 0 or 1') + if self.value % 2 != parity: + raise TraitError('value and parity should be consistent') + return proposal['value'] + +parity_check = Parity(value=2) + +# Changing required parity and value together while holding cross validation +with parity_check.hold_trait_notifications(): + parity_check.value = 1 + parity_check.parity = 1 +``` + +However, we **recommend** that custom cross-validators don't modify the state +of the HasTraits instance. diff --git a/contrib/python/traitlets/py2/tests/ya.make b/contrib/python/traitlets/py2/tests/ya.make new file mode 100644 index 0000000000..adcfe34ac8 --- /dev/null +++ b/contrib/python/traitlets/py2/tests/ya.make @@ -0,0 +1,23 @@ +PY2TEST() + +PEERDIR( + contrib/python/traitlets +) + +ENV( + YA_PYTEST_DISABLE_DOCTEST=yes +) + +SRCDIR(contrib/python/traitlets/py2/traitlets) + +TEST_SRCS( + tests/__init__.py + tests/_warnings.py + tests/test_traitlets.py + tests/test_traitlets_enum.py + tests/utils.py +) + +NO_LINT() + +END() diff --git a/contrib/python/traitlets/py2/traitlets/__init__.py b/contrib/python/traitlets/py2/traitlets/__init__.py new file mode 100644 index 0000000000..b609adb565 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/__init__.py @@ -0,0 +1,3 @@ +from .traitlets import * +from .utils.importstring import import_item +from ._version import version_info, __version__ diff --git a/contrib/python/traitlets/py2/traitlets/_version.py b/contrib/python/traitlets/py2/traitlets/_version.py new file mode 100644 index 0000000000..ed16b3c1e1 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/_version.py @@ -0,0 +1,2 @@ +version_info = (4, 3, 3) +__version__ = '.'.join(map(str, version_info)) diff --git a/contrib/python/traitlets/py2/traitlets/config/__init__.py b/contrib/python/traitlets/py2/traitlets/config/__init__.py new file mode 100644 index 0000000000..0ae7d63171 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/config/__init__.py @@ -0,0 +1,8 @@ +# encoding: utf-8 + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from .application import * +from .configurable import * +from .loader import Config diff --git a/contrib/python/traitlets/py2/traitlets/config/application.py b/contrib/python/traitlets/py2/traitlets/config/application.py new file mode 100644 index 0000000000..d3a4c45e77 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/config/application.py @@ -0,0 +1,711 @@ +# encoding: utf-8 +"""A base class for a configurable application.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from __future__ import print_function + +from copy import deepcopy +import json +import logging +import os +import re +import sys +from collections import defaultdict, OrderedDict + +from decorator import decorator + +from traitlets.config.configurable import Configurable, SingletonConfigurable +from traitlets.config.loader import ( + KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, JSONFileConfigLoader +) + +from traitlets.traitlets import ( + Bool, Unicode, List, Enum, Dict, Instance, TraitError, observe, observe_compat, default, +) +from ipython_genutils.importstring import import_item +from ipython_genutils.text import indent, wrap_paragraphs, dedent +from ipython_genutils import py3compat + +import six + +#----------------------------------------------------------------------------- +# Descriptions for the various sections +#----------------------------------------------------------------------------- + +# merge flags&aliases into options +option_description = """ +Arguments that take values are actually convenience aliases to full +Configurables, whose aliases are listed on the help line. For more information +on full configurables, see '--help-all'. +""".strip() # trim newlines of front and back + +keyvalue_description = """ +Parameters are set from command-line arguments of the form: +`--Class.trait=value`. +This line is evaluated in Python, so simple expressions are allowed, e.g.:: +`--C.a='range(3)'` For setting C.a=[0,1,2]. +""".strip() # trim newlines of front and back + +# sys.argv can be missing, for example when python is embedded. See the docs +# for details: http://docs.python.org/2/c-api/intro.html#embedding-python +if not hasattr(sys, "argv"): + sys.argv = [""] + +subcommand_description = """ +Subcommands are launched as `{app} cmd [args]`. For information on using +subcommand 'cmd', do: `{app} cmd -h`. +""" +# get running program name + +#----------------------------------------------------------------------------- +# Application class +#----------------------------------------------------------------------------- + + + +_envvar = os.environ.get('TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR','') +if _envvar.lower() in {'1','true'}: + TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR = True +elif _envvar.lower() in {'0','false',''} : + TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR = False +else: + raise ValueError("Unsupported value for environment variable: 'TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR' is set to '%s' which is none of {'0', '1', 'false', 'true', ''}."% _envvar ) + + +@decorator +def catch_config_error(method, app, *args, **kwargs): + """Method decorator for catching invalid config (Trait/ArgumentErrors) during init. + + On a TraitError (generally caused by bad config), this will print the trait's + message, and exit the app. + + For use on init methods, to prevent invoking excepthook on invalid input. + """ + try: + return method(app, *args, **kwargs) + except (TraitError, ArgumentError) as e: + app.print_help() + app.log.fatal("Bad config encountered during initialization:") + app.log.fatal(str(e)) + app.log.debug("Config at the time: %s", app.config) + app.exit(1) + + +class ApplicationError(Exception): + pass + + +class LevelFormatter(logging.Formatter): + """Formatter with additional `highlevel` record + + This field is empty if log level is less than highlevel_limit, + otherwise it is formatted with self.highlevel_format. + + Useful for adding 'WARNING' to warning messages, + without adding 'INFO' to info, etc. + """ + highlevel_limit = logging.WARN + highlevel_format = " %(levelname)s |" + + def format(self, record): + if record.levelno >= self.highlevel_limit: + record.highlevel = self.highlevel_format % record.__dict__ + else: + record.highlevel = "" + return super(LevelFormatter, self).format(record) + + +class Application(SingletonConfigurable): + """A singleton application with full configuration support.""" + + # The name of the application, will usually match the name of the command + # line application + name = Unicode(u'application') + + # The description of the application that is printed at the beginning + # of the help. + description = Unicode(u'This is an application.') + # default section descriptions + option_description = Unicode(option_description) + keyvalue_description = Unicode(keyvalue_description) + subcommand_description = Unicode(subcommand_description) + + python_config_loader_class = PyFileConfigLoader + json_config_loader_class = JSONFileConfigLoader + + # The usage and example string that goes at the end of the help string. + examples = Unicode() + + # A sequence of Configurable subclasses whose config=True attributes will + # be exposed at the command line. + classes = [] + + def _classes_inc_parents(self): + """Iterate through configurable classes, including configurable parents + + Children should always be after parents, and each class should only be + yielded once. + """ + seen = set() + for c in self.classes: + # We want to sort parents before children, so we reverse the MRO + for parent in reversed(c.mro()): + if issubclass(parent, Configurable) and (parent not in seen): + seen.add(parent) + yield parent + + # The version string of this application. + version = Unicode(u'0.0') + + # the argv used to initialize the application + argv = List() + + # Whether failing to load config files should prevent startup + raise_config_file_errors = Bool(TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR) + + # The log level for the application + log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'), + default_value=logging.WARN, + help="Set the log level by value or name.").tag(config=True) + + @observe('log_level') + @observe_compat + def _log_level_changed(self, change): + """Adjust the log level when log_level is set.""" + new = change.new + if isinstance(new, six.string_types): + new = getattr(logging, new) + self.log_level = new + self.log.setLevel(new) + + _log_formatter_cls = LevelFormatter + + log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", + help="The date format used by logging formatters for %(asctime)s" + ).tag(config=True) + + log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", + help="The Logging format template", + ).tag(config=True) + + @observe('log_datefmt', 'log_format') + @observe_compat + def _log_format_changed(self, change): + """Change the log formatter when log_format is set.""" + _log_handler = self.log.handlers[0] + _log_formatter = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt) + _log_handler.setFormatter(_log_formatter) + + @default('log') + def _log_default(self): + """Start logging for this application. + + The default is to log to stderr using a StreamHandler, if no default + handler already exists. The log level starts at logging.WARN, but this + can be adjusted by setting the ``log_level`` attribute. + """ + log = logging.getLogger(self.__class__.__name__) + log.setLevel(self.log_level) + log.propagate = False + _log = log # copied from Logger.hasHandlers() (new in Python 3.2) + while _log: + if _log.handlers: + return log + if not _log.propagate: + break + else: + _log = _log.parent + if sys.executable and sys.executable.endswith('pythonw.exe'): + # this should really go to a file, but file-logging is only + # hooked up in parallel applications + _log_handler = logging.StreamHandler(open(os.devnull, 'w')) + else: + _log_handler = logging.StreamHandler() + _log_formatter = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt) + _log_handler.setFormatter(_log_formatter) + log.addHandler(_log_handler) + return log + + # the alias map for configurables + aliases = Dict({'log-level' : 'Application.log_level'}) + + # flags for loading Configurables or store_const style flags + # flags are loaded from this dict by '--key' flags + # this must be a dict of two-tuples, the first element being the Config/dict + # and the second being the help string for the flag + flags = Dict() + @observe('flags') + @observe_compat + def _flags_changed(self, change): + """ensure flags dict is valid""" + new = change.new + for key, value in new.items(): + assert len(value) == 2, "Bad flag: %r:%s" % (key, value) + assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s" % (key, value) + assert isinstance(value[1], six.string_types), "Bad flag: %r:%s" % (key, value) + + + # subcommands for launching other applications + # if this is not empty, this will be a parent Application + # this must be a dict of two-tuples, + # the first element being the application class/import string + # and the second being the help string for the subcommand + subcommands = Dict() + # parse_command_line will initialize a subapp, if requested + subapp = Instance('traitlets.config.application.Application', allow_none=True) + + # extra command-line arguments that don't set config values + extra_args = List(Unicode()) + + cli_config = Instance(Config, (), {}, + help="""The subset of our configuration that came from the command-line + + We re-load this configuration after loading config files, + to ensure that it maintains highest priority. + """ + ) + + _loaded_config_files = List() + + def __init__(self, **kwargs): + SingletonConfigurable.__init__(self, **kwargs) + # Ensure my class is in self.classes, so my attributes appear in command line + # options and config files. + cls = self.__class__ + if cls not in self.classes: + if self.classes is cls.classes: + # class attr, assign instead of insert + cls.classes = [cls] + self.classes + else: + self.classes.insert(0, self.__class__) + + @observe('config') + @observe_compat + def _config_changed(self, change): + super(Application, self)._config_changed(change) + self.log.debug('Config changed:') + self.log.debug(repr(change.new)) + + @catch_config_error + def initialize(self, argv=None): + """Do the basic steps to configure me. + + Override in subclasses. + """ + self.parse_command_line(argv) + + + def start(self): + """Start the app mainloop. + + Override in subclasses. + """ + if self.subapp is not None: + return self.subapp.start() + + def print_alias_help(self): + """Print the alias part of the help.""" + if not self.aliases: + return + + lines = [] + classdict = {} + for cls in self.classes: + # include all parents (up to, but excluding Configurable) in available names + for c in cls.mro()[:-3]: + classdict[c.__name__] = c + + for alias, longname in self.aliases.items(): + classname, traitname = longname.split('.',1) + cls = classdict[classname] + + trait = cls.class_traits(config=True)[traitname] + help = cls.class_get_trait_help(trait).splitlines() + # reformat first line + help[0] = help[0].replace(longname, alias) + ' (%s)'%longname + if len(alias) == 1: + help[0] = help[0].replace('--%s='%alias, '-%s '%alias) + lines.extend(help) + # lines.append('') + print(os.linesep.join(lines)) + + def print_flag_help(self): + """Print the flag part of the help.""" + if not self.flags: + return + + lines = [] + for m, (cfg,help) in self.flags.items(): + prefix = '--' if len(m) > 1 else '-' + lines.append(prefix+m) + lines.append(indent(dedent(help.strip()))) + # lines.append('') + print(os.linesep.join(lines)) + + def print_options(self): + if not self.flags and not self.aliases: + return + lines = ['Options'] + lines.append('-'*len(lines[0])) + lines.append('') + for p in wrap_paragraphs(self.option_description): + lines.append(p) + lines.append('') + print(os.linesep.join(lines)) + self.print_flag_help() + self.print_alias_help() + print() + + def print_subcommands(self): + """Print the subcommand part of the help.""" + if not self.subcommands: + return + + lines = ["Subcommands"] + lines.append('-'*len(lines[0])) + lines.append('') + for p in wrap_paragraphs(self.subcommand_description.format( + app=self.name)): + lines.append(p) + lines.append('') + for subc, (cls, help) in self.subcommands.items(): + lines.append(subc) + if help: + lines.append(indent(dedent(help.strip()))) + lines.append('') + print(os.linesep.join(lines)) + + def print_help(self, classes=False): + """Print the help for each Configurable class in self.classes. + + If classes=False (the default), only flags and aliases are printed. + """ + self.print_description() + self.print_subcommands() + self.print_options() + + if classes: + help_classes = self.classes + if help_classes: + print("Class parameters") + print("----------------") + print() + for p in wrap_paragraphs(self.keyvalue_description): + print(p) + print() + + for cls in help_classes: + cls.class_print_help() + print() + else: + print("To see all available configurables, use `--help-all`") + print() + + self.print_examples() + + def document_config_options(self): + """Generate rST format documentation for the config options this application + + Returns a multiline string. + """ + return '\n'.join(c.class_config_rst_doc() + for c in self._classes_inc_parents()) + + + def print_description(self): + """Print the application description.""" + for p in wrap_paragraphs(self.description): + print(p) + print() + + def print_examples(self): + """Print usage and examples. + + This usage string goes at the end of the command line help string + and should contain examples of the application's usage. + """ + if self.examples: + print("Examples") + print("--------") + print() + print(indent(dedent(self.examples.strip()))) + print() + + def print_version(self): + """Print the version string.""" + print(self.version) + + @catch_config_error + def initialize_subcommand(self, subc, argv=None): + """Initialize a subcommand with argv.""" + subapp,help = self.subcommands.get(subc) + + if isinstance(subapp, six.string_types): + subapp = import_item(subapp) + + # clear existing instances + self.__class__.clear_instance() + # instantiate + self.subapp = subapp.instance(parent=self) + # and initialize subapp + self.subapp.initialize(argv) + + def flatten_flags(self): + """flatten flags and aliases, so cl-args override as expected. + + This prevents issues such as an alias pointing to InteractiveShell, + but a config file setting the same trait in TerminalInteraciveShell + getting inappropriate priority over the command-line arg. + + Only aliases with exactly one descendent in the class list + will be promoted. + + """ + # build a tree of classes in our list that inherit from a particular + # it will be a dict by parent classname of classes in our list + # that are descendents + mro_tree = defaultdict(list) + for cls in self.classes: + clsname = cls.__name__ + for parent in cls.mro()[1:-3]: + # exclude cls itself and Configurable,HasTraits,object + mro_tree[parent.__name__].append(clsname) + # flatten aliases, which have the form: + # { 'alias' : 'Class.trait' } + aliases = {} + for alias, cls_trait in self.aliases.items(): + cls,trait = cls_trait.split('.',1) + children = mro_tree[cls] + if len(children) == 1: + # exactly one descendent, promote alias + cls = children[0] + aliases[alias] = '.'.join([cls,trait]) + + # flatten flags, which are of the form: + # { 'key' : ({'Cls' : {'trait' : value}}, 'help')} + flags = {} + for key, (flagdict, help) in self.flags.items(): + newflag = {} + for cls, subdict in flagdict.items(): + children = mro_tree[cls] + # exactly one descendent, promote flag section + if len(children) == 1: + cls = children[0] + newflag[cls] = subdict + flags[key] = (newflag, help) + return flags, aliases + + @catch_config_error + def parse_command_line(self, argv=None): + """Parse the command line arguments.""" + argv = sys.argv[1:] if argv is None else argv + self.argv = [ py3compat.cast_unicode(arg) for arg in argv ] + + if argv and argv[0] == 'help': + # turn `ipython help notebook` into `ipython notebook -h` + argv = argv[1:] + ['-h'] + + if self.subcommands and len(argv) > 0: + # we have subcommands, and one may have been specified + subc, subargv = argv[0], argv[1:] + if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands: + # it's a subcommand, and *not* a flag or class parameter + return self.initialize_subcommand(subc, subargv) + + # Arguments after a '--' argument are for the script IPython may be + # about to run, not IPython iteslf. For arguments parsed here (help and + # version), we want to only search the arguments up to the first + # occurrence of '--', which we're calling interpreted_argv. + try: + interpreted_argv = argv[:argv.index('--')] + except ValueError: + interpreted_argv = argv + + if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')): + self.print_help('--help-all' in interpreted_argv) + self.exit(0) + + if '--version' in interpreted_argv or '-V' in interpreted_argv: + self.print_version() + self.exit(0) + + # flatten flags&aliases, so cl-args get appropriate priority: + flags,aliases = self.flatten_flags() + loader = KVArgParseConfigLoader(argv=argv, aliases=aliases, + flags=flags, log=self.log) + self.cli_config = deepcopy(loader.load_config()) + self.update_config(self.cli_config) + # store unparsed args in extra_args + self.extra_args = loader.extra_args + + @classmethod + def _load_config_files(cls, basefilename, path=None, log=None, raise_config_file_errors=False): + """Load config files (py,json) by filename and path. + + yield each config object in turn. + """ + + if not isinstance(path, list): + path = [path] + for path in path[::-1]: + # path list is in descending priority order, so load files backwards: + pyloader = cls.python_config_loader_class(basefilename+'.py', path=path, log=log) + if log: + log.debug("Looking for %s in %s", basefilename, path or os.getcwd()) + jsonloader = cls.json_config_loader_class(basefilename+'.json', path=path, log=log) + loaded = [] + filenames = [] + for loader in [pyloader, jsonloader]: + config = None + try: + config = loader.load_config() + except ConfigFileNotFound: + pass + except Exception: + # try to get the full filename, but it will be empty in the + # unlikely event that the error raised before filefind finished + filename = loader.full_filename or basefilename + # problem while running the file + if raise_config_file_errors: + raise + if log: + log.error("Exception while loading config file %s", + filename, exc_info=True) + else: + if log: + log.debug("Loaded config file: %s", loader.full_filename) + if config: + for filename, earlier_config in zip(filenames, loaded): + collisions = earlier_config.collisions(config) + if collisions and log: + log.warning("Collisions detected in {0} and {1} config files." + " {1} has higher priority: {2}".format( + filename, loader.full_filename, json.dumps(collisions, indent=2), + )) + yield (config, loader.full_filename) + loaded.append(config) + filenames.append(loader.full_filename) + + @property + def loaded_config_files(self): + """Currently loaded configuration files""" + return self._loaded_config_files[:] + + @catch_config_error + def load_config_file(self, filename, path=None): + """Load config files by filename and path.""" + filename, ext = os.path.splitext(filename) + new_config = Config() + for (config, filename) in self._load_config_files(filename, path=path, log=self.log, + raise_config_file_errors=self.raise_config_file_errors, + ): + new_config.merge(config) + if filename not in self._loaded_config_files: # only add to list of loaded files if not previously loaded + self._loaded_config_files.append(filename) + # add self.cli_config to preserve CLI config priority + new_config.merge(self.cli_config) + self.update_config(new_config) + + + def _classes_in_config_sample(self): + """ + Yields only classes with own traits, and their subclasses. + + Thus, produced sample config-file will contain all classes + on which a trait-value may be overridden: + + - either on the class owning the trait, + - or on its subclasses, even if those subclasses do not define + any traits themselves. + """ + cls_to_config = OrderedDict( (cls, bool(cls.class_own_traits(config=True))) + for cls + in self._classes_inc_parents()) + + def is_any_parent_included(cls): + return any(b in cls_to_config and cls_to_config[b] for b in cls.__bases__) + + ## Mark "empty" classes for inclusion if their parents own-traits, + # and loop until no more classes gets marked. + # + while True: + to_incl_orig = cls_to_config.copy() + cls_to_config = OrderedDict( (cls, inc_yes or is_any_parent_included(cls)) + for cls, inc_yes + in cls_to_config.items()) + if cls_to_config == to_incl_orig: + break + for cl, inc_yes in cls_to_config.items(): + if inc_yes: + yield cl + + def generate_config_file(self): + """generate default config file from Configurables""" + lines = ["# Configuration file for %s." % self.name] + lines.append('') + for cls in self._classes_in_config_sample(): + lines.append(cls.class_config_section()) + return '\n'.join(lines) + + def exit(self, exit_status=0): + self.log.debug("Exiting application: %s" % self.name) + sys.exit(exit_status) + + @classmethod + def launch_instance(cls, argv=None, **kwargs): + """Launch a global instance of this Application + + If a global instance already exists, this reinitializes and starts it + """ + app = cls.instance(**kwargs) + app.initialize(argv) + app.start() + +#----------------------------------------------------------------------------- +# utility functions, for convenience +#----------------------------------------------------------------------------- + +def boolean_flag(name, configurable, set_help='', unset_help=''): + """Helper for building basic --trait, --no-trait flags. + + Parameters + ---------- + + name : str + The name of the flag. + configurable : str + The 'Class.trait' string of the trait to be set/unset with the flag + set_help : unicode + help string for --name flag + unset_help : unicode + help string for --no-name flag + + Returns + ------- + + cfg : dict + A dict with two keys: 'name', and 'no-name', for setting and unsetting + the trait, respectively. + """ + # default helpstrings + set_help = set_help or "set %s=True"%configurable + unset_help = unset_help or "set %s=False"%configurable + + cls,trait = configurable.split('.') + + setter = {cls : {trait : True}} + unsetter = {cls : {trait : False}} + return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)} + + +def get_config(): + """Get the config object for the global Application instance, if there is one + + otherwise return an empty config object + """ + if Application.initialized(): + return Application.instance().config + else: + return Config() diff --git a/contrib/python/traitlets/py2/traitlets/config/configurable.py b/contrib/python/traitlets/py2/traitlets/config/configurable.py new file mode 100644 index 0000000000..1174fcf017 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/config/configurable.py @@ -0,0 +1,432 @@ +# encoding: utf-8 +"""A base class for objects that are configurable.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from __future__ import print_function, absolute_import + +from copy import deepcopy +import warnings + +from .loader import Config, LazyConfigValue, _is_section_key +from traitlets.traitlets import HasTraits, Instance, observe, observe_compat, default +from ipython_genutils.text import indent, dedent, wrap_paragraphs + + +#----------------------------------------------------------------------------- +# Helper classes for Configurables +#----------------------------------------------------------------------------- + + +class ConfigurableError(Exception): + pass + + +class MultipleInstanceError(ConfigurableError): + pass + +#----------------------------------------------------------------------------- +# Configurable implementation +#----------------------------------------------------------------------------- + +class Configurable(HasTraits): + + config = Instance(Config, (), {}) + parent = Instance('traitlets.config.configurable.Configurable', allow_none=True) + + def __init__(self, **kwargs): + """Create a configurable given a config config. + + Parameters + ---------- + config : Config + If this is empty, default values are used. If config is a + :class:`Config` instance, it will be used to configure the + instance. + parent : Configurable instance, optional + The parent Configurable instance of this object. + + Notes + ----- + Subclasses of Configurable must call the :meth:`__init__` method of + :class:`Configurable` *before* doing anything else and using + :func:`super`:: + + class MyConfigurable(Configurable): + def __init__(self, config=None): + super(MyConfigurable, self).__init__(config=config) + # Then any other code you need to finish initialization. + + This ensures that instances will be configured properly. + """ + parent = kwargs.pop('parent', None) + if parent is not None: + # config is implied from parent + if kwargs.get('config', None) is None: + kwargs['config'] = parent.config + self.parent = parent + + config = kwargs.pop('config', None) + + # load kwarg traits, other than config + super(Configurable, self).__init__(**kwargs) + + # load config + if config is not None: + # We used to deepcopy, but for now we are trying to just save + # by reference. This *could* have side effects as all components + # will share config. In fact, I did find such a side effect in + # _config_changed below. If a config attribute value was a mutable type + # all instances of a component were getting the same copy, effectively + # making that a class attribute. + # self.config = deepcopy(config) + self.config = config + else: + # allow _config_default to return something + self._load_config(self.config) + + # Ensure explicit kwargs are applied after loading config. + # This is usually redundant, but ensures config doesn't override + # explicitly assigned values. + for key, value in kwargs.items(): + setattr(self, key, value) + + #------------------------------------------------------------------------- + # Static trait notifiations + #------------------------------------------------------------------------- + + @classmethod + def section_names(cls): + """return section names as a list""" + return [c.__name__ for c in reversed(cls.__mro__) if + issubclass(c, Configurable) and issubclass(cls, c) + ] + + def _find_my_config(self, cfg): + """extract my config from a global Config object + + will construct a Config object of only the config values that apply to me + based on my mro(), as well as those of my parent(s) if they exist. + + If I am Bar and my parent is Foo, and their parent is Tim, + this will return merge following config sections, in this order:: + + [Bar, Foo.bar, Tim.Foo.Bar] + + With the last item being the highest priority. + """ + cfgs = [cfg] + if self.parent: + cfgs.append(self.parent._find_my_config(cfg)) + my_config = Config() + for c in cfgs: + for sname in self.section_names(): + # Don't do a blind getattr as that would cause the config to + # dynamically create the section with name Class.__name__. + if c._has_section(sname): + my_config.merge(c[sname]) + return my_config + + def _load_config(self, cfg, section_names=None, traits=None): + """load traits from a Config object""" + + if traits is None: + traits = self.traits(config=True) + if section_names is None: + section_names = self.section_names() + + my_config = self._find_my_config(cfg) + + # hold trait notifications until after all config has been loaded + with self.hold_trait_notifications(): + for name, config_value in my_config.items(): + if name in traits: + if isinstance(config_value, LazyConfigValue): + # ConfigValue is a wrapper for using append / update on containers + # without having to copy the initial value + initial = getattr(self, name) + config_value = config_value.get_value(initial) + # We have to do a deepcopy here if we don't deepcopy the entire + # config object. If we don't, a mutable config_value will be + # shared by all instances, effectively making it a class attribute. + setattr(self, name, deepcopy(config_value)) + elif not _is_section_key(name) and not isinstance(config_value, Config): + from difflib import get_close_matches + if isinstance(self, LoggingConfigurable): + warn = self.log.warning + else: + warn = lambda msg: warnings.warn(msg, stacklevel=9) + matches = get_close_matches(name, traits) + msg = u"Config option `{option}` not recognized by `{klass}`.".format( + option=name, klass=self.__class__.__name__) + + if len(matches) == 1: + msg += u" Did you mean `{matches}`?".format(matches=matches[0]) + elif len(matches) >= 1: + msg +=" Did you mean one of: `{matches}`?".format(matches=', '.join(sorted(matches))) + warn(msg) + + @observe('config') + @observe_compat + def _config_changed(self, change): + """Update all the class traits having ``config=True`` in metadata. + + For any class trait with a ``config`` metadata attribute that is + ``True``, we update the trait with the value of the corresponding + config entry. + """ + # Get all traits with a config metadata entry that is True + traits = self.traits(config=True) + + # We auto-load config section for this class as well as any parent + # classes that are Configurable subclasses. This starts with Configurable + # and works down the mro loading the config for each section. + section_names = self.section_names() + self._load_config(change.new, traits=traits, section_names=section_names) + + def update_config(self, config): + """Update config and load the new values""" + # traitlets prior to 4.2 created a copy of self.config in order to trigger change events. + # Some projects (IPython < 5) relied upon one side effect of this, + # that self.config prior to update_config was not modified in-place. + # For backward-compatibility, we must ensure that self.config + # is a new object and not modified in-place, + # but config consumers should not rely on this behavior. + self.config = deepcopy(self.config) + # load config + self._load_config(config) + # merge it into self.config + self.config.merge(config) + # TODO: trigger change event if/when dict-update change events take place + # DO NOT trigger full trait-change + + @classmethod + def class_get_help(cls, inst=None): + """Get the help string for this class in ReST format. + + If `inst` is given, it's current trait values will be used in place of + class defaults. + """ + assert inst is None or isinstance(inst, cls) + final_help = [] + final_help.append(u'%s options' % cls.__name__) + final_help.append(len(final_help[0])*u'-') + for k, v in sorted(cls.class_traits(config=True).items()): + help = cls.class_get_trait_help(v, inst) + final_help.append(help) + return '\n'.join(final_help) + + @classmethod + def class_get_trait_help(cls, trait, inst=None): + """Get the help string for a single trait. + + If `inst` is given, it's current trait values will be used in place of + the class default. + """ + assert inst is None or isinstance(inst, cls) + lines = [] + header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__) + lines.append(header) + if inst is not None: + lines.append(indent('Current: %r' % getattr(inst, trait.name), 4)) + else: + try: + dvr = trait.default_value_repr() + except Exception: + dvr = None # ignore defaults we can't construct + if dvr is not None: + if len(dvr) > 64: + dvr = dvr[:61]+'...' + lines.append(indent('Default: %s' % dvr, 4)) + if 'Enum' in trait.__class__.__name__: + # include Enum choices + lines.append(indent('Choices: %r' % (trait.values,))) + + help = trait.help + if help != '': + help = '\n'.join(wrap_paragraphs(help, 76)) + lines.append(indent(help, 4)) + return '\n'.join(lines) + + @classmethod + def class_print_help(cls, inst=None): + """Get the help string for a single trait and print it.""" + print(cls.class_get_help(inst)) + + @classmethod + def class_config_section(cls): + """Get the config class config section""" + def c(s): + """return a commented, wrapped block.""" + s = '\n\n'.join(wrap_paragraphs(s, 78)) + + return '## ' + s.replace('\n', '\n# ') + + # section header + breaker = '#' + '-'*78 + parent_classes = ','.join(p.__name__ for p in cls.__bases__) + s = "# %s(%s) configuration" % (cls.__name__, parent_classes) + lines = [breaker, s, breaker, ''] + # get the description trait + desc = cls.class_traits().get('description') + if desc: + desc = desc.default_value + if not desc: + # no description from trait, use __doc__ + desc = getattr(cls, '__doc__', '') + if desc: + lines.append(c(desc)) + lines.append('') + + for name, trait in sorted(cls.class_own_traits(config=True).items()): + lines.append(c(trait.help)) + lines.append('#c.%s.%s = %s' % (cls.__name__, name, trait.default_value_repr())) + lines.append('') + return '\n'.join(lines) + + @classmethod + def class_config_rst_doc(cls): + """Generate rST documentation for this class' config options. + + Excludes traits defined on parent classes. + """ + lines = [] + classname = cls.__name__ + for k, trait in sorted(cls.class_own_traits(config=True).items()): + ttype = trait.__class__.__name__ + + termline = classname + '.' + trait.name + + # Choices or type + if 'Enum' in ttype: + # include Enum choices + termline += ' : ' + '|'.join(repr(x) for x in trait.values) + else: + termline += ' : ' + ttype + lines.append(termline) + + # Default value + try: + dvr = trait.default_value_repr() + except Exception: + dvr = None # ignore defaults we can't construct + if dvr is not None: + if len(dvr) > 64: + dvr = dvr[:61]+'...' + # Double up backslashes, so they get to the rendered docs + dvr = dvr.replace('\\n', '\\\\n') + lines.append(' Default: ``%s``' % dvr) + lines.append('') + + help = trait.help or 'No description' + lines.append(indent(dedent(help), 4)) + + # Blank line + lines.append('') + + return '\n'.join(lines) + + + +class LoggingConfigurable(Configurable): + """A parent class for Configurables that log. + + Subclasses have a log trait, and the default behavior + is to get the logger from the currently running Application. + """ + + log = Instance('logging.Logger') + @default('log') + def _log_default(self): + from traitlets import log + return log.get_logger() + + +class SingletonConfigurable(LoggingConfigurable): + """A configurable that only allows one instance. + + This class is for classes that should only have one instance of itself + or *any* subclass. To create and retrieve such a class use the + :meth:`SingletonConfigurable.instance` method. + """ + + _instance = None + + @classmethod + def _walk_mro(cls): + """Walk the cls.mro() for parent classes that are also singletons + + For use in instance() + """ + + for subclass in cls.mro(): + if issubclass(cls, subclass) and \ + issubclass(subclass, SingletonConfigurable) and \ + subclass != SingletonConfigurable: + yield subclass + + @classmethod + def clear_instance(cls): + """unset _instance for this class and singleton parents. + """ + if not cls.initialized(): + return + for subclass in cls._walk_mro(): + if isinstance(subclass._instance, cls): + # only clear instances that are instances + # of the calling class + subclass._instance = None + + @classmethod + def instance(cls, *args, **kwargs): + """Returns a global instance of this class. + + This method create a new instance if none have previously been created + and returns a previously created instance is one already exists. + + The arguments and keyword arguments passed to this method are passed + on to the :meth:`__init__` method of the class upon instantiation. + + Examples + -------- + + Create a singleton class using instance, and retrieve it:: + + >>> from traitlets.config.configurable import SingletonConfigurable + >>> class Foo(SingletonConfigurable): pass + >>> foo = Foo.instance() + >>> foo == Foo.instance() + True + + Create a subclass that is retrived using the base class instance:: + + >>> class Bar(SingletonConfigurable): pass + >>> class Bam(Bar): pass + >>> bam = Bam.instance() + >>> bam == Bar.instance() + True + """ + # Create and save the instance + if cls._instance is None: + inst = cls(*args, **kwargs) + # Now make sure that the instance will also be returned by + # parent classes' _instance attribute. + for subclass in cls._walk_mro(): + subclass._instance = inst + + if isinstance(cls._instance, cls): + return cls._instance + else: + raise MultipleInstanceError( + 'Multiple incompatible subclass instances of ' + '%s are being created.' % cls.__name__ + ) + + @classmethod + def initialized(cls): + """Has an instance been created?""" + return hasattr(cls, "_instance") and cls._instance is not None + + + diff --git a/contrib/python/traitlets/py2/traitlets/config/loader.py b/contrib/python/traitlets/py2/traitlets/config/loader.py new file mode 100644 index 0000000000..803b36276f --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/config/loader.py @@ -0,0 +1,857 @@ +# encoding: utf-8 +"""A simple configuration system.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +import argparse +import copy +import logging +import os +import re +import sys +import json +from ast import literal_eval + +from ipython_genutils.path import filefind +from ipython_genutils import py3compat +from ipython_genutils.encoding import DEFAULT_ENCODING +from six import text_type +from traitlets.traitlets import HasTraits, List, Any + +#----------------------------------------------------------------------------- +# Exceptions +#----------------------------------------------------------------------------- + + +class ConfigError(Exception): + pass + +class ConfigLoaderError(ConfigError): + pass + +class ConfigFileNotFound(ConfigError): + pass + +class ArgumentError(ConfigLoaderError): + pass + +#----------------------------------------------------------------------------- +# Argparse fix +#----------------------------------------------------------------------------- + +# Unfortunately argparse by default prints help messages to stderr instead of +# stdout. This makes it annoying to capture long help screens at the command +# line, since one must know how to pipe stderr, which many users don't know how +# to do. So we override the print_help method with one that defaults to +# stdout and use our class instead. + +class ArgumentParser(argparse.ArgumentParser): + """Simple argparse subclass that prints help to stdout by default.""" + + def print_help(self, file=None): + if file is None: + file = sys.stdout + return super(ArgumentParser, self).print_help(file) + + print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__ + +#----------------------------------------------------------------------------- +# Config class for holding config information +#----------------------------------------------------------------------------- + +class LazyConfigValue(HasTraits): + """Proxy object for exposing methods on configurable containers + + Exposes: + + - append, extend, insert on lists + - update on dicts + - update, add on sets + """ + + _value = None + + # list methods + _extend = List() + _prepend = List() + + def append(self, obj): + self._extend.append(obj) + + def extend(self, other): + self._extend.extend(other) + + def prepend(self, other): + """like list.extend, but for the front""" + self._prepend[:0] = other + + _inserts = List() + def insert(self, index, other): + if not isinstance(index, int): + raise TypeError("An integer is required") + self._inserts.append((index, other)) + + # dict methods + # update is used for both dict and set + _update = Any() + def update(self, other): + if self._update is None: + if isinstance(other, dict): + self._update = {} + else: + self._update = set() + self._update.update(other) + + # set methods + def add(self, obj): + self.update({obj}) + + def get_value(self, initial): + """construct the value from the initial one + + after applying any insert / extend / update changes + """ + if self._value is not None: + return self._value + value = copy.deepcopy(initial) + if isinstance(value, list): + for idx, obj in self._inserts: + value.insert(idx, obj) + value[:0] = self._prepend + value.extend(self._extend) + + elif isinstance(value, dict): + if self._update: + value.update(self._update) + elif isinstance(value, set): + if self._update: + value.update(self._update) + self._value = value + return value + + def to_dict(self): + """return JSONable dict form of my data + + Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples. + """ + d = {} + if self._update: + d['update'] = self._update + if self._extend: + d['extend'] = self._extend + if self._prepend: + d['prepend'] = self._prepend + elif self._inserts: + d['inserts'] = self._inserts + return d + + +def _is_section_key(key): + """Is a Config key a section name (does it start with a capital)?""" + if key and key[0].upper()==key[0] and not key.startswith('_'): + return True + else: + return False + + +class Config(dict): + """An attribute based dict that can do smart merges.""" + + def __init__(self, *args, **kwds): + dict.__init__(self, *args, **kwds) + self._ensure_subconfig() + + def _ensure_subconfig(self): + """ensure that sub-dicts that should be Config objects are + + casts dicts that are under section keys to Config objects, + which is necessary for constructing Config objects from dict literals. + """ + for key in self: + obj = self[key] + if _is_section_key(key) \ + and isinstance(obj, dict) \ + and not isinstance(obj, Config): + setattr(self, key, Config(obj)) + + def _merge(self, other): + """deprecated alias, use Config.merge()""" + self.merge(other) + + def merge(self, other): + """merge another config object into this one""" + to_update = {} + for k, v in other.items(): + if k not in self: + to_update[k] = v + else: # I have this key + if isinstance(v, Config) and isinstance(self[k], Config): + # Recursively merge common sub Configs + self[k].merge(v) + else: + # Plain updates for non-Configs + to_update[k] = v + + self.update(to_update) + + def collisions(self, other): + """Check for collisions between two config objects. + + Returns a dict of the form {"Class": {"trait": "collision message"}}`, + indicating which values have been ignored. + + An empty dict indicates no collisions. + """ + collisions = {} + for section in self: + if section not in other: + continue + mine = self[section] + theirs = other[section] + for key in mine: + if key in theirs and mine[key] != theirs[key]: + collisions.setdefault(section, {}) + collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key]) + return collisions + + def __contains__(self, key): + # allow nested contains of the form `"Section.key" in config` + if '.' in key: + first, remainder = key.split('.', 1) + if first not in self: + return False + return remainder in self[first] + + return super(Config, self).__contains__(key) + + # .has_key is deprecated for dictionaries. + has_key = __contains__ + + def _has_section(self, key): + return _is_section_key(key) and key in self + + def copy(self): + return type(self)(dict.copy(self)) + + def __copy__(self): + return self.copy() + + def __deepcopy__(self, memo): + new_config = type(self)() + for key, value in self.items(): + if isinstance(value, (Config, LazyConfigValue)): + # deep copy config objects + value = copy.deepcopy(value, memo) + elif type(value) in {dict, list, set, tuple}: + # shallow copy plain container traits + value = copy.copy(value) + new_config[key] = value + return new_config + + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + if _is_section_key(key): + c = Config() + dict.__setitem__(self, key, c) + return c + elif not key.startswith('_'): + # undefined, create lazy value, used for container methods + v = LazyConfigValue() + dict.__setitem__(self, key, v) + return v + else: + raise KeyError + + def __setitem__(self, key, value): + if _is_section_key(key): + if not isinstance(value, Config): + raise ValueError('values whose keys begin with an uppercase ' + 'char must be Config instances: %r, %r' % (key, value)) + dict.__setitem__(self, key, value) + + def __getattr__(self, key): + if key.startswith('__'): + return dict.__getattr__(self, key) + try: + return self.__getitem__(key) + except KeyError as e: + raise AttributeError(e) + + def __setattr__(self, key, value): + if key.startswith('__'): + return dict.__setattr__(self, key, value) + try: + self.__setitem__(key, value) + except KeyError as e: + raise AttributeError(e) + + def __delattr__(self, key): + if key.startswith('__'): + return dict.__delattr__(self, key) + try: + dict.__delitem__(self, key) + except KeyError as e: + raise AttributeError(e) + + +#----------------------------------------------------------------------------- +# Config loading classes +#----------------------------------------------------------------------------- + + +class ConfigLoader(object): + """A object for loading configurations from just about anywhere. + + The resulting configuration is packaged as a :class:`Config`. + + Notes + ----- + A :class:`ConfigLoader` does one thing: load a config from a source + (file, command line arguments) and returns the data as a :class:`Config` object. + There are lots of things that :class:`ConfigLoader` does not do. It does + not implement complex logic for finding config files. It does not handle + default values or merge multiple configs. These things need to be + handled elsewhere. + """ + + def _log_default(self): + from traitlets.log import get_logger + return get_logger() + + def __init__(self, log=None): + """A base class for config loaders. + + log : instance of :class:`logging.Logger` to use. + By default loger of :meth:`traitlets.config.application.Application.instance()` + will be used + + Examples + -------- + + >>> cl = ConfigLoader() + >>> config = cl.load_config() + >>> config + {} + """ + self.clear() + if log is None: + self.log = self._log_default() + self.log.debug('Using default logger') + else: + self.log = log + + def clear(self): + self.config = Config() + + def load_config(self): + """Load a config from somewhere, return a :class:`Config` instance. + + Usually, this will cause self.config to be set and then returned. + However, in most cases, :meth:`ConfigLoader.clear` should be called + to erase any previous state. + """ + self.clear() + return self.config + + +class FileConfigLoader(ConfigLoader): + """A base class for file based configurations. + + As we add more file based config loaders, the common logic should go + here. + """ + + def __init__(self, filename, path=None, **kw): + """Build a config loader for a filename and path. + + Parameters + ---------- + filename : str + The file name of the config file. + path : str, list, tuple + The path to search for the config file on, or a sequence of + paths to try in order. + """ + super(FileConfigLoader, self).__init__(**kw) + self.filename = filename + self.path = path + self.full_filename = '' + + def _find_file(self): + """Try to find the file by searching the paths.""" + self.full_filename = filefind(self.filename, self.path) + +class JSONFileConfigLoader(FileConfigLoader): + """A JSON file loader for config + + Can also act as a context manager that rewrite the configuration file to disk on exit. + + Example:: + + with JSONFileConfigLoader('myapp.json','/home/jupyter/configurations/') as c: + c.MyNewConfigurable.new_value = 'Updated' + + """ + + def load_config(self): + """Load the config from a file and return it as a Config object.""" + self.clear() + try: + self._find_file() + except IOError as e: + raise ConfigFileNotFound(str(e)) + dct = self._read_file_as_dict() + self.config = self._convert_to_config(dct) + return self.config + + def _read_file_as_dict(self): + with open(self.full_filename) as f: + return json.load(f) + + def _convert_to_config(self, dictionary): + if 'version' in dictionary: + version = dictionary.pop('version') + else: + version = 1 + + if version == 1: + return Config(dictionary) + else: + raise ValueError('Unknown version of JSON config file: {version}'.format(version=version)) + + def __enter__(self): + self.load_config() + return self.config + + def __exit__(self, exc_type, exc_value, traceback): + """ + Exit the context manager but do not handle any errors. + + In case of any error, we do not want to write the potentially broken + configuration to disk. + """ + self.config.version = 1 + json_config = json.dumps(self.config, indent=2) + with open(self.full_filename, 'w') as f: + f.write(json_config) + + + +class PyFileConfigLoader(FileConfigLoader): + """A config loader for pure python files. + + This is responsible for locating a Python config file by filename and + path, then executing it to construct a Config object. + """ + + def load_config(self): + """Load the config from a file and return it as a Config object.""" + self.clear() + try: + self._find_file() + except IOError as e: + raise ConfigFileNotFound(str(e)) + self._read_file_as_dict() + return self.config + + def load_subconfig(self, fname, path=None): + """Injected into config file namespace as load_subconfig""" + if path is None: + path = self.path + + loader = self.__class__(fname, path) + try: + sub_config = loader.load_config() + except ConfigFileNotFound: + # Pass silently if the sub config is not there, + # treat it as an empty config file. + pass + else: + self.config.merge(sub_config) + + def _read_file_as_dict(self): + """Load the config file into self.config, with recursive loading.""" + def get_config(): + """Unnecessary now, but a deprecation warning is more trouble than it's worth.""" + return self.config + + namespace = dict( + c=self.config, + load_subconfig=self.load_subconfig, + get_config=get_config, + __file__=self.full_filename, + ) + fs_encoding = sys.getfilesystemencoding() or 'ascii' + conf_filename = self.full_filename.encode(fs_encoding) + py3compat.execfile(conf_filename, namespace) + + +class CommandLineConfigLoader(ConfigLoader): + """A config loader for command line arguments. + + As we add more command line based loaders, the common logic should go + here. + """ + + def _exec_config_str(self, lhs, rhs): + """execute self.config.<lhs> = <rhs> + + * expands ~ with expanduser + * tries to assign with literal_eval, otherwise assigns with just the string, + allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not* + equivalent are `--C.a=4` and `--C.a='4'`. + """ + rhs = os.path.expanduser(rhs) + try: + # Try to see if regular Python syntax will work. This + # won't handle strings as the quote marks are removed + # by the system shell. + value = literal_eval(rhs) + except (NameError, SyntaxError, ValueError): + # This case happens if the rhs is a string. + value = rhs + + exec(u'self.config.%s = value' % lhs) + + def _load_flag(self, cfg): + """update self.config from a flag, which can be a dict or Config""" + if isinstance(cfg, (dict, Config)): + # don't clobber whole config sections, update + # each section from config: + for sec,c in cfg.items(): + self.config[sec].update(c) + else: + raise TypeError("Invalid flag: %r" % cfg) + +# raw --identifier=value pattern +# but *also* accept '-' as wordsep, for aliases +# accepts: --foo=a +# --Class.trait=value +# --alias-name=value +# rejects: -foo=value +# --foo +# --Class.trait +kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*') + +# just flags, no assignments, with two *or one* leading '-' +# accepts: --foo +# -foo-bar-again +# rejects: --anything=anything +# --two.word + +flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$') + +class KeyValueConfigLoader(CommandLineConfigLoader): + """A config loader that loads key value pairs from the command line. + + This allows command line options to be gives in the following form:: + + ipython --profile="foo" --InteractiveShell.autocall=False + """ + + def __init__(self, argv=None, aliases=None, flags=None, **kw): + """Create a key value pair config loader. + + Parameters + ---------- + argv : list + A list that has the form of sys.argv[1:] which has unicode + elements of the form u"key=value". If this is None (default), + then sys.argv[1:] will be used. + aliases : dict + A dict of aliases for configurable traits. + Keys are the short aliases, Values are the resolved trait. + Of the form: `{'alias' : 'Configurable.trait'}` + flags : dict + A dict of flags, keyed by str name. Vaues can be Config objects, + dicts, or "key=value" strings. If Config or dict, when the flag + is triggered, The flag is loaded as `self.config.update(m)`. + + Returns + ------- + config : Config + The resulting Config object. + + Examples + -------- + + >>> from traitlets.config.loader import KeyValueConfigLoader + >>> cl = KeyValueConfigLoader() + >>> d = cl.load_config(["--A.name='brian'","--B.number=0"]) + >>> sorted(d.items()) + [('A', {'name': 'brian'}), ('B', {'number': 0})] + """ + super(KeyValueConfigLoader, self).__init__(**kw) + if argv is None: + argv = sys.argv[1:] + self.argv = argv + self.aliases = aliases or {} + self.flags = flags or {} + + + def clear(self): + super(KeyValueConfigLoader, self).clear() + self.extra_args = [] + + + def _decode_argv(self, argv, enc=None): + """decode argv if bytes, using stdin.encoding, falling back on default enc""" + uargv = [] + if enc is None: + enc = DEFAULT_ENCODING + for arg in argv: + if not isinstance(arg, text_type): + # only decode if not already decoded + arg = arg.decode(enc) + uargv.append(arg) + return uargv + + + def load_config(self, argv=None, aliases=None, flags=None): + """Parse the configuration and generate the Config object. + + After loading, any arguments that are not key-value or + flags will be stored in self.extra_args - a list of + unparsed command-line arguments. This is used for + arguments such as input files or subcommands. + + Parameters + ---------- + argv : list, optional + A list that has the form of sys.argv[1:] which has unicode + elements of the form u"key=value". If this is None (default), + then self.argv will be used. + aliases : dict + A dict of aliases for configurable traits. + Keys are the short aliases, Values are the resolved trait. + Of the form: `{'alias' : 'Configurable.trait'}` + flags : dict + A dict of flags, keyed by str name. Values can be Config objects + or dicts. When the flag is triggered, The config is loaded as + `self.config.update(cfg)`. + """ + self.clear() + if argv is None: + argv = self.argv + if aliases is None: + aliases = self.aliases + if flags is None: + flags = self.flags + + # ensure argv is a list of unicode strings: + uargv = self._decode_argv(argv) + for idx,raw in enumerate(uargv): + # strip leading '-' + item = raw.lstrip('-') + + if raw == '--': + # don't parse arguments after '--' + # this is useful for relaying arguments to scripts, e.g. + # ipython -i foo.py --matplotlib=qt -- args after '--' go-to-foo.py + self.extra_args.extend(uargv[idx+1:]) + break + + if kv_pattern.match(raw): + lhs,rhs = item.split('=',1) + # Substitute longnames for aliases. + if lhs in aliases: + lhs = aliases[lhs] + if '.' not in lhs: + # probably a mistyped alias, but not technically illegal + self.log.warning("Unrecognized alias: '%s', it will probably have no effect.", raw) + try: + self._exec_config_str(lhs, rhs) + except Exception: + raise ArgumentError("Invalid argument: '%s'" % raw) + + elif flag_pattern.match(raw): + if item in flags: + cfg,help = flags[item] + self._load_flag(cfg) + else: + raise ArgumentError("Unrecognized flag: '%s'"%raw) + elif raw.startswith('-'): + kv = '--'+item + if kv_pattern.match(kv): + raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv)) + else: + raise ArgumentError("Invalid argument: '%s'"%raw) + else: + # keep all args that aren't valid in a list, + # in case our parent knows what to do with them. + self.extra_args.append(item) + return self.config + +class ArgParseConfigLoader(CommandLineConfigLoader): + """A loader that uses the argparse module to load from the command line.""" + + def __init__(self, argv=None, aliases=None, flags=None, log=None, *parser_args, **parser_kw): + """Create a config loader for use with argparse. + + Parameters + ---------- + + argv : optional, list + If given, used to read command-line arguments from, otherwise + sys.argv[1:] is used. + + parser_args : tuple + A tuple of positional arguments that will be passed to the + constructor of :class:`argparse.ArgumentParser`. + + parser_kw : dict + A tuple of keyword arguments that will be passed to the + constructor of :class:`argparse.ArgumentParser`. + + Returns + ------- + config : Config + The resulting Config object. + """ + super(CommandLineConfigLoader, self).__init__(log=log) + self.clear() + if argv is None: + argv = sys.argv[1:] + self.argv = argv + self.aliases = aliases or {} + self.flags = flags or {} + + self.parser_args = parser_args + self.version = parser_kw.pop("version", None) + kwargs = dict(argument_default=argparse.SUPPRESS) + kwargs.update(parser_kw) + self.parser_kw = kwargs + + def load_config(self, argv=None, aliases=None, flags=None): + """Parse command line arguments and return as a Config object. + + Parameters + ---------- + + args : optional, list + If given, a list with the structure of sys.argv[1:] to parse + arguments from. If not given, the instance's self.argv attribute + (given at construction time) is used.""" + self.clear() + if argv is None: + argv = self.argv + if aliases is None: + aliases = self.aliases + if flags is None: + flags = self.flags + self._create_parser(aliases, flags) + self._parse_args(argv) + self._convert_to_config() + return self.config + + def get_extra_args(self): + if hasattr(self, 'extra_args'): + return self.extra_args + else: + return [] + + def _create_parser(self, aliases=None, flags=None): + self.parser = ArgumentParser(*self.parser_args, **self.parser_kw) + self._add_arguments(aliases, flags) + + def _add_arguments(self, aliases=None, flags=None): + raise NotImplementedError("subclasses must implement _add_arguments") + + def _parse_args(self, args): + """self.parser->self.parsed_data""" + # decode sys.argv to support unicode command-line options + enc = DEFAULT_ENCODING + uargs = [py3compat.cast_unicode(a, enc) for a in args] + self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs) + + def _convert_to_config(self): + """self.parsed_data->self.config""" + for k, v in vars(self.parsed_data).items(): + exec("self.config.%s = v"%k, locals(), globals()) + +class KVArgParseConfigLoader(ArgParseConfigLoader): + """A config loader that loads aliases and flags with argparse, + but will use KVLoader for the rest. This allows better parsing + of common args, such as `ipython -c 'print 5'`, but still gets + arbitrary config with `ipython --InteractiveShell.use_readline=False`""" + + def _add_arguments(self, aliases=None, flags=None): + self.alias_flags = {} + # print aliases, flags + if aliases is None: + aliases = self.aliases + if flags is None: + flags = self.flags + paa = self.parser.add_argument + for key,value in aliases.items(): + if key in flags: + # flags + nargs = '?' + else: + nargs = None + if len(key) is 1: + paa('-'+key, '--'+key, type=text_type, dest=value, nargs=nargs) + else: + paa('--'+key, type=text_type, dest=value, nargs=nargs) + for key, (value, help) in flags.items(): + if key in self.aliases: + # + self.alias_flags[self.aliases[key]] = value + continue + if len(key) is 1: + paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value) + else: + paa('--'+key, action='append_const', dest='_flags', const=value) + + def _convert_to_config(self): + """self.parsed_data->self.config, parse unrecognized extra args via KVLoader.""" + # remove subconfigs list from namespace before transforming the Namespace + if '_flags' in self.parsed_data: + subcs = self.parsed_data._flags + del self.parsed_data._flags + else: + subcs = [] + + for k, v in vars(self.parsed_data).items(): + if v is None: + # it was a flag that shares the name of an alias + subcs.append(self.alias_flags[k]) + else: + # eval the KV assignment + self._exec_config_str(k, v) + + for subc in subcs: + self._load_flag(subc) + + if self.extra_args: + sub_parser = KeyValueConfigLoader(log=self.log) + sub_parser.load_config(self.extra_args) + self.config.merge(sub_parser.config) + self.extra_args = sub_parser.extra_args + + +def load_pyconfig_files(config_files, path): + """Load multiple Python config files, merging each of them in turn. + + Parameters + ========== + config_files : list of str + List of config files names to load and merge into the config. + path : unicode + The full path to the location of the config files. + """ + config = Config() + for cf in config_files: + loader = PyFileConfigLoader(cf, path=path) + try: + next_config = loader.load_config() + except ConfigFileNotFound: + pass + except: + raise + else: + config.merge(next_config) + return config diff --git a/contrib/python/traitlets/py2/traitlets/config/manager.py b/contrib/python/traitlets/py2/traitlets/config/manager.py new file mode 100644 index 0000000000..5e5ebde9af --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/config/manager.py @@ -0,0 +1,88 @@ +"""Manager to read and modify config data in JSON files. +""" +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. +import errno +import io +import json +import os + +from six import PY3 +from traitlets.config import LoggingConfigurable +from traitlets.traitlets import Unicode + + +def recursive_update(target, new): + """Recursively update one dictionary using another. + + None values will delete their keys. + """ + for k, v in new.items(): + if isinstance(v, dict): + if k not in target: + target[k] = {} + recursive_update(target[k], v) + if not target[k]: + # Prune empty subdicts + del target[k] + + elif v is None: + target.pop(k, None) + + else: + target[k] = v + + +class BaseJSONConfigManager(LoggingConfigurable): + """General JSON config manager + + Deals with persisting/storing config in a json file + """ + + config_dir = Unicode('.') + + def ensure_config_dir_exists(self): + try: + os.makedirs(self.config_dir, 0o755) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + def file_name(self, section_name): + return os.path.join(self.config_dir, section_name+'.json') + + def get(self, section_name): + """Retrieve the config data for the specified section. + + Returns the data as a dictionary, or an empty dictionary if the file + doesn't exist. + """ + filename = self.file_name(section_name) + if os.path.isfile(filename): + with io.open(filename, encoding='utf-8') as f: + return json.load(f) + else: + return {} + + def set(self, section_name, data): + """Store the given config data. + """ + filename = self.file_name(section_name) + self.ensure_config_dir_exists() + + if PY3: + f = io.open(filename, 'w', encoding='utf-8') + else: + f = open(filename, 'wb') + with f: + json.dump(data, f, indent=2) + + def update(self, section_name, new_data): + """Modify the config section by recursively updating it with new_data. + + Returns the modified config data as a dictionary. + """ + data = self.get(section_name) + recursive_update(data, new_data) + self.set(section_name, data) + return data diff --git a/contrib/python/traitlets/py2/traitlets/log.py b/contrib/python/traitlets/py2/traitlets/log.py new file mode 100644 index 0000000000..af86b325f5 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/log.py @@ -0,0 +1,27 @@ +"""Grab the global logger instance.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +import logging + +_logger = None + +def get_logger(): + """Grab the global logger instance. + + If a global Application is instantiated, grab its logger. + Otherwise, grab the root logger. + """ + global _logger + + if _logger is None: + from .config import Application + if Application.initialized(): + _logger = Application.instance().log + else: + _logger = logging.getLogger('traitlets') + # Add a NullHandler to silence warnings about not being + # initialized, per best practice for libraries. + _logger.addHandler(logging.NullHandler()) + return _logger diff --git a/contrib/python/traitlets/py2/traitlets/tests/__init__.py b/contrib/python/traitlets/py2/traitlets/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/tests/__init__.py diff --git a/contrib/python/traitlets/py2/traitlets/tests/_warnings.py b/contrib/python/traitlets/py2/traitlets/tests/_warnings.py new file mode 100644 index 0000000000..f135d1f67e --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/tests/_warnings.py @@ -0,0 +1,107 @@ +# From scikit-image: https://github.com/scikit-image/scikit-image/blob/c2f8c4ab123ebe5f7b827bc495625a32bb225c10/skimage/_shared/_warnings.py +# Licensed under modified BSD license + +__all__ = ['all_warnings', 'expected_warnings'] + +from contextlib import contextmanager +import sys +import warnings +import inspect +import re + + +@contextmanager +def all_warnings(): + """ + Context for use in testing to ensure that all warnings are raised. + Examples + -------- + >>> import warnings + >>> def foo(): + ... warnings.warn(RuntimeWarning("bar")) + We raise the warning once, while the warning filter is set to "once". + Hereafter, the warning is invisible, even with custom filters: + >>> with warnings.catch_warnings(): + ... warnings.simplefilter('once') + ... foo() + We can now run ``foo()`` without a warning being raised: + >>> from numpy.testing import assert_warns + >>> foo() + To catch the warning, we call in the help of ``all_warnings``: + >>> with all_warnings(): + ... assert_warns(RuntimeWarning, foo) + """ + + # Whenever a warning is triggered, Python adds a __warningregistry__ + # member to the *calling* module. The exercize here is to find + # and eradicate all those breadcrumbs that were left lying around. + # + # We proceed by first searching all parent calling frames and explicitly + # clearing their warning registries (necessary for the doctests above to + # pass). Then, we search for all submodules of skimage and clear theirs + # as well (necessary for the skimage test suite to pass). + + frame = inspect.currentframe() + if frame: + for f in inspect.getouterframes(frame): + f[0].f_locals['__warningregistry__'] = {} + del frame + + for mod_name, mod in list(sys.modules.items()): + if 'six.moves' in mod_name: + continue + try: + mod.__warningregistry__.clear() + except AttributeError: + pass + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + yield w + + +@contextmanager +def expected_warnings(matching): + """Context for use in testing to catch known warnings matching regexes + + Parameters + ---------- + matching : list of strings or compiled regexes + Regexes for the desired warning to catch + Examples + -------- + >>> from skimage import data, img_as_ubyte, img_as_float + >>> with expected_warnings(['precision loss']): + ... d = img_as_ubyte(img_as_float(data.coins())) + Notes + ----- + Uses `all_warnings` to ensure all warnings are raised. + Upon exiting, it checks the recorded warnings for the desired matching + pattern(s). + Raises a ValueError if any match was not found or an unexpected + warning was raised. + Allows for three types of behaviors: "and", "or", and "optional" matches. + This is done to accomodate different build enviroments or loop conditions + that may produce different warnings. The behaviors can be combined. + If you pass multiple patterns, you get an orderless "and", where all of the + warnings must be raised. + If you use the "|" operator in a pattern, you can catch one of several warnings. + Finally, you can use "|\A\Z" in a pattern to signify it as optional. + """ + with all_warnings() as w: + # enter context + yield w + # exited user context, check the recorded warnings + remaining = [m for m in matching if not '\A\Z' in m.split('|')] + for warn in w: + found = False + for match in matching: + if re.search(match, str(warn.message)) is not None: + found = True + if match in remaining: + remaining.remove(match) + if not found: + raise ValueError('Unexpected warning: %s' % str(warn.message)) + if len(remaining) > 0: + msg = 'No warning raised matching:\n%s' % '\n'.join(remaining) + raise ValueError(msg) diff --git a/contrib/python/traitlets/py2/traitlets/tests/test_traitlets.py b/contrib/python/traitlets/py2/traitlets/tests/test_traitlets.py new file mode 100644 index 0000000000..11b334cb60 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/tests/test_traitlets.py @@ -0,0 +1,2419 @@ +# encoding: utf-8 +"""Tests for traitlets.traitlets.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. +# +# Adapted from enthought.traits, Copyright (c) Enthought, Inc., +# also under the terms of the Modified BSD License. + +import pickle +import re +import sys +from ._warnings import expected_warnings + +from unittest import TestCase +import pytest +from pytest import mark + +from traitlets import ( + HasTraits, MetaHasTraits, TraitType, Any, Bool, CBytes, Dict, Enum, + Int, CInt, Long, CLong, Integer, Float, CFloat, Complex, Bytes, Unicode, + TraitError, Union, All, Undefined, Type, This, Instance, TCPAddress, + List, Tuple, ObjectName, DottedObjectName, CRegExp, link, directional_link, + ForwardDeclaredType, ForwardDeclaredInstance, validate, observe, default, + observe_compat, BaseDescriptor, HasDescriptors, +) + +import six + +def change_dict(*ordered_values): + change_names = ('name', 'old', 'new', 'owner', 'type') + return dict(zip(change_names, ordered_values)) + +#----------------------------------------------------------------------------- +# Helper classes for testing +#----------------------------------------------------------------------------- + + +class HasTraitsStub(HasTraits): + + def notify_change(self, change): + self._notify_name = change['name'] + self._notify_old = change['old'] + self._notify_new = change['new'] + self._notify_type = change['type'] + + +#----------------------------------------------------------------------------- +# Test classes +#----------------------------------------------------------------------------- + + +class TestTraitType(TestCase): + + def test_get_undefined(self): + class A(HasTraits): + a = TraitType + a = A() + with self.assertRaises(TraitError): + a.a + + def test_set(self): + class A(HasTraitsStub): + a = TraitType + + a = A() + a.a = 10 + self.assertEqual(a.a, 10) + self.assertEqual(a._notify_name, 'a') + self.assertEqual(a._notify_old, Undefined) + self.assertEqual(a._notify_new, 10) + + def test_validate(self): + class MyTT(TraitType): + def validate(self, inst, value): + return -1 + class A(HasTraitsStub): + tt = MyTT + + a = A() + a.tt = 10 + self.assertEqual(a.tt, -1) + + def test_default_validate(self): + class MyIntTT(TraitType): + def validate(self, obj, value): + if isinstance(value, int): + return value + self.error(obj, value) + class A(HasTraits): + tt = MyIntTT(10) + a = A() + self.assertEqual(a.tt, 10) + + # Defaults are validated when the HasTraits is instantiated + class B(HasTraits): + tt = MyIntTT('bad default') + self.assertRaises(TraitError, B) + + def test_info(self): + class A(HasTraits): + tt = TraitType + a = A() + self.assertEqual(A.tt.info(), 'any value') + + def test_error(self): + class A(HasTraits): + tt = TraitType + a = A() + self.assertRaises(TraitError, A.tt.error, a, 10) + + def test_deprecated_dynamic_initializer(self): + class A(HasTraits): + x = Int(10) + def _x_default(self): + return 11 + class B(A): + x = Int(20) + class C(A): + def _x_default(self): + return 21 + + a = A() + self.assertEqual(a._trait_values, {}) + self.assertEqual(a.x, 11) + self.assertEqual(a._trait_values, {'x': 11}) + b = B() + self.assertEqual(b.x, 20) + self.assertEqual(b._trait_values, {'x': 20}) + c = C() + self.assertEqual(c._trait_values, {}) + self.assertEqual(c.x, 21) + self.assertEqual(c._trait_values, {'x': 21}) + # Ensure that the base class remains unmolested when the _default + # initializer gets overridden in a subclass. + a = A() + c = C() + self.assertEqual(a._trait_values, {}) + self.assertEqual(a.x, 11) + self.assertEqual(a._trait_values, {'x': 11}) + + def test_dynamic_initializer(self): + + class A(HasTraits): + x = Int(10) + + @default('x') + def _default_x(self): + return 11 + + class B(A): + x = Int(20) + + class C(A): + + @default('x') + def _default_x(self): + return 21 + + a = A() + self.assertEqual(a._trait_values, {}) + self.assertEqual(a.x, 11) + self.assertEqual(a._trait_values, {'x': 11}) + b = B() + self.assertEqual(b.x, 20) + self.assertEqual(b._trait_values, {'x': 20}) + c = C() + self.assertEqual(c._trait_values, {}) + self.assertEqual(c.x, 21) + self.assertEqual(c._trait_values, {'x': 21}) + # Ensure that the base class remains unmolested when the _default + # initializer gets overridden in a subclass. + a = A() + c = C() + self.assertEqual(a._trait_values, {}) + self.assertEqual(a.x, 11) + self.assertEqual(a._trait_values, {'x': 11}) + + def test_tag_metadata(self): + class MyIntTT(TraitType): + metadata = {'a': 1, 'b': 2} + a = MyIntTT(10).tag(b=3, c=4) + self.assertEqual(a.metadata, {'a': 1, 'b': 3, 'c': 4}) + + def test_metadata_localized_instance(self): + class MyIntTT(TraitType): + metadata = {'a': 1, 'b': 2} + a = MyIntTT(10) + b = MyIntTT(10) + a.metadata['c'] = 3 + # make sure that changing a's metadata didn't change b's metadata + self.assertNotIn('c', b.metadata) + + def test_union_metadata(self): + class Foo(HasTraits): + bar = (Int().tag(ta=1) | Dict().tag(ta=2, ti='b')).tag(ti='a') + foo = Foo() + # At this point, no value has been set for bar, so value-specific + # is not set. + self.assertEqual(foo.trait_metadata('bar', 'ta'), None) + self.assertEqual(foo.trait_metadata('bar', 'ti'), 'a') + foo.bar = {} + self.assertEqual(foo.trait_metadata('bar', 'ta'), 2) + self.assertEqual(foo.trait_metadata('bar', 'ti'), 'b') + foo.bar = 1 + self.assertEqual(foo.trait_metadata('bar', 'ta'), 1) + self.assertEqual(foo.trait_metadata('bar', 'ti'), 'a') + + def test_union_default_value(self): + class Foo(HasTraits): + bar = Union([Dict(), Int()], default_value=1) + foo = Foo() + self.assertEqual(foo.bar, 1) + + def test_deprecated_metadata_access(self): + class MyIntTT(TraitType): + metadata = {'a': 1, 'b': 2} + a = MyIntTT(10) + with expected_warnings(["use the instance .metadata dictionary directly"]*2): + a.set_metadata('key', 'value') + v = a.get_metadata('key') + self.assertEqual(v, 'value') + with expected_warnings(["use the instance .help string directly"]*2): + a.set_metadata('help', 'some help') + v = a.get_metadata('help') + self.assertEqual(v, 'some help') + + def test_trait_types_deprecated(self): + with expected_warnings(["Traits should be given as instances"]): + class C(HasTraits): + t = Int + + def test_trait_types_list_deprecated(self): + with expected_warnings(["Traits should be given as instances"]): + class C(HasTraits): + t = List(Int) + + def test_trait_types_tuple_deprecated(self): + with expected_warnings(["Traits should be given as instances"]): + class C(HasTraits): + t = Tuple(Int) + + def test_trait_types_dict_deprecated(self): + with expected_warnings(["Traits should be given as instances"]): + class C(HasTraits): + t = Dict(Int) + +class TestHasDescriptorsMeta(TestCase): + + def test_metaclass(self): + self.assertEqual(type(HasTraits), MetaHasTraits) + + class A(HasTraits): + a = Int() + + a = A() + self.assertEqual(type(a.__class__), MetaHasTraits) + self.assertEqual(a.a,0) + a.a = 10 + self.assertEqual(a.a,10) + + class B(HasTraits): + b = Int() + + b = B() + self.assertEqual(b.b,0) + b.b = 10 + self.assertEqual(b.b,10) + + class C(HasTraits): + c = Int(30) + + c = C() + self.assertEqual(c.c,30) + c.c = 10 + self.assertEqual(c.c,10) + + def test_this_class(self): + class A(HasTraits): + t = This() + tt = This() + class B(A): + tt = This() + ttt = This() + self.assertEqual(A.t.this_class, A) + self.assertEqual(B.t.this_class, A) + self.assertEqual(B.tt.this_class, B) + self.assertEqual(B.ttt.this_class, B) + +class TestHasDescriptors(TestCase): + + def test_setup_instance(self): + + class FooDescriptor(BaseDescriptor): + + def instance_init(self, inst): + foo = inst.foo # instance should have the attr + + class HasFooDescriptors(HasDescriptors): + + fd = FooDescriptor() + + def setup_instance(self, *args, **kwargs): + self.foo = kwargs.get('foo', None) + super(HasFooDescriptors, self).setup_instance(*args, **kwargs) + + hfd = HasFooDescriptors(foo='bar') + +class TestHasTraitsNotify(TestCase): + + def setUp(self): + self._notify1 = [] + self._notify2 = [] + + def notify1(self, name, old, new): + self._notify1.append((name, old, new)) + + def notify2(self, name, old, new): + self._notify2.append((name, old, new)) + + def test_notify_all(self): + + class A(HasTraits): + a = Int() + b = Float() + + a = A() + a.on_trait_change(self.notify1) + a.a = 0 + self.assertEqual(len(self._notify1),0) + a.b = 0.0 + self.assertEqual(len(self._notify1),0) + a.a = 10 + self.assertTrue(('a',0,10) in self._notify1) + a.b = 10.0 + self.assertTrue(('b',0.0,10.0) in self._notify1) + self.assertRaises(TraitError,setattr,a,'a','bad string') + self.assertRaises(TraitError,setattr,a,'b','bad string') + self._notify1 = [] + a.on_trait_change(self.notify1,remove=True) + a.a = 20 + a.b = 20.0 + self.assertEqual(len(self._notify1),0) + + def test_notify_one(self): + + class A(HasTraits): + a = Int() + b = Float() + + a = A() + a.on_trait_change(self.notify1, 'a') + a.a = 0 + self.assertEqual(len(self._notify1),0) + a.a = 10 + self.assertTrue(('a',0,10) in self._notify1) + self.assertRaises(TraitError,setattr,a,'a','bad string') + + def test_subclass(self): + + class A(HasTraits): + a = Int() + + class B(A): + b = Float() + + b = B() + self.assertEqual(b.a,0) + self.assertEqual(b.b,0.0) + b.a = 100 + b.b = 100.0 + self.assertEqual(b.a,100) + self.assertEqual(b.b,100.0) + + def test_notify_subclass(self): + + class A(HasTraits): + a = Int() + + class B(A): + b = Float() + + b = B() + b.on_trait_change(self.notify1, 'a') + b.on_trait_change(self.notify2, 'b') + b.a = 0 + b.b = 0.0 + self.assertEqual(len(self._notify1),0) + self.assertEqual(len(self._notify2),0) + b.a = 10 + b.b = 10.0 + self.assertTrue(('a',0,10) in self._notify1) + self.assertTrue(('b',0.0,10.0) in self._notify2) + + def test_static_notify(self): + + class A(HasTraits): + a = Int() + _notify1 = [] + def _a_changed(self, name, old, new): + self._notify1.append((name, old, new)) + + a = A() + a.a = 0 + # This is broken!!! + self.assertEqual(len(a._notify1),0) + a.a = 10 + self.assertTrue(('a',0,10) in a._notify1) + + class B(A): + b = Float() + _notify2 = [] + def _b_changed(self, name, old, new): + self._notify2.append((name, old, new)) + + b = B() + b.a = 10 + b.b = 10.0 + self.assertTrue(('a',0,10) in b._notify1) + self.assertTrue(('b',0.0,10.0) in b._notify2) + + def test_notify_args(self): + + def callback0(): + self.cb = () + def callback1(name): + self.cb = (name,) + def callback2(name, new): + self.cb = (name, new) + def callback3(name, old, new): + self.cb = (name, old, new) + def callback4(name, old, new, obj): + self.cb = (name, old, new, obj) + + class A(HasTraits): + a = Int() + + a = A() + a.on_trait_change(callback0, 'a') + a.a = 10 + self.assertEqual(self.cb,()) + a.on_trait_change(callback0, 'a', remove=True) + + a.on_trait_change(callback1, 'a') + a.a = 100 + self.assertEqual(self.cb,('a',)) + a.on_trait_change(callback1, 'a', remove=True) + + a.on_trait_change(callback2, 'a') + a.a = 1000 + self.assertEqual(self.cb,('a',1000)) + a.on_trait_change(callback2, 'a', remove=True) + + a.on_trait_change(callback3, 'a') + a.a = 10000 + self.assertEqual(self.cb,('a',1000,10000)) + a.on_trait_change(callback3, 'a', remove=True) + + a.on_trait_change(callback4, 'a') + a.a = 100000 + self.assertEqual(self.cb,('a',10000,100000,a)) + self.assertEqual(len(a._trait_notifiers['a']['change']), 1) + a.on_trait_change(callback4, 'a', remove=True) + + self.assertEqual(len(a._trait_notifiers['a']['change']), 0) + + def test_notify_only_once(self): + + class A(HasTraits): + listen_to = ['a'] + + a = Int(0) + b = 0 + + def __init__(self, **kwargs): + super(A, self).__init__(**kwargs) + self.on_trait_change(self.listener1, ['a']) + + def listener1(self, name, old, new): + self.b += 1 + + class B(A): + + c = 0 + d = 0 + + def __init__(self, **kwargs): + super(B, self).__init__(**kwargs) + self.on_trait_change(self.listener2) + + def listener2(self, name, old, new): + self.c += 1 + + def _a_changed(self, name, old, new): + self.d += 1 + + b = B() + b.a += 1 + self.assertEqual(b.b, b.c) + self.assertEqual(b.b, b.d) + b.a += 1 + self.assertEqual(b.b, b.c) + self.assertEqual(b.b, b.d) + +class TestObserveDecorator(TestCase): + + def setUp(self): + self._notify1 = [] + self._notify2 = [] + + def notify1(self, change): + self._notify1.append(change) + + def notify2(self, change): + self._notify2.append(change) + + def test_notify_all(self): + + class A(HasTraits): + a = Int() + b = Float() + + a = A() + a.observe(self.notify1) + a.a = 0 + self.assertEqual(len(self._notify1),0) + a.b = 0.0 + self.assertEqual(len(self._notify1),0) + a.a = 10 + change = change_dict('a', 0, 10, a, 'change') + self.assertTrue(change in self._notify1) + a.b = 10.0 + change = change_dict('b', 0.0, 10.0, a, 'change') + self.assertTrue(change in self._notify1) + self.assertRaises(TraitError,setattr,a,'a','bad string') + self.assertRaises(TraitError,setattr,a,'b','bad string') + self._notify1 = [] + a.unobserve(self.notify1) + a.a = 20 + a.b = 20.0 + self.assertEqual(len(self._notify1),0) + + def test_notify_one(self): + + class A(HasTraits): + a = Int() + b = Float() + + a = A() + a.observe(self.notify1, 'a') + a.a = 0 + self.assertEqual(len(self._notify1),0) + a.a = 10 + change = change_dict('a', 0, 10, a, 'change') + self.assertTrue(change in self._notify1) + self.assertRaises(TraitError,setattr,a,'a','bad string') + + def test_subclass(self): + + class A(HasTraits): + a = Int() + + class B(A): + b = Float() + + b = B() + self.assertEqual(b.a,0) + self.assertEqual(b.b,0.0) + b.a = 100 + b.b = 100.0 + self.assertEqual(b.a,100) + self.assertEqual(b.b,100.0) + + def test_notify_subclass(self): + + class A(HasTraits): + a = Int() + + class B(A): + b = Float() + + b = B() + b.observe(self.notify1, 'a') + b.observe(self.notify2, 'b') + b.a = 0 + b.b = 0.0 + self.assertEqual(len(self._notify1),0) + self.assertEqual(len(self._notify2),0) + b.a = 10 + b.b = 10.0 + change = change_dict('a', 0, 10, b, 'change') + self.assertTrue(change in self._notify1) + change = change_dict('b', 0.0, 10.0, b, 'change') + self.assertTrue(change in self._notify2) + + def test_static_notify(self): + + class A(HasTraits): + a = Int() + b = Int() + _notify1 = [] + _notify_any = [] + + @observe('a') + def _a_changed(self, change): + self._notify1.append(change) + + @observe(All) + def _any_changed(self, change): + self._notify_any.append(change) + + a = A() + a.a = 0 + self.assertEqual(len(a._notify1),0) + a.a = 10 + change = change_dict('a', 0, 10, a, 'change') + self.assertTrue(change in a._notify1) + a.b = 1 + self.assertEqual(len(a._notify_any), 2) + change = change_dict('b', 0, 1, a, 'change') + self.assertTrue(change in a._notify_any) + + class B(A): + b = Float() + _notify2 = [] + @observe('b') + def _b_changed(self, change): + self._notify2.append(change) + + b = B() + b.a = 10 + b.b = 10.0 + change = change_dict('a', 0, 10, b, 'change') + self.assertTrue(change in b._notify1) + change = change_dict('b', 0.0, 10.0, b, 'change') + self.assertTrue(change in b._notify2) + + def test_notify_args(self): + + def callback0(): + self.cb = () + def callback1(change): + self.cb = change + + class A(HasTraits): + a = Int() + + a = A() + a.on_trait_change(callback0, 'a') + a.a = 10 + self.assertEqual(self.cb,()) + a.unobserve(callback0, 'a') + + a.observe(callback1, 'a') + a.a = 100 + change = change_dict('a', 10, 100, a, 'change') + self.assertEqual(self.cb, change) + self.assertEqual(len(a._trait_notifiers['a']['change']), 1) + a.unobserve(callback1, 'a') + + self.assertEqual(len(a._trait_notifiers['a']['change']), 0) + + def test_notify_only_once(self): + + class A(HasTraits): + listen_to = ['a'] + + a = Int(0) + b = 0 + + def __init__(self, **kwargs): + super(A, self).__init__(**kwargs) + self.observe(self.listener1, ['a']) + + def listener1(self, change): + self.b += 1 + + class B(A): + + c = 0 + d = 0 + + def __init__(self, **kwargs): + super(B, self).__init__(**kwargs) + self.observe(self.listener2) + + def listener2(self, change): + self.c += 1 + + @observe('a') + def _a_changed(self, change): + self.d += 1 + + b = B() + b.a += 1 + self.assertEqual(b.b, b.c) + self.assertEqual(b.b, b.d) + b.a += 1 + self.assertEqual(b.b, b.c) + self.assertEqual(b.b, b.d) + + +class TestHasTraits(TestCase): + + def test_trait_names(self): + class A(HasTraits): + i = Int() + f = Float() + a = A() + self.assertEqual(sorted(a.trait_names()),['f','i']) + self.assertEqual(sorted(A.class_trait_names()),['f','i']) + self.assertTrue(a.has_trait('f')) + self.assertFalse(a.has_trait('g')) + + def test_trait_metadata_deprecated(self): + with expected_warnings(['metadata should be set using the \.tag\(\) method']): + class A(HasTraits): + i = Int(config_key='MY_VALUE') + a = A() + self.assertEqual(a.trait_metadata('i','config_key'), 'MY_VALUE') + + def test_trait_metadata(self): + class A(HasTraits): + i = Int().tag(config_key='MY_VALUE') + a = A() + self.assertEqual(a.trait_metadata('i','config_key'), 'MY_VALUE') + + def test_trait_metadata_default(self): + class A(HasTraits): + i = Int() + a = A() + self.assertEqual(a.trait_metadata('i', 'config_key'), None) + self.assertEqual(a.trait_metadata('i', 'config_key', 'default'), 'default') + + def test_traits(self): + class A(HasTraits): + i = Int() + f = Float() + a = A() + self.assertEqual(a.traits(), dict(i=A.i, f=A.f)) + self.assertEqual(A.class_traits(), dict(i=A.i, f=A.f)) + + def test_traits_metadata(self): + class A(HasTraits): + i = Int().tag(config_key='VALUE1', other_thing='VALUE2') + f = Float().tag(config_key='VALUE3', other_thing='VALUE2') + j = Int(0) + a = A() + self.assertEqual(a.traits(), dict(i=A.i, f=A.f, j=A.j)) + traits = a.traits(config_key='VALUE1', other_thing='VALUE2') + self.assertEqual(traits, dict(i=A.i)) + + # This passes, but it shouldn't because I am replicating a bug in + # traits. + traits = a.traits(config_key=lambda v: True) + self.assertEqual(traits, dict(i=A.i, f=A.f, j=A.j)) + + def test_traits_metadata_deprecated(self): + with expected_warnings(['metadata should be set using the \.tag\(\) method']*2): + class A(HasTraits): + i = Int(config_key='VALUE1', other_thing='VALUE2') + f = Float(config_key='VALUE3', other_thing='VALUE2') + j = Int(0) + a = A() + self.assertEqual(a.traits(), dict(i=A.i, f=A.f, j=A.j)) + traits = a.traits(config_key='VALUE1', other_thing='VALUE2') + self.assertEqual(traits, dict(i=A.i)) + + # This passes, but it shouldn't because I am replicating a bug in + # traits. + traits = a.traits(config_key=lambda v: True) + self.assertEqual(traits, dict(i=A.i, f=A.f, j=A.j)) + + + def test_init(self): + class A(HasTraits): + i = Int() + x = Float() + a = A(i=1, x=10.0) + self.assertEqual(a.i, 1) + self.assertEqual(a.x, 10.0) + + def test_positional_args(self): + class A(HasTraits): + i = Int(0) + def __init__(self, i): + super(A, self).__init__() + self.i = i + + a = A(5) + self.assertEqual(a.i, 5) + # should raise TypeError if no positional arg given + self.assertRaises(TypeError, A) + +#----------------------------------------------------------------------------- +# Tests for specific trait types +#----------------------------------------------------------------------------- + + +class TestType(TestCase): + + def test_default(self): + + class B(object): pass + class A(HasTraits): + klass = Type(allow_none=True) + + a = A() + self.assertEqual(a.klass, object) + + a.klass = B + self.assertEqual(a.klass, B) + self.assertRaises(TraitError, setattr, a, 'klass', 10) + + def test_default_options(self): + + class B(object): pass + class C(B): pass + class A(HasTraits): + # Different possible combinations of options for default_value + # and klass. default_value=None is only valid with allow_none=True. + k1 = Type() + k2 = Type(None, allow_none=True) + k3 = Type(B) + k4 = Type(klass=B) + k5 = Type(default_value=None, klass=B, allow_none=True) + k6 = Type(default_value=C, klass=B) + + self.assertIs(A.k1.default_value, object) + self.assertIs(A.k1.klass, object) + self.assertIs(A.k2.default_value, None) + self.assertIs(A.k2.klass, object) + self.assertIs(A.k3.default_value, B) + self.assertIs(A.k3.klass, B) + self.assertIs(A.k4.default_value, B) + self.assertIs(A.k4.klass, B) + self.assertIs(A.k5.default_value, None) + self.assertIs(A.k5.klass, B) + self.assertIs(A.k6.default_value, C) + self.assertIs(A.k6.klass, B) + + a = A() + self.assertIs(a.k1, object) + self.assertIs(a.k2, None) + self.assertIs(a.k3, B) + self.assertIs(a.k4, B) + self.assertIs(a.k5, None) + self.assertIs(a.k6, C) + + def test_value(self): + + class B(object): pass + class C(object): pass + class A(HasTraits): + klass = Type(B) + + a = A() + self.assertEqual(a.klass, B) + self.assertRaises(TraitError, setattr, a, 'klass', C) + self.assertRaises(TraitError, setattr, a, 'klass', object) + a.klass = B + + def test_allow_none(self): + + class B(object): pass + class C(B): pass + class A(HasTraits): + klass = Type(B) + + a = A() + self.assertEqual(a.klass, B) + self.assertRaises(TraitError, setattr, a, 'klass', None) + a.klass = C + self.assertEqual(a.klass, C) + + def test_validate_klass(self): + + class A(HasTraits): + klass = Type('no strings allowed') + + self.assertRaises(ImportError, A) + + class A(HasTraits): + klass = Type('rub.adub.Duck') + + self.assertRaises(ImportError, A) + + def test_validate_default(self): + + class B(object): pass + class A(HasTraits): + klass = Type('bad default', B) + + self.assertRaises(ImportError, A) + + class C(HasTraits): + klass = Type(None, B) + + self.assertRaises(TraitError, C) + + def test_str_klass(self): + + class A(HasTraits): + klass = Type('ipython_genutils.ipstruct.Struct') + + from ipython_genutils.ipstruct import Struct + a = A() + a.klass = Struct + self.assertEqual(a.klass, Struct) + + self.assertRaises(TraitError, setattr, a, 'klass', 10) + + def test_set_str_klass(self): + + class A(HasTraits): + klass = Type() + + a = A(klass='ipython_genutils.ipstruct.Struct') + from ipython_genutils.ipstruct import Struct + self.assertEqual(a.klass, Struct) + +class TestInstance(TestCase): + + def test_basic(self): + class Foo(object): pass + class Bar(Foo): pass + class Bah(object): pass + + class A(HasTraits): + inst = Instance(Foo, allow_none=True) + + a = A() + self.assertTrue(a.inst is None) + a.inst = Foo() + self.assertTrue(isinstance(a.inst, Foo)) + a.inst = Bar() + self.assertTrue(isinstance(a.inst, Foo)) + self.assertRaises(TraitError, setattr, a, 'inst', Foo) + self.assertRaises(TraitError, setattr, a, 'inst', Bar) + self.assertRaises(TraitError, setattr, a, 'inst', Bah()) + + def test_default_klass(self): + class Foo(object): pass + class Bar(Foo): pass + class Bah(object): pass + + class FooInstance(Instance): + klass = Foo + + class A(HasTraits): + inst = FooInstance(allow_none=True) + + a = A() + self.assertTrue(a.inst is None) + a.inst = Foo() + self.assertTrue(isinstance(a.inst, Foo)) + a.inst = Bar() + self.assertTrue(isinstance(a.inst, Foo)) + self.assertRaises(TraitError, setattr, a, 'inst', Foo) + self.assertRaises(TraitError, setattr, a, 'inst', Bar) + self.assertRaises(TraitError, setattr, a, 'inst', Bah()) + + def test_unique_default_value(self): + class Foo(object): pass + class A(HasTraits): + inst = Instance(Foo,(),{}) + + a = A() + b = A() + self.assertTrue(a.inst is not b.inst) + + def test_args_kw(self): + class Foo(object): + def __init__(self, c): self.c = c + class Bar(object): pass + class Bah(object): + def __init__(self, c, d): + self.c = c; self.d = d + + class A(HasTraits): + inst = Instance(Foo, (10,)) + a = A() + self.assertEqual(a.inst.c, 10) + + class B(HasTraits): + inst = Instance(Bah, args=(10,), kw=dict(d=20)) + b = B() + self.assertEqual(b.inst.c, 10) + self.assertEqual(b.inst.d, 20) + + class C(HasTraits): + inst = Instance(Foo, allow_none=True) + c = C() + self.assertTrue(c.inst is None) + + def test_bad_default(self): + class Foo(object): pass + + class A(HasTraits): + inst = Instance(Foo) + + a = A() + with self.assertRaises(TraitError): + a.inst + + def test_instance(self): + class Foo(object): pass + + def inner(): + class A(HasTraits): + inst = Instance(Foo()) + + self.assertRaises(TraitError, inner) + + +class TestThis(TestCase): + + def test_this_class(self): + class Foo(HasTraits): + this = This() + + f = Foo() + self.assertEqual(f.this, None) + g = Foo() + f.this = g + self.assertEqual(f.this, g) + self.assertRaises(TraitError, setattr, f, 'this', 10) + + def test_this_inst(self): + class Foo(HasTraits): + this = This() + + f = Foo() + f.this = Foo() + self.assertTrue(isinstance(f.this, Foo)) + + def test_subclass(self): + class Foo(HasTraits): + t = This() + class Bar(Foo): + pass + f = Foo() + b = Bar() + f.t = b + b.t = f + self.assertEqual(f.t, b) + self.assertEqual(b.t, f) + + def test_subclass_override(self): + class Foo(HasTraits): + t = This() + class Bar(Foo): + t = This() + f = Foo() + b = Bar() + f.t = b + self.assertEqual(f.t, b) + self.assertRaises(TraitError, setattr, b, 't', f) + + def test_this_in_container(self): + + class Tree(HasTraits): + value = Unicode() + leaves = List(This()) + + tree = Tree( + value='foo', + leaves=[Tree(value='bar'), Tree(value='buzz')] + ) + + with self.assertRaises(TraitError): + tree.leaves = [1, 2] + +class TraitTestBase(TestCase): + """A best testing class for basic trait types.""" + + def assign(self, value): + self.obj.value = value + + def coerce(self, value): + return value + + def test_good_values(self): + if hasattr(self, '_good_values'): + for value in self._good_values: + self.assign(value) + self.assertEqual(self.obj.value, self.coerce(value)) + + def test_bad_values(self): + if hasattr(self, '_bad_values'): + for value in self._bad_values: + try: + self.assertRaises(TraitError, self.assign, value) + except AssertionError: + assert False, value + + def test_default_value(self): + if hasattr(self, '_default_value'): + self.assertEqual(self._default_value, self.obj.value) + + def test_allow_none(self): + if (hasattr(self, '_bad_values') and hasattr(self, '_good_values') and + None in self._bad_values): + trait=self.obj.traits()['value'] + try: + trait.allow_none = True + self._bad_values.remove(None) + #skip coerce. Allow None casts None to None. + self.assign(None) + self.assertEqual(self.obj.value,None) + self.test_good_values() + self.test_bad_values() + finally: + #tear down + trait.allow_none = False + self._bad_values.append(None) + + def tearDown(self): + # restore default value after tests, if set + if hasattr(self, '_default_value'): + self.obj.value = self._default_value + + +class AnyTrait(HasTraits): + + value = Any() + +class AnyTraitTest(TraitTestBase): + + obj = AnyTrait() + + _default_value = None + _good_values = [10.0, 'ten', u'ten', [10], {'ten': 10},(10,), None, 1j] + _bad_values = [] + +class UnionTrait(HasTraits): + + value = Union([Type(), Bool()]) + +class UnionTraitTest(TraitTestBase): + + obj = UnionTrait(value='ipython_genutils.ipstruct.Struct') + _good_values = [int, float, True] + _bad_values = [[], (0,), 1j] + +class OrTrait(HasTraits): + + value = Bool() | Unicode() + +class OrTraitTest(TraitTestBase): + + obj = OrTrait() + _good_values = [True, False, 'ten'] + _bad_values = [[], (0,), 1j] + +class IntTrait(HasTraits): + + value = Int(99, min=-100) + +class TestInt(TraitTestBase): + + obj = IntTrait() + _default_value = 99 + _good_values = [10, -10] + _bad_values = ['ten', u'ten', [10], {'ten': 10}, (10,), None, 1j, + 10.1, -10.1, '10L', '-10L', '10.1', '-10.1', u'10L', + u'-10L', u'10.1', u'-10.1', '10', '-10', u'10', -200] + if not six.PY3: + _bad_values.extend([long(10), long(-10), 10*sys.maxint, -10*sys.maxint]) + + +class CIntTrait(HasTraits): + value = CInt('5') + +class TestCInt(TraitTestBase): + obj = CIntTrait() + + _default_value = 5 + _good_values = ['10', '-10', u'10', u'-10', 10, 10.0, -10.0, 10.1] + _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,), + None, 1j, '10.1', u'10.1'] + + def coerce(self, n): + return int(n) + + +class MinBoundCIntTrait(HasTraits): + value = CInt('5', min=3) + +class TestMinBoundCInt(TestCInt): + obj = MinBoundCIntTrait() + + _default_value = 5 + _good_values = [3, 3.0, '3'] + _bad_values = [2.6, 2, -3, -3.0] + + +class LongTrait(HasTraits): + + value = Long(99 if six.PY3 else long(99)) + +class TestLong(TraitTestBase): + + obj = LongTrait() + + _default_value = 99 if six.PY3 else long(99) + _good_values = [10, -10] + _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,), + None, 1j, 10.1, -10.1, '10', '-10', '10L', '-10L', '10.1', + '-10.1', u'10', u'-10', u'10L', u'-10L', u'10.1', + u'-10.1'] + if not six.PY3: + # maxint undefined on py3, because int == long + _good_values.extend([long(10), long(-10), 10*sys.maxint, -10*sys.maxint]) + _bad_values.extend([[long(10)], (long(10),)]) + + @mark.skipif(six.PY3, reason="not relevant on py3") + def test_cast_small(self): + """Long casts ints to long""" + self.obj.value = 10 + self.assertEqual(type(self.obj.value), long) + + +class MinBoundLongTrait(HasTraits): + value = Long(99 if six.PY3 else long(99), min=5) + +class TestMinBoundLong(TraitTestBase): + obj = MinBoundLongTrait() + + _default_value = 99 if six.PY3 else long(99) + _good_values = [5, 10] + _bad_values = [4, -10] + + +class MaxBoundLongTrait(HasTraits): + value = Long(5 if six.PY3 else long(5), max=10) + +class TestMaxBoundLong(TraitTestBase): + obj = MaxBoundLongTrait() + + _default_value = 5 if six.PY3 else long(5) + _good_values = [10, -2] + _bad_values = [11, 20] + + +class CLongTrait(HasTraits): + value = CLong('5') + +class TestCLong(TraitTestBase): + obj = CLongTrait() + + _default_value = 5 if six.PY3 else long(5) + _good_values = ['10', '-10', u'10', u'-10', 10, 10.0, -10.0, 10.1] + _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,), + None, 1j, '10.1', u'10.1'] + + def coerce(self, n): + return int(n) if six.PY3 else long(n) + + +class MaxBoundCLongTrait(HasTraits): + value = CLong('5', max=10) + +class TestMaxBoundCLong(TestCLong): + obj = MaxBoundCLongTrait() + + _default_value = 5 if six.PY3 else long(5) + _good_values = [10, '10', 10.3] + _bad_values = [11.0, '11'] + + +class IntegerTrait(HasTraits): + value = Integer(1) + +class TestInteger(TestLong): + obj = IntegerTrait() + _default_value = 1 + + def coerce(self, n): + return int(n) + + @mark.skipif(six.PY3, reason="not relevant on py3") + def test_cast_small(self): + """Integer casts small longs to int""" + + self.obj.value = long(100) + self.assertEqual(type(self.obj.value), int) + + +class MinBoundIntegerTrait(HasTraits): + value = Integer(5, min=3) + +class TestMinBoundInteger(TraitTestBase): + obj = MinBoundIntegerTrait() + + _default_value = 5 + _good_values = 3, 20 + _bad_values = [2, -10] + + +class MaxBoundIntegerTrait(HasTraits): + value = Integer(1, max=3) + +class TestMaxBoundInteger(TraitTestBase): + obj = MaxBoundIntegerTrait() + + _default_value = 1 + _good_values = 3, -2 + _bad_values = [4, 10] + + +class FloatTrait(HasTraits): + + value = Float(99.0, max=200.0) + +class TestFloat(TraitTestBase): + + obj = FloatTrait() + + _default_value = 99.0 + _good_values = [10, -10, 10.1, -10.1] + _bad_values = ['ten', u'ten', [10], {'ten': 10}, (10,), None, + 1j, '10', '-10', '10L', '-10L', '10.1', '-10.1', u'10', + u'-10', u'10L', u'-10L', u'10.1', u'-10.1', 201.0] + if not six.PY3: + _bad_values.extend([long(10), long(-10)]) + + +class CFloatTrait(HasTraits): + + value = CFloat('99.0', max=200.0) + +class TestCFloat(TraitTestBase): + + obj = CFloatTrait() + + _default_value = 99.0 + _good_values = [10, 10.0, 10.5, '10.0', '10', '-10', '10.0', u'10'] + _bad_values = ['ten', u'ten', [10], {'ten': 10}, (10,), None, 1j, + 200.1, '200.1'] + + def coerce(self, v): + return float(v) + + +class ComplexTrait(HasTraits): + + value = Complex(99.0-99.0j) + +class TestComplex(TraitTestBase): + + obj = ComplexTrait() + + _default_value = 99.0-99.0j + _good_values = [10, -10, 10.1, -10.1, 10j, 10+10j, 10-10j, + 10.1j, 10.1+10.1j, 10.1-10.1j] + _bad_values = [u'10L', u'-10L', 'ten', [10], {'ten': 10},(10,), None] + if not six.PY3: + _bad_values.extend([long(10), long(-10)]) + + +class BytesTrait(HasTraits): + + value = Bytes(b'string') + +class TestBytes(TraitTestBase): + + obj = BytesTrait() + + _default_value = b'string' + _good_values = [b'10', b'-10', b'10L', + b'-10L', b'10.1', b'-10.1', b'string'] + _bad_values = [10, -10, 10.1, -10.1, 1j, [10], + ['ten'],{'ten': 10},(10,), None, u'string'] + if not six.PY3: + _bad_values.extend([long(10), long(-10)]) + + +class UnicodeTrait(HasTraits): + + value = Unicode(u'unicode') + +class TestUnicode(TraitTestBase): + + obj = UnicodeTrait() + + _default_value = u'unicode' + _good_values = ['10', '-10', '10L', '-10L', '10.1', + '-10.1', '', u'', 'string', u'string', u"€"] + _bad_values = [10, -10, 10.1, -10.1, 1j, + [10], ['ten'], [u'ten'], {'ten': 10},(10,), None] + if not six.PY3: + _bad_values.extend([long(10), long(-10)]) + + +class ObjectNameTrait(HasTraits): + value = ObjectName("abc") + +class TestObjectName(TraitTestBase): + obj = ObjectNameTrait() + + _default_value = "abc" + _good_values = ["a", "gh", "g9", "g_", "_G", u"a345_"] + _bad_values = [1, "", u"€", "9g", "!", "#abc", "aj@", "a.b", "a()", "a[0]", + None, object(), object] + if sys.version_info[0] < 3: + _bad_values.append(u"þ") + else: + _good_values.append(u"þ") # þ=1 is valid in Python 3 (PEP 3131). + + +class DottedObjectNameTrait(HasTraits): + value = DottedObjectName("a.b") + +class TestDottedObjectName(TraitTestBase): + obj = DottedObjectNameTrait() + + _default_value = "a.b" + _good_values = ["A", "y.t", "y765.__repr__", "os.path.join", u"os.path.join"] + _bad_values = [1, u"abc.€", "_.@", ".", ".abc", "abc.", ".abc.", None] + if sys.version_info[0] < 3: + _bad_values.append(u"t.þ") + else: + _good_values.append(u"t.þ") + + +class TCPAddressTrait(HasTraits): + value = TCPAddress() + +class TestTCPAddress(TraitTestBase): + + obj = TCPAddressTrait() + + _default_value = ('127.0.0.1',0) + _good_values = [('localhost',0),('192.168.0.1',1000),('www.google.com',80)] + _bad_values = [(0,0),('localhost',10.0),('localhost',-1), None] + +class ListTrait(HasTraits): + + value = List(Int()) + +class TestList(TraitTestBase): + + obj = ListTrait() + + _default_value = [] + _good_values = [[], [1], list(range(10)), (1,2)] + _bad_values = [10, [1,'a'], 'a'] + + def coerce(self, value): + if value is not None: + value = list(value) + return value + +class Foo(object): + pass + +class NoneInstanceListTrait(HasTraits): + + value = List(Instance(Foo)) + +class TestNoneInstanceList(TraitTestBase): + + obj = NoneInstanceListTrait() + + _default_value = [] + _good_values = [[Foo(), Foo()], []] + _bad_values = [[None], [Foo(), None]] + + +class InstanceListTrait(HasTraits): + + value = List(Instance(__name__+'.Foo')) + +class TestInstanceList(TraitTestBase): + + obj = InstanceListTrait() + + def test_klass(self): + """Test that the instance klass is properly assigned.""" + self.assertIs(self.obj.traits()['value']._trait.klass, Foo) + + _default_value = [] + _good_values = [[Foo(), Foo()], []] + _bad_values = [['1', 2,], '1', [Foo], None] + +class UnionListTrait(HasTraits): + + value = List(Int() | Bool()) + +class TestUnionListTrait(HasTraits): + + obj = UnionListTrait() + + _default_value = [] + _good_values = [[True, 1], [False, True]] + _bad_values = [[1, 'True'], False] + + +class LenListTrait(HasTraits): + + value = List(Int(), [0], minlen=1, maxlen=2) + +class TestLenList(TraitTestBase): + + obj = LenListTrait() + + _default_value = [0] + _good_values = [[1], [1,2], (1,2)] + _bad_values = [10, [1,'a'], 'a', [], list(range(3))] + + def coerce(self, value): + if value is not None: + value = list(value) + return value + +class TupleTrait(HasTraits): + + value = Tuple(Int(allow_none=True), default_value=(1,)) + +class TestTupleTrait(TraitTestBase): + + obj = TupleTrait() + + _default_value = (1,) + _good_values = [(1,), (0,), [1]] + _bad_values = [10, (1, 2), ('a'), (), None] + + def coerce(self, value): + if value is not None: + value = tuple(value) + return value + + def test_invalid_args(self): + self.assertRaises(TypeError, Tuple, 5) + self.assertRaises(TypeError, Tuple, default_value='hello') + t = Tuple(Int(), CBytes(), default_value=(1,5)) + +class LooseTupleTrait(HasTraits): + + value = Tuple((1,2,3)) + +class TestLooseTupleTrait(TraitTestBase): + + obj = LooseTupleTrait() + + _default_value = (1,2,3) + _good_values = [(1,), [1], (0,), tuple(range(5)), tuple('hello'), ('a',5), ()] + _bad_values = [10, 'hello', {}, None] + + def coerce(self, value): + if value is not None: + value = tuple(value) + return value + + def test_invalid_args(self): + self.assertRaises(TypeError, Tuple, 5) + self.assertRaises(TypeError, Tuple, default_value='hello') + t = Tuple(Int(), CBytes(), default_value=(1,5)) + + +class MultiTupleTrait(HasTraits): + + value = Tuple(Int(), Bytes(), default_value=[99,b'bottles']) + +class TestMultiTuple(TraitTestBase): + + obj = MultiTupleTrait() + + _default_value = (99,b'bottles') + _good_values = [(1,b'a'), (2,b'b')] + _bad_values = ((),10, b'a', (1,b'a',3), (b'a',1), (1, u'a')) + +class CRegExpTrait(HasTraits): + + value = CRegExp(r'') + +class TestCRegExp(TraitTestBase): + + def coerce(self, value): + return re.compile(value) + + obj = CRegExpTrait() + + _default_value = re.compile(r'') + _good_values = [r'\d+', re.compile(r'\d+')] + _bad_values = ['(', None, ()] + +class DictTrait(HasTraits): + value = Dict() + +def test_dict_assignment(): + d = dict() + c = DictTrait() + c.value = d + d['a'] = 5 + assert d == c.value + assert c.value is d + + +class UniformlyValidatedDictTrait(HasTraits): + + value = Dict(trait=Unicode(), + default_value={'foo': '1'}) + + +class TestInstanceUniformlyValidatedDict(TraitTestBase): + + obj = UniformlyValidatedDictTrait() + + _default_value = {'foo': '1'} + _good_values = [{'foo': '0', 'bar': '1'}] + _bad_values = [{'foo': 0, 'bar': '1'}] + + +class KeyValidatedDictTrait(HasTraits): + + value = Dict(traits={'foo': Int()}, + default_value={'foo': 1}) + + +class TestInstanceKeyValidatedDict(TraitTestBase): + + obj = KeyValidatedDictTrait() + + _default_value = {'foo': 1} + _good_values = [{'foo': 0, 'bar': '1'}, {'foo': 0, 'bar': 1}] + _bad_values = [{'foo': '0', 'bar': '1'}] + + +class FullyValidatedDictTrait(HasTraits): + + value = Dict(trait=Unicode(), + traits={'foo': Int()}, + default_value={'foo': 1}) + + +class TestInstanceFullyValidatedDict(TraitTestBase): + + obj = FullyValidatedDictTrait() + + _default_value = {'foo': 1} + _good_values = [{'foo': 0, 'bar': '1'}, {'foo': 1, 'bar': '2'}] + _bad_values = [{'foo': 0, 'bar': 1}, {'foo': '0', 'bar': '1'}] + + +def test_dict_default_value(): + """Check that the `{}` default value of the Dict traitlet constructor is + actually copied.""" + + class Foo(HasTraits): + d1 = Dict() + d2 = Dict() + + foo = Foo() + assert foo.d1 == {} + assert foo.d2 == {} + assert foo.d1 is not foo.d2 + + +class TestValidationHook(TestCase): + + def test_parity_trait(self): + """Verify that the early validation hook is effective""" + + class Parity(HasTraits): + + value = Int(0) + parity = Enum(['odd', 'even'], default_value='even') + + @validate('value') + def _value_validate(self, proposal): + value = proposal['value'] + if self.parity == 'even' and value % 2: + raise TraitError('Expected an even number') + if self.parity == 'odd' and (value % 2 == 0): + raise TraitError('Expected an odd number') + return value + + u = Parity() + u.parity = 'odd' + u.value = 1 # OK + with self.assertRaises(TraitError): + u.value = 2 # Trait Error + + u.parity = 'even' + u.value = 2 # OK + + def test_multiple_validate(self): + """Verify that we can register the same validator to multiple names""" + + class OddEven(HasTraits): + + odd = Int(1) + even = Int(0) + + @validate('odd', 'even') + def check_valid(self, proposal): + if proposal['trait'].name == 'odd' and not proposal['value'] % 2: + raise TraitError('odd should be odd') + if proposal['trait'].name == 'even' and proposal['value'] % 2: + raise TraitError('even should be even') + + u = OddEven() + u.odd = 3 # OK + with self.assertRaises(TraitError): + u.odd = 2 # Trait Error + + u.even = 2 # OK + with self.assertRaises(TraitError): + u.even = 3 # Trait Error + + + +class TestLink(TestCase): + + def test_connect_same(self): + """Verify two traitlets of the same type can be linked together using link.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + a = A(value=9) + b = A(value=8) + + # Conenct the two classes. + c = link((a, 'value'), (b, 'value')) + + # Make sure the values are the same at the point of linking. + self.assertEqual(a.value, b.value) + + # Change one of the values to make sure they stay in sync. + a.value = 5 + self.assertEqual(a.value, b.value) + b.value = 6 + self.assertEqual(a.value, b.value) + + def test_link_different(self): + """Verify two traitlets of different types can be linked together using link.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + class B(HasTraits): + count = Int() + a = A(value=9) + b = B(count=8) + + # Conenct the two classes. + c = link((a, 'value'), (b, 'count')) + + # Make sure the values are the same at the point of linking. + self.assertEqual(a.value, b.count) + + # Change one of the values to make sure they stay in sync. + a.value = 5 + self.assertEqual(a.value, b.count) + b.count = 4 + self.assertEqual(a.value, b.count) + + def test_unlink(self): + """Verify two linked traitlets can be unlinked.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + a = A(value=9) + b = A(value=8) + + # Connect the two classes. + c = link((a, 'value'), (b, 'value')) + a.value = 4 + c.unlink() + + # Change one of the values to make sure they don't stay in sync. + a.value = 5 + self.assertNotEqual(a.value, b.value) + + def test_callbacks(self): + """Verify two linked traitlets have their callbacks called once.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + class B(HasTraits): + count = Int() + a = A(value=9) + b = B(count=8) + + # Register callbacks that count. + callback_count = [] + def a_callback(name, old, new): + callback_count.append('a') + a.on_trait_change(a_callback, 'value') + def b_callback(name, old, new): + callback_count.append('b') + b.on_trait_change(b_callback, 'count') + + # Connect the two classes. + c = link((a, 'value'), (b, 'count')) + + # Make sure b's count was set to a's value once. + self.assertEqual(''.join(callback_count), 'b') + del callback_count[:] + + # Make sure a's value was set to b's count once. + b.count = 5 + self.assertEqual(''.join(callback_count), 'ba') + del callback_count[:] + + # Make sure b's count was set to a's value once. + a.value = 4 + self.assertEqual(''.join(callback_count), 'ab') + del callback_count[:] + +class TestDirectionalLink(TestCase): + def test_connect_same(self): + """Verify two traitlets of the same type can be linked together using directional_link.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + a = A(value=9) + b = A(value=8) + + # Conenct the two classes. + c = directional_link((a, 'value'), (b, 'value')) + + # Make sure the values are the same at the point of linking. + self.assertEqual(a.value, b.value) + + # Change one the value of the source and check that it synchronizes the target. + a.value = 5 + self.assertEqual(b.value, 5) + # Change one the value of the target and check that it has no impact on the source + b.value = 6 + self.assertEqual(a.value, 5) + + def test_tranform(self): + """Test transform link.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + a = A(value=9) + b = A(value=8) + + # Conenct the two classes. + c = directional_link((a, 'value'), (b, 'value'), lambda x: 2 * x) + + # Make sure the values are correct at the point of linking. + self.assertEqual(b.value, 2 * a.value) + + # Change one the value of the source and check that it modifies the target. + a.value = 5 + self.assertEqual(b.value, 10) + # Change one the value of the target and check that it has no impact on the source + b.value = 6 + self.assertEqual(a.value, 5) + + def test_link_different(self): + """Verify two traitlets of different types can be linked together using link.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + class B(HasTraits): + count = Int() + a = A(value=9) + b = B(count=8) + + # Conenct the two classes. + c = directional_link((a, 'value'), (b, 'count')) + + # Make sure the values are the same at the point of linking. + self.assertEqual(a.value, b.count) + + # Change one the value of the source and check that it synchronizes the target. + a.value = 5 + self.assertEqual(b.count, 5) + # Change one the value of the target and check that it has no impact on the source + b.value = 6 + self.assertEqual(a.value, 5) + + def test_unlink(self): + """Verify two linked traitlets can be unlinked.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + a = A(value=9) + b = A(value=8) + + # Connect the two classes. + c = directional_link((a, 'value'), (b, 'value')) + a.value = 4 + c.unlink() + + # Change one of the values to make sure they don't stay in sync. + a.value = 5 + self.assertNotEqual(a.value, b.value) + +class Pickleable(HasTraits): + + i = Int() + @observe('i') + def _i_changed(self, change): pass + @validate('i') + def _i_validate(self, commit): + return commit['value'] + + j = Int() + + def __init__(self): + with self.hold_trait_notifications(): + self.i = 1 + self.on_trait_change(self._i_changed, 'i') + +def test_pickle_hastraits(): + c = Pickleable() + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + p = pickle.dumps(c, protocol) + c2 = pickle.loads(p) + assert c2.i == c.i + assert c2.j == c.j + + c.i = 5 + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + p = pickle.dumps(c, protocol) + c2 = pickle.loads(p) + assert c2.i == c.i + assert c2.j == c.j + + +def test_hold_trait_notifications(): + changes = [] + + class Test(HasTraits): + a = Integer(0) + b = Integer(0) + + def _a_changed(self, name, old, new): + changes.append((old, new)) + + def _b_validate(self, value, trait): + if value != 0: + raise TraitError('Only 0 is a valid value') + return value + + # Test context manager and nesting + t = Test() + with t.hold_trait_notifications(): + with t.hold_trait_notifications(): + t.a = 1 + assert t.a == 1 + assert changes == [] + t.a = 2 + assert t.a == 2 + with t.hold_trait_notifications(): + t.a = 3 + assert t.a == 3 + assert changes == [] + t.a = 4 + assert t.a == 4 + assert changes == [] + t.a = 4 + assert t.a == 4 + assert changes == [] + + assert changes == [(0, 4)] + # Test roll-back + try: + with t.hold_trait_notifications(): + t.b = 1 # raises a Trait error + except: + pass + assert t.b == 0 + + +class RollBack(HasTraits): + bar = Int() + def _bar_validate(self, value, trait): + if value: + raise TraitError('foobar') + return value + + +class TestRollback(TestCase): + + def test_roll_back(self): + + def assign_rollback(): + RollBack(bar=1) + + self.assertRaises(TraitError, assign_rollback) + + +class CacheModification(HasTraits): + foo = Int() + bar = Int() + + def _bar_validate(self, value, trait): + self.foo = value + return value + + def _foo_validate(self, value, trait): + self.bar = value + return value + + +def test_cache_modification(): + CacheModification(foo=1) + CacheModification(bar=1) + + +class OrderTraits(HasTraits): + notified = Dict() + + a = Unicode() + b = Unicode() + c = Unicode() + d = Unicode() + e = Unicode() + f = Unicode() + g = Unicode() + h = Unicode() + i = Unicode() + j = Unicode() + k = Unicode() + l = Unicode() + + def _notify(self, name, old, new): + """check the value of all traits when each trait change is triggered + + This verifies that the values are not sensitive + to dict ordering when loaded from kwargs + """ + # check the value of the other traits + # when a given trait change notification fires + self.notified[name] = { + c: getattr(self, c) for c in 'abcdefghijkl' + } + + def __init__(self, **kwargs): + self.on_trait_change(self._notify) + super(OrderTraits, self).__init__(**kwargs) + +def test_notification_order(): + d = {c:c for c in 'abcdefghijkl'} + obj = OrderTraits() + assert obj.notified == {} + obj = OrderTraits(**d) + notifications = { + c: d for c in 'abcdefghijkl' + } + assert obj.notified == notifications + + + +### +# Traits for Forward Declaration Tests +### +class ForwardDeclaredInstanceTrait(HasTraits): + + value = ForwardDeclaredInstance('ForwardDeclaredBar', allow_none=True) + +class ForwardDeclaredTypeTrait(HasTraits): + + value = ForwardDeclaredType('ForwardDeclaredBar', allow_none=True) + +class ForwardDeclaredInstanceListTrait(HasTraits): + + value = List(ForwardDeclaredInstance('ForwardDeclaredBar')) + +class ForwardDeclaredTypeListTrait(HasTraits): + + value = List(ForwardDeclaredType('ForwardDeclaredBar')) +### +# End Traits for Forward Declaration Tests +### + +### +# Classes for Forward Declaration Tests +### +class ForwardDeclaredBar(object): + pass + +class ForwardDeclaredBarSub(ForwardDeclaredBar): + pass +### +# End Classes for Forward Declaration Tests +### + +### +# Forward Declaration Tests +### +class TestForwardDeclaredInstanceTrait(TraitTestBase): + + obj = ForwardDeclaredInstanceTrait() + _default_value = None + _good_values = [None, ForwardDeclaredBar(), ForwardDeclaredBarSub()] + _bad_values = ['foo', 3, ForwardDeclaredBar, ForwardDeclaredBarSub] + +class TestForwardDeclaredTypeTrait(TraitTestBase): + + obj = ForwardDeclaredTypeTrait() + _default_value = None + _good_values = [None, ForwardDeclaredBar, ForwardDeclaredBarSub] + _bad_values = ['foo', 3, ForwardDeclaredBar(), ForwardDeclaredBarSub()] + +class TestForwardDeclaredInstanceList(TraitTestBase): + + obj = ForwardDeclaredInstanceListTrait() + + def test_klass(self): + """Test that the instance klass is properly assigned.""" + self.assertIs(self.obj.traits()['value']._trait.klass, ForwardDeclaredBar) + + _default_value = [] + _good_values = [ + [ForwardDeclaredBar(), ForwardDeclaredBarSub()], + [], + ] + _bad_values = [ + ForwardDeclaredBar(), + [ForwardDeclaredBar(), 3, None], + '1', + # Note that this is the type, not an instance. + [ForwardDeclaredBar], + [None], + None, + ] + +class TestForwardDeclaredTypeList(TraitTestBase): + + obj = ForwardDeclaredTypeListTrait() + + def test_klass(self): + """Test that the instance klass is properly assigned.""" + self.assertIs(self.obj.traits()['value']._trait.klass, ForwardDeclaredBar) + + _default_value = [] + _good_values = [ + [ForwardDeclaredBar, ForwardDeclaredBarSub], + [], + ] + _bad_values = [ + ForwardDeclaredBar, + [ForwardDeclaredBar, 3], + '1', + # Note that this is an instance, not the type. + [ForwardDeclaredBar()], + [None], + None, + ] +### +# End Forward Declaration Tests +### + +class TestDynamicTraits(TestCase): + + def setUp(self): + self._notify1 = [] + + def notify1(self, name, old, new): + self._notify1.append((name, old, new)) + + def test_notify_all(self): + + class A(HasTraits): + pass + + a = A() + self.assertTrue(not hasattr(a, 'x')) + self.assertTrue(not hasattr(a, 'y')) + + # Dynamically add trait x. + a.add_traits(x=Int()) + self.assertTrue(hasattr(a, 'x')) + self.assertTrue(isinstance(a, (A, ))) + + # Dynamically add trait y. + a.add_traits(y=Float()) + self.assertTrue(hasattr(a, 'y')) + self.assertTrue(isinstance(a, (A, ))) + self.assertEqual(a.__class__.__name__, A.__name__) + + # Create a new instance and verify that x and y + # aren't defined. + b = A() + self.assertTrue(not hasattr(b, 'x')) + self.assertTrue(not hasattr(b, 'y')) + + # Verify that notification works like normal. + a.on_trait_change(self.notify1) + a.x = 0 + self.assertEqual(len(self._notify1), 0) + a.y = 0.0 + self.assertEqual(len(self._notify1), 0) + a.x = 10 + self.assertTrue(('x', 0, 10) in self._notify1) + a.y = 10.0 + self.assertTrue(('y', 0.0, 10.0) in self._notify1) + self.assertRaises(TraitError, setattr, a, 'x', 'bad string') + self.assertRaises(TraitError, setattr, a, 'y', 'bad string') + self._notify1 = [] + a.on_trait_change(self.notify1, remove=True) + a.x = 20 + a.y = 20.0 + self.assertEqual(len(self._notify1), 0) + + +def test_enum_no_default(): + class C(HasTraits): + t = Enum(['a', 'b']) + + c = C() + c.t = 'a' + assert c.t == 'a' + + c = C() + + with pytest.raises(TraitError): + t = c.t + + c = C(t='b') + assert c.t == 'b' + + +def test_default_value_repr(): + class C(HasTraits): + t = Type('traitlets.HasTraits') + t2 = Type(HasTraits) + n = Integer(0) + lis = List() + d = Dict() + + assert C.t.default_value_repr() == "'traitlets.HasTraits'" + assert C.t2.default_value_repr() == "'traitlets.traitlets.HasTraits'" + assert C.n.default_value_repr() == '0' + assert C.lis.default_value_repr() == '[]' + assert C.d.default_value_repr() == '{}' + + +class TransitionalClass(HasTraits): + + d = Any() + @default('d') + def _d_default(self): + return TransitionalClass + + parent_super = False + calls_super = Integer(0) + + @default('calls_super') + def _calls_super_default(self): + return -1 + + @observe('calls_super') + @observe_compat + def _calls_super_changed(self, change): + self.parent_super = change + + parent_override = False + overrides = Integer(0) + + @observe('overrides') + @observe_compat + def _overrides_changed(self, change): + self.parent_override = change + + +class SubClass(TransitionalClass): + def _d_default(self): + return SubClass + + subclass_super = False + def _calls_super_changed(self, name, old, new): + self.subclass_super = True + super(SubClass, self)._calls_super_changed(name, old, new) + + subclass_override = False + def _overrides_changed(self, name, old, new): + self.subclass_override = True + + +def test_subclass_compat(): + obj = SubClass() + obj.calls_super = 5 + assert obj.parent_super + assert obj.subclass_super + obj.overrides = 5 + assert obj.subclass_override + assert not obj.parent_override + assert obj.d is SubClass + + +class DefinesHandler(HasTraits): + parent_called = False + + trait = Integer() + @observe('trait') + def handler(self, change): + self.parent_called = True + + +class OverridesHandler(DefinesHandler): + child_called = False + + @observe('trait') + def handler(self, change): + self.child_called = True + + +def test_subclass_override_observer(): + obj = OverridesHandler() + obj.trait = 5 + assert obj.child_called + assert not obj.parent_called + + +class DoesntRegisterHandler(DefinesHandler): + child_called = False + + def handler(self, change): + self.child_called = True + + +def test_subclass_override_not_registered(): + """Subclass that overrides observer and doesn't re-register unregisters both""" + obj = DoesntRegisterHandler() + obj.trait = 5 + assert not obj.child_called + assert not obj.parent_called + + +class AddsHandler(DefinesHandler): + child_called = False + + @observe('trait') + def child_handler(self, change): + self.child_called = True + +def test_subclass_add_observer(): + obj = AddsHandler() + obj.trait = 5 + assert obj.child_called + assert obj.parent_called + + +def test_observe_iterables(): + + class C(HasTraits): + i = Integer() + s = Unicode() + + c = C() + recorded = {} + def record(change): + recorded['change'] = change + + # observe with names=set + c.observe(record, names={'i', 's'}) + c.i = 5 + assert recorded['change'].name == 'i' + assert recorded['change'].new == 5 + c.s = 'hi' + assert recorded['change'].name == 's' + assert recorded['change'].new == 'hi' + + # observe with names=custom container with iter, contains + class MyContainer(object): + def __init__(self, container): + self.container = container + + def __iter__(self): + return iter(self.container) + + def __contains__(self, key): + return key in self.container + + c.observe(record, names=MyContainer({'i', 's'})) + c.i = 10 + assert recorded['change'].name == 'i' + assert recorded['change'].new == 10 + c.s = 'ok' + assert recorded['change'].name == 's' + assert recorded['change'].new == 'ok' + + +def test_super_args(): + class SuperRecorder(object): + def __init__(self, *args, **kwargs): + self.super_args = args + self.super_kwargs = kwargs + + class SuperHasTraits(HasTraits, SuperRecorder): + i = Integer() + + obj = SuperHasTraits('a1', 'a2', b=10, i=5, c='x') + assert obj.i == 5 + assert not hasattr(obj, 'b') + assert not hasattr(obj, 'c') + assert obj.super_args == ('a1' , 'a2') + assert obj.super_kwargs == {'b': 10 , 'c': 'x'} + +def test_super_bad_args(): + class SuperHasTraits(HasTraits): + a = Integer() + + if sys.version_info < (3,): + # Legacy Python, object.__init__ warns itself, instead of raising + w = ['object.__init__'] + else: + w = ["Passing unrecoginized arguments"] + with expected_warnings(w): + obj = SuperHasTraits(a=1, b=2) + assert obj.a == 1 + assert not hasattr(obj, 'b') diff --git a/contrib/python/traitlets/py2/traitlets/tests/test_traitlets_enum.py b/contrib/python/traitlets/py2/traitlets/tests/test_traitlets_enum.py new file mode 100644 index 0000000000..82259ae6c5 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/tests/test_traitlets_enum.py @@ -0,0 +1,181 @@ +# -*- coding: UTF-8 -*- +# pylint: disable=missing-docstring, too-few-public-methods +""" +Test the trait-type ``UseEnum``. +""" + +import unittest +import enum +from ipython_genutils.py3compat import string_types +from traitlets import HasTraits, TraitError, UseEnum + + +# ----------------------------------------------------------------------------- +# TEST SUPPORT: +# ----------------------------------------------------------------------------- +class Color(enum.Enum): + red = 1 + green = 2 + blue = 3 + yellow = 4 + +class OtherColor(enum.Enum): + red = 0 + green = 1 + + +# ----------------------------------------------------------------------------- +# TESTSUITE: +# ----------------------------------------------------------------------------- +class TestUseEnum(unittest.TestCase): + # pylint: disable=invalid-name + + class Example(HasTraits): + color = UseEnum(Color, help="Color enum") + + def test_assign_enum_value(self): + example = self.Example() + example.color = Color.green + self.assertEqual(example.color, Color.green) + + def test_assign_all_enum_values(self): + # pylint: disable=no-member + enum_values = [value for value in Color.__members__.values()] + for value in enum_values: + self.assertIsInstance(value, Color) + example = self.Example() + example.color = value + self.assertEqual(example.color, value) + self.assertIsInstance(value, Color) + + def test_assign_enum_value__with_other_enum_raises_error(self): + example = self.Example() + with self.assertRaises(TraitError): + example.color = OtherColor.green + + def test_assign_enum_name_1(self): + # -- CONVERT: string => Enum value (item) + example = self.Example() + example.color = "red" + self.assertEqual(example.color, Color.red) + + def test_assign_enum_value_name(self): + # -- CONVERT: string => Enum value (item) + # pylint: disable=no-member + enum_names = [enum_val.name for enum_val in Color.__members__.values()] + for value in enum_names: + self.assertIsInstance(value, string_types) + example = self.Example() + enum_value = Color.__members__.get(value) + example.color = value + self.assertIs(example.color, enum_value) + self.assertEqual(example.color.name, value) + + def test_assign_scoped_enum_value_name(self): + # -- CONVERT: string => Enum value (item) + scoped_names = ["Color.red", "Color.green", "Color.blue", "Color.yellow"] + for value in scoped_names: + example = self.Example() + example.color = value + self.assertIsInstance(example.color, Color) + self.assertEqual(str(example.color), value) + + def test_assign_bad_enum_value_name__raises_error(self): + # -- CONVERT: string => Enum value (item) + bad_enum_names = ["UNKNOWN_COLOR", "RED", "Green", "blue2"] + for value in bad_enum_names: + example = self.Example() + with self.assertRaises(TraitError): + example.color = value + + def test_assign_enum_value_number_1(self): + # -- CONVERT: number => Enum value (item) + example = self.Example() + example.color = 1 # == Color.red.value + example.color = Color.red.value + self.assertEqual(example.color, Color.red) + + def test_assign_enum_value_number(self): + # -- CONVERT: number => Enum value (item) + # pylint: disable=no-member + enum_numbers = [enum_val.value + for enum_val in Color.__members__.values()] + for value in enum_numbers: + self.assertIsInstance(value, int) + example = self.Example() + example.color = value + self.assertIsInstance(example.color, Color) + self.assertEqual(example.color.value, value) + + def test_assign_bad_enum_value_number__raises_error(self): + # -- CONVERT: number => Enum value (item) + bad_numbers = [-1, 0, 5] + for value in bad_numbers: + self.assertIsInstance(value, int) + assert UseEnum(Color).select_by_number(value, None) is None + example = self.Example() + with self.assertRaises(TraitError): + example.color = value + + def test_ctor_without_default_value(self): + # -- IMPLICIT: default_value = Color.red (first enum-value) + class Example2(HasTraits): + color = UseEnum(Color) + + example = Example2() + self.assertEqual(example.color, Color.red) + + def test_ctor_with_default_value_as_enum_value(self): + # -- CONVERT: number => Enum value (item) + class Example2(HasTraits): + color = UseEnum(Color, default_value=Color.green) + + example = Example2() + self.assertEqual(example.color, Color.green) + + + def test_ctor_with_default_value_none_and_not_allow_none(self): + # -- IMPLICIT: default_value = Color.red (first enum-value) + class Example2(HasTraits): + color1 = UseEnum(Color, default_value=None, allow_none=False) + color2 = UseEnum(Color, default_value=None) + example = Example2() + self.assertEqual(example.color1, Color.red) + self.assertEqual(example.color2, Color.red) + + def test_ctor_with_default_value_none_and_allow_none(self): + class Example2(HasTraits): + color1 = UseEnum(Color, default_value=None, allow_none=True) + color2 = UseEnum(Color, allow_none=True) + + example = Example2() + self.assertIs(example.color1, None) + self.assertIs(example.color2, None) + + def test_assign_none_without_allow_none_resets_to_default_value(self): + class Example2(HasTraits): + color1 = UseEnum(Color, allow_none=False) + color2 = UseEnum(Color) + + example = Example2() + example.color1 = None + example.color2 = None + self.assertIs(example.color1, Color.red) + self.assertIs(example.color2, Color.red) + + def test_assign_none_to_enum_or_none(self): + class Example2(HasTraits): + color = UseEnum(Color, allow_none=True) + + example = Example2() + example.color = None + self.assertIs(example.color, None) + + def test_assign_bad_value_with_to_enum_or_none(self): + class Example2(HasTraits): + color = UseEnum(Color, allow_none=True) + + example = Example2() + with self.assertRaises(TraitError): + example.color = "BAD_VALUE" + diff --git a/contrib/python/traitlets/py2/traitlets/tests/utils.py b/contrib/python/traitlets/py2/traitlets/tests/utils.py new file mode 100644 index 0000000000..88845d8519 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/tests/utils.py @@ -0,0 +1,39 @@ +import sys + +from subprocess import Popen, PIPE + +def get_output_error_code(cmd): + """Get stdout, stderr, and exit code from running a command""" + p = Popen(cmd, stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + out = out.decode('utf8', 'replace') + err = err.decode('utf8', 'replace') + return out, err, p.returncode + + +def check_help_output(pkg, subcommand=None): + """test that `python -m PKG [subcommand] -h` works""" + cmd = [sys.executable, '-m', pkg] + if subcommand: + cmd.extend(subcommand) + cmd.append('-h') + out, err, rc = get_output_error_code(cmd) + assert rc == 0, err + assert "Traceback" not in err + assert "Options" in out + assert "--help-all" in out + return out, err + + +def check_help_all_output(pkg, subcommand=None): + """test that `python -m PKG --help-all` works""" + cmd = [sys.executable, '-m', pkg] + if subcommand: + cmd.extend(subcommand) + cmd.append('--help-all') + out, err, rc = get_output_error_code(cmd) + assert rc == 0, err + assert "Traceback" not in err + assert "Options" in out + assert "Class parameters" in out + return out, err diff --git a/contrib/python/traitlets/py2/traitlets/traitlets.py b/contrib/python/traitlets/py2/traitlets/traitlets.py new file mode 100644 index 0000000000..c07daf7400 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/traitlets.py @@ -0,0 +1,2690 @@ +# encoding: utf-8 +""" +A lightweight Traits like module. + +This is designed to provide a lightweight, simple, pure Python version of +many of the capabilities of enthought.traits. This includes: + +* Validation +* Type specification with defaults +* Static and dynamic notification +* Basic predefined types +* An API that is similar to enthought.traits + +We don't support: + +* Delegation +* Automatic GUI generation +* A full set of trait types. Most importantly, we don't provide container + traits (list, dict, tuple) that can trigger notifications if their + contents change. +* API compatibility with enthought.traits + +There are also some important difference in our design: + +* enthought.traits does not validate default values. We do. + +We choose to create this module because we need these capabilities, but +we need them to be pure Python so they work in all Python implementations, +including Jython and IronPython. + +Inheritance diagram: + +.. inheritance-diagram:: traitlets.traitlets + :parts: 3 +""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. +# +# Adapted from enthought.traits, Copyright (c) Enthought, Inc., +# also under the terms of the Modified BSD License. + +import contextlib +import inspect +import os +import re +import sys +import types +import enum +try: + from types import ClassType, InstanceType + ClassTypes = (ClassType, type) +except: + ClassTypes = (type,) +from warnings import warn, warn_explicit + +import six + +from .utils.getargspec import getargspec +from .utils.importstring import import_item +from .utils.sentinel import Sentinel +from .utils.bunch import Bunch + +SequenceTypes = (list, tuple, set, frozenset) + +#----------------------------------------------------------------------------- +# Basic classes +#----------------------------------------------------------------------------- + + +Undefined = Sentinel('Undefined', 'traitlets', +''' +Used in Traitlets to specify that no defaults are set in kwargs +''' +) + +All = Sentinel('All', 'traitlets', +''' +Used in Traitlets to listen to all types of notification or to notifications +from all trait attributes. +''' +) + +# Deprecated alias +NoDefaultSpecified = Undefined + +class TraitError(Exception): + pass + +#----------------------------------------------------------------------------- +# Utilities +#----------------------------------------------------------------------------- + +from ipython_genutils.py3compat import cast_unicode_py2 + +_name_re = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*$") + +def isidentifier(s): + if six.PY2: + return bool(_name_re.match(s)) + else: + return s.isidentifier() + +_deprecations_shown = set() +def _should_warn(key): + """Add our own checks for too many deprecation warnings. + + Limit to once per package. + """ + env_flag = os.environ.get('TRAITLETS_ALL_DEPRECATIONS') + if env_flag and env_flag != '0': + return True + + if key not in _deprecations_shown: + _deprecations_shown.add(key) + return True + else: + return False + +def _deprecated_method(method, cls, method_name, msg): + """Show deprecation warning about a magic method definition. + + Uses warn_explicit to bind warning to method definition instead of triggering code, + which isn't relevant. + """ + warn_msg = "{classname}.{method_name} is deprecated in traitlets 4.1: {msg}".format( + classname=cls.__name__, method_name=method_name, msg=msg + ) + + for parent in inspect.getmro(cls): + if method_name in parent.__dict__: + cls = parent + break + # limit deprecation messages to once per package + package_name = cls.__module__.split('.', 1)[0] + key = (package_name, msg) + if not _should_warn(key): + return + try: + fname = inspect.getsourcefile(method) or "<unknown>" + lineno = inspect.getsourcelines(method)[1] or 0 + except (IOError, TypeError) as e: + # Failed to inspect for some reason + warn(warn_msg + ('\n(inspection failed) %s' % e), DeprecationWarning) + 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. + """ + return (isinstance(t, TraitType) or + (isinstance(t, type) and issubclass(t, TraitType))) + + +def parse_notifier_name(names): + """Convert the name argument to a list of names. + + Examples + -------- + + >>> parse_notifier_name([]) + [All] + >>> parse_notifier_name('a') + ['a'] + >>> parse_notifier_name(['a', 'b']) + ['a', 'b'] + >>> parse_notifier_name(All) + [All] + """ + if names is All or isinstance(names, six.string_types): + return [names] + else: + if not names or All in names: + return [All] + for n in names: + if not isinstance(n, six.string_types): + raise TypeError("names must be strings, not %r" % n) + return names + + +class _SimpleTest: + def __init__ ( self, value ): self.value = value + def __call__ ( self, test ): + return test == self.value + def __repr__(self): + return "<SimpleTest(%r)" % self.value + def __str__(self): + return self.__repr__() + + +def getmembers(object, predicate=None): + """A safe version of inspect.getmembers that handles missing attributes. + + This is useful when there are descriptor based attributes that for + some reason raise AttributeError even though they exist. This happens + in zope.inteface with the __provides__ attribute. + """ + results = [] + for key in dir(object): + try: + value = getattr(object, key) + except AttributeError: + pass + else: + if not predicate or predicate(value): + results.append((key, value)) + results.sort() + return results + +def _validate_link(*tuples): + """Validate arguments for traitlet link functions""" + for t in tuples: + if not len(t) == 2: + raise TypeError("Each linked traitlet must be specified as (HasTraits, 'trait_name'), not %r" % t) + obj, trait_name = t + if not isinstance(obj, HasTraits): + raise TypeError("Each object must be HasTraits, not %r" % type(obj)) + if not trait_name in obj.traits(): + raise TypeError("%r has no trait %r" % (obj, trait_name)) + +class link(object): + """Link traits from different objects together so they remain in sync. + + Parameters + ---------- + source : (object / attribute name) pair + target : (object / attribute name) pair + + Examples + -------- + + >>> c = link((src, 'value'), (tgt, 'value')) + >>> src.value = 5 # updates other objects as well + """ + updating = False + + def __init__(self, source, target): + _validate_link(source, target) + self.source, self.target = source, target + try: + setattr(target[0], target[1], getattr(source[0], source[1])) + finally: + source[0].observe(self._update_target, names=source[1]) + target[0].observe(self._update_source, names=target[1]) + + @contextlib.contextmanager + def _busy_updating(self): + self.updating = True + try: + yield + finally: + self.updating = False + + def _update_target(self, change): + if self.updating: + return + with self._busy_updating(): + setattr(self.target[0], self.target[1], change.new) + + def _update_source(self, change): + if self.updating: + return + with self._busy_updating(): + setattr(self.source[0], self.source[1], change.new) + + def unlink(self): + self.source[0].unobserve(self._update_target, names=self.source[1]) + self.target[0].unobserve(self._update_source, names=self.target[1]) + self.source, self.target = None, None + + +class directional_link(object): + """Link the trait of a source object with traits of target objects. + + Parameters + ---------- + source : (object, attribute name) pair + target : (object, attribute name) pair + transform: callable (optional) + Data transformation between source and target. + + Examples + -------- + + >>> c = directional_link((src, 'value'), (tgt, 'value')) + >>> src.value = 5 # updates target objects + >>> tgt.value = 6 # does not update source object + """ + updating = False + + def __init__(self, source, target, transform=None): + self._transform = transform if transform else lambda x: x + _validate_link(source, target) + self.source, self.target = source, target + try: + setattr(target[0], target[1], + self._transform(getattr(source[0], source[1]))) + finally: + self.source[0].observe(self._update, names=self.source[1]) + + @contextlib.contextmanager + def _busy_updating(self): + self.updating = True + try: + yield + finally: + self.updating = False + + def _update(self, change): + if self.updating: + return + with self._busy_updating(): + setattr(self.target[0], self.target[1], + self._transform(change.new)) + + def unlink(self): + self.source[0].unobserve(self._update, names=self.source[1]) + self.source, self.target = None, None + +dlink = directional_link + + +#----------------------------------------------------------------------------- +# Base Descriptor Class +#----------------------------------------------------------------------------- + + +class BaseDescriptor(object): + """Base descriptor class + + Notes + ----- + This implements Python's descriptor prototol. + + This class is the base class for all such descriptors. The + only magic we use is a custom metaclass for the main :class:`HasTraits` + class that does the following: + + 1. Sets the :attr:`name` attribute of every :class:`BaseDescriptor` + instance in the class dict to the name of the attribute. + 2. Sets the :attr:`this_class` attribute of every :class:`BaseDescriptor` + instance in the class dict to the *class* that declared the trait. + This is used by the :class:`This` trait to allow subclasses to + accept superclasses for :class:`This` values. + """ + + name = None + this_class = None + + def class_init(self, cls, name): + """Part of the initialization which may depend on the underlying + HasDescriptors class. + + It is typically overloaded for specific types. + + This method is called by :meth:`MetaHasDescriptors.__init__` + passing the class (`cls`) and `name` under which the descriptor + has been assigned. + """ + self.this_class = cls + self.name = name + + def instance_init(self, obj): + """Part of the initialization which may depend on the underlying + HasDescriptors instance. + + It is typically overloaded for specific types. + + This method is called by :meth:`HasTraits.__new__` and in the + :meth:`BaseDescriptor.instance_init` method of descriptors holding + other descriptors. + """ + pass + + +class TraitType(BaseDescriptor): + """A base class for all trait types. + """ + + metadata = {} + default_value = Undefined + allow_none = False + read_only = False + info_text = 'any value' + + def __init__(self, default_value=Undefined, allow_none=False, read_only=None, help=None, + config=None, **kwargs): + """Declare a traitlet. + + If *allow_none* is True, None is a valid value in addition to any + values that are normally valid. The default is up to the subclass. + For most trait types, the default value for ``allow_none`` is False. + + Extra metadata can be associated with the traitlet using the .tag() convenience method + or by using the traitlet instance's .metadata dictionary. + """ + if default_value is not Undefined: + self.default_value = default_value + if allow_none: + self.allow_none = allow_none + if read_only is not None: + self.read_only = read_only + self.help = help if help is not None else '' + + if len(kwargs) > 0: + stacklevel = 1 + f = inspect.currentframe() + # count supers to determine stacklevel for warning + while f.f_code.co_name == '__init__': + stacklevel += 1 + f = f.f_back + mod = f.f_globals.get('__name__') or '' + pkg = mod.split('.', 1)[0] + key = tuple(['metadata-tag', pkg] + sorted(kwargs)) + if _should_warn(key): + warn("metadata %s was set from the constructor. " + "With traitlets 4.1, metadata should be set using the .tag() method, " + "e.g., Int().tag(key1='value1', key2='value2')" % (kwargs,), + DeprecationWarning, stacklevel=stacklevel) + if len(self.metadata) > 0: + self.metadata = self.metadata.copy() + self.metadata.update(kwargs) + else: + self.metadata = kwargs + else: + self.metadata = self.metadata.copy() + if config is not None: + self.metadata['config'] = config + + # We add help to the metadata during a deprecation period so that + # code that looks for the help string there can find it. + if help is not None: + self.metadata['help'] = help + + def get_default_value(self): + """DEPRECATED: Retrieve the static default value for this trait. + + Use self.default_value instead + """ + warn("get_default_value is deprecated in traitlets 4.0: use the .default_value attribute", DeprecationWarning, + stacklevel=2) + return self.default_value + + def init_default_value(self, obj): + """DEPRECATED: Set the static default value for the trait type. + """ + warn("init_default_value is deprecated in traitlets 4.0, and may be removed in the future", DeprecationWarning, + stacklevel=2) + value = self._validate(obj, self.default_value) + obj._trait_values[self.name] = value + return value + + def _dynamic_default_callable(self, obj): + """Retrieve a callable to calculate the default for this traitlet. + + This looks for: + + * default generators registered with the @default descriptor. + * obj._{name}_default() on the class with the traitlet, or a subclass + that obj belongs to. + * trait.make_dynamic_default, which is defined by Instance + + If neither exist, it returns None + """ + # Traitlets without a name are not on the instance, e.g. in List or Union + if self.name: + + # Only look for default handlers in classes derived from self.this_class. + mro = type(obj).mro() + meth_name = '_%s_default' % self.name + for cls in mro[:mro.index(self.this_class) + 1]: + if hasattr(cls, '_trait_default_generators'): + default_handler = cls._trait_default_generators.get(self.name) + if default_handler is not None and default_handler.this_class == cls: + return types.MethodType(default_handler.func, obj) + + if meth_name in cls.__dict__: + method = getattr(obj, meth_name) + return method + + return getattr(self, 'make_dynamic_default', None) + + def instance_init(self, obj): + # If no dynamic initialiser is present, and the trait implementation or + # use provides a static default, transfer that to obj._trait_values. + with obj.cross_validation_lock: + if (self._dynamic_default_callable(obj) is None) \ + and (self.default_value is not Undefined): + v = self._validate(obj, self.default_value) + if self.name is not None: + obj._trait_values[self.name] = v + + def get(self, obj, cls=None): + try: + value = obj._trait_values[self.name] + except KeyError: + # Check for a dynamic initializer. + dynamic_default = self._dynamic_default_callable(obj) + if dynamic_default is None: + raise TraitError("No default value found for %s trait of %r" + % (self.name, obj)) + value = self._validate(obj, dynamic_default()) + obj._trait_values[self.name] = value + return value + except Exception: + # This should never be reached. + raise TraitError('Unexpected error in TraitType: ' + 'default value not set properly') + else: + return value + + def __get__(self, obj, cls=None): + """Get the value of the trait by self.name for the instance. + + Default values are instantiated when :meth:`HasTraits.__new__` + is called. Thus by the time this method gets called either the + default value or a user defined value (they called :meth:`__set__`) + is in the :class:`HasTraits` instance. + """ + if obj is None: + return self + else: + return self.get(obj, cls) + + def set(self, obj, value): + new_value = self._validate(obj, value) + try: + old_value = obj._trait_values[self.name] + except KeyError: + old_value = self.default_value + + obj._trait_values[self.name] = new_value + try: + silent = bool(old_value == new_value) + except: + # if there is an error in comparing, default to notify + silent = False + if silent is not True: + # we explicitly compare silent to True just in case the equality + # comparison above returns something other than True/False + obj._notify_trait(self.name, old_value, new_value) + + def __set__(self, obj, value): + """Set the value of the trait by self.name for the instance. + + Values pass through a validation stage where errors are raised when + impropper types, or types that cannot be coerced, are encountered. + """ + if self.read_only: + raise TraitError('The "%s" trait is read-only.' % self.name) + else: + self.set(obj, value) + + def _validate(self, obj, value): + if value is None and self.allow_none: + return value + if hasattr(self, 'validate'): + value = self.validate(obj, value) + if obj._cross_validation_lock is False: + value = self._cross_validate(obj, value) + return value + + def _cross_validate(self, obj, value): + if self.name in obj._trait_validators: + proposal = Bunch({'trait': self, 'value': value, 'owner': obj}) + value = obj._trait_validators[self.name](obj, proposal) + elif hasattr(obj, '_%s_validate' % self.name): + meth_name = '_%s_validate' % self.name + cross_validate = getattr(obj, meth_name) + _deprecated_method(cross_validate, obj.__class__, meth_name, + "use @validate decorator instead.") + value = cross_validate(value, self) + return value + + def __or__(self, other): + if isinstance(other, Union): + return Union([self] + other.trait_types) + else: + return Union([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)) + 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. + + Use .metadata[key] or .metadata.get(key, default) instead. + """ + if key == 'help': + msg = "use the instance .help string directly, like x.help" + else: + msg = "use the instance .metadata dictionary directly, like x.metadata[key] or x.metadata.get(key, default)" + warn("Deprecated in traitlets 4.1, " + msg, DeprecationWarning, stacklevel=2) + return self.metadata.get(key, default) + + def set_metadata(self, key, value): + """DEPRECATED: Set a metadata key/value. + + Use .metadata[key] = value instead. + """ + if key == 'help': + msg = "use the instance .help string directly, like x.help = value" + else: + msg = "use the instance .metadata dictionary directly, like x.metadata[key] = value" + warn("Deprecated in traitlets 4.1, " + msg, DeprecationWarning, stacklevel=2) + self.metadata[key] = value + + def tag(self, **metadata): + """Sets metadata and returns self. + + This allows convenient metadata tagging when initializing the trait, such as: + + >>> Int(0).tag(config=True, sync=True) + """ + maybe_constructor_keywords = set(metadata.keys()).intersection({'help','allow_none', 'read_only', 'default_value'}) + if maybe_constructor_keywords: + warn('The following attributes are set in using `tag`, but seem to be constructor keywords arguments: %s '% + maybe_constructor_keywords, UserWarning, stacklevel=2) + + self.metadata.update(metadata) + return self + + def default_value_repr(self): + return repr(self.default_value) + +#----------------------------------------------------------------------------- +# The HasTraits implementation +#----------------------------------------------------------------------------- + +class _CallbackWrapper(object): + """An object adapting a on_trait_change callback into an observe callback. + + The comparison operator __eq__ is implemented to enable removal of wrapped + callbacks. + """ + + def __init__(self, cb): + self.cb = cb + # Bound methods have an additional 'self' argument. + offset = -1 if isinstance(self.cb, types.MethodType) else 0 + self.nargs = len(getargspec(cb)[0]) + offset + if (self.nargs > 4): + raise TraitError('a trait changed callback must have 0-4 arguments.') + + def __eq__(self, other): + # The wrapper is equal to the wrapped element + if isinstance(other, _CallbackWrapper): + return self.cb == other.cb + else: + return self.cb == other + + def __call__(self, change): + # The wrapper is callable + if self.nargs == 0: + self.cb() + elif self.nargs == 1: + self.cb(change.name) + elif self.nargs == 2: + self.cb(change.name, change.new) + elif self.nargs == 3: + self.cb(change.name, change.old, change.new) + elif self.nargs == 4: + self.cb(change.name, change.old, change.new, change.owner) + +def _callback_wrapper(cb): + if isinstance(cb, _CallbackWrapper): + return cb + else: + return _CallbackWrapper(cb) + + +class MetaHasDescriptors(type): + """A metaclass for HasDescriptors. + + This metaclass makes sure that any TraitType class attributes are + instantiated and sets their name attribute. + """ + + def __new__(mcls, name, bases, classdict): + """Create the HasDescriptors class.""" + for k, v in classdict.items(): + # ---------------------------------------------------------------- + # Support of deprecated behavior allowing for TraitType types + # to be used instead of TraitType instances. + if inspect.isclass(v) and issubclass(v, TraitType): + warn("Traits should be given as instances, not types (for example, `Int()`, not `Int`)." + " Passing types is deprecated in traitlets 4.1.", + DeprecationWarning, stacklevel=2) + classdict[k] = v() + # ---------------------------------------------------------------- + + return super(MetaHasDescriptors, mcls).__new__(mcls, name, bases, classdict) + + def __init__(cls, name, bases, classdict): + """Finish initializing the HasDescriptors class.""" + super(MetaHasDescriptors, cls).__init__(name, bases, classdict) + cls.setup_class(classdict) + + def setup_class(cls, classdict): + """Setup descriptor instance on the class + + This sets the :attr:`this_class` and :attr:`name` attributes of each + BaseDescriptor in the class dict of the newly created ``cls`` before + calling their :attr:`class_init` method. + """ + for k, v in classdict.items(): + if isinstance(v, BaseDescriptor): + v.class_init(cls, k) + + +class MetaHasTraits(MetaHasDescriptors): + """A metaclass for HasTraits.""" + + def setup_class(cls, classdict): + cls._trait_default_generators = {} + super(MetaHasTraits, cls).setup_class(classdict) + + +def observe(*names, **kwargs): + """A decorator which can be used to observe Traits on a class. + + The handler passed to the decorator will be called with one ``change`` + dict argument. The change dictionary at least holds a 'type' key and a + 'name' key, corresponding respectively to the type of notification and the + name of the attribute that triggered the notification. + + Other keys may be passed depending on the value of 'type'. In the case + where type is 'change', we also have the following keys: + * ``owner`` : the HasTraits instance + * ``old`` : the old value of the modified trait attribute + * ``new`` : the new value of the modified trait attribute + * ``name`` : the name of the modified trait attribute. + + Parameters + ---------- + *names + The str names of the Traits to observe on the object. + type: str, kwarg-only + The type of event to observe (e.g. 'change') + """ + if not names: + raise TypeError("Please specify at least one trait name to observe.") + for name in names: + if name is not All and not isinstance(name, six.string_types): + raise TypeError("trait names to observe must be strings or All, not %r" % name) + return ObserveHandler(names, type=kwargs.get('type', 'change')) + + +def observe_compat(func): + """Backward-compatibility shim decorator for observers + + Use with: + + @observe('name') + @observe_compat + def _foo_changed(self, change): + ... + + With this, `super()._foo_changed(self, name, old, new)` in subclasses will still work. + Allows adoption of new observer API without breaking subclasses that override and super. + """ + def compatible_observer(self, change_or_name, old=Undefined, new=Undefined): + if isinstance(change_or_name, dict): + change = change_or_name + else: + clsname = self.__class__.__name__ + warn("A parent of %s._%s_changed has adopted the new (traitlets 4.1) @observe(change) API" % ( + clsname, change_or_name), DeprecationWarning) + change = Bunch( + type='change', + old=old, + new=new, + name=change_or_name, + owner=self, + ) + return func(self, change) + return compatible_observer + + +def validate(*names): + """A decorator to register cross validator of HasTraits object's state + when a Trait is set. + + The handler passed to the decorator must have one ``proposal`` dict argument. + The proposal dictionary must hold the following keys: + * ``owner`` : the HasTraits instance + * ``value`` : the proposed value for the modified trait attribute + * ``trait`` : the TraitType instance associated with the attribute + + Parameters + ---------- + names + The str names of the Traits to validate. + + Notes + ----- + Since the owner has access to the ``HasTraits`` instance via the 'owner' key, + the registered cross validator could potentially make changes to attributes + of the ``HasTraits`` instance. However, we recommend not to do so. The reason + is that the cross-validation of attributes may run in arbitrary order when + exiting the ``hold_trait_notifications`` context, and such changes may not + commute. + """ + if not names: + raise TypeError("Please specify at least one trait name to validate.") + for name in names: + if name is not All and not isinstance(name, six.string_types): + raise TypeError("trait names to validate must be strings or All, not %r" % name) + return ValidateHandler(names) + + +def default(name): + """ A decorator which assigns a dynamic default for a Trait on a HasTraits object. + + Parameters + ---------- + name + The str name of the Trait on the object whose default should be generated. + + Notes + ----- + Unlike observers and validators which are properties of the HasTraits + instance, default value generators are class-level properties. + + Besides, default generators are only invoked if they are registered in + subclasses of `this_type`. + + :: + + class A(HasTraits): + bar = Int() + + @default('bar') + def get_bar_default(self): + return 11 + + + class B(A): + bar = Float() # This trait ignores the default generator defined in + # the base class A + + + class C(B): + + @default('bar') + def some_other_default(self): # This default generator should not be + return 3.0 # ignored since it is defined in a + # class derived from B.a.this_class. + """ + if not isinstance(name, six.string_types): + raise TypeError("Trait name must be a string or All, not %r" % name) + return DefaultHandler(name) + + +class EventHandler(BaseDescriptor): + + def _init_call(self, func): + self.func = func + return self + + def __call__(self, *args, **kwargs): + """Pass `*args` and `**kwargs` to the handler's function if it exists.""" + if hasattr(self, 'func'): + return self.func(*args, **kwargs) + else: + return self._init_call(*args, **kwargs) + + def __get__(self, inst, cls=None): + if inst is None: + return self + return types.MethodType(self.func, inst) + + +class ObserveHandler(EventHandler): + + def __init__(self, names, type): + self.trait_names = names + self.type = type + + def instance_init(self, inst): + inst.observe(self, self.trait_names, type=self.type) + + +class ValidateHandler(EventHandler): + + def __init__(self, names): + self.trait_names = names + + def instance_init(self, inst): + inst._register_validator(self, self.trait_names) + + +class DefaultHandler(EventHandler): + + def __init__(self, name): + self.trait_name = name + + def class_init(self, cls, name): + super(DefaultHandler, self).class_init(cls, name) + cls._trait_default_generators[self.trait_name] = self + + +class HasDescriptors(six.with_metaclass(MetaHasDescriptors, object)): + """The base class for all classes that have descriptors. + """ + + def __new__(cls, *args, **kwargs): + # This is needed because object.__new__ only accepts + # the cls argument. + new_meth = super(HasDescriptors, cls).__new__ + if new_meth is object.__new__: + inst = new_meth(cls) + else: + inst = new_meth(cls, *args, **kwargs) + inst.setup_instance(*args, **kwargs) + return inst + + def setup_instance(self, *args, **kwargs): + """ + This is called **before** self.__init__ is called. + """ + self._cross_validation_lock = False + cls = self.__class__ + for key in dir(cls): + # Some descriptors raise AttributeError like zope.interface's + # __provides__ attributes even though they exist. This causes + # AttributeErrors even though they are listed in dir(cls). + try: + value = getattr(cls, key) + except AttributeError: + pass + else: + if isinstance(value, BaseDescriptor): + value.instance_init(self) + + +class HasTraits(six.with_metaclass(MetaHasTraits, HasDescriptors)): + + def setup_instance(self, *args, **kwargs): + self._trait_values = {} + self._trait_notifiers = {} + self._trait_validators = {} + super(HasTraits, self).setup_instance(*args, **kwargs) + + def __init__(self, *args, **kwargs): + # Allow trait values to be set using keyword arguments. + # We need to use setattr for this to trigger validation and + # notifications. + super_args = args + super_kwargs = {} + with self.hold_trait_notifications(): + for key, value in kwargs.items(): + if self.has_trait(key): + setattr(self, key, value) + else: + # passthrough args that don't set traits to super + super_kwargs[key] = value + try: + super(HasTraits, self).__init__(*super_args, **super_kwargs) + except TypeError as e: + arg_s_list = [ repr(arg) for arg in super_args ] + for k, v in super_kwargs.items(): + arg_s_list.append("%s=%r" % (k, v)) + arg_s = ', '.join(arg_s_list) + warn( + "Passing unrecoginized arguments to super({classname}).__init__({arg_s}).\n" + "{error}\n" + "This is deprecated in traitlets 4.2." + "This error will be raised in a future release of traitlets." + .format( + arg_s=arg_s, classname=self.__class__.__name__, + error=e, + ), + DeprecationWarning, + stacklevel=2, + ) + + def __getstate__(self): + d = self.__dict__.copy() + # event handlers stored on an instance are + # expected to be reinstantiated during a + # recall of instance_init during __setstate__ + d['_trait_notifiers'] = {} + d['_trait_validators'] = {} + return d + + def __setstate__(self, state): + self.__dict__ = state.copy() + + # event handlers are reassigned to self + cls = self.__class__ + for key in dir(cls): + # Some descriptors raise AttributeError like zope.interface's + # __provides__ attributes even though they exist. This causes + # AttributeErrors even though they are listed in dir(cls). + try: + value = getattr(cls, key) + except AttributeError: + pass + else: + if isinstance(value, EventHandler): + value.instance_init(self) + + @property + @contextlib.contextmanager + def cross_validation_lock(self): + """ + A contextmanager for running a block with our cross validation lock set + to True. + + At the end of the block, the lock's value is restored to its value + prior to entering the block. + """ + if self._cross_validation_lock: + yield + return + else: + try: + self._cross_validation_lock = True + yield + finally: + self._cross_validation_lock = False + + @contextlib.contextmanager + def hold_trait_notifications(self): + """Context manager for bundling trait change notifications and cross + validation. + + Use this when doing multiple trait assignments (init, config), to avoid + race conditions in trait notifiers requesting other trait values. + All trait notifications will fire after all values have been assigned. + """ + if self._cross_validation_lock: + yield + return + else: + cache = {} + notify_change = self.notify_change + + def compress(past_changes, change): + """Merges the provided change with the last if possible.""" + if past_changes is None: + return [change] + else: + if past_changes[-1]['type'] == 'change' and change.type == 'change': + past_changes[-1]['new'] = change.new + else: + # In case of changes other than 'change', append the notification. + past_changes.append(change) + return past_changes + + def hold(change): + name = change.name + cache[name] = compress(cache.get(name), change) + + try: + # Replace notify_change with `hold`, caching and compressing + # notifications, disable cross validation and yield. + self.notify_change = hold + self._cross_validation_lock = True + yield + # Cross validate final values when context is released. + for name in list(cache.keys()): + trait = getattr(self.__class__, name) + value = trait._cross_validate(self, getattr(self, name)) + self.set_trait(name, value) + except TraitError as e: + # Roll back in case of TraitError during final cross validation. + self.notify_change = lambda x: None + for name, changes in cache.items(): + for change in changes[::-1]: + # TODO: Separate in a rollback function per notification type. + if change.type == 'change': + if change.old is not Undefined: + self.set_trait(name, change.old) + else: + self._trait_values.pop(name) + cache = {} + raise e + finally: + self._cross_validation_lock = False + # Restore method retrieval from class + del self.notify_change + + # trigger delayed notifications + for changes in cache.values(): + for change in changes: + self.notify_change(change) + + def _notify_trait(self, name, old_value, new_value): + self.notify_change(Bunch( + name=name, + old=old_value, + new=new_value, + owner=self, + type='change', + )) + + def notify_change(self, change): + if not isinstance(change, Bunch): + # cast to bunch if given a dict + change = Bunch(change) + name, type = change.name, change.type + + callables = [] + callables.extend(self._trait_notifiers.get(name, {}).get(type, [])) + callables.extend(self._trait_notifiers.get(name, {}).get(All, [])) + callables.extend(self._trait_notifiers.get(All, {}).get(type, [])) + callables.extend(self._trait_notifiers.get(All, {}).get(All, [])) + + # Now static ones + magic_name = '_%s_changed' % name + if hasattr(self, magic_name): + class_value = getattr(self.__class__, magic_name) + if not isinstance(class_value, ObserveHandler): + _deprecated_method(class_value, self.__class__, magic_name, + "use @observe and @unobserve instead.") + cb = getattr(self, magic_name) + # Only append the magic method if it was not manually registered + if cb not in callables: + callables.append(_callback_wrapper(cb)) + + # Call them all now + # Traits catches and logs errors here. I allow them to raise + for c in callables: + # Bound methods have an additional 'self' argument. + + if isinstance(c, _CallbackWrapper): + c = c.__call__ + elif isinstance(c, EventHandler) and c.name is not None: + c = getattr(self, c.name) + + c(change) + + def _add_notifiers(self, handler, name, type): + if name not in self._trait_notifiers: + nlist = [] + self._trait_notifiers[name] = {type: nlist} + else: + if type not in self._trait_notifiers[name]: + nlist = [] + self._trait_notifiers[name][type] = nlist + else: + nlist = self._trait_notifiers[name][type] + if handler not in nlist: + nlist.append(handler) + + def _remove_notifiers(self, handler, name, type): + try: + if handler is None: + del self._trait_notifiers[name][type] + else: + self._trait_notifiers[name][type].remove(handler) + except KeyError: + pass + + def on_trait_change(self, handler=None, name=None, remove=False): + """DEPRECATED: Setup a handler to be called when a trait changes. + + This is used to setup dynamic notifications of trait changes. + + Static handlers can be created by creating methods on a HasTraits + subclass with the naming convention '_[traitname]_changed'. Thus, + to create static handler for the trait 'a', create the method + _a_changed(self, name, old, new) (fewer arguments can be used, see + below). + + If `remove` is True and `handler` is not specified, all change + handlers for the specified name are uninstalled. + + Parameters + ---------- + handler : callable, None + A callable that is called when a trait changes. Its + signature can be handler(), handler(name), handler(name, new), + handler(name, old, new), or handler(name, old, new, self). + name : list, str, None + If None, the handler will apply to all traits. If a list + of str, handler will apply to all names in the list. If a + str, the handler will apply just to that name. + remove : bool + If False (the default), then install the handler. If True + then unintall it. + """ + warn("on_trait_change is deprecated in traitlets 4.1: use observe instead", + DeprecationWarning, stacklevel=2) + if name is None: + name = All + if remove: + self.unobserve(_callback_wrapper(handler), names=name) + else: + self.observe(_callback_wrapper(handler), names=name) + + def observe(self, handler, names=All, type='change'): + """Setup a handler to be called when a trait changes. + + This is used to setup dynamic notifications of trait changes. + + Parameters + ---------- + handler : callable + A callable that is called when a trait changes. Its + signature should be ``handler(change)``, where ``change`` is a + dictionary. The change dictionary at least holds a 'type' key. + * ``type``: the type of notification. + Other keys may be passed depending on the value of 'type'. In the + case where type is 'change', we also have the following keys: + * ``owner`` : the HasTraits instance + * ``old`` : the old value of the modified trait attribute + * ``new`` : the new value of the modified trait attribute + * ``name`` : the name of the modified trait attribute. + names : list, str, All + If names is All, the handler will apply to all traits. If a list + of str, handler will apply to all names in the list. If a + str, the handler will apply just to that name. + type : str, All (default: 'change') + The type of notification to filter by. If equal to All, then all + notifications are passed to the observe handler. + """ + names = parse_notifier_name(names) + for n in names: + self._add_notifiers(handler, n, type) + + def unobserve(self, handler, names=All, type='change'): + """Remove a trait change handler. + + This is used to unregister handlers to trait change notifications. + + Parameters + ---------- + handler : callable + The callable called when a trait attribute changes. + names : list, str, All (default: All) + The names of the traits for which the specified handler should be + uninstalled. If names is All, the specified handler is uninstalled + from the list of notifiers corresponding to all changes. + type : str or All (default: 'change') + The type of notification to filter by. If All, the specified handler + is uninstalled from the list of notifiers corresponding to all types. + """ + names = parse_notifier_name(names) + for n in names: + self._remove_notifiers(handler, n, type) + + def unobserve_all(self, name=All): + """Remove trait change handlers of any type for the specified name. + If name is not specified, removes all trait notifiers.""" + if name is All: + self._trait_notifiers = {} + else: + try: + del self._trait_notifiers[name] + except KeyError: + pass + + def _register_validator(self, handler, names): + """Setup a handler to be called when a trait should be cross validated. + + This is used to setup dynamic notifications for cross-validation. + + If a validator is already registered for any of the provided names, a + TraitError is raised and no new validator is registered. + + Parameters + ---------- + handler : callable + A callable that is called when the given trait is cross-validated. + Its signature is handler(proposal), where proposal is a Bunch (dictionary with attribute access) + with the following attributes/keys: + * ``owner`` : the HasTraits instance + * ``value`` : the proposed value for the modified trait attribute + * ``trait`` : the TraitType instance associated with the attribute + names : List of strings + The names of the traits that should be cross-validated + """ + for name in names: + magic_name = '_%s_validate' % name + if hasattr(self, magic_name): + class_value = getattr(self.__class__, magic_name) + if not isinstance(class_value, ValidateHandler): + _deprecated_method(class_value, self.__class, magic_name, + "use @validate decorator instead.") + for name in names: + self._trait_validators[name] = handler + + def add_traits(self, **traits): + """Dynamically add trait attributes to the HasTraits instance.""" + self.__class__ = type(self.__class__.__name__, (self.__class__,), + traits) + for trait in traits.values(): + trait.instance_init(self) + + def set_trait(self, name, value): + """Forcibly sets trait attribute, including read-only attributes.""" + cls = self.__class__ + if not self.has_trait(name): + raise TraitError("Class %s does not have a trait named %s" % + (cls.__name__, name)) + else: + getattr(cls, name).set(self, value) + + @classmethod + def class_trait_names(cls, **metadata): + """Get a list of all the names of this class' traits. + + This method is just like the :meth:`trait_names` method, + but is unbound. + """ + return list(cls.class_traits(**metadata)) + + @classmethod + def class_traits(cls, **metadata): + """Get a ``dict`` of all the traits of this class. The dictionary + is keyed on the name and the values are the TraitType objects. + + This method is just like the :meth:`traits` method, but is unbound. + + The TraitTypes returned don't know anything about the values + that the various HasTrait's instances are holding. + + The metadata kwargs allow functions to be passed in which + filter traits based on metadata values. The functions should + take a single value as an argument and return a boolean. If + any function returns False, then the trait is not included in + the output. If a metadata key doesn't exist, None will be passed + to the function. + """ + traits = dict([memb for memb in getmembers(cls) if + isinstance(memb[1], TraitType)]) + + if len(metadata) == 0: + return traits + + result = {} + for name, trait in traits.items(): + for meta_name, meta_eval in metadata.items(): + if type(meta_eval) is not types.FunctionType: + meta_eval = _SimpleTest(meta_eval) + if not meta_eval(trait.metadata.get(meta_name, None)): + break + else: + result[name] = trait + + return result + + @classmethod + def class_own_traits(cls, **metadata): + """Get a dict of all the traitlets defined on this class, not a parent. + + Works like `class_traits`, except for excluding traits from parents. + """ + sup = super(cls, cls) + return {n: t for (n, t) in cls.class_traits(**metadata).items() + if getattr(sup, n, None) is not t} + + def has_trait(self, name): + """Returns True if the object has a trait with the specified name.""" + return isinstance(getattr(self.__class__, name, None), TraitType) + + def trait_names(self, **metadata): + """Get a list of all the names of this class' traits.""" + return list(self.traits(**metadata)) + + def traits(self, **metadata): + """Get a ``dict`` of all the traits of this class. The dictionary + is keyed on the name and the values are the TraitType objects. + + The TraitTypes returned don't know anything about the values + that the various HasTrait's instances are holding. + + The metadata kwargs allow functions to be passed in which + filter traits based on metadata values. The functions should + take a single value as an argument and return a boolean. If + any function returns False, then the trait is not included in + the output. If a metadata key doesn't exist, None will be passed + to the function. + """ + traits = dict([memb for memb in getmembers(self.__class__) if + isinstance(memb[1], TraitType)]) + + if len(metadata) == 0: + return traits + + result = {} + for name, trait in traits.items(): + for meta_name, meta_eval in metadata.items(): + if type(meta_eval) is not types.FunctionType: + meta_eval = _SimpleTest(meta_eval) + if not meta_eval(trait.metadata.get(meta_name, None)): + break + else: + result[name] = trait + + return result + + def trait_metadata(self, traitname, key, default=None): + """Get metadata values for trait by key.""" + try: + trait = getattr(self.__class__, traitname) + except AttributeError: + raise TraitError("Class %s does not have a trait named %s" % + (self.__class__.__name__, traitname)) + metadata_name = '_' + traitname + '_metadata' + if hasattr(self, metadata_name) and key in getattr(self, metadata_name): + return getattr(self, metadata_name).get(key, default) + else: + return trait.metadata.get(key, default) + + @classmethod + def class_own_trait_events(cls, name): + """Get a dict of all event handlers defined on this class, not a parent. + + Works like ``event_handlers``, except for excluding traits from parents. + """ + sup = super(cls, cls) + return {n: e for (n, e) in cls.events(name).items() + if getattr(sup, n, None) is not e} + + @classmethod + def trait_events(cls, name=None): + """Get a ``dict`` of all the event handlers of this class. + + Parameters + ---------- + name: str (default: None) + The name of a trait of this class. If name is ``None`` then all + the event handlers of this class will be returned instead. + + Returns + ------- + The event handlers associated with a trait name, or all event handlers. + """ + events = {} + for k, v in getmembers(cls): + if isinstance(v, EventHandler): + if name is None: + events[k] = v + elif name in v.trait_names: + events[k] = v + elif hasattr(v, 'tags'): + if cls.trait_names(**v.tags): + events[k] = v + return events + +#----------------------------------------------------------------------------- +# Actual TraitTypes implementations/subclasses +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# TraitTypes subclasses for handling classes and instances of classes +#----------------------------------------------------------------------------- + + +class ClassBasedTraitType(TraitType): + """ + A trait with error reporting and string -> type resolution for Type, + Instance and This. + """ + + def _resolve_string(self, string): + """ + Resolve a string supplied for a type into an actual object. + """ + 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.""" + + def __init__ (self, default_value=Undefined, klass=None, **kwargs): + """Construct a Type trait + + A Type trait specifies that its values must be subclasses of + a particular class. + + If only ``default_value`` is given, it is used for the ``klass`` as + well. If neither are given, both default to ``object``. + + Parameters + ---------- + default_value : class, str or None + The default value must be a subclass of klass. If an str, + the str must be a fully specified class name, like 'foo.bar.Bah'. + The string is resolved into real class, when the parent + :class:`HasTraits` class is instantiated. + klass : class, str [ default object ] + Values of this trait must be a subclass of klass. The klass + may be specified in a string like: 'foo.bar.MyClass'. + The string is resolved into real class, when the parent + :class:`HasTraits` class is instantiated. + allow_none : bool [ default False ] + Indicates whether None is allowed as an assignable value. + """ + if default_value is Undefined: + new_default_value = object if (klass is None) else klass + else: + new_default_value = default_value + + if klass is None: + if (default_value is None) or (default_value is Undefined): + klass = object + else: + klass = default_value + + if not (inspect.isclass(klass) or isinstance(klass, six.string_types)): + raise TraitError("A Type trait must specify a class.") + + self.klass = klass + + super(Type, self).__init__(new_default_value, **kwargs) + + def validate(self, obj, value): + """Validates that the value is a valid object instance.""" + if isinstance(value, six.string_types): + try: + value = self._resolve_string(value) + except ImportError: + raise TraitError("The '%s' trait of %s instance must be a type, but " + "%r could not be imported" % (self.name, obj, value)) + try: + if issubclass(value, self.klass): + return value + except: + pass + + self.error(obj, value) + + def info(self): + """ Returns a description of the trait.""" + if isinstance(self.klass, six.string_types): + klass = self.klass + else: + klass = self.klass.__module__ + '.' + self.klass.__name__ + result = "a subclass of '%s'" % klass + if self.allow_none: + return result + ' or None' + return result + + def instance_init(self, obj): + self._resolve_classes() + super(Type, self).instance_init(obj) + + def _resolve_classes(self): + if isinstance(self.klass, six.string_types): + self.klass = self._resolve_string(self.klass) + if isinstance(self.default_value, six.string_types): + self.default_value = self._resolve_string(self.default_value) + + def default_value_repr(self): + value = self.default_value + if isinstance(value, six.string_types): + return repr(value) + else: + return repr('{}.{}'.format(value.__module__, value.__name__)) + + +class Instance(ClassBasedTraitType): + """A trait whose value must be an instance of a specified class. + + The value can also be an instance of a subclass of the specified class. + + Subclasses can declare default classes by overriding the klass attribute + """ + + klass = None + + def __init__(self, klass=None, args=None, kw=None, **kwargs): + """Construct an Instance trait. + + This trait allows values that are instances of a particular + class or its subclasses. Our implementation is quite different + from that of enthough.traits as we don't allow instances to be used + for klass and we handle the ``args`` and ``kw`` arguments differently. + + Parameters + ---------- + klass : class, str + The class that forms the basis for the trait. Class names + can also be specified as strings, like 'foo.bar.Bar'. + args : tuple + Positional arguments for generating the default value. + kw : dict + Keyword arguments for generating the default value. + allow_none : bool [ default False ] + Indicates whether None is allowed as a value. + + Notes + ----- + If both ``args`` and ``kw`` are None, then the default value is None. + If ``args`` is a tuple and ``kw`` is a dict, then the default is + created as ``klass(*args, **kw)``. If exactly one of ``args`` or ``kw`` is + None, the None is replaced by ``()`` or ``{}``, respectively. + """ + if klass is None: + klass = self.klass + + if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, six.string_types)): + self.klass = klass + else: + raise TraitError('The klass attribute must be a class' + ' not: %r' % klass) + + if (kw is not None) and not isinstance(kw, dict): + raise TraitError("The 'kw' argument must be a dict or None.") + if (args is not None) and not isinstance(args, tuple): + raise TraitError("The 'args' argument must be a tuple or None.") + + self.default_args = args + self.default_kwargs = kw + + super(Instance, self).__init__(**kwargs) + + def validate(self, obj, value): + if isinstance(value, self.klass): + return value + else: + self.error(obj, value) + + def info(self): + if isinstance(self.klass, six.string_types): + klass = self.klass + else: + klass = self.klass.__name__ + result = class_of(klass) + if self.allow_none: + return result + ' or None' + + return result + + def instance_init(self, obj): + self._resolve_classes() + super(Instance, self).instance_init(obj) + + def _resolve_classes(self): + if isinstance(self.klass, six.string_types): + self.klass = self._resolve_string(self.klass) + + def make_dynamic_default(self): + if (self.default_args is None) and (self.default_kwargs is None): + return None + return self.klass(*(self.default_args or ()), + **(self.default_kwargs or {})) + + def default_value_repr(self): + return repr(self.make_dynamic_default()) + + +class ForwardDeclaredMixin(object): + """ + Mixin for forward-declared versions of Instance and Type. + """ + def _resolve_string(self, string): + """ + Find the specified class name by looking for it in the module in which + our this_class attribute was defined. + """ + modname = self.this_class.__module__ + return import_item('.'.join([modname, string])) + + +class ForwardDeclaredType(ForwardDeclaredMixin, Type): + """ + Forward-declared version of Type. + """ + pass + + +class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance): + """ + Forward-declared version of Instance. + """ + pass + + +class This(ClassBasedTraitType): + """A trait for instances of the class containing this trait. + + Because how how and when class bodies are executed, the ``This`` + trait can only have a default value of None. This, and because we + always validate default values, ``allow_none`` is *always* true. + """ + + info_text = 'an instance of the same type as the receiver or None' + + def __init__(self, **kwargs): + super(This, self).__init__(None, **kwargs) + + def validate(self, obj, value): + # What if value is a superclass of obj.__class__? This is + # complicated if it was the superclass that defined the This + # trait. + if isinstance(value, self.this_class) or (value is None): + return value + else: + self.error(obj, value) + + +class Union(TraitType): + """A trait type representing a Union type.""" + + def __init__(self, trait_types, **kwargs): + """Construct a Union trait. + + This trait allows values that are allowed by at least one of the + specified trait types. A Union traitlet cannot have metadata on + its own, besides the metadata of the listed types. + + Parameters + ---------- + trait_types: sequence + The list of trait types of length at least 1. + + Notes + ----- + Union([Float(), Bool(), Int()]) attempts to validate the provided values + with the validation function of Float, then Bool, and finally Int. + """ + self.trait_types = trait_types + self.info_text = " or ".join([tt.info() for tt in self.trait_types]) + super(Union, self).__init__(**kwargs) + + def class_init(self, cls, name): + for trait_type in self.trait_types: + trait_type.class_init(cls, None) + super(Union, self).class_init(cls, name) + + def instance_init(self, obj): + for trait_type in self.trait_types: + trait_type.instance_init(obj) + super(Union, self).instance_init(obj) + + def validate(self, obj, value): + with obj.cross_validation_lock: + for trait_type in self.trait_types: + try: + v = trait_type._validate(obj, value) + # In the case of an element trait, the name is None + if self.name is not None: + setattr(obj, '_' + self.name + '_metadata', trait_type.metadata) + return v + except TraitError: + continue + self.error(obj, value) + + def __or__(self, other): + if isinstance(other, Union): + return Union(self.trait_types + other.trait_types) + else: + return Union(self.trait_types + [other]) + + def make_dynamic_default(self): + if self.default_value is not Undefined: + return self.default_value + for trait_type in self.trait_types: + if trait_type.default_value is not Undefined: + return trait_type.default_value + elif hasattr(trait_type, 'make_dynamic_default'): + return trait_type.make_dynamic_default() + + +#----------------------------------------------------------------------------- +# Basic TraitTypes implementations/subclasses +#----------------------------------------------------------------------------- + + +class Any(TraitType): + """A trait which allows any value.""" + default_value = None + info_text = 'any value' + + +def _validate_bounds(trait, obj, value): + """ + Validate that a number to be applied to a trait is between bounds. + + If value is not between min_bound and max_bound, this raises a + TraitError with an error message appropriate for this trait. + """ + if trait.min is not None and value < trait.min: + raise TraitError( + "The value of the '{name}' trait of {klass} instance should " + "not be less than {min_bound}, but a value of {value} was " + "specified".format( + name=trait.name, klass=class_of(obj), + value=value, min_bound=trait.min)) + if trait.max is not None and value > trait.max: + raise TraitError( + "The value of the '{name}' trait of {klass} instance should " + "not be greater than {max_bound}, but a value of {value} was " + "specified".format( + name=trait.name, klass=class_of(obj), + value=value, max_bound=trait.max)) + return value + + +class Int(TraitType): + """An int trait.""" + + default_value = 0 + info_text = 'an int' + + def __init__(self, default_value=Undefined, allow_none=False, **kwargs): + self.min = kwargs.pop('min', None) + self.max = kwargs.pop('max', None) + super(Int, self).__init__(default_value=default_value, + allow_none=allow_none, **kwargs) + + def validate(self, obj, value): + if not isinstance(value, int): + self.error(obj, value) + return _validate_bounds(self, obj, value) + + +class CInt(Int): + """A casting version of the int trait.""" + + def validate(self, obj, value): + try: + value = int(value) + except: + self.error(obj, value) + return _validate_bounds(self, obj, value) + + +if six.PY2: + class Long(TraitType): + """A long integer trait.""" + + default_value = 0 + info_text = 'a long' + + def __init__(self, default_value=Undefined, allow_none=False, **kwargs): + self.min = kwargs.pop('min', None) + self.max = kwargs.pop('max', None) + super(Long, self).__init__( + default_value=default_value, + allow_none=allow_none, **kwargs) + + def _validate_long(self, obj, value): + if isinstance(value, long): + return value + if isinstance(value, int): + return long(value) + self.error(obj, value) + + def validate(self, obj, value): + value = self._validate_long(obj, value) + return _validate_bounds(self, obj, value) + + + class CLong(Long): + """A casting version of the long integer trait.""" + + def validate(self, obj, value): + try: + value = long(value) + except: + self.error(obj, value) + return _validate_bounds(self, obj, value) + + + class Integer(TraitType): + """An integer trait. + + Longs that are unnecessary (<= sys.maxint) are cast to ints.""" + + default_value = 0 + info_text = 'an integer' + + def __init__(self, default_value=Undefined, allow_none=False, **kwargs): + self.min = kwargs.pop('min', None) + self.max = kwargs.pop('max', None) + super(Integer, self).__init__( + default_value=default_value, + allow_none=allow_none, **kwargs) + + def _validate_int(self, obj, value): + if isinstance(value, int): + return value + if isinstance(value, long): + # downcast longs that fit in int: + # note that int(n > sys.maxint) returns a long, so + # we don't need a condition on this cast + return int(value) + if sys.platform == "cli": + from System import Int64 + if isinstance(value, Int64): + return int(value) + self.error(obj, value) + + def validate(self, obj, value): + value = self._validate_int(obj, value) + return _validate_bounds(self, obj, value) + +else: + Long, CLong = Int, CInt + Integer = Int + + +class Float(TraitType): + """A float trait.""" + + default_value = 0.0 + info_text = 'a float' + + def __init__(self, default_value=Undefined, allow_none=False, **kwargs): + self.min = kwargs.pop('min', -float('inf')) + self.max = kwargs.pop('max', float('inf')) + super(Float, self).__init__(default_value=default_value, + allow_none=allow_none, **kwargs) + + def validate(self, obj, value): + if isinstance(value, int): + value = float(value) + if not isinstance(value, float): + self.error(obj, value) + return _validate_bounds(self, obj, value) + + +class CFloat(Float): + """A casting version of the float trait.""" + + def validate(self, obj, value): + try: + value = float(value) + except: + self.error(obj, value) + return _validate_bounds(self, obj, value) + + +class Complex(TraitType): + """A trait for complex numbers.""" + + default_value = 0.0 + 0.0j + info_text = 'a complex number' + + def validate(self, obj, value): + if isinstance(value, complex): + return value + if isinstance(value, (float, int)): + return complex(value) + self.error(obj, value) + + +class CComplex(Complex): + """A casting version of the complex number trait.""" + + def validate (self, obj, value): + try: + return complex(value) + except: + self.error(obj, value) + +# We should always be explicit about whether we're using bytes or unicode, both +# for Python 3 conversion and for reliable unicode behaviour on Python 2. So +# we don't have a Str type. +class Bytes(TraitType): + """A trait for byte strings.""" + + default_value = b'' + info_text = 'a bytes object' + + def validate(self, obj, value): + if isinstance(value, bytes): + return value + self.error(obj, value) + + +class CBytes(Bytes): + """A casting version of the byte string trait.""" + + def validate(self, obj, value): + try: + return bytes(value) + except: + self.error(obj, value) + + +class Unicode(TraitType): + """A trait for unicode strings.""" + + default_value = u'' + info_text = 'a unicode string' + + def validate(self, obj, value): + if isinstance(value, six.text_type): + return value + if isinstance(value, bytes): + try: + return value.decode('ascii', 'strict') + except UnicodeDecodeError: + msg = "Could not decode {!r} for unicode trait '{}' of {} instance." + raise TraitError(msg.format(value, self.name, class_of(obj))) + self.error(obj, value) + + +class CUnicode(Unicode): + """A casting version of the unicode trait.""" + + def validate(self, obj, value): + try: + return six.text_type(value) + except: + self.error(obj, value) + + +class ObjectName(TraitType): + """A string holding a valid object name in this version of Python. + + This does not check that the name exists in any scope.""" + info_text = "a valid object identifier in Python" + + if six.PY2: + # Python 2: + def coerce_str(self, obj, value): + "In Python 2, coerce ascii-only unicode to str" + if isinstance(value, unicode): + try: + return str(value) + except UnicodeEncodeError: + self.error(obj, value) + return value + else: + coerce_str = staticmethod(lambda _,s: s) + + def validate(self, obj, value): + value = self.coerce_str(obj, value) + + if isinstance(value, six.string_types) and isidentifier(value): + return value + self.error(obj, value) + +class DottedObjectName(ObjectName): + """A string holding a valid dotted object name in Python, such as A.b3._c""" + def validate(self, obj, value): + value = self.coerce_str(obj, value) + + if isinstance(value, six.string_types) and all(isidentifier(a) + for a in value.split('.')): + return value + self.error(obj, value) + + +class Bool(TraitType): + """A boolean (True, False) trait.""" + + default_value = False + info_text = 'a boolean' + + def validate(self, obj, value): + if isinstance(value, bool): + return value + self.error(obj, value) + + +class CBool(Bool): + """A casting version of the boolean trait.""" + + def validate(self, obj, value): + try: + return bool(value) + except: + self.error(obj, value) + + +class Enum(TraitType): + """An enum whose value must be in a given sequence.""" + + def __init__(self, values, default_value=Undefined, **kwargs): + self.values = values + if kwargs.get('allow_none', False) and default_value is Undefined: + default_value = None + super(Enum, self).__init__(default_value, **kwargs) + + def validate(self, obj, value): + if value in self.values: + return value + self.error(obj, value) + + def info(self): + """ Returns a description of the trait.""" + result = 'any of ' + repr(self.values) + if self.allow_none: + return result + ' or None' + return result + +class CaselessStrEnum(Enum): + """An enum of strings where the case should be ignored.""" + + def __init__(self, values, default_value=Undefined, **kwargs): + values = [cast_unicode_py2(value) for value in values] + super(CaselessStrEnum, self).__init__(values, default_value=default_value, **kwargs) + + def validate(self, obj, value): + if isinstance(value, str): + value = cast_unicode_py2(value) + if not isinstance(value, six.string_types): + self.error(obj, value) + + for v in self.values: + if v.lower() == value.lower(): + return v + self.error(obj, value) + +class Container(Instance): + """An instance of a container (list, set, etc.) + + To be subclassed by overriding klass. + """ + klass = None + _cast_types = () + _valid_defaults = SequenceTypes + _trait = None + + def __init__(self, trait=None, default_value=None, **kwargs): + """Create a container trait type from a list, set, or tuple. + + The default value is created by doing ``List(default_value)``, + which creates a copy of the ``default_value``. + + ``trait`` can be specified, which restricts the type of elements + in the container to that TraitType. + + If only one arg is given and it is not a Trait, it is taken as + ``default_value``: + + ``c = List([1, 2, 3])`` + + Parameters + ---------- + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. If unspecified, + types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Trait. Must be list/tuple/set, and + will be cast to the container type. + + allow_none : bool [ default False ] + Whether to allow the value to be None + + **kwargs : any + further keys for extensions to the Trait (e.g. config) + + """ + # allow List([values]): + if default_value is None and not is_trait(trait): + default_value = trait + trait = None + + if default_value is None: + args = () + elif isinstance(default_value, self._valid_defaults): + args = (default_value,) + else: + raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) + + if is_trait(trait): + if isinstance(trait, type): + warn("Traits should be given as instances, not types (for example, `Int()`, not `Int`)." + " Passing types is deprecated in traitlets 4.1.", + DeprecationWarning, stacklevel=3) + self._trait = trait() if isinstance(trait, type) else trait + elif trait is not None: + raise TypeError("`trait` must be a Trait or None, got %s" % repr_type(trait)) + + 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) + value = super(Container, self).validate(obj, value) + if value is None: + return value + + value = self.validate_elements(obj, value) + + return value + + def validate_elements(self, obj, value): + validated = [] + if self._trait is None or isinstance(self._trait, Any): + return value + for v in value: + try: + v = self._trait._validate(obj, v) + except TraitError: + self.element_error(obj, v, self._trait) + else: + validated.append(v) + return self.klass(validated) + + def class_init(self, cls, name): + if isinstance(self._trait, TraitType): + self._trait.class_init(cls, None) + super(Container, self).class_init(cls, name) + + def instance_init(self, obj): + if isinstance(self._trait, TraitType): + self._trait.instance_init(obj) + super(Container, self).instance_init(obj) + + +class List(Container): + """An instance of a Python list.""" + klass = list + _cast_types = (tuple,) + + def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, **kwargs): + """Create a List trait type from a list, set, or tuple. + + The default value is created by doing ``list(default_value)``, + which creates a copy of the ``default_value``. + + ``trait`` can be specified, which restricts the type of elements + in the container to that TraitType. + + If only one arg is given and it is not a Trait, it is taken as + ``default_value``: + + ``c = List([1, 2, 3])`` + + Parameters + ---------- + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. + If unspecified, types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Trait. Must be list/tuple/set, and + will be cast to the container type. + + minlen : Int [ default 0 ] + The minimum length of the input list + + maxlen : Int [ default sys.maxsize ] + The maximum length of the input list + """ + self._minlen = minlen + self._maxlen = maxlen + super(List, self).__init__(trait=trait, default_value=default_value, + **kwargs) + + def length_error(self, obj, value): + e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \ + % (self.name, class_of(obj), self._minlen, self._maxlen, value) + raise TraitError(e) + + def validate_elements(self, obj, value): + length = len(value) + if length < self._minlen or length > self._maxlen: + self.length_error(obj, value) + + return super(List, self).validate_elements(obj, value) + + def validate(self, obj, value): + value = super(List, self).validate(obj, value) + value = self.validate_elements(obj, value) + return value + + +class Set(List): + """An instance of a Python set.""" + klass = set + _cast_types = (tuple, list) + + # Redefine __init__ just to make the docstring more accurate. + def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, + **kwargs): + """Create a Set trait type from a list, set, or tuple. + + The default value is created by doing ``set(default_value)``, + which creates a copy of the ``default_value``. + + ``trait`` can be specified, which restricts the type of elements + in the container to that TraitType. + + If only one arg is given and it is not a Trait, it is taken as + ``default_value``: + + ``c = Set({1, 2, 3})`` + + Parameters + ---------- + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. + If unspecified, types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Trait. Must be list/tuple/set, and + will be cast to the container type. + + minlen : Int [ default 0 ] + The minimum length of the input list + + maxlen : Int [ default sys.maxsize ] + The maximum length of the input list + """ + super(Set, self).__init__(trait, default_value, minlen, maxlen, **kwargs) + + +class Tuple(Container): + """An instance of a Python tuple.""" + klass = tuple + _cast_types = (list,) + + def __init__(self, *traits, **kwargs): + """Create a tuple from a list, set, or tuple. + + Create a fixed-type tuple with Traits: + + ``t = Tuple(Int(), Str(), CStr())`` + + would be length 3, with Int,Str,CStr for each element. + + If only one arg is given and it is not a Trait, it is taken as + default_value: + + ``t = Tuple((1, 2, 3))`` + + Otherwise, ``default_value`` *must* be specified by keyword. + + Parameters + ---------- + + `*traits` : TraitTypes [ optional ] + the types for restricting the contents of the Tuple. If unspecified, + types are not checked. If specified, then each positional argument + corresponds to an element of the tuple. Tuples defined with traits + are of fixed length. + + default_value : SequenceType [ optional ] + The default value for the Tuple. Must be list/tuple/set, and + will be cast to a tuple. If ``traits`` are specified, + ``default_value`` must conform to the shape and type they specify. + """ + default_value = kwargs.pop('default_value', Undefined) + # allow Tuple((values,)): + if len(traits) == 1 and default_value is Undefined and not is_trait(traits[0]): + default_value = traits[0] + traits = () + + if default_value is Undefined: + args = () + elif isinstance(default_value, self._valid_defaults): + args = (default_value,) + else: + raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) + + self._traits = [] + for trait in traits: + if isinstance(trait, type): + warn("Traits should be given as instances, not types (for example, `Int()`, not `Int`)" + " Passing types is deprecated in traitlets 4.1.", + DeprecationWarning, stacklevel=2) + t = trait() if isinstance(trait, type) else trait + self._traits.append(t) + + if self._traits and default_value is None: + # don't allow default to be an empty container if length is specified + args = None + super(Container,self).__init__(klass=self.klass, args=args, **kwargs) + + def validate_elements(self, obj, value): + if not self._traits: + # nothing to validate + return value + if len(value) != len(self._traits): + e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \ + % (self.name, class_of(obj), len(self._traits), repr_type(value)) + raise TraitError(e) + + validated = [] + for t, v in zip(self._traits, value): + try: + v = t._validate(obj, v) + except TraitError: + self.element_error(obj, v, t) + else: + validated.append(v) + return tuple(validated) + + def class_init(self, cls, name): + for trait in self._traits: + if isinstance(trait, TraitType): + trait.class_init(cls, None) + super(Container, self).class_init(cls, name) + + def instance_init(self, obj): + for trait in self._traits: + if isinstance(trait, TraitType): + trait.instance_init(obj) + super(Container, self).instance_init(obj) + + +class Dict(Instance): + """An instance of a Python dict.""" + _trait = None + + def __init__(self, trait=None, traits=None, default_value=Undefined, + **kwargs): + """Create a dict trait type from a Python dict. + + The default value is created by doing ``dict(default_value)``, + which creates a copy of the ``default_value``. + + Parameters + ---------- + + trait : TraitType [ optional ] + The specified trait type to check and use to restrict contents of + the Container. If unspecified, trait types are not checked. + + traits : Dictionary of trait types [ optional ] + A Python dictionary containing the types that are valid for + restricting the content of the Dict Container for certain keys. + + default_value : SequenceType [ optional ] + The default value for the Dict. Must be dict, tuple, or None, and + will be cast to a dict if not None. If `trait` is specified, the + `default_value` must conform to the constraints it specifies. + """ + # Handling positional arguments + if default_value is Undefined and trait is not None: + if not is_trait(trait): + default_value = trait + trait = None + + # Handling default value + if default_value is Undefined: + default_value = {} + if default_value is None: + args = None + elif isinstance(default_value, dict): + args = (default_value,) + elif isinstance(default_value, SequenceTypes): + args = (default_value,) + else: + raise TypeError('default value of Dict was %s' % default_value) + + # Case where a type of TraitType is provided rather than an instance + if is_trait(trait): + if isinstance(trait, type): + warn("Traits should be given as instances, not types (for example, `Int()`, not `Int`)" + " Passing types is deprecated in traitlets 4.1.", + DeprecationWarning, stacklevel=2) + self._trait = trait() if isinstance(trait, type) else trait + elif trait is not None: + raise TypeError("`trait` must be a Trait or None, got %s" % repr_type(trait)) + + self._traits = traits + + super(Dict, self).__init__(klass=dict, 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): + value = super(Dict, self).validate(obj, value) + if value is None: + return value + value = self.validate_elements(obj, value) + return value + + def validate_elements(self, obj, value): + use_dict = bool(self._traits) + default_to = (self._trait or Any()) + if not use_dict and isinstance(default_to, Any): + return value + + validated = {} + for key in value: + if use_dict and key in self._traits: + validate_with = self._traits[key] + else: + validate_with = default_to + try: + v = value[key] + if not isinstance(validate_with, Any): + v = validate_with._validate(obj, v) + except TraitError: + self.element_error(obj, v, validate_with) + else: + validated[key] = v + + return self.klass(validated) + + def class_init(self, cls, name): + if isinstance(self._trait, TraitType): + self._trait.class_init(cls, None) + if self._traits is not None: + for trait in self._traits.values(): + trait.class_init(cls, None) + super(Dict, self).class_init(cls, name) + + def instance_init(self, obj): + if isinstance(self._trait, TraitType): + self._trait.instance_init(obj) + if self._traits is not None: + for trait in self._traits.values(): + trait.instance_init(obj) + super(Dict, self).instance_init(obj) + + +class TCPAddress(TraitType): + """A trait for an (ip, port) tuple. + + This allows for both IPv4 IP addresses as well as hostnames. + """ + + default_value = ('127.0.0.1', 0) + info_text = 'an (ip, port) tuple' + + def validate(self, obj, value): + if isinstance(value, tuple): + if len(value) == 2: + if isinstance(value[0], six.string_types) and isinstance(value[1], int): + port = value[1] + if port >= 0 and port <= 65535: + return value + self.error(obj, value) + +class CRegExp(TraitType): + """A casting compiled regular expression trait. + + Accepts both strings and compiled regular expressions. The resulting + attribute will be a compiled regular expression.""" + + info_text = 'a regular expression' + + def validate(self, obj, value): + try: + return re.compile(value) + except: + self.error(obj, value) + + +class UseEnum(TraitType): + """Use a Enum class as model for the data type description. + Note that if no default-value is provided, the first enum-value is used + as default-value. + + .. sourcecode:: python + + # -- SINCE: Python 3.4 (or install backport: pip install enum34) + import enum + from traitlets import HasTraits, UseEnum + + class Color(enum.Enum): + red = 1 # -- IMPLICIT: default_value + blue = 2 + green = 3 + + class MyEntity(HasTraits): + color = UseEnum(Color, default_value=Color.blue) + + entity = MyEntity(color=Color.red) + entity.color = Color.green # USE: Enum-value (preferred) + entity.color = "green" # USE: name (as string) + entity.color = "Color.green" # USE: scoped-name (as string) + entity.color = 3 # USE: number (as int) + assert entity.color is Color.green + """ + default_value = None + info_text = "Trait type adapter to a Enum class" + + def __init__(self, enum_class, default_value=None, **kwargs): + assert issubclass(enum_class, enum.Enum), \ + "REQUIRE: enum.Enum, but was: %r" % enum_class + allow_none = kwargs.get("allow_none", False) + if default_value is None and not allow_none: + default_value = list(enum_class.__members__.values())[0] + super(UseEnum, self).__init__(default_value=default_value, **kwargs) + self.enum_class = enum_class + self.name_prefix = enum_class.__name__ + "." + + def select_by_number(self, value, default=Undefined): + """Selects enum-value by using its number-constant.""" + assert isinstance(value, int) + enum_members = self.enum_class.__members__ + for enum_item in enum_members.values(): + if enum_item.value == value: + return enum_item + # -- NOT FOUND: + return default + + def select_by_name(self, value, default=Undefined): + """Selects enum-value by using its name or scoped-name.""" + assert isinstance(value, six.string_types) + if value.startswith(self.name_prefix): + # -- SUPPORT SCOPED-NAMES, like: "Color.red" => "red" + value = value.replace(self.name_prefix, "", 1) + return self.enum_class.__members__.get(value, default) + + def validate(self, obj, value): + if isinstance(value, self.enum_class): + return value + elif isinstance(value, int): + # -- CONVERT: number => enum_value (item) + value2 = self.select_by_number(value) + if value2 is not Undefined: + return value2 + elif isinstance(value, six.string_types): + # -- CONVERT: name or scoped_name (as string) => enum_value (item) + value2 = self.select_by_name(value) + if value2 is not Undefined: + return value2 + elif value is None: + if self.allow_none: + return None + else: + return self.default_value + self.error(obj, value) + + def info(self): + """Returns a description of this Enum trait (in case of errors).""" + result = "Any of: %s" % ", ".join(self.enum_class.__members__.keys()) + if self.allow_none: + return result + " or None" + return result diff --git a/contrib/python/traitlets/py2/traitlets/utils/__init__.py b/contrib/python/traitlets/py2/traitlets/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/utils/__init__.py diff --git a/contrib/python/traitlets/py2/traitlets/utils/bunch.py b/contrib/python/traitlets/py2/traitlets/utils/bunch.py new file mode 100644 index 0000000000..2edb830ad6 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/utils/bunch.py @@ -0,0 +1,25 @@ +"""Yet another implementation of bunch + +attribute-access of items on a dict. +""" + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +class Bunch(dict): + """A dict with attribute-access""" + def __getattr__(self, key): + try: + return self.__getitem__(key) + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, value): + self.__setitem__(key, value) + + def __dir__(self): + # py2-compat: can't use super because dict doesn't have __dir__ + names = dir({}) + names.extend(self.keys()) + return names + diff --git a/contrib/python/traitlets/py2/traitlets/utils/getargspec.py b/contrib/python/traitlets/py2/traitlets/utils/getargspec.py new file mode 100644 index 0000000000..0a047379fe --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/utils/getargspec.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" + getargspec excerpted from: + + sphinx.util.inspect + ~~~~~~~~~~~~~~~~~~~ + Helpers for inspecting Python modules. + :copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import inspect +from six import PY3 + +# Unmodified from sphinx below this line + +if PY3: + from functools import partial + + def getargspec(func): + """Like inspect.getargspec but supports functools.partial as well.""" + if inspect.ismethod(func): + func = func.__func__ + if type(func) is partial: + orig_func = func.func + argspec = getargspec(orig_func) + args = list(argspec[0]) + defaults = list(argspec[3] or ()) + kwoargs = list(argspec[4]) + kwodefs = dict(argspec[5] or {}) + if func.args: + args = args[len(func.args):] + for arg in func.keywords or (): + try: + i = args.index(arg) - len(args) + del args[i] + try: + del defaults[i] + except IndexError: + pass + except ValueError: # must be a kwonly arg + i = kwoargs.index(arg) + del kwoargs[i] + del kwodefs[arg] + return inspect.FullArgSpec(args, argspec[1], argspec[2], + tuple(defaults), kwoargs, + kwodefs, argspec[6]) + while hasattr(func, '__wrapped__'): + func = func.__wrapped__ + if not inspect.isfunction(func): + raise TypeError('%r is not a Python function' % func) + return inspect.getfullargspec(func) + +else: # 2.6, 2.7 + from functools import partial + + def getargspec(func): + """Like inspect.getargspec but supports functools.partial as well.""" + if inspect.ismethod(func): + func = func.__func__ + parts = 0, () + if type(func) is partial: + keywords = func.keywords + if keywords is None: + keywords = {} + parts = len(func.args), keywords.keys() + func = func.func + if not inspect.isfunction(func): + raise TypeError('%r is not a Python function' % func) + args, varargs, varkw = inspect.getargs(func.__code__) + func_defaults = func.__defaults__ + if func_defaults is None: + func_defaults = [] + else: + func_defaults = list(func_defaults) + if parts[0]: + args = args[parts[0]:] + if parts[1]: + for arg in parts[1]: + i = args.index(arg) - len(args) + del args[i] + try: + del func_defaults[i] + except IndexError: + pass + return inspect.ArgSpec(args, varargs, varkw, func_defaults) diff --git a/contrib/python/traitlets/py2/traitlets/utils/importstring.py b/contrib/python/traitlets/py2/traitlets/utils/importstring.py new file mode 100644 index 0000000000..5b4f643f41 --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/utils/importstring.py @@ -0,0 +1,42 @@ +# encoding: utf-8 +""" +A simple utility to import something by its string name. +""" +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from ipython_genutils.py3compat import cast_bytes_py2 +from six import string_types + +def import_item(name): + """Import and return ``bar`` given the string ``foo.bar``. + + Calling ``bar = import_item("foo.bar")`` is the functional equivalent of + executing the code ``from foo import bar``. + + Parameters + ---------- + name : string + The fully qualified name of the module/package being imported. + + Returns + ------- + mod : module object + The module that was imported. + """ + if not isinstance(name, string_types): + raise TypeError("import_item accepts strings, not '%s'." % type(name)) + name = cast_bytes_py2(name) + parts = name.rsplit('.', 1) + if len(parts) == 2: + # called with 'foo.bar....' + package, obj = parts + module = __import__(package, fromlist=[obj]) + try: + pak = getattr(module, obj) + except AttributeError: + raise ImportError('No module named %s' % obj) + return pak + else: + # called with un-dotted string + return __import__(parts[0]) diff --git a/contrib/python/traitlets/py2/traitlets/utils/sentinel.py b/contrib/python/traitlets/py2/traitlets/utils/sentinel.py new file mode 100644 index 0000000000..dc57a2591c --- /dev/null +++ b/contrib/python/traitlets/py2/traitlets/utils/sentinel.py @@ -0,0 +1,17 @@ +"""Sentinel class for constants with useful reprs""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +class Sentinel(object): + + def __init__(self, name, module, docstring=None): + self.name = name + self.module = module + if docstring: + self.__doc__ = docstring + + + def __repr__(self): + return str(self.module)+'.'+self.name + diff --git a/contrib/python/traitlets/py2/ya.make b/contrib/python/traitlets/py2/ya.make new file mode 100644 index 0000000000..3c26d78a20 --- /dev/null +++ b/contrib/python/traitlets/py2/ya.make @@ -0,0 +1,48 @@ +# Generated by devtools/yamaker (pypi). + +PY2_LIBRARY() + +PROVIDES(python_traitlets) + +VERSION(4.3.3) + +LICENSE(BSD-3-Clause) + +PEERDIR( + contrib/deprecated/python/enum34 + contrib/python/decorator + contrib/python/ipython-genutils + contrib/python/six +) + +NO_LINT() + +PY_SRCS( + TOP_LEVEL + traitlets/__init__.py + traitlets/_version.py + traitlets/config/__init__.py + traitlets/config/application.py + traitlets/config/configurable.py + traitlets/config/loader.py + traitlets/config/manager.py + traitlets/log.py + traitlets/traitlets.py + traitlets/utils/__init__.py + traitlets/utils/bunch.py + traitlets/utils/getargspec.py + traitlets/utils/importstring.py + traitlets/utils/sentinel.py +) + +RESOURCE_FILES( + PREFIX contrib/python/traitlets/py2/ + .dist-info/METADATA + .dist-info/top_level.txt +) + +END() + +RECURSE_FOR_TESTS( + tests +) |