diff -Nru traitlets-4.0.0/CONTRIBUTING.md traitlets-4.3.2/CONTRIBUTING.md --- traitlets-4.0.0/CONTRIBUTING.md 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/CONTRIBUTING.md 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,3 @@ +# Contributing + +We follow the [IPython Contributing Guide](https://github.com/ipython/ipython/blob/master/CONTRIBUTING.md). diff -Nru traitlets-4.0.0/COPYING.md traitlets-4.3.2/COPYING.md --- traitlets-4.0.0/COPYING.md 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/COPYING.md 2017-02-23 10:21:19.000000000 +0000 @@ -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 -Nru traitlets-4.0.0/debian/changelog traitlets-4.3.2/debian/changelog --- traitlets-4.0.0/debian/changelog 2015-09-28 09:05:14.000000000 +0000 +++ traitlets-4.3.2/debian/changelog 2017-09-28 12:43:26.000000000 +0000 @@ -1,3 +1,57 @@ +traitlets (4.3.2-1ubuntu1~xenial1) xenial; urgency=medium + + * Backport latest debian package. + + -- Paul McEnery Thu, 28 Sep 2017 13:42:59 +0100 + +traitlets (4.3.2-1) unstable; urgency=medium + + * New upstream release. + * Update debian/watch. + * Push standards-version to 4.0.0. + + -- Julien Puydt Mon, 03 Jul 2017 16:35:14 +0200 + +traitlets (4.3.1-1) unstable; urgency=medium + + * Marked the -doc package as Multi-Arch:foreign (tracker hint). + * Pushed dh compat to 10. + * New upstream release. + * Added python-enum34, python-pytest, python3-pytest and + python-sphinx-rtd-theme to the deps. + * Added the upstream changelog to the packages. + * Forced dep on python-enum34 for python-traitlets. + + -- Julien Puydt Sun, 02 Oct 2016 15:50:47 +0200 + +traitlets (4.2.2-2) unstable; urgency=medium + + * Added a patch to intersphinx configuration to disable network + access. (Closes: #830628) + + -- Julien Puydt Sun, 10 Jul 2016 08:27:57 +0200 + +traitlets (4.2.2-1) unstable; urgency=medium + + * New upstream release + * Pushed standards-version to 3.9.8 + * Completed debian/copyright + + -- Julien Puydt Sun, 03 Jul 2016 11:36:13 +0200 + +traitlets (4.2.1-1) unstable; urgency=medium + + [ Ondřej Nový ] + * Fixed VCS URL (https) + + [ Julien Puydt ] + * New upstream version + * Pushed standards-version to 3.9.7 + * Added setuptools, decorator and mock as build-dep + * Simplified d/rules wrt to tests (let dh magic work) + + -- Julien Puydt Sun, 17 Apr 2016 09:35:01 +0200 + traitlets (4.0.0-1) unstable; urgency=medium * Initial release. (Closes: #799242) diff -Nru traitlets-4.0.0/debian/compat traitlets-4.3.2/debian/compat --- traitlets-4.0.0/debian/compat 2015-09-28 09:05:14.000000000 +0000 +++ traitlets-4.3.2/debian/compat 2017-07-03 14:35:14.000000000 +0000 @@ -1 +1 @@ -9 +10 diff -Nru traitlets-4.0.0/debian/control traitlets-4.3.2/debian/control --- traitlets-4.0.0/debian/control 2015-09-28 09:05:14.000000000 +0000 +++ traitlets-4.3.2/debian/control 2017-07-03 14:35:14.000000000 +0000 @@ -3,18 +3,36 @@ Uploaders: Julien Puydt Section: python Priority: optional -Standards-Version: 3.9.6 +Standards-Version: 4.0.0 Homepage: https://github.com/ipython/traitlets -Build-Depends: bc, debhelper (>= 9), dh-python, python, python3-all, python-sphinx, - python-nose, python3-nose, python-ipython-genutils, python3-ipython-genutils +Build-Depends: bc, + debhelper (>= 10), + dh-python, + python, + python-decorator, + python-enum34, + python-ipython-genutils, + python-mock, + python-nose, + python-pytest, + python-setuptools, + python-sphinx, + python-sphinx-rtd-theme, + python3-all, + python3-decorator, + python3-ipython-genutils, + python3-mock, + python3-nose, + python3-pytest, + python3-setuptools X-Python-Version: >= 2.7 X-Python3-Version: >= 3.2 -Vcs-Git: git://anonscm.debian.org/python-modules/packages/traitlets.git -Vcs-Browser: http://anonscm.debian.org/cgit/python-modules/packages/traitlets.git +Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/traitlets.git +Vcs-Browser: https://anonscm.debian.org/cgit/python-modules/packages/traitlets.git Package: python-traitlets Architecture: all -Depends: ${misc:Depends}, ${python:Depends} +Depends: ${misc:Depends}, ${python:Depends}, python-enum34 Description: Lightweight Traits-like package for Python 2 A lightweight pure-Python derivative of Enthought Traits, used for configuring Python objects. @@ -37,8 +55,8 @@ Package: python-traitlets-doc Section: doc Architecture: all -Depends: ${sphinxdoc:Depends}, - ${misc:Depends}, +Multi-Arch: foreign +Depends: ${misc:Depends}, ${sphinxdoc:Depends} Description: Lightweight Traits-like package for Python A lightweight pure-Python derivative of Enthought Traits, used for configuring Python objects. diff -Nru traitlets-4.0.0/debian/copyright traitlets-4.3.2/debian/copyright --- traitlets-4.0.0/debian/copyright 2015-09-28 09:05:14.000000000 +0000 +++ traitlets-4.3.2/debian/copyright 2017-07-03 14:35:14.000000000 +0000 @@ -5,6 +5,10 @@ Copyright: 2001-2015, IPython Development Team License: BSD-3-clause +Files: traitlets/utils/getargspec.py +Copyright: 2007-2015 Sphinx team +License: BSD-3-clause + Files: debian/* Copyright: 2015 Julien Puydt License: BSD-3-clause diff -Nru traitlets-4.0.0/debian/.git-dpm traitlets-4.3.2/debian/.git-dpm --- traitlets-4.0.0/debian/.git-dpm 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/debian/.git-dpm 2017-07-03 14:35:14.000000000 +0000 @@ -0,0 +1,11 @@ +# see git-dpm(1) from git-dpm package +0e0f52a17f9a139fb650b6cfbca68cdf8f6fed56 +0e0f52a17f9a139fb650b6cfbca68cdf8f6fed56 +4a82de264df400c92fab25103a343857a04f12e3 +4a82de264df400c92fab25103a343857a04f12e3 +traitlets_4.3.2.orig.tar.gz +b5fe8695f0568ac6582dc41151a731cee4596894 +92130 +debianTag="debian/%e%v" +patchedTag="patched/%e%v" +upstreamTag="upstream/%e%u" diff -Nru traitlets-4.0.0/debian/patches/0001-use-setuptools-for-everything-so-python-Depends-work.patch traitlets-4.3.2/debian/patches/0001-use-setuptools-for-everything-so-python-Depends-work.patch --- traitlets-4.0.0/debian/patches/0001-use-setuptools-for-everything-so-python-Depends-work.patch 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/debian/patches/0001-use-setuptools-for-everything-so-python-Depends-work.patch 2017-07-03 14:35:14.000000000 +0000 @@ -0,0 +1,24 @@ +From 2c5ad267a7eddcc757eceda817098612c805cbdd Mon Sep 17 00:00:00 2001 +From: Julien Puydt +Date: Tue, 6 Oct 2015 08:25:56 +0200 +Subject: use setuptools for everything (so python*:Depends works) + +Forwarded: no +--- + setup.py | 3 +-- + 1 file changed, 1 insertion(+), 2 deletions(-) + +diff --git a/setup.py b/setup.py +index b7245db..e8fdd75 100644 +--- a/setup.py ++++ b/setup.py +@@ -73,8 +73,7 @@ setup_args = dict( + ], + ) + +-if 'develop' in sys.argv or any(a.startswith('bdist') for a in sys.argv): +- import setuptools ++import setuptools + + setuptools_args = {} + diff -Nru traitlets-4.0.0/debian/patches/0002-Patch-intersphinx-configuration-to-avoid-network-acc.patch traitlets-4.3.2/debian/patches/0002-Patch-intersphinx-configuration-to-avoid-network-acc.patch --- traitlets-4.0.0/debian/patches/0002-Patch-intersphinx-configuration-to-avoid-network-acc.patch 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/debian/patches/0002-Patch-intersphinx-configuration-to-avoid-network-acc.patch 2017-07-03 14:35:14.000000000 +0000 @@ -0,0 +1,29 @@ +From 0e0f52a17f9a139fb650b6cfbca68cdf8f6fed56 Mon Sep 17 00:00:00 2001 +From: Julien Puydt +Date: Sun, 10 Jul 2016 08:24:44 +0200 +Subject: Patch intersphinx configuration to avoid network access + +--- + docs/source/conf.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/docs/source/conf.py b/docs/source/conf.py +index 3f97482..289b9ee 100644 +--- a/docs/source/conf.py ++++ b/docs/source/conf.py +@@ -312,7 +312,7 @@ texinfo_documents = [ + + + # Example configuration for intersphinx: refer to the Python standard library. +-intersphinx_mapping = {'https://docs.python.org/': None} ++intersphinx_mapping = {'/usr/share/doc/python-doc/html': None} + + # Read The Docs + # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org +@@ -322,4 +322,4 @@ if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +-# otherwise, readthedocs.org uses their theme by default, so no need to specify it +\ No newline at end of file ++# otherwise, readthedocs.org uses their theme by default, so no need to specify it diff -Nru traitlets-4.0.0/debian/patches/fix_distutils traitlets-4.3.2/debian/patches/fix_distutils --- traitlets-4.0.0/debian/patches/fix_distutils 2015-09-28 09:05:14.000000000 +0000 +++ traitlets-4.3.2/debian/patches/fix_distutils 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -Description: Fix distutils detection of required deps (hence the package's!) -Author: Julien Puydt -Forwarded: yes (upstream issue #95) ---- a/setup.py -+++ b/setup.py -@@ -76,7 +76,7 @@ - - setuptools_args = {} - --install_requires = setuptools_args['install_requires'] = [ -+setup_args['requires'] = setuptools_args['install_requires'] = [ - 'ipython_genutils', - 'decorator', - ] diff -Nru traitlets-4.0.0/debian/patches/series traitlets-4.3.2/debian/patches/series --- traitlets-4.0.0/debian/patches/series 2015-09-28 09:05:14.000000000 +0000 +++ traitlets-4.3.2/debian/patches/series 2017-07-03 14:35:14.000000000 +0000 @@ -1 +1,2 @@ -fix_distutils +0001-use-setuptools-for-everything-so-python-Depends-work.patch +0002-Patch-intersphinx-configuration-to-avoid-network-acc.patch diff -Nru traitlets-4.0.0/debian/rules traitlets-4.3.2/debian/rules --- traitlets-4.0.0/debian/rules 2015-09-28 09:05:14.000000000 +0000 +++ traitlets-4.3.2/debian/rules 2017-07-03 14:35:14.000000000 +0000 @@ -3,16 +3,15 @@ #export DH_VERBOSE=1 export PYBUILD_NAME=traitlets export LC_ALL=C.UTF-8 -export PYBUILD_TEST_ARGS={interpreter} -m unittest discover -s traitlets/tests -v %: dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild -override_dh_auto_test: - PYBUILD_SYSTEM=custom dh_auto_test - override_dh_sphinxdoc: ifeq (,$(findstring nodocs, $(DEB_BUILD_OPTIONS))) PYTHONPATH=. sphinx-build -b html docs/source debian/python-traitlets-doc/usr/share/doc/python-traitlets-doc/html dh_sphinxdoc -O--buildsystem=pybuild endif + +override_dh_installchangelogs: + dh_installchangelogs docs/source/changelog.rst diff -Nru traitlets-4.0.0/debian/watch traitlets-4.3.2/debian/watch --- traitlets-4.0.0/debian/watch 2015-09-28 09:05:14.000000000 +0000 +++ traitlets-4.3.2/debian/watch 2017-07-03 14:35:14.000000000 +0000 @@ -1,3 +1,2 @@ version=3 -opts=uversionmangle=s/\.(dev[0-9]*)/~$1/ \ -http://pypi.debian.net/traitlets/traitlets-(.*)\.tar\.gz +https://github.com/ipython/traitlets/tags .*/archive/([\d\.-]+)\.tar\.gz diff -Nru traitlets-4.0.0/docs/environment.yml traitlets-4.3.2/docs/environment.yml --- traitlets-4.0.0/docs/environment.yml 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/docs/environment.yml 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,9 @@ +name: traitlets +channels: + - conda-forge + - conda +dependencies: + - python=3 + - ipython_genutils + - sphinx + - sphinx_rtd_theme diff -Nru traitlets-4.0.0/docs/readme-docs.md traitlets-4.3.2/docs/readme-docs.md --- traitlets-4.0.0/docs/readme-docs.md 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/docs/readme-docs.md 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,70 @@ +# Documenting traitlets + + +[Documentation for `traitlets`](https://traitlets.readthedocs.io/en/latest/) +is hosted on ReadTheDocs. + + +## Build documentation locally + +#### Change directory to documentation root: + + cd docs + +#### Create environment + +* [**conda**] Create conda env (and install relevant dependencies): + + conda env create -f environment.yml + +* [**pip**] Create virtual environment (and install relevant dependencies): + + virtualenv traitlets_docs -p python3 + pip install -r requirements.txt + +#### Activate the newly built environment `traitlets_docs` + +* [**conda**] Activate conda env: + + source activate traitlets_docs + +* [**pip**] The virtualenv should have been automatically activated. If + not: + + source activate + +#### Build documentation using: + +* Makefile for Linux and OS X: + + make html + +* make.bat for Windows: + + make.bat html + + +#### Display the documentation locally + +* Navigate to `build/html/index.html` in your browser. + +* Or alternatively you may run a local server to display + the docs. In Python 3: + + python -m http.server 8000 + + In your browser, go to `http://localhost:8000`. + + +## Developing Documentation + +[Jupyter documentation guide](https://jupyter.readthedocs.io/en/latest/contrib_docs/index.html) + + +## Helpful files and directories + +* `source/conf.py` - Sphinx build configuration file +* `source` directory - source for documentation +* `source/index.rst` - Main landing page of the Sphinx documentation +* `requirements.txt` - list of packages to install when using pip +* `environment.yml` - list of packages to install when using conda \ No newline at end of file diff -Nru traitlets-4.0.0/docs/requirements.txt traitlets-4.3.2/docs/requirements.txt --- traitlets-4.0.0/docs/requirements.txt 2015-06-18 16:46:16.000000000 +0000 +++ traitlets-4.3.2/docs/requirements.txt 2017-02-23 10:21:19.000000000 +0000 @@ -1 +1,4 @@ ipython_genutils +Sphinx +sphinx_rtd_theme +-e ../. diff -Nru traitlets-4.0.0/docs/source/api.rst traitlets-4.3.2/docs/source/api.rst --- traitlets-4.0.0/docs/source/api.rst 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/docs/source/api.rst 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,127 @@ +Traitlets API reference +======================= + +.. currentmodule:: traitlets + +Any class with trait attributes must inherit from :class:`HasTraits`. + +.. autoclass:: HasTraits + + .. automethod:: has_trait + + .. automethod:: trait_names + + .. automethod:: class_trait_names + + .. automethod:: traits + + .. automethod:: class_traits + + .. automethod:: trait_metadata + + .. automethod:: add_traits + +You then declare the trait attributes on the class like this:: + + from traitlets import HasTraits, Int, Unicode + + class Requester(HasTraits): + url = Unicode() + timeout = Int(30) # 30 will be the default value + +For the available trait types and the arguments you can give them, see +:doc:`trait_types`. + + +Dynamic default values +---------------------- + +.. autofunction:: default + +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. For example:: + + import getpass + + class Identity(HasTraits): + username = Unicode() + + @default('username') + def _username_default(self): + return getpass.getuser() + + +Callbacks when trait attributes change +-------------------------------------- + +.. autofunction:: observe + +To do something when a trait attribute is changed, decorate a method with :func:`traitlets.observe`. +The method will be called with a single argument, a dictionary of the form:: + + { + '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' + } + +For example:: + + 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)) + + +.. versionchanged:: 4.1 + + The ``_{trait}_changed`` magic method-name approach is deprecated. + +You can also add callbacks to a trait dynamically: + +.. automethod:: HasTraits.observe + +.. note:: + + If a trait attribute with a dynamic default value has another value set + before it is used, the default will not be calculated. + Any callbacks on that trait will will fire, and *old_value* will be ``None``. + +Validating proposed changes +--------------------------- + +.. autofunction:: validate + +Validator methods can be used to enforce certain aspects of a property. +These are called on proposed changes, +and can raise a TraitError if the change should be rejected, +or coerce the value if it should be accepted with some modification. +This can be useful for things such as ensuring a path string is always absolute, +or check if it points to an existing directory. + +For example:: + + from traitlets import HasTraits, Unicode, validate, TraitError + + class TraitletsExample(HasTraits): + path = Unicode('', help="a path") + + @validate('path') + def _check_prime(self, proposal): + path = proposal['value'] + if not path.endswith('/'): + # ensure path always has trailing / + path = path + '/' + if not os.path.exists(path): + raise TraitError("path %r does not exist" % path) + return path + + + diff -Nru traitlets-4.0.0/docs/source/changelog.rst traitlets-4.3.2/docs/source/changelog.rst --- traitlets-4.0.0/docs/source/changelog.rst 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/docs/source/changelog.rst 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,114 @@ +Changes in Traitlets +==================== + +4.3 +--- + +4.3.2 +***** + +`4.3.2 on GitHub`_ + +4.3.2 is a tiny release, relaxing some of the deprecations introduced in 4.1: + +- using :meth:`_traitname_default()` without the ``@default`` decorator is no longer + deprecated. +- Passing ``config=True`` in traitlets constructors is no longer deprecated. + +4.3.1 +***** + +`4.3.1 on GitHub`_ + +- Compatibility fix for Python 3.6a1 +- Fix bug in Application.classes getting extra entries when multiple Applications are instantiated in the same process. + +4.3.0 +***** + +`4.3.0 on GitHub`_ + +- Improve the generated config file output. +- Allow TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR env to override :attr:`Application.raise_config_file_errors`, + so that config file errors can result in exiting immediately. +- Avoid using root logger. If no application logger is registered, + the ``'traitlets'`` logger will be used instead of the root logger. +- Change/Validation arguments are now Bunch objects, allowing attribute-access, + in addition to dictionary access. +- Reduce number of common deprecation messages in certain cases. +- Ensure command-line options always have higher priority than config files. +- Add bounds on numeric traits. +- Improves various error messages. + + +4.2 +--- + +4.2.2 - 2016-07-01 +****************** + +`4.2.2 on GitHub`_ + +Partially revert a change in 4.1 that prevented IPython's command-line options from taking priority over config files. + + +4.2.1 - 2016-03-14 +****************** + +`4.2.1 on GitHub`_ + +Demotes warning about unused arguments in ``HasTraits.__init__`` introduced in 4.2.0 to DeprecationWarning. + +4.2.0 - 2016-03-14 +****************** + +`4.2 on GitHub`_ + +- :class:`JSONFileConfigLoader` can be used as a context manager for updating configuration. +- If a value in config does not map onto a configurable trait, + a message is displayed that the value will have no effect. +- Unused arguments are passed to ``super()`` in ``HasTraits.__init__``, + improving support for multiple inheritance. +- Various bugfixes and improvements in the new API introduced in 4.1. +- Application subclasses may specify ``raise_config_file_errors = True`` + to exit on failure to load config files, + instead of the default of logging the failures. + + +4.1 - 2016-01-15 +---------------- + +`4.1 on GitHub`_ + +Traitlets 4.1 introduces a totally new decorator-based API for configuring traitlets. +Highlights: + +- Decorators are used, rather than magic method names, for registering trait-related methods. See :doc:`using_traitlets` and :doc:`migration` for more info. +- Deprecate ``Trait(config=True)`` in favor of ``Trait().tag(config=True)``. In general, metadata is added via ``tag`` instead of the constructor. + +Other changes: + +- Trait attributes initialized with ``read_only=True`` can only be set with the ``set_trait`` method. + Attempts to directly modify a read-only trait attribute raises a ``TraitError``. +- The directional link now takes an optional `transform` attribute allowing the modification of the value. +- Various fixes and improvements to config-file generation (fixed ordering, Undefined showing up, etc.) +- Warn on unrecognized traits that aren't configurable, to avoid silently ignoring mistyped config. + + +4.0 - 2015-06-19 +---------------- + +`4.0 on GitHub`_ + +First release of traitlets as a standalone package. + + + +.. _`4.0 on GitHub`: https://github.com/ipython/traitlets/milestones/4.0 +.. _`4.1 on GitHub`: https://github.com/ipython/traitlets/milestones/4.1 +.. _`4.2 on GitHub`: https://github.com/ipython/traitlets/milestones/4.2 +.. _`4.2.1 on GitHub`: https://github.com/ipython/traitlets/milestones/4.2.1 +.. _`4.2.2 on GitHub`: https://github.com/ipython/traitlets/milestones/4.2.2 +.. _`4.3.0 on GitHub`: https://github.com/ipython/traitlets/milestones/4.3 +.. _`4.3.1 on GitHub`: https://github.com/ipython/traitlets/milestones/4.3.1 +.. _`4.3.2 on GitHub`: https://github.com/ipython/traitlets/milestones/4.3.2 diff -Nru traitlets-4.0.0/docs/source/config.rst traitlets-4.3.2/docs/source/config.rst --- traitlets-4.0.0/docs/source/config.rst 2015-05-27 22:55:28.000000000 +0000 +++ traitlets-4.3.2/docs/source/config.rst 2017-02-23 10:21:19.000000000 +0000 @@ -111,15 +111,19 @@ The answers to these questions are provided by the various :class:`~traitlets.config.Configurable` subclasses that an application uses. Let's look at how this would work for a simple configurable -subclass:: +subclass + +.. code-block:: python # Sample configurable: from traitlets.config.configurable import Configurable from traitlets import Int, Float, Unicode, Bool class MyClass(Configurable): - name = Unicode(u'defaultname', config=True) - ranking = Integer(0, config=True) + name = Unicode(u'defaultname' + help="the name of the object" + ).tag(config=True) + ranking = Integer(0, help="the class's ranking").tag(config=True) value = Float(99.0) # The rest of the class implementation would go here.. @@ -127,7 +131,9 @@ of which (``name``, ``ranking``) can be configured. All of the attributes are given types and default values. If a :class:`MyClass` is instantiated, but not configured, these default values will be used. But let's see how -to configure this class in a configuration file:: +to configure this class in a configuration file + +.. code-block:: python # Sample config file c.MyClass.name = 'coolname' @@ -231,7 +237,7 @@ to be reflected in the configuration system. Here is a simple example:: from traitlets.config.configurable import Configurable - from traitlets import Int, Float, Unicode, Bool + from traitlets import Integer, Float, Unicode, Bool class Foo(Configurable): name = Unicode(u'fooname', config=True) @@ -285,9 +291,22 @@ c.InteractiveShell.use_readline=False c.BaseIPythonApplication.profile='myprofile' -to your config file. Key/Value arguments *always* take a value, separated by '=' +to your configuration file. Key/Value arguments *always* take a value, separated by '=' and no spaces. +.. note:: + + By default any error in configuration files with lead to this configuration + file be ignored by default. Application subclasses may specify + `raise_config_file_errors = True` to exit on failure to load config files, + instead of the default of logging the failures. + +.. versionadded:: 4.3 + + The environement variable ``TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR`` + to ``'1'`` or ``'true'`` to change the defautl value of ``raise_config_file_errors``. + + Common Arguments ---------------- diff -Nru traitlets-4.0.0/docs/source/conf.py traitlets-4.3.2/docs/source/conf.py --- traitlets-4.0.0/docs/source/conf.py 2015-05-27 22:55:28.000000000 +0000 +++ traitlets-4.3.2/docs/source/conf.py 2017-02-23 10:21:19.000000000 +0000 @@ -313,3 +313,13 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'https://docs.python.org/': None} + +# Read The Docs +# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it \ No newline at end of file diff -Nru traitlets-4.0.0/docs/source/index.rst traitlets-4.3.2/docs/source/index.rst --- traitlets-4.0.0/docs/source/index.rst 2015-06-18 16:46:16.000000000 +0000 +++ traitlets-4.3.2/docs/source/index.rst 2017-02-23 10:21:19.000000000 +0000 @@ -18,4 +18,8 @@ using_traitlets trait_types defining_traits + api config + utils + migration + changelog diff -Nru traitlets-4.0.0/docs/source/migration.rst traitlets-4.3.2/docs/source/migration.rst --- traitlets-4.0.0/docs/source/migration.rst 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/docs/source/migration.rst 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,265 @@ +Migration from Traitlets 4.0 to Traitlets 4.1 +============================================= + +Traitlets 4.1 introduces a totally new decorator-based API for +configuring traitlets and a couple of other changes. + +However, it is a backward-compatible release and the deprecated APIs +will be supported for some time. + +Separation of metadata and keyword arguments in ``TraitType`` contructors +------------------------------------------------------------------------- + +In traitlets 4.0, trait types constructors used all unrecognized keyword +arguments passed to the constructor (like ``sync`` or ``config``) to +populate the ``metadata`` dictionary. + +In trailets 4.1, we deprecated this behavior. The preferred method to +populate the metadata for a trait type instance is to use the new +``tag`` method. + +.. code:: python + + x = Int(allow_none=True, sync=True) # deprecated + x = Int(allow_none=True).tag(sync=True) # ok + +We also deprecated the ``get_metadata`` method. The metadata of a trait +type instance can directly be accessed via the ``metadata`` attribute. + +Deprecation of ``on_trait_change`` +---------------------------------- + +The most important change in this release is the deprecation of the +``on_trait_change`` method. + +Instead, we introduced two methods, ``observe`` and ``unobserve`` to +register and unregister handlers (instead of passing ``remove=True`` to +``on_trait_change`` for the removal). + +- The ``observe`` method takes one positional argument (the handler), + and two keyword arguments, ``names`` and ``type``, which are used to + filter by notification type or by the names of the observed trait + attribute. The special value ``All`` corresponds to listening to all + the notification types or all notifications from the trait + attributes. The ``names`` argument can be a list of string, a string, + or ``All`` and ``type`` can be a string or ``All``. + +- The observe handler's signature is different from the signature of + on\_trait\_change. It takes a single change dictionary argument, + containing + +.. code:: python + + { + 'type': The type of notification. + } + +In the case where ``type`` is the string ``'change'``, the following +additional attributes are provided: + +.. code:: python + + { + 'owner': the HasTraits instance, + 'old': the old trait attribute value, + 'new': the new trait attribute value, + 'name': the name of the changing attribute, + } + +The ``type`` key in the change dictionary is meant to enable protocols +for other notification types. By default, its value is equal to the +``'change'`` string which corresponds to the change of a trait value. + +**Example:** + +.. code:: python + + from traitlets import HasTraits, Int, Unicode + + class Foo(HasTraits): + + bar = Int() + baz = Unicode() + + def handle_change(change): + print("{name} changed from {old} to {new}".format(**change)) + + foo = Foo() + foo.observe(handle_change, names='bar') + +The new ``@observe`` decorator +------------------------------ + +The use of the magic methods ``_{trait}_changed`` as change handlers is +deprecated, in favor of a new ``@observe`` method decorator. + +The ``@observe`` method decorator takes the names of traits to be observed as positional arguments and +has a ``type`` keyword-only argument (defaulting to ``'change'``) to filter +by notification type. + +**Example:** + +.. code:: python + + class Foo(HasTraits): + bar = Int() + baz = EnventfulContainer() # hypothetical trait type emitting + # other notifications types + + @observe('bar') # 'change' notifications for `bar` + def handler_bar(self, change): + pass + + @observe('baz ', type='element_change') # 'element_change' notifications for `baz` + def handler_baz(self, change): + pass + + @observe('bar', 'baz', type=All) # all notifications for `bar` and `baz` + def handler_all(self, change): + pass + +dynamic defaults generation with decorators +------------------------------------------- + +The use of the magic methods ``_{trait}_default`` for dynamic default +generation is not deprecated, but a new ``@default`` method decorator +is added. + +**Example:** + +Default generators should only be called if they are registered in +subclasses of ``trait.this_type``. + +.. code:: python + + from traitlets import HasTraits, Int, Float, default + + class A(HasTraits): + bar = Int() + + @default('bar') + def get_bar_default(self): + return 11 + + class B(A): + bar = Float() # This ignores the default generator + # defined in the base class A + + class C(B): + + @default('bar') + def some_other_default(self): # This should not be ignored since + return 3.0 # it is defined in a class derived + # from B.a.this_class. + +Deprecation of magic method for cross-validation +------------------------------------------------ + +``traitlets`` enables custom cross validation between the different +attributes of a ``HasTraits`` instance. For example, a slider value +should remain bounded by the ``min`` and ``max`` attribute. This +validation occurs before the trait notification fires. + +The use of the magic methods ``_{name}_validate`` for custom +cross-validation is deprecated, in favor of a new ``@validate`` method +decorator. + +The method decorated with the ``@validate`` decorator take a single +``proposal`` dictionary + +.. code:: python + + { + 'trait': the trait type instance being validated + 'value': the proposed value, + 'owner': the underlying HasTraits instance, + } + +Custom validators may raise ``TraitError`` exceptions in case of invalid +proposal, and should return the value that will be eventually assigned. + +**Example:** + +.. code:: 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 + +The presence of the ``owner`` key in the proposal dictionary enable the +use of other attributes of the object in the cross validation logic. +However, we recommend that the custom cross validator don't modify the +other attributes of the object but only coerce the proposed value. + +Backward-compatible upgrades +---------------------------- + +One challenge in adoption of a changing API is how to adopt the new API +while maintaining backward compatibility for subclasses, +as event listeners methods are *de facto* public APIs. + +Take for instance the following class: + +.. code:: python + + from traitlets import HasTraits, Unicode + + class Parent(HasTraits): + prefix = Unicode() + path = Unicode() + def _path_changed(self, name, old, new): + self.prefix = os.path.dirname(new) + +And you know another package has the subclass: + +.. code:: python + + from parent import Parent + class Child(Parent): + def _path_changed(self, name, old, new): + super()._path_changed(name, old, new) + if not os.path.exists(new): + os.makedirs(new) + +If the parent package wants to upgrade without breaking Child, +it needs to preserve the signature of ``_path_changed``. +For this, we have provided an ``@observe_compat`` decorator, +which automatically shims the deprecated signature into the new signature: + +.. code:: python + + from traitlets import HasTraits, Unicode, observe, observe_compat + + class Parent(HasTraits): + prefix = Unicode() + path = Unicode() + + @observe('path') + @observe_compat # <- this allows super()._path_changed in subclasses to work with the old signature. + def _path_changed(self, change): + self.prefix = os.path.dirname(change['value']) + diff -Nru traitlets-4.0.0/docs/source/trait_types.rst traitlets-4.3.2/docs/source/trait_types.rst --- traitlets-4.0.0/docs/source/trait_types.rst 2015-05-27 22:55:28.000000000 +0000 +++ traitlets-4.3.2/docs/source/trait_types.rst 2017-02-23 10:21:19.000000000 +0000 @@ -101,6 +101,8 @@ .. autoclass:: CaselessStrEnum +.. autoclass:: UseEnum + .. autoclass:: TCPAddress .. autoclass:: CRegExp diff -Nru traitlets-4.0.0/docs/source/using_traitlets.rst traitlets-4.3.2/docs/source/using_traitlets.rst --- traitlets-4.0.0/docs/source/using_traitlets.rst 2015-06-18 16:46:16.000000000 +0000 +++ traitlets-4.3.2/docs/source/using_traitlets.rst 2017-02-23 10:21:19.000000000 +0000 @@ -1,70 +1,122 @@ Using Traitlets =============== -.. currentmodule:: traitlets +In short, traitlets let the user define classes that have -Any class with traitlet attributes must inherit from :class:`HasTraits`. +1. Attributes (traits) with type checking and dynamically computed + default values +2. Traits emit change events when attributes are modified +3. Traitlets perform some validation and allow coercion of new trait + values on assignment. They also allow the user to define custom + validation logic for attributes based on the value of other + attributes. + +Default values, and checking type and value +------------------------------------------- + +At its most basic, traitlets provides type checking, and dynamic default +value generation of attributes on :class:``traitlets.HasTraits`` +subclasses: -.. autoclass:: HasTraits +.. code:: python - .. automethod:: has_trait + import getpass - .. automethod:: trait_names + class Identity(HasTraits): + username = Unicode() - .. automethod:: class_trait_names + @default('username') + def _default_username(self): + return getpass.getuser() - .. automethod:: traits +.. code:: python - .. automethod:: class_traits + class Foo(HasTraits): + bar = Int() - .. automethod:: trait_metadata + foo = Foo(bar='3') # raises a TraitError - .. automethod:: add_traits +:: -You then declare the traitlets on the class like this:: + TraitError: The 'bar' trait of a Foo instance must be an int, + but a value of '3' was specified - from traitlets import HasTraits, Int, Unicode +observe +------- - class Requester(HasTraits): - url = Unicode() - timeout = Int(30) # 30 will be the default value +Traitlets implement the observer pattern -For the available traitlet types and the arguments you can give them, see -:doc:`trait_types`. +.. code:: python -Dynamic default values ----------------------- + class Foo(HasTraits): + bar = Int() + baz = Unicode() -To calculate a default value dynamically, give your class a method named -:samp:`_{traitname}_default`. This will be called on the instance, -and should return the default value. For example:: + foo = Foo() - import getpass + def func(change): + print(change['old']) + print(change['new']) # as of traitlets 4.3, one should be able to + # write print(change.new) instead - class Identity(HasTraits): - username = Unicode() - def _username_default(self): - return getpass.getuser() + foo.observe(func, names=['bar']) + foo.bar = 1 # prints '0\n 1' + foo.baz = 'abc' # prints nothing + +When observers are methods of the class, a decorator syntax can be used. + +.. code:: python + + class Foo(HasTraits): + bar = Int() + baz = Unicode() + + @observe('bar') + def _observe_bar(self, change): + print(change['old']) + print(change['new']) + +Validation +---------- + +Custom validation logic on trait classes + +.. code:: python -Callbacks when traitlets change -------------------------------- + from traitlets import HasTraits, TraitError, Int, Bool, validate -To do something when a traitlet is changed, define a method named -:samp:`_{traitname}_changed`. This can have several possible signatures: + class Parity(HasTraits): + value = Int() + parity = Int() -.. class:: TraitletsCallbacksExample + @validate('value') + def _valid_value(self, proposal): + if proposal['value'] % 2 != self.parity: + raise TraitError('value and parity should be consistent') + return proposal['value'] - .. method:: _traitlet1_changed() - _traitlet2_changed(traitlet_name) - _traitlet3_changed(traitlet_name, new_value) - _traitlet4_changed(traitlet_name, old_value, new_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'] -You can also add callbacks to a trait dynamically: + parity_check = Parity(value=2) -.. automethod:: HasTraits.on_trait_change + # Changing required parity and value together while holding cross validation + with parity_check.hold_trait_notifications(): + parity_check.value = 1 + parity_check.parity = 1 -.. note:: +In the case where the a validation error occurs when +``hold_trait_notifications`` context manager is released, changes are +rolled back to the initial state. - If a traitlet with a dynamic default value has another value set before it is - used, the default will not be calculated. - Any callbacks on that trait will will fire, and *old_value* will be ``None``. +- Finally, trait type can have other events than trait changes. This + capability was added so as to enable notifications on change of + values in container classes. The items available in the dictionary + passed to the observer registered with ``observe`` depends on the + event type. diff -Nru traitlets-4.0.0/docs/source/utils.rst traitlets-4.3.2/docs/source/utils.rst --- traitlets-4.0.0/docs/source/utils.rst 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/docs/source/utils.rst 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,15 @@ +Utils +===== + +.. module:: traitlets + +A simple utility to import something by its string name. + +.. autofunction:: import_item + +Links +----- + +.. autoclass:: link + +.. autoclass:: directional_link diff -Nru traitlets-4.0.0/examples/myapp.py traitlets-4.3.2/examples/myapp.py --- traitlets-4.0.0/examples/myapp.py 2015-04-17 18:11:28.000000000 +0000 +++ traitlets-4.3.2/examples/myapp.py 2017-02-23 10:21:19.000000000 +0000 @@ -42,24 +42,24 @@ """ - i = Int(0, config=True, help="The integer i.") - j = Int(1, config=True, help="The integer j.") - name = Unicode(u'Brian', config=True, help="First name.") + i = Int(0, help="The integer i.").tag(config=True) + j = Int(1, help="The integer j.").tag(config=True) + name = Unicode(u'Brian', help="First name.").tag(config=True) class Bar(Configurable): - enabled = Bool(True, config=True, help="Enable bar.") + enabled = Bool(True, help="Enable bar.").tag(config=True) class MyApp(Application): name = Unicode(u'myapp') - running = Bool(False, config=True, - help="Is the app running?") + running = Bool(False, + help="Is the app running?").tag(config=True) classes = List([Bar, Foo]) - config_file = Unicode(u'', config=True, - help="Load this config file") + config_file = Unicode(u'', + help="Load this config file").tag(config=True) aliases = Dict(dict(i='Foo.i',j='Foo.j',name='Foo.name', running='MyApp.running', enabled='Bar.enabled', log_level='MyApp.log_level')) diff -Nru traitlets-4.0.0/.gitignore traitlets-4.3.2/.gitignore --- traitlets-4.0.0/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/.gitignore 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,19 @@ +MANIFEST +build +dist +_build +docs/man/*.gz +docs/gh-pages +IPython/html/notebook/static/mathjax +IPython/html/static/style/*.map +*.py[co] +__pycache__ +*.egg-info +*~ +*.bak +.ipynb_checkpoints +.tox +.DS_Store +\#*# +.#* +.coverage diff -Nru traitlets-4.0.0/.mailmap traitlets-4.3.2/.mailmap --- traitlets-4.0.0/.mailmap 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/.mailmap 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,149 @@ +A. J. Holyoake ajholyoake +Aaron Culich Aaron Culich +Aron Ahmadia ahmadia +Benjamin Ragan-Kelley +Benjamin Ragan-Kelley Min RK +Benjamin Ragan-Kelley MinRK +Barry Wark Barry Wark +Ben Edwards Ben Edwards +Bradley M. Froehle Bradley M. Froehle +Bradley M. Froehle Bradley Froehle +Brandon Parsons Brandon Parsons +Brian E. Granger Brian Granger +Brian E. Granger Brian Granger <> +Brian E. Granger bgranger <> +Brian E. Granger bgranger +Christoph Gohlke cgohlke +Cyrille Rossant rossant +Damián Avila damianavila +Damián Avila damianavila +Damon Allen damontallen +Darren Dale darren.dale <> +Darren Dale Darren Dale <> +Dav Clark Dav Clark <> +Dav Clark Dav Clark +David Hirschfeld dhirschfeld +David P. Sanders David P. Sanders +David Warde-Farley David Warde-Farley <> +Doug Blank Doug Blank +Eugene Van den Bulke Eugene Van den Bulke +Evan Patterson +Evan Patterson +Evan Patterson +Evan Patterson +Evan Patterson epatters +Evan Patterson epatters +Ernie French Ernie French +Ernie French ernie french +Ernie French ernop +Fernando Perez +Fernando Perez Fernando Perez +Fernando Perez fperez <> +Fernando Perez fptest <> +Fernando Perez fptest1 <> +Fernando Perez Fernando Perez +Fernando Perez Fernando Perez <> +Fernando Perez Fernando Perez +Frank Murphy Frank Murphy +Gabriel Becker gmbecker +Gael Varoquaux gael.varoquaux <> +Gael Varoquaux gvaroquaux +Gael Varoquaux Gael Varoquaux <> +Ingolf Becker watercrossing +Jake Vanderplas Jake Vanderplas +Jakob Gager jakobgager +Jakob Gager jakobgager +Jakob Gager jakobgager +Jason Grout +Jason Grout +Jason Gors jason gors +Jason Gors jgors +Jens Hedegaard Nielsen Jens Hedegaard Nielsen +Jens Hedegaard Nielsen Jens H Nielsen +Jens Hedegaard Nielsen Jens H. Nielsen +Jez Ng Jez Ng +Jonathan Frederic Jonathan Frederic +Jonathan Frederic Jonathan Frederic +Jonathan Frederic Jonathan Frederic +Jonathan Frederic jon +Jonathan Frederic U-Jon-PC\Jon +Jonathan March Jonathan March +Jonathan March jdmarch +Jörgen Stenarson Jörgen Stenarson +Jörgen Stenarson Jorgen Stenarson +Jörgen Stenarson Jorgen Stenarson <> +Jörgen Stenarson jstenar +Jörgen Stenarson jstenar <> +Jörgen Stenarson Jörgen Stenarson +Juergen Hasch juhasch +Juergen Hasch juhasch +Julia Evans Julia Evans +Kester Tong KesterTong +Kyle Kelley Kyle Kelley +Kyle Kelley rgbkrk +Laurent Dufréchou +Laurent Dufréchou +Laurent Dufréchou laurent dufrechou <> +Laurent Dufréchou laurent.dufrechou <> +Laurent Dufréchou Laurent Dufrechou <> +Laurent Dufréchou laurent.dufrechou@gmail.com <> +Laurent Dufréchou ldufrechou +Lorena Pantano Lorena +Luis Pedro Coelho Luis Pedro Coelho +Marc Molla marcmolla +Martín Gaitán Martín Gaitán +Matthias Bussonnier Matthias BUSSONNIER +Matthias Bussonnier Bussonnier Matthias +Matthias Bussonnier Matthias BUSSONNIER +Matthias Bussonnier Matthias Bussonnier +Michael Droettboom Michael Droettboom +Nicholas Bollweg Nicholas Bollweg (Nick) +Nicolas Rougier +Nikolay Koldunov Nikolay Koldunov +Omar Andrés Zapata Mesa Omar Andres Zapata Mesa +Omar Andrés Zapata Mesa Omar Andres Zapata Mesa +Pankaj Pandey Pankaj Pandey +Pascal Schetelat pascal-schetelat +Paul Ivanov Paul Ivanov +Pauli Virtanen Pauli Virtanen <> +Pauli Virtanen Pauli Virtanen +Pierre Gerold Pierre Gerold +Pietro Berkes Pietro Berkes +Piti Ongmongkolkul piti118 +Prabhu Ramachandran Prabhu Ramachandran <> +Puneeth Chaganti Puneeth Chaganti +Robert Kern rkern <> +Robert Kern Robert Kern +Robert Kern Robert Kern +Robert Kern Robert Kern <> +Robert Marchman Robert Marchman +Satrajit Ghosh Satrajit Ghosh +Satrajit Ghosh Satrajit Ghosh +Scott Sanderson Scott Sanderson +smithj1 smithj1 +smithj1 smithj1 +Steven Johnson stevenJohnson +Steven Silvester blink1073 +S. Weber s8weber +Stefan van der Walt Stefan van der Walt +Silvia Vinyes Silvia +Silvia Vinyes silviav12 +Sylvain Corlay +Sylvain Corlay sylvain.corlay +Ted Drain TD22057 +Théophile Studer Théophile Studer +Thomas Kluyver Thomas +Thomas Spura Thomas Spura +Timo Paulssen timo +vds vds2212 +vds vds +Ville M. Vainio +Ville M. Vainio ville +Ville M. Vainio ville +Ville M. Vainio vivainio <> +Ville M. Vainio Ville M. Vainio +Ville M. Vainio Ville M. Vainio +Walter Doerwald walter.doerwald <> +Walter Doerwald Walter Doerwald <> +W. Trevor King W. Trevor King +Yoval P. y-p diff -Nru traitlets-4.0.0/MANIFEST.in traitlets-4.3.2/MANIFEST.in --- traitlets-4.0.0/MANIFEST.in 2015-04-17 18:11:28.000000000 +0000 +++ traitlets-4.3.2/MANIFEST.in 2017-02-23 10:21:19.000000000 +0000 @@ -1,3 +1,6 @@ +include CONTRIBUTING.md +include COPYING.md +include README.md # Documentation graft docs diff -Nru traitlets-4.0.0/PKG-INFO traitlets-4.3.2/PKG-INFO --- traitlets-4.0.0/PKG-INFO 2015-06-19 20:23:20.000000000 +0000 +++ traitlets-4.3.2/PKG-INFO 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -Metadata-Version: 1.1 -Name: traitlets -Version: 4.0.0 -Summary: Traitlets Python config system -Home-page: http://ipython.org -Author: IPython Development Team -Author-email: ipython-dev@scipy.org -License: BSD -Description: A configuration system for Python applications. -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 diff -Nru traitlets-4.0.0/README.md traitlets-4.3.2/README.md --- traitlets-4.0.0/README.md 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/README.md 2017-02-23 10:21:19.000000000 +0000 @@ -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 -Nru traitlets-4.0.0/readthedocs.yml traitlets-4.3.2/readthedocs.yml --- traitlets-4.0.0/readthedocs.yml 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/readthedocs.yml 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,9 @@ +conda: + file: docs/environment.yml +python: + version: 3 + setup_py_install: true + pip install: true +formats: + - epub + - pdf \ No newline at end of file diff -Nru traitlets-4.0.0/setup.cfg traitlets-4.3.2/setup.cfg --- traitlets-4.0.0/setup.cfg 2015-06-19 20:23:20.000000000 +0000 +++ traitlets-4.3.2/setup.cfg 2017-02-23 10:21:19.000000000 +0000 @@ -1,8 +1,2 @@ [bdist_wheel] -universal = 1 - -[egg_info] -tag_svn_revision = 0 -tag_build = -tag_date = 0 - +universal=1 diff -Nru traitlets-4.0.0/setup.py traitlets-4.3.2/setup.py --- traitlets-4.0.0/setup.py 2015-06-18 16:46:16.000000000 +0000 +++ traitlets-4.3.2/setup.py 2017-02-23 10:21:19.000000000 +0000 @@ -68,6 +68,8 @@ 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', ], ) @@ -78,11 +80,17 @@ install_requires = setuptools_args['install_requires'] = [ 'ipython_genutils', + 'six', 'decorator', ] extras_require = setuptools_args['extras_require'] = { - + 'test': ['pytest'], + 'test:python_version=="2.7"': ["mock"], + # -- SUPPORT UNIFORM-WHEELS: Extra packages for Python 2.7, 3.3 + # SEE: https://bitbucket.org/pypa/wheel/ , CHANGES.txt (v0.24.0) + ':python_version=="2.7"': ["enum34"], + ':python_version=="3.3"': ["enum34"], } if 'setuptools' in sys.modules: diff -Nru traitlets-4.0.0/traitlets/config/application.py traitlets-4.3.2/traitlets/config/application.py --- traitlets-4.0.0/traitlets/config/application.py 2015-06-08 17:46:37.000000000 +0000 +++ traitlets-4.3.2/traitlets/config/application.py 2017-02-23 10:21:19.000000000 +0000 @@ -6,13 +6,13 @@ from __future__ import print_function +from copy import deepcopy import json import logging import os import re import sys -from copy import deepcopy -from collections import defaultdict +from collections import defaultdict, OrderedDict from decorator import decorator @@ -22,12 +22,13 @@ ) from traitlets.traitlets import ( - Unicode, List, Enum, Dict, Instance, TraitError + 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 -from ipython_genutils.py3compat import string_types, iteritems + +import six #----------------------------------------------------------------------------- # Descriptions for the various sections @@ -62,13 +63,24 @@ # 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: @@ -84,25 +96,26 @@ 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.""" @@ -118,7 +131,7 @@ 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 @@ -145,41 +158,47 @@ # 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, - config=True, - help="Set the log level by value or name.") - def _log_level_changed(self, name, old, new): + 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.""" - if isinstance(new, string_types): + 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", config=True, + + log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", help="The date format used by logging formatters for %(asctime)s" - ) - def _log_datefmt_changed(self, name, old, new): - self._log_format_changed('log_format', self.log_format, self.log_format) - - log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True, + ).tag(config=True) + + log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", help="The Logging format template", - ) - def _log_format_changed(self, name, old, new): + ).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=new, datefmt=self.log_datefmt) + _log_formatter = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt) _log_handler.setFormatter(_log_formatter) - - log = Instance(logging.Logger) + @default('log') def _log_default(self): """Start logging for this application. @@ -198,7 +217,7 @@ break else: _log = _log.parent - if sys.executable.endswith('pythonw.exe'): + 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')) @@ -217,12 +236,15 @@ # 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() - def _flags_changed(self, name, old, new): + @observe('flags') + @observe_compat + def _flags_changed(self, change): """ensure flags dict is valid""" - for key,value in iteritems(new): - 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], string_types), "Bad flag: %r:%s"%(key,value) + 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 @@ -235,20 +257,35 @@ subapp = Instance('traitlets.config.application.Application', allow_none=True) # extra command-line arguments that don't set config values - extra_args = List(Unicode) + 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. + """ + ) 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. - if self.__class__ not in self.classes: - self.classes.insert(0, self.__class__) + 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__) - def _config_changed(self, name, old, new): - SingletonConfigurable._config_changed(self, name, old, new) + @observe('config') + @observe_compat + def _config_changed(self, change): + super(Application, self)._config_changed(change) self.log.debug('Config changed:') - self.log.debug(repr(new)) + self.log.debug(repr(change.new)) @catch_config_error def initialize(self, argv=None): @@ -279,7 +316,7 @@ for c in cls.mro()[:-3]: classdict[c.__name__] = c - for alias, longname in iteritems(self.aliases): + for alias, longname in self.aliases.items(): classname, traitname = longname.split('.',1) cls = classdict[classname] @@ -299,7 +336,7 @@ return lines = [] - for m, (cfg,help) in iteritems(self.flags): + for m, (cfg,help) in self.flags.items(): prefix = '--' if len(m) > 1 else '-' lines.append(prefix+m) lines.append(indent(dedent(help.strip()))) @@ -332,7 +369,7 @@ app=self.name)): lines.append(p) lines.append('') - for subc, (cls, help) in iteritems(self.subcommands): + for subc, (cls, help) in self.subcommands.items(): lines.append(subc) if help: lines.append(indent(dedent(help.strip()))) @@ -399,41 +436,31 @@ """Print the version string.""" print(self.version) - def update_config(self, config): - """Fire the traits events when the config is updated.""" - # Save a copy of the current config. - newconfig = deepcopy(self.config) - # Merge the new config into the current one. - newconfig.merge(config) - # Save the combined config as self.config, which triggers the traits - # events. - self.config = newconfig - @catch_config_error def initialize_subcommand(self, subc, argv=None): """Initialize a subcommand with argv.""" subapp,help = self.subcommands.get(subc) - if isinstance(subapp, string_types): + if isinstance(subapp, six.string_types): subapp = import_item(subapp) # clear existing instances self.__class__.clear_instance() # instantiate - self.subapp = subapp.instance(config=self.config) + 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 @@ -447,20 +474,20 @@ # flatten aliases, which have the form: # { 'alias' : 'Class.trait' } aliases = {} - for alias, cls_trait in iteritems(self.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 iteritems(self.flags): + for key, (flagdict, help) in self.flags.items(): newflag = {} - for cls, subdict in iteritems(flagdict): + for cls, subdict in flagdict.items(): children = mro_tree[cls] # exactly one descendent, promote flag section if len(children) == 1: @@ -474,7 +501,7 @@ """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'] @@ -502,34 +529,34 @@ 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) - config = loader.load_config() - self.update_config(config) + 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): + 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("Attempting to load config file %s.py in path %s", basefilename, path) + log.debug("Looking for %s in %s", basefilename, path or os.getcwd()) jsonloader = cls.json_config_loader_class(basefilename+'.json', path=path, log=log) - if log: - log.debug("Attempting to load config file %s.json in path %s", basefilename, path) config = None + loaded = [] + filenames = [] for loader in [pyloader, jsonloader]: try: config = loader.load_config() @@ -540,6 +567,8 @@ # 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) @@ -547,33 +576,70 @@ if log: log.debug("Loaded config file: %s", loader.full_filename) if config: - yield config - - raise StopIteration + 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 + loaded.append(config) + filenames.append(loader.full_filename) + @catch_config_error def load_config_file(self, filename, path=None): """Load config files by filename and path.""" filename, ext = os.path.splitext(filename) - loaded = [] - for config in self._load_config_files(filename, path=path, log=self.log): - loaded.append(config) - self.update_config(config) - if len(loaded) > 1: - collisions = loaded[0].collisions(loaded[1]) - if collisions: - self.log.warning("Collisions detected in {0}.py and {0}.json config files." - " {0}.json has higher priority: {1}".format( - filename, json.dumps(collisions, indent=2), - )) + new_config = Config() + for config in self._load_config_files(filename, path=path, log=self.log, + raise_config_file_errors=self.raise_config_file_errors, + ): + new_config.merge(config) + # 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_inc_parents(): + for cls in self._classes_in_config_sample(): lines.append(cls.class_config_section()) return '\n'.join(lines) @@ -584,7 +650,7 @@ @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) @@ -630,7 +696,7 @@ 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(): diff -Nru traitlets-4.0.0/traitlets/config/configurable.py traitlets-4.3.2/traitlets/config/configurable.py --- traitlets-4.0.0/traitlets/config/configurable.py 2015-06-18 16:46:16.000000000 +0000 +++ traitlets-4.3.2/traitlets/config/configurable.py 2017-02-23 10:21:19.000000000 +0000 @@ -4,15 +4,14 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -from __future__ import print_function +from __future__ import print_function, absolute_import -import logging from copy import deepcopy +import warnings -from .loader import Config, LazyConfigValue -from traitlets.traitlets import HasTraits, Instance +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 -from ipython_genutils.py3compat import iteritems #----------------------------------------------------------------------------- @@ -67,12 +66,12 @@ 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 @@ -86,7 +85,7 @@ 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. @@ -96,25 +95,25 @@ #------------------------------------------------------------------------- # 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] @@ -128,20 +127,20 @@ 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 iteritems(my_config): + 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 @@ -152,9 +151,26 @@ # 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)) - - def _config_changed(self, name, old, new): - """Update all the class traits having ``config=True`` as metadata. + 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 @@ -167,22 +183,28 @@ # 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(new, traits=traits, section_names=section_names) + self._load_config(change.new, traits=traits, section_names=section_names) def update_config(self, config): - """Fire the traits events when the config is updated.""" - # Save a copy of the current config. - newconfig = deepcopy(self.config) - # Merge the new config into the current one. - newconfig.merge(config) - # Save the combined config as self.config, which triggers the traits - # events. - self.config = newconfig + """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. """ @@ -198,7 +220,7 @@ @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. """ @@ -210,7 +232,7 @@ lines.append(indent('Current: %r' % getattr(inst, trait.name), 4)) else: try: - dvr = repr(trait.default_value) + dvr = trait.default_value_repr() except Exception: dvr = None # ignore defaults we can't construct if dvr is not None: @@ -221,8 +243,8 @@ # include Enum choices lines.append(indent('Choices: %r' % (trait.values,))) - help = trait.get_metadata('help') - if help is not None: + help = trait.help + if help != '': help = '\n'.join(wrap_paragraphs(help, 76)) lines.append(indent(help, 4)) return '\n'.join(lines) @@ -239,27 +261,27 @@ """return a commented, wrapped block.""" s = '\n\n'.join(wrap_paragraphs(s, 78)) - return '# ' + s.replace('\n', '\n# ') + return '## ' + s.replace('\n', '\n# ') # section header breaker = '#' + '-'*78 - s = "# %s configuration" % cls.__name__ + 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 - else: - # no description trait, use __doc__ + 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 iteritems(cls.class_own_traits(config=True)): - help = trait.get_metadata('help') or '' - lines.append(c(help)) - lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.default_value)) + 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) @@ -286,11 +308,10 @@ # Default value try: - dv = trait.default_value - dvr = repr(dv) + dvr = trait.default_value_repr() except Exception: - dvr = dv = None # ignore defaults we can't construct - if (dv is not None) and (dvr is not None): + 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 @@ -298,11 +319,8 @@ lines.append(' Default: ``%s``' % dvr) lines.append('') - help = trait.get_metadata('help') - if help is not None: - lines.append(indent(dedent(help), 4)) - else: - lines.append(' No description') + help = trait.help or 'No description' + lines.append(indent(dedent(help), 4)) # Blank line lines.append('') @@ -311,7 +329,21 @@ -class SingletonConfigurable(Configurable): +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 @@ -397,16 +429,4 @@ return hasattr(cls, "_instance") and cls._instance is not None -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') - def _log_default(self): - from traitlets import log - return log.get_logger() - diff -Nru traitlets-4.0.0/traitlets/config/loader.py traitlets-4.3.2/traitlets/config/loader.py --- traitlets-4.0.0/traitlets/config/loader.py 2015-05-20 20:04:09.000000000 +0000 +++ traitlets-4.3.2/traitlets/config/loader.py 2017-02-23 10:21:19.000000000 +0000 @@ -16,7 +16,7 @@ from ipython_genutils.path import filefind from ipython_genutils import py3compat from ipython_genutils.encoding import DEFAULT_ENCODING -from ipython_genutils.py3compat import unicode_type, iteritems +from six import text_type from traitlets.traitlets import HasTraits, List, Any #----------------------------------------------------------------------------- @@ -182,16 +182,16 @@ def merge(self, other): """merge another config object into this one""" to_update = {} - for k, v in iteritems(other): + for k, v in other.items(): if k not in self: - to_update[k] = copy.deepcopy(v) + 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] = copy.deepcopy(v) + to_update[k] = v self.update(to_update) @@ -385,7 +385,16 @@ self.full_filename = filefind(self.filename, self.path) class JSONFileConfigLoader(FileConfigLoader): - """A JSON file loader for config""" + """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.""" @@ -407,13 +416,29 @@ version = dictionary.pop('version') else: version = 1 - self.log.warning("Unrecognized JSON config file version, assuming version {}".format(version)) 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. @@ -496,7 +521,7 @@ if isinstance(cfg, (dict, Config)): # don't clobber whole config sections, update # each section from config: - for sec,c in iteritems(cfg): + for sec,c in cfg.items(): self.config[sec].update(c) else: raise TypeError("Invalid flag: %r" % cfg) @@ -578,7 +603,7 @@ if enc is None: enc = DEFAULT_ENCODING for arg in argv: - if not isinstance(arg, unicode_type): + if not isinstance(arg, text_type): # only decode if not already decoded arg = arg.decode(enc) uargv.append(arg) @@ -744,7 +769,7 @@ def _convert_to_config(self): """self.parsed_data->self.config""" - for k, v in iteritems(vars(self.parsed_data)): + for k, v in vars(self.parsed_data).items(): exec("self.config.%s = v"%k, locals(), globals()) class KVArgParseConfigLoader(ArgParseConfigLoader): @@ -761,17 +786,17 @@ if flags is None: flags = self.flags paa = self.parser.add_argument - for key,value in iteritems(aliases): + for key,value in aliases.items(): if key in flags: # flags nargs = '?' else: nargs = None if len(key) is 1: - paa('-'+key, '--'+key, type=unicode_type, dest=value, nargs=nargs) + paa('-'+key, '--'+key, type=text_type, dest=value, nargs=nargs) else: - paa('--'+key, type=unicode_type, dest=value, nargs=nargs) - for key, (value, help) in iteritems(flags): + 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 @@ -790,7 +815,7 @@ else: subcs = [] - for k, v in iteritems(vars(self.parsed_data)): + 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]) diff -Nru traitlets-4.0.0/traitlets/config/manager.py traitlets-4.3.2/traitlets/config/manager.py --- traitlets-4.0.0/traitlets/config/manager.py 2015-04-17 18:11:28.000000000 +0000 +++ traitlets-4.3.2/traitlets/config/manager.py 2017-02-23 10:21:19.000000000 +0000 @@ -7,8 +7,8 @@ import json import os +from six import PY3 from traitlets.config import LoggingConfigurable -from ipython_genutils.py3compat import PY3 from traitlets.traitlets import Unicode @@ -43,7 +43,7 @@ def ensure_config_dir_exists(self): try: - os.mkdir(self.config_dir, 0o755) + os.makedirs(self.config_dir, 0o755) except OSError as e: if e.errno != errno.EEXIST: raise diff -Nru traitlets-4.0.0/traitlets/config/tests/test_application.py traitlets-4.3.2/traitlets/config/tests/test_application.py --- traitlets-4.0.0/traitlets/config/tests/test_application.py 2015-04-17 18:11:28.000000000 +0000 +++ traitlets-4.3.2/traitlets/config/tests/test_application.py 2017-02-23 10:21:19.000000000 +0000 @@ -6,17 +6,24 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +import json import logging import os from io import StringIO from unittest import TestCase +try: + from unittest import mock +except ImportError: + import mock + pjoin = os.path.join -import nose.tools as nt +from pytest import mark from traitlets.config.configurable import Configurable from traitlets.config.loader import Config +from traitlets.tests.utils import check_help_output, check_help_all_output from traitlets.config.application import ( Application @@ -30,25 +37,26 @@ class Foo(Configurable): - i = Integer(0, config=True, help="The integer i.") - j = Integer(1, config=True, help="The integer j.") - name = Unicode(u'Brian', config=True, help="First name.") + i = Integer(0, help="The integer i.").tag(config=True) + j = Integer(1, help="The integer j.").tag(config=True) + name = Unicode(u'Brian', help="First name.").tag(config=True) class Bar(Configurable): - b = Integer(0, config=True, help="The integer b.") - enabled = Bool(True, config=True, help="Enable bar.") + b = Integer(0, help="The integer b.").tag(config=True) + enabled = Bool(True, help="Enable bar.").tag(config=True) class MyApp(Application): name = Unicode(u'myapp') - running = Bool(False, config=True, - help="Is the app running?") + running = Bool(False, help="Is the app running?").tag(config=True) classes = List([Bar, Foo]) - config_file = Unicode(u'', config=True, - help="Load this config file") + config_file = Unicode(u'', help="Load this config file").tag(config=True) + + warn_tpyo = Unicode(u"yes the name is wrong on purpose", config=True, + help="Should print a warning if `MyApp.warn-typo=...` command is passed") aliases = Dict({ 'i' : 'Foo.i', @@ -57,13 +65,13 @@ 'enabled' : 'Bar.enabled', 'log-level' : 'Application.log_level', }) - + flags = Dict(dict(enable=({'Bar': {'enabled' : True}}, "Set Bar.enabled to True"), disable=({'Bar': {'enabled' : False}}, "Set Bar.enabled to False"), crit=({'Application' : {'log_level' : logging.CRITICAL}}, "set level=CRITICAL"), )) - + def init_foo(self): self.foo = Foo(parent=self) @@ -82,7 +90,7 @@ app.log_format = "%(message)s" app.log_datefmt = "%Y-%m-%d %H:%M" app.log.info("hello") - nt.assert_in("hello", stream.getvalue()) + assert "hello" in stream.getvalue() def test_basic(self): app = MyApp() @@ -109,6 +117,67 @@ self.assertEqual(app.foo.j, 10) self.assertEqual(app.bar.enabled, False) + def test_cli_priority(self): + """Test that loading config files does not override CLI options""" + name = 'config.py' + class TestApp(Application): + value = Unicode().tag(config=True) + config_file_loaded = Bool().tag(config=True) + aliases = {'v': 'TestApp.value'} + app = TestApp() + with TemporaryDirectory() as td: + config_file = pjoin(td, name) + with open(config_file, 'w') as f: + f.writelines([ + "c.TestApp.value = 'config file'\n", + "c.TestApp.config_file_loaded = True\n" + ]) + + app.parse_command_line(['--v=cli']) + assert 'value' in app.config.TestApp + assert app.config.TestApp.value == 'cli' + assert app.value == 'cli' + + app.load_config_file(name, path=[td]) + assert app.config_file_loaded + assert app.config.TestApp.value == 'cli' + assert app.value == 'cli' + + def test_ipython_cli_priority(self): + # this test is almost entirely redundant with above, + # but we can keep it around in case of subtle issues creeping into + # the exact sequence IPython follows. + name = 'config.py' + class TestApp(Application): + value = Unicode().tag(config=True) + config_file_loaded = Bool().tag(config=True) + aliases = {'v': 'TestApp.value'} + app = TestApp() + with TemporaryDirectory() as td: + config_file = pjoin(td, name) + with open(config_file, 'w') as f: + f.writelines([ + "c.TestApp.value = 'config file'\n", + "c.TestApp.config_file_loaded = True\n" + ]) + # follow IPython's config-loading sequence to ensure CLI priority is preserved + app.parse_command_line(['--v=cli']) + # this is where IPython makes a mistake: + # it assumes app.config will not be modified, + # and storing a reference is storing a copy + cli_config = app.config + assert 'value' in app.config.TestApp + assert app.config.TestApp.value == 'cli' + assert app.value == 'cli' + app.load_config_file(name, path=[td]) + assert app.config_file_loaded + # enforce cl-opts override config file opts: + # this is where IPython makes a mistake: it assumes + # that cl_config is a different object, but it isn't. + app.update_config(cli_config) + assert app.config.TestApp.value == 'cli' + assert app.value == 'cli' + def test_flags(self): app = MyApp() app.parse_command_line(["--disable"]) @@ -117,7 +186,7 @@ app.parse_command_line(["--enable"]) app.init_bar() self.assertEqual(app.bar.enabled, True) - + def test_aliases(self): app = MyApp() app.parse_command_line(["--i=5", "--j=10"]) @@ -125,7 +194,7 @@ self.assertEqual(app.foo.i, 5) app.init_foo() self.assertEqual(app.foo.j, 10) - + def test_flag_clobber(self): """test that setting flags doesn't clobber existing settings""" app = MyApp() @@ -137,7 +206,20 @@ app.init_bar() self.assertEqual(app.bar.enabled, True) self.assertEqual(app.bar.b, 10) - + + def test_warn_autocorrect(self): + stream = StringIO() + app = MyApp(log_level=logging.INFO) + app.log.handlers = [logging.StreamHandler(stream)] + + cfg = Config() + cfg.MyApp.warn_typo = "WOOOO" + app.config = cfg + + self.assertIn("warn_typo", stream.getvalue()) + self.assertIn("warn_tpyo", stream.getvalue()) + + def test_flatten_flags(self): cfg = Config() cfg.MyApp.log_level = logging.WARN @@ -149,7 +231,7 @@ self.assertEqual(app.log_level, logging.CRITICAL) # this would be app.config.Application.log_level if it failed: self.assertEqual(app.config.MyApp.log_level, logging.CRITICAL) - + def test_flatten_aliases(self): cfg = Config() cfg.MyApp.log_level = logging.WARN @@ -161,7 +243,7 @@ self.assertEqual(app.log_level, logging.CRITICAL) # this would be app.config.Application.log_level if it failed: self.assertEqual(app.config.MyApp.log_level, "CRITICAL") - + def test_extra_args(self): app = MyApp() app.parse_command_line(["--Bar.b=5", 'extra', "--disable", 'args']) @@ -175,11 +257,32 @@ self.assertEqual(app.bar.enabled, True) self.assertEqual(app.bar.b, 5) self.assertEqual(app.extra_args, ['extra', '--disable', 'args']) - + def test_unicode_argv(self): app = MyApp() app.parse_command_line(['ünîcødé']) - + + def test_document_config_option(self): + app = MyApp() + app.document_config_options() + + def test_generate_config_file(self): + app = MyApp() + assert 'The integer b.' in app.generate_config_file() + + def test_generate_config_file_classes_to_include(self): + class NoTraits(Foo, Bar): + pass + + app = MyApp() + app.classes.append(NoTraits) + conf_txt = app.generate_config_file() + self.assertIn('The integer b.', conf_txt) + self.assertIn('# Bar(Configurable)', conf_txt) + self.assertIn('# Foo(Configurable)', conf_txt) + self.assertNotIn('# Configurable', conf_txt) + self.assertIn('# NoTraits(Foo,Bar)', conf_txt) + def test_multi_file(self): app = MyApp() app.log = logging.getLogger() @@ -197,3 +300,80 @@ app.init_bar() self.assertEqual(app.bar.b, 1) + @mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs') + def test_log_collisions(self): + app = MyApp() + app.log = logging.getLogger() + app.log.setLevel(logging.INFO) + name = 'config' + with TemporaryDirectory('_1') as td: + with open(pjoin(td, name + '.py'), 'w') as f: + f.write("get_config().Bar.b = 1") + with open(pjoin(td, name + '.json'), 'w') as f: + json.dump({ + 'Bar': { + 'b': 2 + } + }, f) + with self.assertLogs(app.log, logging.WARNING) as captured: + app.load_config_file(name, path=[td]) + app.init_bar() + assert app.bar.b == 2 + output = '\n'.join(captured.output) + assert 'Collision' in output + assert '1 ignored, using 2' in output + assert pjoin(td, name + '.py') in output + assert pjoin(td, name + '.json') in output + + @mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs') + def test_log_bad_config(self): + app = MyApp() + app.log = logging.getLogger() + name = 'config.py' + with TemporaryDirectory() as td: + with open(pjoin(td, name), 'w') as f: + f.write("syntax error()") + with self.assertLogs(app.log, logging.ERROR) as captured: + app.load_config_file(name, path=[td]) + output = '\n'.join(captured.output) + self.assertIn('SyntaxError', output) + + def test_raise_on_bad_config(self): + app = MyApp() + app.raise_config_file_errors = True + app.log = logging.getLogger() + name = 'config.py' + with TemporaryDirectory() as td: + with open(pjoin(td, name), 'w') as f: + f.write("syntax error()") + with self.assertRaises(SyntaxError): + app.load_config_file(name, path=[td]) + + +class DeprecatedApp(Application): + override_called = False + parent_called = False + def _config_changed(self, name, old, new): + self.override_called = True + def _capture(*args): + self.parent_called = True + with mock.patch.object(self.log, 'debug', _capture): + super(DeprecatedApp, self)._config_changed(name, old, new) + + +def test_deprecated_notifier(): + app = DeprecatedApp() + assert not app.override_called + assert not app.parent_called + app.config = Config({'A': {'b': 'c'}}) + assert app.override_called + assert app.parent_called + + +def test_help_output(): + check_help_output(__name__) + check_help_all_output(__name__) + +if __name__ == '__main__': + # for test_help_output: + MyApp.launch_instance() \ No newline at end of file diff -Nru traitlets-4.0.0/traitlets/config/tests/test_configurable.py traitlets-4.3.2/traitlets/config/tests/test_configurable.py --- traitlets-4.0.0/traitlets/config/tests/test_configurable.py 2015-04-17 18:11:28.000000000 +0000 +++ traitlets-4.3.2/traitlets/config/tests/test_configurable.py 2017-02-23 10:21:19.000000000 +0000 @@ -4,24 +4,30 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +import logging from unittest import TestCase +from pytest import mark + from traitlets.config.configurable import ( - Configurable, - SingletonConfigurable + Configurable, + LoggingConfigurable, + SingletonConfigurable, ) from traitlets.traitlets import ( Integer, Float, Unicode, List, Dict, Set, + _deprecations_shown, ) from traitlets.config.loader import Config -from ipython_genutils.py3compat import PY3 +from six import PY3 +from ...tests._warnings import expected_warnings class MyConfigurable(Configurable): - a = Integer(1, config=True, help="The integer a.") - b = Float(1.0, config=True, help="The integer b.") + a = Integer(1, help="The integer a.").tag(config=True) + b = Float(1.0, help="The integer b.").tag(config=True) c = Unicode('no config') @@ -49,13 +55,13 @@ mc_help_inst = mc_help_inst.replace(u"", u"") class Foo(Configurable): - a = Integer(0, config=True, help="The integer a.") - b = Unicode('nope', config=True) + a = Integer(0, help="The integer a.").tag(config=True) + b = Unicode('nope').tag(config=True) class Bar(Foo): - b = Unicode('gotit', config=False, help="The string b.") - c = Float(config=True, help="The string c.") + b = Unicode('gotit', help="The string b.").tag(config=False) + c = Float(help="The string c.").tag(config=True) class TestConfigurable(TestCase): @@ -102,7 +108,8 @@ config.Bar.b = 'later' config.Bar.c = 100.0 f = Foo(config=config) - b = Bar(config=f.config) + with expected_warnings(['`b` not recognized']): + b = Bar(config=f.config) self.assertEqual(f.a, 10) self.assertEqual(f.b, 'wow') self.assertEqual(b.b, 'gotit') @@ -122,11 +129,13 @@ config.Foo.a = 1 config.Bar.b = 'or' # Up above b is config=False, so this won't do it. config.Bar.c = 10.0 - c = Bar(config=config) + with expected_warnings(['`b` not recognized']): + c = Bar(config=config) self.assertEqual(c.a, config.Foo.a) self.assertEqual(c.b, 'gotit') self.assertEqual(c.c, config.Bar.c) - c = Bar(a=2, b='and', c=20.0, config=config) + with expected_warnings(['`b` not recognized']): + c = Bar(a=2, b='and', c=20.0, config=config) self.assertEqual(c.a, 2) self.assertEqual(c.b, 'and') self.assertEqual(c.c, 20.0) @@ -263,15 +272,15 @@ self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b) class Containers(Configurable): - lis = List(config=True) + lis = List().tag(config=True) def _lis_default(self): return [-1] - s = Set(config=True) + s = Set().tag(config=True) def _s_default(self): return {'a'} - d = Dict(config=True) + d = Dict().tag(config=True) def _d_default(self): return {'a' : 'b'} @@ -353,12 +362,22 @@ m.update_config(c2) self.assertEqual(m.a, 15) + def test_update_self(self): + """update_config with same config object still triggers config_changed""" + c = Config() + c.MyConfigurable.a = 5 + m = MyConfigurable(config=c) + self.assertEqual(m.a, 5) + c.MyConfigurable.a = 10 + m.update_config(c) + self.assertEqual(m.a, 10) + def test_config_default(self): class SomeSingleton(SingletonConfigurable): pass class DefaultConfigurable(Configurable): - a = Integer(config=True) + a = Integer().tag(config=True) def _config_default(self): if SomeSingleton.initialized(): return SomeSingleton.instance().config @@ -376,3 +395,65 @@ self.assertIs(d2.config, single.config) self.assertEqual(d2.a, 5) + def test_config_default_deprecated(self): + """Make sure configurables work even with the deprecations in traitlets""" + class SomeSingleton(SingletonConfigurable): + pass + + # reset deprecation limiter + _deprecations_shown.clear() + with expected_warnings([]): + class DefaultConfigurable(Configurable): + a = Integer(config=True) + def _config_default(self): + if SomeSingleton.initialized(): + return SomeSingleton.instance().config + return Config() + + c = Config() + c.DefaultConfigurable.a = 5 + + d1 = DefaultConfigurable() + self.assertEqual(d1.a, 0) + + single = SomeSingleton.instance(config=c) + + d2 = DefaultConfigurable() + self.assertIs(d2.config, single.config) + self.assertEqual(d2.a, 5) + + +class TestLogger(TestCase): + + class A(LoggingConfigurable): + foo = Integer(config=True) + bar = Integer(config=True) + baz = Integer(config=True) + + @mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs') + def test_warn_match(self): + logger = logging.getLogger('test_warn_match') + cfg = Config({'A': {'bat': 5}}) + with self.assertLogs(logger, logging.WARNING) as captured: + a = TestLogger.A(config=cfg, log=logger) + + output = '\n'.join(captured.output) + self.assertIn('Did you mean one of: `bar, baz`?', output) + self.assertIn('Config option `bat` not recognized by `A`.', output) + + cfg = Config({'A': {'fool': 5}}) + with self.assertLogs(logger, logging.WARNING) as captured: + a = TestLogger.A(config=cfg, log=logger) + + output = '\n'.join(captured.output) + self.assertIn('Config option `fool` not recognized by `A`.', output) + self.assertIn('Did you mean `foo`?', output) + + cfg = Config({'A': {'totally_wrong': 5}}) + with self.assertLogs(logger, logging.WARNING) as captured: + a = TestLogger.A(config=cfg, log=logger) + + output = '\n'.join(captured.output) + self.assertIn('Config option `totally_wrong` not recognized by `A`.', output) + self.assertNotIn('Did you mean', output) + diff -Nru traitlets-4.0.0/traitlets/config/tests/test_loader.py traitlets-4.3.2/traitlets/config/tests/test_loader.py --- traitlets-4.0.0/traitlets/config/tests/test_loader.py 2015-04-17 18:11:28.000000000 +0000 +++ traitlets-4.3.2/traitlets/config/tests/test_loader.py 2017-02-23 10:21:19.000000000 +0000 @@ -9,14 +9,10 @@ import os import pickle import sys - from tempfile import mkstemp from unittest import TestCase -from nose import SkipTest -import nose.tools as nt - - +from pytest import skip from traitlets.config.loader import ( Config, @@ -99,7 +95,45 @@ cl = JSONFileConfigLoader(fname, log=log) config = cl.load_config() self._check_conf(config) - + + def test_context_manager(self): + + fd, fname = mkstemp('.json') + f = os.fdopen(fd, 'w') + f.write('{}') + f.close() + + cl = JSONFileConfigLoader(fname, log=log) + + value = 'context_manager' + + with cl as c: + c.MyAttr.value = value + + self.assertEqual(cl.config.MyAttr.value, value) + + # check that another loader does see the change + cl2 = JSONFileConfigLoader(fname, log=log) + self.assertEqual(cl.config.MyAttr.value, value) + + def test_json_context_bad_write(self): + fd, fname = mkstemp('.json') + f = os.fdopen(fd, 'w') + f.write('{}') + f.close() + + with JSONFileConfigLoader(fname, log=log) as config: + config.A.b = 1 + + with self.assertRaises(TypeError): + with JSONFileConfigLoader(fname, log=log) as config: + config.A.cant_json = lambda x: x + + loader = JSONFileConfigLoader(fname, log=log) + cfg = loader.load_config() + assert cfg.A.b == 1 + assert 'cant_json' not in cfg.A + def test_collision(self): a = Config() b = Config() @@ -135,7 +169,7 @@ f.close() # Unlink the file cl = JSONFileConfigLoader(fname, log=log) - with nt.assert_raises(ValueError): + with self.assertRaises(ValueError): cl.load_config() @@ -238,7 +272,7 @@ try: barg = uarg.encode(sys.stdin.encoding) except (TypeError, UnicodeEncodeError): - raise SkipTest("sys.stdin.encoding can't handle 'é'") + raise skip("sys.stdin.encoding can't handle 'é'") cl = self.klass(log=log) config = cl.load_config([barg]) @@ -407,13 +441,13 @@ assert isinstance(foo, LazyConfigValue) self.assertIn('foo', cfg) - def test_merge_copies(self): + def test_merge_no_copies(self): c = Config() c2 = Config() c2.Foo.trait = [] c.merge(c2) c2.Foo.trait.append(1) - self.assertIsNot(c.Foo, c2.Foo) - self.assertEqual(c.Foo.trait, []) + self.assertIs(c.Foo, c2.Foo) + self.assertEqual(c.Foo.trait, [1]) self.assertEqual(c2.Foo.trait, [1]) diff -Nru traitlets-4.0.0/traitlets/log.py traitlets-4.3.2/traitlets/log.py --- traitlets-4.0.0/traitlets/log.py 2015-04-17 18:11:28.000000000 +0000 +++ traitlets-4.3.2/traitlets/log.py 2017-02-23 10:21:19.000000000 +0000 @@ -9,17 +9,19 @@ 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: - logging.basicConfig() - _logger = logging.getLogger() + _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 -Nru traitlets-4.0.0/traitlets/tests/test_traitlets_enum.py traitlets-4.3.2/traitlets/tests/test_traitlets_enum.py --- traitlets-4.0.0/traitlets/tests/test_traitlets_enum.py 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/traitlets/tests/test_traitlets_enum.py 2017-02-23 10:21:19.000000000 +0000 @@ -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 -Nru traitlets-4.0.0/traitlets/tests/test_traitlets.py traitlets-4.3.2/traitlets/tests/test_traitlets.py --- traitlets-4.0.0/traitlets/tests/test_traitlets.py 2015-06-18 16:46:16.000000000 +0000 +++ traitlets-4.3.2/traitlets/tests/test_traitlets.py 2017-02-23 10:21:19.000000000 +0000 @@ -1,5 +1,5 @@ # encoding: utf-8 -"""Tests for ipython_genutils.traitlets.""" +"""Tests for traitlets.traitlets.""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. @@ -10,20 +10,26 @@ import pickle import re import sys -from unittest import TestCase +from ._warnings import expected_warnings -import nose.tools as nt -from nose import SkipTest +from unittest import TestCase +import pytest +from pytest import mark from traitlets import ( HasTraits, MetaHasTraits, TraitType, Any, Bool, CBytes, Dict, Enum, - Int, Long, Integer, Float, Complex, Bytes, Unicode, TraitError, - Union, Undefined, Type, This, Instance, TCPAddress, List, Tuple, - ObjectName, DottedObjectName, CRegExp, link, directional_link, - ForwardDeclaredType, ForwardDeclaredInstance, + 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, ) -from ipython_genutils import py3compat -from ipython_genutils.testing.decorators import skipif + +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 @@ -32,10 +38,11 @@ class HasTraitsStub(HasTraits): - def _notify_trait(self, name, old, new): - self._notify_name = name - self._notify_old = old - self._notify_new = new + 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'] #----------------------------------------------------------------------------- @@ -102,7 +109,7 @@ a = A() self.assertRaises(TraitError, A.tt.error, a, 10) - def test_dynamic_initializer(self): + def test_deprecated_dynamic_initializer(self): class A(HasTraits): x = Int(10) def _x_default(self): @@ -132,15 +139,119 @@ 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 TestHasTraitsMeta(TestCase): +class TestHasDescriptorsMeta(TestCase): def test_metaclass(self): self.assertEqual(type(HasTraits), MetaHasTraits) class A(HasTraits): - a = Int + a = Int() a = A() self.assertEqual(type(a.__class__), MetaHasTraits) @@ -176,6 +287,25 @@ 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): @@ -191,8 +321,8 @@ def test_notify_all(self): class A(HasTraits): - a = Int - b = Float + a = Int() + b = Float() a = A() a.on_trait_change(self.notify1) @@ -215,8 +345,8 @@ def test_notify_one(self): class A(HasTraits): - a = Int - b = Float + a = Int() + b = Float() a = A() a.on_trait_change(self.notify1, 'a') @@ -229,10 +359,10 @@ def test_subclass(self): class A(HasTraits): - a = Int + a = Int() class B(A): - b = Float + b = Float() b = B() self.assertEqual(b.a,0) @@ -245,10 +375,10 @@ def test_notify_subclass(self): class A(HasTraits): - a = Int + a = Int() class B(A): - b = Float + b = Float() b = B() b.on_trait_change(self.notify1, 'a') @@ -265,7 +395,7 @@ def test_static_notify(self): class A(HasTraits): - a = Int + a = Int() _notify1 = [] def _a_changed(self, name, old, new): self._notify1.append((name, old, new)) @@ -278,7 +408,7 @@ self.assertTrue(('a',0,10) in a._notify1) class B(A): - b = Float + b = Float() _notify2 = [] def _b_changed(self, name, old, new): self._notify2.append((name, old, new)) @@ -299,9 +429,11 @@ 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 = Int() a = A() a.on_trait_change(callback0, 'a') @@ -324,7 +456,13 @@ self.assertEqual(self.cb,('a',1000,10000)) a.on_trait_change(callback3, 'a', remove=True) - self.assertEqual(len(a._trait_notifiers['a']),0) + 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): @@ -364,22 +502,226 @@ 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 + 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(config_key='MY_VALUE') + i = Int().tag(config_key='MY_VALUE') a = A() self.assertEqual(a.trait_metadata('i','config_key'), 'MY_VALUE') @@ -392,16 +734,16 @@ def test_traits(self): class A(HasTraits): - i = Int - f = Float + 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(config_key='VALUE1', other_thing='VALUE2') - f = Float(config_key='VALUE3', other_thing='VALUE2') + 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)) @@ -413,6 +755,23 @@ 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() @@ -658,7 +1017,7 @@ def test_this_class(self): class Foo(HasTraits): - this = This + this = This() f = Foo() self.assertEqual(f.this, None) @@ -706,7 +1065,7 @@ tree = Tree( value='foo', - leaves=[Tree('bar'), Tree('buzz')] + leaves=[Tree(value='bar'), Tree(value='buzz')] ) with self.assertRaises(TraitError): @@ -764,7 +1123,7 @@ class AnyTrait(HasTraits): - value = Any + value = Any() class AnyTraitTest(TraitTestBase): @@ -806,36 +1165,110 @@ _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 py3compat.PY3: + 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 py3compat.PY3 else long(99)) + value = Long(99 if six.PY3 else long(99)) class TestLong(TraitTestBase): obj = LongTrait() - _default_value = 99 if py3compat.PY3 else long(99) + _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 py3compat.PY3: + 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),)]) - @skipif(py3compat.PY3, "not relevant on py3") + @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) @@ -846,16 +1279,36 @@ def coerce(self, n): return int(n) - @skipif(py3compat.PY3, "not relevant on py3") + @mark.skipif(six.PY3, reason="not relevant on py3") def test_cast_small(self): """Integer casts small longs to int""" - if py3compat.PY3: - raise SkipTest("not relevant on py3") 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) @@ -869,10 +1322,27 @@ _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 py3compat.PY3: + 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) @@ -885,7 +1355,7 @@ _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 py3compat.PY3: + if not six.PY3: _bad_values.extend([long(10), long(-10)]) @@ -902,7 +1372,7 @@ 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 py3compat.PY3: + if not six.PY3: _bad_values.extend([long(10), long(-10)]) @@ -919,7 +1389,7 @@ '-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 py3compat.PY3: + if not six.PY3: _bad_values.extend([long(10), long(-10)]) @@ -967,7 +1437,7 @@ class ListTrait(HasTraits): - value = List(Int) + value = List(Int()) class TestList(TraitTestBase): @@ -1029,7 +1499,7 @@ class LenListTrait(HasTraits): - value = List(Int, [0], minlen=1, maxlen=2) + value = List(Int(), [0], minlen=1, maxlen=2) class TestLenList(TraitTestBase): @@ -1064,7 +1534,7 @@ 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)) + t = Tuple(Int(), CBytes(), default_value=(1,5)) class LooseTupleTrait(HasTraits): @@ -1086,12 +1556,12 @@ 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)) + t = Tuple(Int(), CBytes(), default_value=(1,5)) class MultiTupleTrait(HasTraits): - value = Tuple(Int, Bytes, default_value=[99,b'bottles']) + value = Tuple(Int(), Bytes(), default_value=[99,b'bottles']) class TestMultiTuple(TraitTestBase): @@ -1124,22 +1594,54 @@ c = DictTrait() c.value = d d['a'] = 5 - nt.assert_equal(d, c.value) - nt.assert_true(c.value is d) + assert d == c.value + assert c.value is d -class ValidatedDictTrait(HasTraits): + +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 TestInstanceDict(TraitTestBase): - obj = ValidatedDictTrait() +class TestInstanceFullyValidatedDict(TraitTestBase): + + obj = FullyValidatedDictTrait() _default_value = {'foo': 1} - _good_values = [{'0': 'foo', 'foo': 1}, {'1': 'bar', 'foo': 2}] - _bad_values = [{'0': 0, 'foo': 1}, {'1': 'bar', 'foo': 'bar'}] + _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(): @@ -1151,9 +1653,9 @@ d2 = Dict() foo = Foo() - nt.assert_equal(foo.d1, {}) - nt.assert_equal(foo.d2, {}) - nt.assert_is_not(foo.d1, foo.d2) + assert foo.d1 == {} + assert foo.d2 == {} + assert foo.d1 is not foo.d2 class TestValidationHook(TestCase): @@ -1166,7 +1668,9 @@ value = Int(0) parity = Enum(['odd', 'even'], default_value='even') - def _value_validate(self, value, trait): + @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): @@ -1182,6 +1686,31 @@ 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): @@ -1307,6 +1836,28 @@ 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.""" @@ -1350,29 +1901,35 @@ 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 _i_default(self): - return 1 - - def _i_changed(self, name, old, new): - self.j = new + 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) - nt.assert_equal(c2.i, c.i) - nt.assert_equal(c2.j, c.j) + 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) - nt.assert_equal(c2.i, c.i) - nt.assert_equal(c2.j, c.j) + assert c2.i == c.i + assert c2.j == c.j def test_hold_trait_notifications(): @@ -1395,29 +1952,29 @@ with t.hold_trait_notifications(): with t.hold_trait_notifications(): t.a = 1 - nt.assert_equal(t.a, 1) - nt.assert_equal(changes, []) + assert t.a == 1 + assert changes == [] t.a = 2 - nt.assert_equal(t.a, 2) + assert t.a == 2 with t.hold_trait_notifications(): t.a = 3 - nt.assert_equal(t.a, 3) - nt.assert_equal(changes, []) + assert t.a == 3 + assert changes == [] t.a = 4 - nt.assert_equal(t.a, 4) - nt.assert_equal(changes, []) + assert t.a == 4 + assert changes == [] t.a = 4 - nt.assert_equal(t.a, 4) - nt.assert_equal(changes, []) + assert t.a == 4 + assert changes == [] - nt.assert_equal(changes, [(0, 4)]) + assert changes == [(0, 4)] # Test roll-back try: with t.hold_trait_notifications(): t.b = 1 # raises a Trait error except: pass - nt.assert_equal(t.b, 0) + assert t.b == 0 class RollBack(HasTraits): @@ -1491,12 +2048,12 @@ def test_notification_order(): d = {c:c for c in 'abcdefghijkl'} obj = OrderTraits() - nt.assert_equal(obj.notified, {}) + assert obj.notified == {} obj = OrderTraits(**d) notifications = { c: d for c in 'abcdefghijkl' } - nt.assert_equal(obj.notified, notifications) + assert obj.notified == notifications @@ -1663,8 +2220,200 @@ c = C() - with nt.assert_raises(TraitError): + 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 -Nru traitlets-4.0.0/traitlets/tests/utils.py traitlets-4.3.2/traitlets/tests/utils.py --- traitlets-4.0.0/traitlets/tests/utils.py 2015-04-17 18:11:28.000000000 +0000 +++ traitlets-4.3.2/traitlets/tests/utils.py 2017-02-23 10:21:19.000000000 +0000 @@ -1,5 +1,4 @@ import sys -import nose.tools as nt from subprocess import Popen, PIPE @@ -19,10 +18,10 @@ cmd.extend(subcommand) cmd.append('-h') out, err, rc = get_output_error_code(cmd) - nt.assert_equal(rc, 0, err) - nt.assert_not_in("Traceback", err) - nt.assert_in("Options", out) - nt.assert_in("--help-all", out) + assert rc == 0, err + assert "Traceback" not in err + assert "Options" in out + assert "--help-all" in out return out, err @@ -33,8 +32,8 @@ cmd.extend(subcommand) cmd.append('--help-all') out, err, rc = get_output_error_code(cmd) - nt.assert_equal(rc, 0, err) - nt.assert_not_in("Traceback", err) - nt.assert_in("Options", out) - nt.assert_in("Class parameters", out) + assert rc == 0, err + assert "Traceback" not in err + assert "Options" in out + assert "Class parameters" in out return out, err diff -Nru traitlets-4.0.0/traitlets/tests/_warnings.py traitlets-4.3.2/traitlets/tests/_warnings.py --- traitlets-4.0.0/traitlets/tests/_warnings.py 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/traitlets/tests/_warnings.py 2017-02-23 10:21:19.000000000 +0000 @@ -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 -Nru traitlets-4.0.0/traitlets/traitlets.py traitlets-4.3.2/traitlets/traitlets.py --- traitlets-4.0.0/traitlets/traitlets.py 2015-06-18 16:46:16.000000000 +0000 +++ traitlets-4.3.2/traitlets/traitlets.py 2017-02-23 10:21:19.000000000 +0000 @@ -42,23 +42,25 @@ import contextlib import inspect +import os import re import sys import types -from types import FunctionType +import enum try: from types import ClassType, InstanceType ClassTypes = (ClassType, type) except: ClassTypes = (type,) -from warnings import warn +from warnings import warn, warn_explicit -from ipython_genutils import py3compat -from ipython_genutils.py3compat import iteritems, string_types +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) #----------------------------------------------------------------------------- @@ -72,6 +74,13 @@ ''' ) +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 @@ -82,19 +91,72 @@ # 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 -def class_of ( object ): + 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 "" + 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, py3compat.string_types ): + if isinstance( object, six.string_types ): return add_article( object ) return add_article( object.__class__.__name__ ) -def add_article ( name ): +def add_article(name): """ Returns a string containing the correct indefinite article ('a' or 'an') prefixed to the specified string. """ @@ -109,7 +171,7 @@ error messages. """ the_type = type(obj) - if (not py3compat.PY3) and the_type is InstanceType: + if six.PY2 and the_type is InstanceType: # Old-style class. the_type = obj.__class__ msg = '%r %r' % (obj, the_type) @@ -123,27 +185,30 @@ (isinstance(t, type) and issubclass(t, TraitType))) -def parse_notifier_name(name): +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']) + >>> parse_notifier_name(['a', 'b']) ['a', 'b'] - >>> parse_notifier_name(None) - ['anytrait'] + >>> parse_notifier_name(All) + [All] """ - if isinstance(name, string_types): - return [name] - elif name is None: - return ['anytrait'] - elif isinstance(name, (list, tuple)): - for n in name: - assert isinstance(n, string_types), "names must be strings" - return name + 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: @@ -186,7 +251,6 @@ 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. @@ -209,8 +273,8 @@ try: setattr(target[0], target[1], getattr(source[0], source[1])) finally: - source[0].on_trait_change(self._update_target, source[1]) - target[0].on_trait_change(self._update_source, target[1]) + source[0].observe(self._update_target, names=source[1]) + target[0].observe(self._update_source, names=target[1]) @contextlib.contextmanager def _busy_updating(self): @@ -220,21 +284,21 @@ finally: self.updating = False - def _update_target(self, name, old, new): + def _update_target(self, change): if self.updating: return with self._busy_updating(): - setattr(self.target[0], self.target[1], new) + setattr(self.target[0], self.target[1], change.new) - def _update_source(self, name, old, new): + def _update_source(self, change): if self.updating: return with self._busy_updating(): - setattr(self.source[0], self.source[1], new) + setattr(self.source[0], self.source[1], change.new) def unlink(self): - self.source[0].on_trait_change(self._update_target, self.source[1], remove=True) - self.target[0].on_trait_change(self._update_source, self.target[1], remove=True) + 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 @@ -245,6 +309,8 @@ ---------- source : (object, attribute name) pair target : (object, attribute name) pair + transform: callable (optional) + Data transformation between source and target. Examples -------- @@ -255,13 +321,15 @@ """ updating = False - def __init__(self, source, target): + 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], getattr(source[0], source[1])) + setattr(target[0], target[1], + self._transform(getattr(source[0], source[1]))) finally: - self.source[0].on_trait_change(self._update, self.source[1]) + self.source[0].observe(self._update, names=self.source[1]) @contextlib.contextmanager def _busy_updating(self): @@ -271,29 +339,31 @@ finally: self.updating = False - def _update(self, name, old, new): + def _update(self, change): if self.updating: return with self._busy_updating(): - setattr(self.target[0], self.target[1], new) + setattr(self.target[0], self.target[1], + self._transform(change.new)) def unlink(self): - self.source[0].on_trait_change(self._update, self.source[1], remove=True) + self.source[0].unobserve(self._update, names=self.source[1]) self.source, self.target = None, None dlink = directional_link #----------------------------------------------------------------------------- -# Base Descriptor Class +# Base Descriptor Class #----------------------------------------------------------------------------- + class BaseDescriptor(object): """Base descriptor class Notes ----- - This implements Python's descriptor prototol. + 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` @@ -310,9 +380,22 @@ 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 - HasTraits instance. + HasDescriptors instance. It is typically overloaded for specific types. @@ -330,58 +413,71 @@ metadata = {} default_value = Undefined allow_none = False + read_only = False info_text = 'any value' - def __init__(self, default_value=Undefined, allow_none=None, **metadata): + 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, but - most trait types have ``allow_none=False`` by default. + 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 information about the traitlet can be passed in as keyword - arguments (``**metadata``). For instance, the config system uses 'config' - and 'help' keywords. + 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 is not None: + if allow_none: self.allow_none = allow_none - - if 'default' in metadata: - # Warn the user that they probably meant default_value. - warn( - "Parameter 'default' passed to TraitType. " - "Did you mean 'default_value'?" - ) - - if len(metadata) > 0: + 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(metadata) + self.metadata = self.metadata.copy() + self.metadata.update(kwargs) else: - self._metadata = metadata + self.metadata = kwargs else: - self._metadata = self.metadata - - self.init() - - def init(self): - pass + 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: use the .default_value attribute", + 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, and may be removed in the future", + 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 @@ -392,30 +488,59 @@ This looks for: - - obj._{name}_default() on the class with the traitlet, or a subclass + * 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 + * 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]: + 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__: - return getattr(obj, meth_name) + 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. - 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 + 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. @@ -428,25 +553,9 @@ if obj is None: return self else: - 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 + return self.get(obj, cls) - def __set__(self, obj, value): + def set(self, obj, value): new_value = self._validate(obj, value) try: old_value = obj._trait_values[self.name] @@ -464,6 +573,17 @@ # 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 @@ -474,8 +594,14 @@ return value def _cross_validate(self, obj, value): - if hasattr(obj, '_%s_validate' % self.name): - cross_validate = getattr(obj, '_%s_validate' % self.name) + 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 @@ -499,72 +625,345 @@ raise TraitError(e) def get_metadata(self, key, default=None): - return getattr(self, '_metadata', {}).get(key, default) + """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): - getattr(self, '_metadata', {})[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 MetaHasTraits(type): - """A metaclass for HasTraits. +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 HasTraits class. + """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() + # ---------------------------------------------------------------- - This instantiates all TraitTypes in the class dict and sets their - :attr:`name` attribute. - """ - # print "MetaHasTraitlets (mcls, name): ", mcls, name - # print "MetaHasTraitlets (bases): ", bases - # print "MetaHasTraitlets (classdict): ", classdict - for k,v in iteritems(classdict): - if isinstance(v, BaseDescriptor): - v.name = k - elif inspect.isclass(v): - if issubclass(v, TraitType): - vinst = v() - vinst.name = k - classdict[k] = vinst - return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict) + return super(MetaHasDescriptors, mcls).__new__(mcls, name, bases, classdict) def __init__(cls, name, bases, classdict): - """Finish initializing the HasTraits class. - - This sets the :attr:`this_class` attribute of each BaseDescriptor in the - class dict to the newly created class ``cls``. + """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 iteritems(classdict): + for k, v in classdict.items(): if isinstance(v, BaseDescriptor): - v.this_class = cls - super(MetaHasTraits, cls).__init__(name, bases, classdict) + 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 -class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)): - """The base class for all classes that have traitlets. + 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, **kw): + def __new__(cls, *args, **kwargs): # This is needed because object.__new__ only accepts # the cls argument. - new_meth = super(HasTraits, cls).__new__ + new_meth = super(HasDescriptors, cls).__new__ if new_meth is object.__new__: inst = new_meth(cls) else: - inst = new_meth(cls, **kw) - inst._trait_values = {} - inst._trait_notifiers = {} - inst._cross_validation_lock = True - # Here we tell all the TraitType instances to set their default - # values on the instance. + 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 @@ -575,17 +974,95 @@ pass else: if isinstance(value, BaseDescriptor): - value.instance_init(inst) - inst._cross_validation_lock = False - return inst + value.instance_init(self) - def __init__(self, *args, **kw): + +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 iteritems(kw): - setattr(self, key, value) + 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): @@ -596,119 +1073,132 @@ 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 is True: + if self._cross_validation_lock: yield return else: cache = {} - _notify_trait = self._notify_trait + notify_change = self.notify_change - def merge(previous, current): - """merges notifications of the form (name, old, value)""" - if previous is None: - return current + def compress(past_changes, change): + """Merges the provided change with the last if possible.""" + if past_changes is None: + return [change] else: - return (current[0], previous[1], current[2]) - - def hold(*a): - cache[a[0]] = merge(cache.get(a[0]), a) + 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: - self._notify_trait = hold + # 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()): - if hasattr(self, '_%s_validate' % name): - cross_validate = getattr(self, '_%s_validate' % name) - setattr(self, name, cross_validate(getattr(self, name), self)) + trait = getattr(self.__class__, name) + value = trait._cross_validate(self, getattr(self, name)) + self.set_trait(name, value) except TraitError as e: - self._notify_trait = lambda *x: None - for name, value in cache.items(): - if value[1] is not Undefined: - setattr(self, name, value[1]) - else: - self._trait_values.pop(name) + # 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._notify_trait = _notify_trait self._cross_validation_lock = False - if isinstance(_notify_trait, types.MethodType): - # FIXME: remove when support is bumped to 3.4. - # when original method is restored, - # remove the redundant value from __dict__ - # (only used to preserve pickleability on Python < 3.4) - self.__dict__.pop('_notify_trait', None) + # Restore method retrieval from class + del self.notify_change # trigger delayed notifications - for v in cache.values(): - self._notify_trait(*v) + 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 - # First dynamic ones callables = [] - callables.extend(self._trait_notifiers.get(name,[])) - callables.extend(self._trait_notifiers.get('anytrait',[])) + 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 - try: - cb = getattr(self, '_%s_changed' % name) - except: - pass - else: - callables.append(cb) + 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: - # Traits catches and logs errors here. I allow them to raise - if callable(c): - argspec = getargspec(c) - - nargs = len(argspec[0]) - # Bound methods have an additional 'self' argument - # I don't know how to treat unbound methods, but they - # can't really be used for callbacks. - if isinstance(c, types.MethodType): - offset = -1 - else: - offset = 0 - if nargs + offset == 0: - c() - elif nargs + offset == 1: - c(name) - elif nargs + offset == 2: - c(name, new_value) - elif nargs + offset == 3: - c(name, old_value, new_value) - else: - raise TraitError('a trait changed callback ' - 'must have 0-3 arguments.') - else: - raise TraitError('a trait changed callback ' - 'must be callable.') + # 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): + def _add_notifiers(self, handler, name, type): if name not in self._trait_notifiers: nlist = [] - self._trait_notifiers[name] = nlist + self._trait_notifiers[name] = {type: nlist} else: - nlist = self._trait_notifiers[name] + 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): - if name in self._trait_notifiers: - nlist = self._trait_notifiers[name] - try: - index = nlist.index(handler) - except ValueError: - pass + def _remove_notifiers(self, handler, name, type): + try: + if handler is None: + del self._trait_notifiers[name][type] else: - del nlist[index] + self._trait_notifiers[name][type].remove(handler) + except KeyError: + pass - def on_trait_change(self, handler, name=None, remove=False): - """Setup a handler to be called when a trait changes. + 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. @@ -718,12 +1208,15 @@ _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 + handler : callable, None A callable that is called when a trait changes. Its - signature can be handler(), handler(name), handler(name, new) - or handler(name, old, new). + 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 @@ -732,14 +1225,122 @@ 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: - names = parse_notifier_name(name) - for n in names: - self._remove_notifiers(handler, n) - else: - names = parse_notifier_name(name) - for n in names: - self._add_notifiers(handler, n) + 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): @@ -748,11 +1349,11 @@ This method is just like the :meth:`trait_names` method, but is unbound. """ - return cls.class_traits(**metadata).keys() + return list(cls.class_traits(**metadata)) @classmethod def class_traits(cls, **metadata): - """Get a `dict` of all the traits of this class. The dictionary + """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. @@ -764,10 +1365,8 @@ 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. This does not allow for any simple way of - testing that a metadata name exists and has any - value because get_metadata returns None if a metadata key - doesn't exist. + 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)]) @@ -775,14 +1374,12 @@ if len(metadata) == 0: return traits - for meta_name, meta_eval in metadata.items(): - if type(meta_eval) is not FunctionType: - metadata[meta_name] = _SimpleTest(meta_eval) - result = {} for name, trait in traits.items(): for meta_name, meta_eval in metadata.items(): - if not meta_eval(trait.get_metadata(meta_name)): + 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 @@ -802,13 +1399,13 @@ 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 self.traits(**metadata).keys() + return list(self.traits(**metadata)) def traits(self, **metadata): - """Get a `dict` of all the traits of this class. The dictionary + """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 @@ -818,10 +1415,8 @@ 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. This does not allow for any simple way of - testing that a metadata name exists and has any - value because get_metadata returns None if a metadata key - doesn't exist. + 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)]) @@ -829,14 +1424,12 @@ if len(metadata) == 0: return traits - for meta_name, meta_eval in metadata.items(): - if type(meta_eval) is not FunctionType: - metadata[meta_name] = _SimpleTest(meta_eval) - result = {} for name, trait in traits.items(): for meta_name, meta_eval in metadata.items(): - if not meta_eval(trait.get_metadata(meta_name)): + 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 @@ -850,15 +1443,47 @@ 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.get_metadata(key, default) + return trait.metadata.get(key, default) - 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) + @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 @@ -883,7 +1508,7 @@ def error(self, obj, value): kind = type(value) - if (not py3compat.PY3) and kind is InstanceType: + 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 ) ) @@ -902,7 +1527,7 @@ class Type(ClassBasedTraitType): """A trait whose value must be a subclass of a specified class.""" - def __init__ (self, default_value=Undefined, klass=None, **metadata): + def __init__ (self, default_value=Undefined, klass=None, **kwargs): """Construct a Type trait A Type trait specifies that its values must be subclasses of @@ -937,16 +1562,16 @@ else: klass = default_value - if not (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)): + if not (inspect.isclass(klass) or isinstance(klass, six.string_types)): raise TraitError("A Type trait must specify a class.") - self.klass = klass + self.klass = klass - super(Type, self).__init__(new_default_value, **metadata) + 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, py3compat.string_types): + if isinstance(value, six.string_types): try: value = self._resolve_string(value) except ImportError: @@ -962,10 +1587,10 @@ def info(self): """ Returns a description of the trait.""" - if isinstance(self.klass, py3compat.string_types): + if isinstance(self.klass, six.string_types): klass = self.klass else: - klass = self.klass.__module__+'.'+self.klass.__name__ + klass = self.klass.__module__ + '.' + self.klass.__name__ result = "a subclass of '%s'" % klass if self.allow_none: return result + ' or None' @@ -976,11 +1601,18 @@ super(Type, self).instance_init(obj) def _resolve_classes(self): - if isinstance(self.klass, py3compat.string_types): + if isinstance(self.klass, six.string_types): self.klass = self._resolve_string(self.klass) - if isinstance(self.default_value, py3compat.string_types): + 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. @@ -992,7 +1624,7 @@ klass = None - def __init__(self, klass=None, args=None, kw=None, **metadata): + def __init__(self, klass=None, args=None, kw=None, **kwargs): """Construct an Instance trait. This trait allows values that are instances of a particular @@ -1009,7 +1641,7 @@ Positional arguments for generating the default value. kw : dict Keyword arguments for generating the default value. - allow_none : bool [default True] + allow_none : bool [ default False ] Indicates whether None is allowed as a value. Notes @@ -1021,8 +1653,8 @@ """ if klass is None: klass = self.klass - - if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)): + + 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' @@ -1036,7 +1668,7 @@ self.default_args = args self.default_kwargs = kw - super(Instance, self).__init__(**metadata) + super(Instance, self).__init__(**kwargs) def validate(self, obj, value): if isinstance(value, self.klass): @@ -1045,7 +1677,7 @@ self.error(obj, value) def info(self): - if isinstance(self.klass, py3compat.string_types): + if isinstance(self.klass, six.string_types): klass = self.klass else: klass = self.klass.__name__ @@ -1060,7 +1692,7 @@ super(Instance, self).instance_init(obj) def _resolve_classes(self): - if isinstance(self.klass, py3compat.string_types): + if isinstance(self.klass, six.string_types): self.klass = self._resolve_string(self.klass) def make_dynamic_default(self): @@ -1069,6 +1701,9 @@ 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): """ @@ -1107,8 +1742,8 @@ info_text = 'an instance of the same type as the receiver or None' - def __init__(self, **metadata): - super(This, self).__init__(None, **metadata) + 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 @@ -1123,7 +1758,7 @@ class Union(TraitType): """A trait type representing a Union type.""" - def __init__(self, trait_types, **metadata): + def __init__(self, trait_types, **kwargs): """Construct a Union trait. This trait allows values that are allowed by at least one of the @@ -1141,24 +1776,30 @@ with the validation function of Float, then Bool, and finally Int. """ self.trait_types = trait_types - self.info_text = " or ".join([tt.info_text for tt in self.trait_types]) - self.default_value = self.trait_types[0].default_value - super(Union, self).__init__(**metadata) + 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.this_class = self.this_class trait_type.instance_init(obj) super(Union, self).instance_init(obj) def validate(self, obj, value): - for trait_type in self.trait_types: - try: - v = trait_type._validate(obj, value) - self._metadata = trait_type._metadata - return v - except TraitError: - continue + 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): @@ -1167,6 +1808,16 @@ 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 #----------------------------------------------------------------------------- @@ -1178,14 +1829,37 @@ 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=None, **kwargs): + 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, @@ -1194,17 +1868,7 @@ def validate(self, obj, value): if not isinstance(value, int): self.error(obj, value) - if self.max is not None and value > self.max: - raise TraitError("The value of the '%s' trait of %s instance should " - "not be greater than %s, but a value of %s was " - "specified" % (self.name, class_of(obj), - self.max, value)) - if self.min is not None and value < self.min: - raise TraitError("The value of the '%s' trait of %s instance should " - "not be less than %s, but a value of %s was " - "specified" % (self.name, class_of(obj), - self.min, value)) - return value + return _validate_bounds(self, obj, value) class CInt(Int): @@ -1212,36 +1876,48 @@ def validate(self, obj, value): try: - return int(value) + value = int(value) except: self.error(obj, value) + return _validate_bounds(self, obj, value) -if py3compat.PY3: - Long, CLong = Int, CInt - Integer = Int -else: + +if six.PY2: class Long(TraitType): """A long integer trait.""" default_value = 0 info_text = 'a long' - def validate(self, obj, value): + 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: - return long(value) + value = long(value) except: self.error(obj, value) + return _validate_bounds(self, obj, value) + class Integer(TraitType): """An integer trait. @@ -1251,7 +1927,14 @@ default_value = 0 info_text = 'an integer' - def validate(self, obj, value): + 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): @@ -1265,6 +1948,14 @@ 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.""" @@ -1272,11 +1963,10 @@ default_value = 0.0 info_text = 'a float' - def __init__(self, default_value=Undefined, - allow_none=None, **kwargs): + 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, + super(Float, self).__init__(default_value=default_value, allow_none=allow_none, **kwargs) def validate(self, obj, value): @@ -1284,12 +1974,7 @@ value = float(value) if not isinstance(value, float): self.error(obj, value) - if value > self.max or value < self.min: - raise TraitError("The value of the '%s' trait of %s instance should " - "be between %s and %s, but a value of %s was " - "specified" % (self.name, class_of(obj), - self.min, self.max, value)) - return value + return _validate_bounds(self, obj, value) class CFloat(Float): @@ -1297,9 +1982,11 @@ def validate(self, obj, value): try: - return float(value) + value = float(value) except: self.error(obj, value) + return _validate_bounds(self, obj, value) + class Complex(TraitType): """A trait for complex numbers.""" @@ -1356,7 +2043,7 @@ info_text = 'a unicode string' def validate(self, obj, value): - if isinstance(value, py3compat.unicode_type): + if isinstance(value, six.text_type): return value if isinstance(value, bytes): try: @@ -1372,7 +2059,7 @@ def validate(self, obj, value): try: - return py3compat.unicode_type(value) + return six.text_type(value) except: self.error(obj, value) @@ -1383,11 +2070,7 @@ This does not check that the name exists in any scope.""" info_text = "a valid object identifier in Python" - if py3compat.PY3: - # Python 3: - coerce_str = staticmethod(lambda _,s: s) - - else: + if six.PY2: # Python 2: def coerce_str(self, obj, value): "In Python 2, coerce ascii-only unicode to str" @@ -1397,11 +2080,13 @@ 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, string_types) and py3compat.isidentifier(value): + if isinstance(value, six.string_types) and isidentifier(value): return value self.error(obj, value) @@ -1410,7 +2095,8 @@ def validate(self, obj, value): value = self.coerce_str(obj, value) - if isinstance(value, string_types) and py3compat.isidentifier(value, dotted=True): + if isinstance(value, six.string_types) and all(isidentifier(a) + for a in value.split('.')): return value self.error(obj, value) @@ -1440,11 +2126,11 @@ class Enum(TraitType): """An enum whose value must be in a given sequence.""" - def __init__(self, values, default_value=Undefined, **metadata): + def __init__(self, values, default_value=Undefined, **kwargs): self.values = values - if metadata.get('allow_none', False) and default_value is Undefined: + if kwargs.get('allow_none', False) and default_value is Undefined: default_value = None - super(Enum, self).__init__(default_value, **metadata) + super(Enum, self).__init__(default_value, **kwargs) def validate(self, obj, value): if value in self.values: @@ -1461,8 +2147,14 @@ 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 not isinstance(value, py3compat.string_types): + 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: @@ -1480,7 +2172,7 @@ _valid_defaults = SequenceTypes _trait = None - def __init__(self, trait=None, default_value=None, **metadata): + 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)``, @@ -1492,7 +2184,7 @@ If only one arg is given and it is not a Trait, it is taken as ``default_value``: - ``c = List([1,2,3])`` + ``c = List([1, 2, 3])`` Parameters ---------- @@ -1508,7 +2200,7 @@ allow_none : bool [ default False ] Whether to allow the value to be None - **metadata : any + **kwargs : any further keys for extensions to the Trait (e.g. config) """ @@ -1525,11 +2217,15 @@ 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, **metadata) + 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." \ @@ -1560,9 +2256,13 @@ 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.this_class = self.this_class self._trait.instance_init(obj) super(Container, self).instance_init(obj) @@ -1572,7 +2272,7 @@ klass = list _cast_types = (tuple,) - def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, **metadata): + 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)``, @@ -1584,7 +2284,7 @@ If only one arg is given and it is not a Trait, it is taken as ``default_value``: - ``c = List([1,2,3])`` + ``c = List([1, 2, 3])`` Parameters ---------- @@ -1606,7 +2306,7 @@ self._minlen = minlen self._maxlen = maxlen super(List, self).__init__(trait=trait, default_value=default_value, - **metadata) + **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." \ @@ -1633,7 +2333,7 @@ # Redefine __init__ just to make the docstring more accurate. def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, - **metadata): + **kwargs): """Create a Set trait type from a list, set, or tuple. The default value is created by doing ``set(default_value)``, @@ -1645,7 +2345,7 @@ If only one arg is given and it is not a Trait, it is taken as ``default_value``: - ``c = Set({1,2,3})`` + ``c = Set({1, 2, 3})`` Parameters ---------- @@ -1664,7 +2364,7 @@ maxlen : Int [ default sys.maxsize ] The maximum length of the input list """ - super(Set, self).__init__(trait, default_value, minlen, maxlen, **metadata) + super(Set, self).__init__(trait, default_value, minlen, maxlen, **kwargs) class Tuple(Container): @@ -1672,19 +2372,19 @@ klass = tuple _cast_types = (list,) - def __init__(self, *traits, **metadata): + 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)`` + ``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))`` + ``t = Tuple((1, 2, 3))`` Otherwise, ``default_value`` *must* be specified by keyword. @@ -1699,17 +2399,16 @@ 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, the - `default_value` must conform to the shape and type they specify. + will be cast to a tuple. If ``traits`` are specified, + ``default_value`` must conform to the shape and type they specify. """ - default_value = metadata.pop('default_value', None) - + default_value = kwargs.pop('default_value', Undefined) # allow Tuple((values,)): - if len(traits) == 1 and default_value is None and not is_trait(traits[0]): + if len(traits) == 1 and default_value is Undefined and not is_trait(traits[0]): default_value = traits[0] traits = () - if default_value is None: + if default_value is Undefined: args = () elif isinstance(default_value, self._valid_defaults): args = (default_value,) @@ -1718,13 +2417,17 @@ 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, **metadata) + super(Container,self).__init__(klass=self.klass, args=args, **kwargs) def validate_elements(self, obj, value): if not self._traits: @@ -1745,10 +2448,15 @@ 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.this_class = self.this_class trait.instance_init(obj) super(Container, self).instance_init(obj) @@ -1758,19 +2466,22 @@ _trait = None def __init__(self, trait=None, traits=None, default_value=Undefined, - **metadata): - """Create a dict trait type from a dict. + **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 type for restricting the contents of the Container. If - unspecified, types are not checked. + 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] - The type for restricting the content of the Dictionary for certain - keys. + 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 @@ -1797,13 +2508,17 @@ # 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, **metadata) + 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." \ @@ -1818,35 +2533,41 @@ return value def validate_elements(self, obj, value): - if self._traits is not None: - for key in self._traits: - if key not in value: - raise TraitError("Missing required '%s' key for the '%s' trait of %s instance" - % (key, self.name, class_of(obj))) - if self._traits is None and (self._trait is None or - isinstance(self._trait, Any)): + 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: - v = value[key] + if use_dict and key in self._traits: + validate_with = self._traits[key] + else: + validate_with = default_to try: - if self._traits is not None and key in self._traits: - v = self._traits[key]._validate(obj, v) - else: - v = self._trait._validate(obj, v) + v = value[key] + if not isinstance(validate_with, Any): + v = validate_with._validate(obj, v) except TraitError: - self.element_error(obj, v, self._trait) + 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.this_class = self.this_class self._trait.instance_init(obj) if self._traits is not None: for trait in self._traits.values(): - trait.this_class = self.this_class trait.instance_init(obj) super(Dict, self).instance_init(obj) @@ -1863,7 +2584,7 @@ def validate(self, obj, value): if isinstance(value, tuple): if len(value) == 2: - if isinstance(value[0], py3compat.string_types) and isinstance(value[1], int): + if isinstance(value[0], six.string_types) and isinstance(value[1], int): port = value[1] if port >= 0 and port <= 65535: return value @@ -1882,3 +2603,88 @@ 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 -Nru traitlets-4.0.0/traitlets/utils/bunch.py traitlets-4.3.2/traitlets/utils/bunch.py --- traitlets-4.0.0/traitlets/utils/bunch.py 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/traitlets/utils/bunch.py 2017-02-23 10:21:19.000000000 +0000 @@ -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 -Nru traitlets-4.0.0/traitlets/utils/getargspec.py traitlets-4.3.2/traitlets/utils/getargspec.py --- traitlets-4.0.0/traitlets/utils/getargspec.py 2015-04-29 19:06:58.000000000 +0000 +++ traitlets-4.3.2/traitlets/utils/getargspec.py 2017-02-23 10:21:19.000000000 +0000 @@ -10,7 +10,7 @@ """ import inspect -from ipython_genutils.py3compat import PY3 +from six import PY3 # Unmodified from sphinx below this line diff -Nru traitlets-4.0.0/traitlets/utils/importstring.py traitlets-4.3.2/traitlets/utils/importstring.py --- traitlets-4.0.0/traitlets/utils/importstring.py 2015-04-29 19:06:58.000000000 +0000 +++ traitlets-4.3.2/traitlets/utils/importstring.py 2017-02-23 10:21:19.000000000 +0000 @@ -2,10 +2,11 @@ """ 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``. @@ -23,7 +24,9 @@ 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....' diff -Nru traitlets-4.0.0/traitlets/utils/tests/test_bunch.py traitlets-4.3.2/traitlets/utils/tests/test_bunch.py --- traitlets-4.0.0/traitlets/utils/tests/test_bunch.py 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/traitlets/utils/tests/test_bunch.py 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,14 @@ +from ..bunch import Bunch + +def test_bunch(): + b = Bunch(x=5, y=10) + assert 'y' in b + assert 'x' in b + assert b.x == 5 + b['a'] = 'hi' + assert b.a == 'hi' + +def test_bunch_dir(): + b = Bunch(x=5, y=10) + assert 'x' in dir(b) + assert 'keys' in dir(b) diff -Nru traitlets-4.0.0/traitlets/utils/tests/test_importstring.py traitlets-4.3.2/traitlets/utils/tests/test_importstring.py --- traitlets-4.0.0/traitlets/utils/tests/test_importstring.py 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/traitlets/utils/tests/test_importstring.py 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,30 @@ +# encoding: utf-8 +# 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. +"""Tests for traitlets.utils.importstring.""" + +import os +from unittest import TestCase + +from ..importstring import import_item + + +class TestImportItem(TestCase): + + def test_import_unicode(self): + self.assertIs(os, import_item(u'os')) + self.assertIs(os.path, import_item(u'os.path')) + self.assertIs(os.path.join, import_item(u'os.path.join')) + + def test_bad_input(self): + class NotAString(object): + pass + msg = ( + "import_item accepts strings, " + "not '%s'." % NotAString + ) + with self.assertRaisesRegexp(TypeError, msg): + import_item(NotAString()) diff -Nru traitlets-4.0.0/traitlets/_version.py traitlets-4.3.2/traitlets/_version.py --- traitlets-4.0.0/traitlets/_version.py 2015-06-19 20:20:17.000000000 +0000 +++ traitlets-4.3.2/traitlets/_version.py 2017-02-23 10:21:19.000000000 +0000 @@ -1,2 +1,2 @@ -version_info = (4, 0, 0) +version_info = (4, 3, 2) __version__ = '.'.join(map(str, version_info)) diff -Nru traitlets-4.0.0/traitlets.egg-info/dependency_links.txt traitlets-4.3.2/traitlets.egg-info/dependency_links.txt --- traitlets-4.0.0/traitlets.egg-info/dependency_links.txt 2015-06-19 20:23:20.000000000 +0000 +++ traitlets-4.3.2/traitlets.egg-info/dependency_links.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ - diff -Nru traitlets-4.0.0/traitlets.egg-info/PKG-INFO traitlets-4.3.2/traitlets.egg-info/PKG-INFO --- traitlets-4.0.0/traitlets.egg-info/PKG-INFO 2015-06-19 20:23:20.000000000 +0000 +++ traitlets-4.3.2/traitlets.egg-info/PKG-INFO 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -Metadata-Version: 1.1 -Name: traitlets -Version: 4.0.0 -Summary: Traitlets Python config system -Home-page: http://ipython.org -Author: IPython Development Team -Author-email: ipython-dev@scipy.org -License: BSD -Description: A configuration system for Python applications. -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 diff -Nru traitlets-4.0.0/traitlets.egg-info/requires.txt traitlets-4.3.2/traitlets.egg-info/requires.txt --- traitlets-4.0.0/traitlets.egg-info/requires.txt 2015-06-19 20:23:20.000000000 +0000 +++ traitlets-4.3.2/traitlets.egg-info/requires.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -ipython_genutils -decorator diff -Nru traitlets-4.0.0/traitlets.egg-info/SOURCES.txt traitlets-4.3.2/traitlets.egg-info/SOURCES.txt --- traitlets-4.0.0/traitlets.egg-info/SOURCES.txt 2015-06-19 20:23:20.000000000 +0000 +++ traitlets-4.3.2/traitlets.egg-info/SOURCES.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,38 +0,0 @@ -MANIFEST.in -setup.cfg -setup.py -docs/Makefile -docs/make.bat -docs/requirements.txt -docs/source/conf.py -docs/source/config.rst -docs/source/defining_traits.rst -docs/source/index.rst -docs/source/trait_types.rst -docs/source/using_traitlets.rst -examples/myapp.py -traitlets/__init__.py -traitlets/_version.py -traitlets/log.py -traitlets/traitlets.py -traitlets.egg-info/PKG-INFO -traitlets.egg-info/SOURCES.txt -traitlets.egg-info/dependency_links.txt -traitlets.egg-info/requires.txt -traitlets.egg-info/top_level.txt -traitlets/config/__init__.py -traitlets/config/application.py -traitlets/config/configurable.py -traitlets/config/loader.py -traitlets/config/manager.py -traitlets/config/tests/__init__.py -traitlets/config/tests/test_application.py -traitlets/config/tests/test_configurable.py -traitlets/config/tests/test_loader.py -traitlets/tests/__init__.py -traitlets/tests/test_traitlets.py -traitlets/tests/utils.py -traitlets/utils/__init__.py -traitlets/utils/getargspec.py -traitlets/utils/importstring.py -traitlets/utils/sentinel.py \ No newline at end of file diff -Nru traitlets-4.0.0/traitlets.egg-info/top_level.txt traitlets-4.3.2/traitlets.egg-info/top_level.txt --- traitlets-4.0.0/traitlets.egg-info/top_level.txt 2015-06-19 20:23:20.000000000 +0000 +++ traitlets-4.3.2/traitlets.egg-info/top_level.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -traitlets diff -Nru traitlets-4.0.0/.travis.yml traitlets-4.3.2/.travis.yml --- traitlets-4.0.0/.travis.yml 1970-01-01 00:00:00.000000000 +0000 +++ traitlets-4.3.2/.travis.yml 2017-02-23 10:21:19.000000000 +0000 @@ -0,0 +1,15 @@ +language: python +python: + - 3.5 + - 3.4 + - 2.7 + - 3.3 + - 3.6-dev +sudo: false +install: + - pip install . + - pip install traitlets[test] pytest-cov codecov +script: + - py.test --cov traitlets -v traitlets +after_success: + - codecov