apptools-4.5.0/0000755000076500000240000000000013547652535014661 5ustar mdickinsonstaff00000000000000apptools-4.5.0/PKG-INFO0000644000076500000240000001045413547652535015762 0ustar mdickinsonstaff00000000000000Metadata-Version: 2.1 Name: apptools Version: 4.5.0 Summary: application tools Home-page: https://docs.enthought.com/apptools Author: Enthought, Inc. Author-email: info@enthought.com Maintainer: ETS Developers Maintainer-email: enthought-dev@enthought.com License: BSD Download-URL: https://www.github.com/enthought/apptools Description: =========================== apptools: application tools =========================== .. image:: https://api.travis-ci.org/enthought/apptools.png?branch=master :target: https://travis-ci.org/enthought/apptools :alt: Build status .. image:: http://codecov.io/github/enthought/apptools/coverage.svg?branch=master :target: http://codecov.io/github/enthought/apptools?branch=master :alt: Coverage report Documentation: http://docs.enthought.com/apptools Source Code: http://www.github.com/enthought/apptools The apptools project includes a set of packages that Enthought has found useful in creating a number of applications. They implement functionality that is commonly needed by many applications - **apptools.appscripting**: Framework for scripting applications. - **apptools.help**: Provides a plugin for displaying documents and examples and running demos in Envisage Workbench applications. - **apptools.io**: Provides an abstraction for files and folders in a file system. - **apptools.logger**: Convenience functions for creating logging handlers - **apptools.naming**: Manages naming contexts, supporting non-string data types and scoped preferences - **apptools.permissions**: Supports limiting access to parts of an application unless the user is appropriately authorised (not full-blown security). - **apptools.persistence**: Supports pickling the state of a Python object to a dictionary, which can then be flexibly applied in restoring the state of the object. - **apptools.preferences**: Manages application preferences. - **apptools.selection**: Manages the communication between providers and listener of selected items in an application. - **apptools.scripting**: A framework for automatic recording of Python scripts. - **apptools.sweet_pickle**: Handles class-level versioning, to support loading of saved data that exist over several generations of internal class structures. - **apptools.template**: Supports creating templatizable object hierarchies. - **apptools.type_manager**: Manages type extensions, including factories to generate adapters, and hooks for methods and functions. - **apptools.undo**: Supports undoing and scripting application commands. Prerequisites ------------- All packages in apptools require: * `traits `_ The `apptools.preferences` package requires: * `configobj `_ Many of the packages provide optional user interfaces using Pyface and Traitsui. In additon, many of the packages are designed to work with the Envisage plug-in system, althought most can be used independently: * `envisage `_ * `pyface `_ * `traitsui `_ Platform: Windows Platform: Linux Platform: Mac OS-X Platform: Unix Platform: Solaris Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Description-Content-Type: text/x-rst apptools-4.5.0/TODO.txt0000644000076500000240000000126611640354733016163 0ustar mdickinsonstaff00000000000000* Take a critical look at whether apptools.template should be in this project. It appears to be adding a lot of dependencies the other packages do not need. For now, its use is controlled through an extra. * Refactor the state_pickler.py module to break the need to import tvtk at all. * Check to see if the pyface.resource package is redundant with the envisage.resource code. If so, let's minimize to one location. Note that, at least as of Jan 2008, we are considering moving pyface.resource into the TraitsGUI project to resolve issues with dependencies there. * Port apptools.help to envisage2, currently it relies on the old envisage, and is excluded from the tests. apptools-4.5.0/MANIFEST.in0000644000076500000240000000064312502031725016400 0ustar mdickinsonstaff00000000000000include README.rst include CHANGES.txt include LICENSE.txt include MANIFEST.in include TODO.txt include image_LICENSE.txt include image_LICENSE_OOo.txt graft docs prune docs/build recursive-exclude docs *.pyc graft examples recursive-exclude examples *.pyc graft integrationtests recursive-exclude integrationtests *.pyc recursive-include apptools *.py recursive-include apptools *.ini recursive-include apptools *.png apptools-4.5.0/integrationtests/0000755000076500000240000000000013547652535020267 5ustar mdickinsonstaff00000000000000apptools-4.5.0/integrationtests/persistence/0000755000076500000240000000000013547652535022613 5ustar mdickinsonstaff00000000000000apptools-4.5.0/integrationtests/persistence/update2.py0000644000076500000240000000106611640354733024523 0ustar mdickinsonstaff00000000000000# Update class names from the immediately prior version only # to ensure that cycles are not possible from apptools.persistence.updater import Updater def update_project(self, state): print 'updating to v2' metadata = state['metadata'] metadata['version'] = 2 metadata['updater'] = 22 return state class Update2(Updater): def __init__(self): self.refactorings = { ("__main__", "Foo1"): ("__main__", "Foo2"), } self.setstates = { ("cplab.project", "Project"): update_project } apptools-4.5.0/integrationtests/persistence/update3.py0000644000076500000240000000106711640354733024525 0ustar mdickinsonstaff00000000000000# Update class names from the immediately prior version only # to ensure that cycles are not possible from apptools.persistence.updater import Updater def update_project(self, state): print 'updating to v3' metadata = state['metadata'] metadata['version'] = 3 metadata['finished'] = True return state class Update3(Updater): def __init__(self): self.refactorings = { ("__main__", "Foo1"): ("__main__", "Foo2"), } self.setstates = { ("cplab.project", "Project"): update_project } apptools-4.5.0/integrationtests/persistence/test_persistence.py0000644000076500000240000000432311640354733026541 0ustar mdickinsonstaff00000000000000class Foo0: """ The original class written with no expectation of being upgraded """ def __init__(self): self.prenom = 'didier' self.surnom = 'enfant' class Foo1: """ Now to handle both Foo v0 and Foo v1 we need to add more code ...""" def __init__(self, firstname, lastname): """ This does not get called when the class is unpickled.""" self.firstname = firstname self.lastname = lastname class Foo: def __str__(self): result = ['-----------------------------------------------------------'] keys = dir(self) for key in keys: result.append('%s ---> %s' % (key, getattr(self, key))) result.append('-----------------------------------------------------------') return '\n'.join(result) def __setstate__(self, state): print 'calling setstate on the real Foo' state['set'] = True self.__dict__.update(state) def save(fname, str): f=open(fname, 'w') f.write(str) f.close() return if __name__ == '__main__': # Create dummy test data ....... #from cStringIO import StringIO import pickle obj = Foo0() print obj t0 = pickle.dumps(obj) #.replace('Foo0', 'Foo') save('foo0.txt', t0) '''obj = Foo1('duncan', 'child') t1 = pickle.dumps(obj).replace('Foo1', 'Foo') save('foo1.txt', t1) obj = Foo2('duncan child') t2 = pickle.dumps(obj).replace('Foo2', 'Foo') save('foo2.txt', t2) obj = Foo3('duncan child') t3 = pickle.dumps(obj).replace('Foo3', 'Foo') save('foo3.txt', t3) ''' print '====================================================================' from apptools.persistence.versioned_unpickler import VersionedUnpickler from update1 import Update1 # Try and read them back in ... f = open('foo0.txt') import sys rev = 1 __import__('integrationtests.persistence.update%d' % rev) mod = sys.modules['integrationtests.persistence.update%d' % rev] klass = getattr(mod, 'Update%d' % rev) updater = klass() print '%s %s' % (rev, updater) p = VersionedUnpickler(f, updater).load() print p print 'Restored version %s %s' % (p.lastname, p.firstname) #print p.set apptools-4.5.0/integrationtests/persistence/update1.py0000644000076500000240000000162711640354733024525 0ustar mdickinsonstaff00000000000000# Update class names from the immediately prior version only # to ensure that cycles are not possible from apptools.persistence.updater import Updater def cleanup_foo(self, state): print 'cleaning up Foo0' state['firstname'] = state['prenom'] state['lastname'] = state['surnom'] del state['prenom'] del state['surnom'] '''for key in state: print '%s state ---> %s' % (key, state[key]) ''' #self.__setstate_original__(state) self.__dict__.update(state) def update_project(self, state): print 'updating to v1' metadata = state['metadata'] metadata['version'] = 1 metadata['diesel'] = 'E300TD' return state class Update1(Updater): def __init__(self): self.refactorings = { ("__main__", "Foo0"): ("__main__", "Foo"), } self.setstates = { ("cplab.project", "Project"): update_project } apptools-4.5.0/docs/0000755000076500000240000000000013547652535015611 5ustar mdickinsonstaff00000000000000apptools-4.5.0/docs/Makefile0000644000076500000240000000607612317502217017243 0ustar mdickinsonstaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/apptools.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/apptools.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." apptools-4.5.0/docs/source/0000755000076500000240000000000013547652535017111 5ustar mdickinsonstaff00000000000000apptools-4.5.0/docs/source/index.rst0000644000076500000240000000056012317502217020734 0ustar mdickinsonstaff00000000000000AppTools Documentation ============================================== .. toctree:: :maxdepth: 2 :glob: appscripting/* permissions/Introduction permissions/ApplicationAPI permissions/DefaultPolicyManagerDataAPI permissions/DefaultUserManagerDataAPI preferences/* scripting/* undo/* selection/* * :ref:`search` apptools-4.5.0/docs/source/preferences/0000755000076500000240000000000013547652535021412 5ustar mdickinsonstaff00000000000000apptools-4.5.0/docs/source/preferences/PreferencesInEnvisage.rst0000644000076500000240000000311211640354733026342 0ustar mdickinsonstaff00000000000000Preferences in Envisage ======================= This section discusses how an Envisage application uses the preferences mechanism. Envisage tries not to dictate too much, and so this describes the default behaviour, but you are free to override it as desired. Envisage uses the default implementation of the ScopedPreferences_ class which is made available via the application's 'preferences' trait:: >>> application = Application(id='myapplication') >>> application.preferences.set('acme.ui.bgcolor', 'yellow') >>> application.preferences.get('acme.ui.bgcolor') 'yellow' Hence, you use the Envisage preferences just like you would any other scoped preferences. It also registers itself as the default preferences node used by the PreferencesHelper_ class. Hence you don't need to provide a preferences node explicitly to your helper:: >>> helper = SplashScreenPreferences() >>> helper.bgcolor 'blue' >>> helper.width 100 >>> helper.ratio 1.0 >>> helper.visible True The only extra thing that Envisage does for you is to provide an extension point that allows you to contribute any number of '.ini' files that are loaded into the default scope when the application is started. e.g. To contribute a preference file for my plugin I might use:: class MyPlugin(Plugin): ... @contributes_to('envisage.preferences') def get_preferences(self, application): return ['pkgfile://mypackage:preferences.ini'] .. _PreferencesHelper: ../../enthought/preferences/preferences_helper.py .. _ScopedPreferences: ../../enthought/preferences/scoped_preferences.py apptools-4.5.0/docs/source/preferences/Preferences.rst0000644000076500000240000002505711640354733024405 0ustar mdickinsonstaff00000000000000Preferences =========== The preferences package provides a simple API for managing application preferences. The classes in the package are implemented using a layered approach where the lowest layer provides access to the raw preferences mechanism and each layer on top providing more convenient ways to get and set preference values. The Basic Preferences Mechanism =============================== Lets start by taking a look at the lowest layer which consists of the IPreferences_ interface and its default implementation in the Preferences_ class. This layer implements the basic preferences system which is a hierarchical arrangement of preferences 'nodes' (where each node is simply an object that implements the IPreferences_ interface). Nodes in the hierarchy can contain preference settings and/or child nodes. This layer also provides a default way to read and write preferences from the filesystem using the excellent ConfigObj_ package. This all sounds a bit complicated but, believe me, it isn't! To prove it (hopefully) lets look at an example. Say I have the following preferences in a file 'example.ini':: [acme.ui] bgcolor = blue width = 50 ratio = 1.0 visible = True [acme.ui.splash_screen] image = splash fgcolor = red I can create a preferences hierarchy from this file by:: >>> from apptools.preferences.api import Preferences >>> preferences = Preferences(filename='example.ini') >>> preferences.dump() Node() {} Node(acme) {} Node(ui) {'bgcolor': 'blue', 'ratio': '1.0', 'width': '50', 'visible': 'True'} Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'} The 'dump' method (useful for debugging etc) simply 'pretty prints' a preferences hierarchy. The dictionary next to each node contains the node's actual preferences. In this case, the root node (the node with no name) is the preferences object that we created. This node now has one child node 'acme', which contains no preferences. The 'acme' node has one child, 'ui', which contains some preferences (e.g. 'bgcolor') and also a child node 'splash_screen' which also contains preferences (e.g. 'image'). To look up a preference we use:: >>> preferences.get('acme.ui.bgcolor') 'blue' If no such preferences exists then, by default, None is returned:: >>> preferences.get('acme.ui.bogus') is None True You can also specify an explicit default value:: >>> preferences.get('acme.ui.bogus', 'fred') 'fred' To set a preference we use:: >>> preferences.set('acme.ui.bgcolor', 'red') >>> preferences.get('acme.ui.bgcolor') 'red' And to make sure the preferences are saved back to disk:: >>> preferences.flush() To add a new preference value we simply set it:: >>> preferences.set('acme.ui.fgcolor', 'black') >>> preferences.get('acme.ui.fgcolor') 'black' Any missing nodes in a call to 'set' are created automatically, hence:: >>> preferences.set('acme.ui.button.fgcolor', 'white') >>> preferences.get('acme.ui.button.fgcolor') 'white' Preferences can also be 'inherited'. e.g. Notice that the 'splash_screen' node does not contain a 'bgcolor' preference, and hence:: >>> preferences.get('acme.ui.splash_screen.bgcolor') is None True But if we allow the 'inheritance' of preference values then:: >>> preferences.get('acme.ui.splash_screen.bgcolor', inherit=True) 'red' By using 'inheritance' here the preferences system will try the following preferences:: 'acme.ui.splash_screen.bgcolor' 'acme.ui.bgcolor' 'acme.bgcolor' 'bgcolor' Strings, Glorious Strings ------------------------- At this point it is worth mentioning that preferences are *always* stored and returned as strings. This is because of the limitations of the traditional '.ini' file format i.e. they don't contain any type information! Now before you start panicking, this doesn't mean that all of your preferences have to be strings! Currently the preferences system allows, strings(!), booleans, ints, longs, floats and complex numbers. When you store a non-string value it gets converted to a string for you, but you *always* get a string back:: >>> preferences.get('acme.ui.width') '50' >>> preferences.set('acme.ui.width', 100) >>> preferences.get('acme.ui.width') '100' >>> preferences.get('acme.ui.visible') 'True' >>> preferences.set('acme.ui.visible', False) >>> preferences.get('acme.ui.visible') 'False' This is obviously not terribly convenient, and so the following section discusses how we associate type information with our preferences to make getting and setting them more natural. Preferences and Types ===================== As mentioned previously, we would like to be able to get and set non-string preferences in a more convenient way. This is where the PreferencesHelper_ class comes in. Let's take another look at 'example.ini':: [acme.ui] bgcolor = blue width = 50 ratio = 1.0 visible = True [acme.ui.splash_screen] image = splash fgcolor = red Say, I am interested in the preferences in the 'acme.ui' section. I can use a preferences helper as follows:: from apptools.preferences.api import PreferencesHelper class SplashScreenPreferences(PreferencesHelper): """ A preferences helper for the splash screen. """ PREFERENCES_PATH = 'acme.ui' bgcolor = Str width = Int ratio = Float visible = Bool >>> preferences = Preferences(filename='example.ini') >>> helper = SplashScreenPreferences(preferences=preferences) >>> helper.bgcolor 'blue' >>> helper.width 100 >>> helper.ratio 1.0 >>> helper.visible True And, obviously, I can set the value of the preferences via the helper too:: >>> helper.ratio = 0.5 And if you want to prove to yourself it really did set the preference:: >>> preferences.get('acme.ui.ratio') '0.5' Using a preferences helper you also get notified via the usual trait mechanism when the preferences are changed (either via the helper or via the preferences node directly:: def listener(obj, trait_name, old, new): print trait_name, old, new >>> helper.on_trait_change(listener) >>> helper.ratio = 0.75 ratio 0.5 0.75 >>> preferences.set('acme.ui.ratio', 0.33) ratio 0.75 0.33 If you always use the same preference node as the root of your preferences you can also set the class attribute 'PreferencesHelper.preferences' to be that node and from then on in, you don't have to pass a preferences collection in each time you create a helper:: >>> PreferencesHelper.preferences = Preferences(filename='example.ini') >>> helper = SplashScreenPreferences() >>> helper.bgcolor 'blue' >>> helper.width 100 >>> helper.ratio 1.0 >>> helper.visible True Scoped Preferences ================== In many applications the idea of preferences scopes is useful. In a scoped system, an actual preference value can be stored in any scope and when a call is made to the 'get' method the scopes are searched in order of precedence. The default implementation (in the ScopedPreferences_ class) provides two scopes by default: 1) The application scope This scope stores itself in the 'ETSConfig.application_home' directory. This scope is generally used when *setting* any user preferences. 2) The default scope This scope is transient (i.e. it does not store itself anywhere). This scope is generally used to load any predefined default values into the preferences system. If you are happy with the default arrangement, then using the scoped preferences is just like using the plain old non-scoped version:: >>> from apptools.preferences.api import ScopedPreferences >>> preferences = ScopedPreferences(filename='example.ini') >>> preferences.load('example.ini') >>> p.dump() Node() {} Node(application) {} Node(acme) {} Node(ui) {'bgcolor': 'blue', 'ratio': '1.0', 'width': '50', 'visible': 'True'} Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'} Node(default) {} Here you can see that the root node now has a child node representing each scope. When we are getting and setting preferences using scopes we generally want the following behaviour: a) When we get a preference we want to look it up in each scope in order. The first scope that contains a value 'wins'. b) When we set a preference, we want to set it in the first scope. By default this means that when we set a preference it will be set in the application scope. This is exactly what we want as the application scope is the scope that is persistent. So usually, we just use the scoped preferences as before:: >>> preferences.get('acme.ui.bgcolor') 'blue' >>> preferences.set('acme.ui.bgcolor', 'red') >>> preferences.dump() Node() {} Node(application) {} Node(acme) {} Node(ui) {'bgcolor': 'red', 'ratio': '1.0', 'width': '50', 'visible': 'True'} Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'} Node(default) {} And, conveniently, preference helpers work just the same with scoped preferences too:: >>> PreferencesHelper.preferences = ScopedPreferences(filename='example.ini') >>> helper = SplashScreenPreferences() >>> helper.bgcolor 'blue' >>> helper.width 100 >>> helper.ratio 1.0 >>> helper.visible True Accessing a particular scope ---------------------------- Should you care about getting or setting a preference in a particular scope then you use the following syntax:: >>> preferences.set('default/acme.ui.bgcolor', 'red') >>> preferences.get('default/acme.ui.bgcolor') 'red' >>> preferences.dump() Node() {} Node(application) {} Node(acme) {} Node(ui) {'bgcolor': 'red', 'ratio': '1.0', 'width': '50', 'visible': 'True'} Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'} Node(default) {} Node(acme) {} Node(ui) {'bgcolor': 'red'} You can also get hold of a scope via:: >>> default = preferences.get_scope('default') And then perform any of the usual operations on it. Further Reading =============== So that's a quick tour around the basic useage of the preferences API. For more imformation about what is provided take a look at the API_ documentation. If you are using Envisage to build your applications then you might also be interested in the `Preferences in Envisage`_ section. .. _API: api/index.html .. _ConfigObj: http://www.voidspace.org.uk/python/configobj.html .. _IPreferences: ../../enthought/preferences/i_preferences.py .. _Preferences: ../../enthought/preferences/preferences.py .. _PreferencesHelper: ../../enthought/preferences/preferences_helper.py .. _ScopedPreferences: ../../enthought/preferences/scoped_preferences.py .. _`Preferences in Envisage`: PreferencesInEnvisage.html apptools-4.5.0/docs/source/scripting/0000755000076500000240000000000013547652535021113 5ustar mdickinsonstaff00000000000000apptools-4.5.0/docs/source/scripting/introduction.rst0000644000076500000240000001513613547637361024374 0ustar mdickinsonstaff00000000000000.. _automatic-script-recording: Automatic script recording =========================== This package provides a very handy and powerful Python script recording facility. This can be used to: - record all actions performed on a traits based UI into a *human readable*, Python script that should be able to recreate your UI actions. - easily learn the scripting API of an application. This package is not just a toy framework and is powerful enough to provide full script recording to the Mayavi_ application. Mayavi is a powerful 3D visualization tool that is part of ETS_. .. _Mayavi: http://code.enthought.com/projects/mayavi .. _ETS: http://code.enthought.com/projects/tool-suite.php .. _scripting-api: The scripting API ------------------ The scripting API primarily allows you to record UI actions for objects that have Traits. Technically the framework listens to all trait changes so will work outside a UI. We do not document the full API here, the best place to look for that is the ``apptools.scripting.recorder`` module which is reasonably well documented. We provide a high level overview of the library. The quickest way to get started is to look at a small example. .. _scripting-api-example: A tour by example ~~~~~~~~~~~~~~~~~~~ The following example is taken from the test suite. Consider a set of simple objects organized in a hierarchy:: from traits.api import (HasTraits, Float, Instance, Str, List, Bool, HasStrictTraits, Tuple, Range, TraitPrefixMap, Trait) from apptools.scripting.api import (Recorder, recordable, set_recorder) class Property(HasStrictTraits): color = Tuple(Range(0.0, 1.0), Range(0.0, 1.0), Range(0.0, 1.0)) opacity = Range(0.0, 1.0, 1.0) representation = Trait('surface', TraitPrefixMap({'surface':2, 'wireframe': 1, 'points': 0})) class Toy(HasTraits): color = Str type = Str # Note the use of the trait metadata to ignore this trait. ignore = Bool(False, record=False) class Child(HasTraits): name = Str('child') age = Float(10.0) # The recorder walks through sub-instances if they are marked # with record=True property = Instance(Property, (), record=True) toy = Instance(Toy, record=True) friends = List(Str) # The decorator records the method. @recordable def grow(self, x): """Increase age by x years.""" self.age += x class Parent(HasTraits): children = List(Child, record=True) recorder = Instance(Recorder, record=False) Using these simple classes we first create a simple object hierarchy as follows:: p = Parent() c = Child() t = Toy() c.toy = t p.children.append(c) Given this hierarchy, we'd like to be able to record a script. To do this we setup the recording infrastructure:: from mayavi.core.recorder import Recorder, set_recorder # Create a recorder. r = Recorder() # Set the global recorder so the decorator works. set_recorder(r) r.register(p) r.recording = True The key method here is the ``r.register(p)`` call above. It looks at the traits of ``p`` and finds all traits and nested objects that specify a ``record=True`` in their trait metadata (all methods starting and ending with ``_`` are ignored). All sub-objects are in turn registered with the recorder and so on. Callbacks are attached to traits changes and these are wired up to produce readable and executable code. The ``set_recorder(r)`` call is also very important and sets the global recorder so the framework listens to any functions that are decorated with the ``recordable`` decorator. Now lets test this out like so:: # The following will be recorded. c.name = 'Shiva' c.property.representation = 'w' c.property.opacity = 0.4 c.grow(1) To see what's been recorded do this:: print r.script This prints:: child = parent.children[0] child.name = 'Shiva' child.property.representation = 'wireframe' child.property.opacity = 0.40000000000000002 child.grow(1) The recorder internally maintains a mapping between objects and unique names for each object. It also stores the information about the location of a particular object in the object hierarchy. For example, the path to the ``Toy`` instance in the hierarchy above is ``parent.children[0].toy``. Since scripting with lists this way can be tedious, the recorder first instantiates the ``child``:: child = parent.children[0] Subsequent lines use the ``child`` attribute. The recorder always tries to instantiate the object referred to using its path information in this manner. To record a function or method call one must simply decorate the function/method with the ``recordable`` decorator. Nested recordable functions are not recorded and trait changes are also not recorded if done inside a recordable function. .. note:: 1. It is very important to note that the global recorder must be set via the ``set_recorder`` method. The ``recordable`` decorator relies on this being set to work. 2. The ``recordable`` decorator will work with plain Python classes and with functions too. To stop recording do this:: r.unregister(p) r.recording = False The ``r.unregister(p)`` reverses the ``r.register(p)`` call and unregisters all nested objects as well. .. _recorder-advanced-uses: Advanced use cases ~~~~~~~~~~~~~~~~~~~~ Here are a few advanced use cases. - The API also provides a ``RecorderWithUI`` class that provides a simple user interface that prints the recorded script and allows the user to save the script. - Sometimes it is not enough to just record trait changes, one may want to pass an arbitrary string or command when recording is occurring. To allow for this, if one defines a ``recorder`` trait on the object, it is set to the current recorder. One can then use this recorder to do whatever one wants. This is very convenient. - To ignore specific traits one must specify either a ``record=False`` metadata to the trait definition or specify a list of strings to the ``register`` method in the ``ignore`` keyword argument. - If you want to use a specific name for an object on the script you can pass the ``script_id`` parameter to the register function. For more details on the recorder itself we suggest reading the module source code. It is fairly well documented and with the above background should be enough to get you going. apptools-4.5.0/docs/source/conf.py0000644000076500000240000001326013547637361020412 0ustar mdickinsonstaff00000000000000# -*- coding: utf-8 -*- # # EnvisageCore documentation build configuration file, created by # sphinx-quickstart on Fri Jul 18 17:09:28 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. #sys.path.append(os.path.abspath('some/directory')) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'traits.util.trait_documenter'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = 'apptools' copyright = '2008-2016, Enthought' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. # The full version, including alpha/beta/rc tags. d = {} execfile(os.path.join('..', '..', 'apptools', '__init__.py'), d) version = release = d['__version__'] # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. #exclude_dirs = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'default.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. html_logo = 'e-logo-rev.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = 'et.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'AppToolsdoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'AppTools.tex', 'AppTools Documentation', 'Enthought', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True apptools-4.5.0/docs/source/appscripting/0000755000076500000240000000000013547652535021614 5ustar mdickinsonstaff00000000000000apptools-4.5.0/docs/source/appscripting/Introduction.rst0000644000076500000240000003504313547637361025034 0ustar mdickinsonstaff00000000000000Application Scripting Framework =============================== The Application Scripting Framework is a component of the Enthought Tool Suite that provides developers with an API that allows traits based objects to be made scriptable. Operations on a scriptable object can be recorded in a script and subsequently replayed. The framework is completely configurable. Alternate implementations of all major components can be provided if necessary. Framework Concepts ------------------ The following are the concepts supported by the framework. - Scriptable Type A scriptable type is a sub-type of ``HasTraits`` that has scriptable methods and scriptable traits. If a scriptable method is called, or a scriptable trait is set, then that action can be recorded in a script and subsequently replayed. If the ``__init__()`` method is scriptable then the creation of an object from the type can be recorded. Scriptable types can be explicitly defined or created dynamically from any sub-type of ``HasTraits``. - Scriptable API The set of a scriptable type's scriptable methods and traits constitutes the type's scriptable API. The API can be defined explicitly using the ``scriptable`` decorator (for methods) or the ``Scriptable`` wrapper (for traits). For scriptable types that are created dynamically then the API can be defined in terms of one or more types or interfaces or an explicit list of method and trait names. By default, all public methods and traits (ie. those whose name does not begin with an underscore) are part of the API. It is also possible to then explicitly exclude a list of method and trait names. - Scriptable Object A scriptable object is an instance of a scriptable type. Scriptable objects can be explicitly created by calling the scriptable type. Alternatively a non-scriptable object can be made scriptable dynamically. - Script A script is a Python script and may be a recording or written from scratch. If the creation of scriptable objects can be recorded, then it may be possible for a recording to be run directly by the Python interpreter and independently of the application that made the recording. Otherwise the application must run the script and first create any scriptable objects referred to in the script. - Binding A script runs in a namespace which is, by default, empty. If the scriptable objects referred to in a script are not created by the script (because their type's ``__init__()`` method isn't scriptable) then they must be created by the application and added to the namespace. Adding an object to the namespace is called binding. Scriptable objects whose creation can be recorded will automatically bind themselves when they are created. It also possible to bind an object factory rather than the object itself. The factory will be called, and the object created, only if the object is needed by the script when it is run. This is typically used by plugins. The name that an object is bound to need bear no relation to the object's name within the application. Names may be dotted names (eg. ``aaa.bbb.ccc``) and appropriate objects representing the intermediate parts of such a name will be created automatically. An event is fired whenever an object is bound (or when a bound factory is invoked). This allows other objects (eg. an embedded Python shell) to expose scriptable objects in other ways. - Script Manager A script manager is responsible for the recording and subsequent playback of scripts. An application has a single script manager instance which can be explicitly set or created automatically. Limitations ----------- In the current implementation scriptable Trait container types (eg. List, Dict) may only contain objects corresponding to fundamental Python types (eg. int, bool, str). API Overview ------------ This section gives an overview of the API implemented by the framework. The complete API_ documentation is available as endo generated HTML. The example_ application demonstrates some the features of the framework. Module Level Objects .................... ``get_script_manager()`` The application's script manager is returned. One will be created automatically if needed. ``set_script_manager(script_manager)`` The application's script manager will be set to ``script_manager`` replacing any existing script manager. ``scriptable`` This is a decorator used to explicitly mark methods as being scriptable. Any call to a scriptable method is recorded. If a type's ``__init__()`` method is decorated then the creation of the object will be recorded. ``Scriptable`` This is a wrapper for a trait to explicitly mark it as being scriptable. Any change to the value of the trait will be recorded. Simple reads of the trait will not be recorded unless unless the value read is bound to another scriptable trait or passed as an argument to a scriptable method. Passing ``has_side_effects=True`` when wrapping the trait will ensure that a read will always be recorded. ``create_scriptable_type(script_type, name=None, bind_policy='auto', api=None, includes=None, excludes=None, script_init=True)`` This creates a new type based on an existing type but with certain methods and traits marked as being scriptable. Scriptable objects can then be created by calling the type. ``script_type`` is the existing, non-scriptable, type. The new type will be a sub-type of it. The ``api``, ``includes`` and ``excludes`` arguments determine which methods and traits are made scriptable. By default, all public methods and traits (ie. those whose name does not begin with an underscore) are made scriptable. The ``name`` and ``bind_policy`` arguments determine how scriptable objects are bound when they are created. ``name`` is the name that an object will be bound to. It defaults to the name of ``script_type`` with the first character forced to lower case. ``name`` may be a dotted name, eg. ``aaa.bb.c``. ``bind_policy`` determines what happens if an object is already bound to the name. If it is ``auto`` then a numerical suffix will be added to the name of the new object. If it is ``unique`` then an exception will be raised. If it is ``rebind`` then the object currently bound to the name will be unbound. ``api`` is a class or interface (or a list of classes or interfaces) that is used to provide the names of the methods and traits to be made scriptable. The class or interface effectively defines the scripting API. If ``api`` is not specified then ``includes`` is a list of method and trait names that are made scriptable. If ``api`` and ``includes`` are not specified then ``excludes`` is a list of method and trait names that are *not* made scriptable. If ``script_init`` is set then the ``__init__()`` method is made scriptable irrespective of the ``api``, ``includes`` and ``excludes`` arguments. If ``script_init`` is not set then objects must be explicitly bound and ``name`` and ``bind_policy`` are ignored. ``make_object_scriptable(obj, api=None, includes=None, excludes=None)`` This takes an existing unscriptable object and makes it scriptable. It works by calling ``create_scriptable_type()`` on the the objects existing type and replacing that existing type with the new scriptable type. See the description of ``create_scriptable_type()`` for an explanation of the ``api``, ``includes`` and ``excludes`` arguments. ScriptManager ............. The ``ScriptManager`` class is the default implementation of the ``IScriptManager`` interface. ``bind_event`` This event is fired whenever an object is bound or unbound. The event's argument implements the ``IBindEvent`` interface. ``recording`` This trait is set if a script is currently being recorded. It is updated automatically by the script manager. ``script`` This trait contains the text of the script currently being recorded (or the last recorded script if one is not being currently recorded). It is updated automatically by the script manager. ``script_updated`` This event is fired whenever the ``script`` trait is updated. The event's argument is the script manager. ``bind(self, obj, name=None, bind_policy='unique', api=None, includes=None, excludes=None)`` This method makes an object scriptable and binds it to a name. See the description of ``create_scriptable_type()`` for an explanation of the ``api``, ``includes``, ``excludes``, ``name`` and ``bind_policy`` arguments. ``bind_factory(self, factory, name, bind_policy='unique', api=None, includes=None, excludes=None)`` This method binds an object factory to a name. The factory is called to create the object (and make it scriptable) only when the object is needed by a running script. See the description of ``create_scriptable_type()`` for an explanation of the ``name`` and ``bind_policy`` arguments. ``run(self, script)`` This method runs a script in a namespace containing all currently bound objects. ``script`` is any object that can be used by Python's ``exec`` statement including a string or a file-like object. ``run_file(self, file_name)`` This method runs a script in a namespace containing all currently bound objects. ``file_name`` is the name of a file containing the script. ``start_recording(self)`` This method starts the recording of a script. ``stop_recording(self)`` This method stops the recording of the current script. IBindEvent .......... The ``IBindEvent`` interface defines the interface that is implemented by the object passed when the script manager's ``bind_event`` is fired. ``name`` This trait is the name being bound or unbound. ``obj`` This trait is the obj being bound to ``name`` or None if ``name`` is being unbound. StartRecordingAction .................... The ``StartRecordingAction`` class is a canned PyFace action that starts the recording of changes to scriptable objects to a script. StopRecordingAction ................... The ``StopRecordingAction`` class is a canned PyFace action that ends the recording of changes to scriptable objects to a script. Implementing Application Scripting ---------------------------------- The key part of supporting application scripting is to design an appropriate scripting API and to ensure than the application itself uses the API so that changes to the data can be recorded. The framework provides many ways to specify the scripting API. Which approach is appropriate in a particular case will depend on when it is a new application, or whether scripting is being added to an existing application, and how complex the application's data model is. Static Specification .................... A scripting API is specified statically by the explicit use of the ``scriptable`` decorator and the ``Scriptable`` trait wrapper. For example:: from apptools.appscripting.api import scriptable, Scriptable from traits.api import HasTraits, Int, Str class DataModel(HasTraits): foo = Scriptable(Str) bar = Scriptable(Int, has_side_effects=True) @scriptable def baz(self): pass def weeble(self) pass # Create the scriptable object. It's creation won't be recorded because # __init__() isn't decorated. obj = DataModel() # These will be recorded. obj.foo = '' obj.bar = 10 obj.baz() # This will not be recorded. obj.weeble() # This won't be recorded unless 'f' is passed to something that is # recorded. f = obj.foo # This will be recorded because we set 'has_side_effects'. b = obj.bar Dynamic Specification ..................... A scripting API can also be specified dynamically. The following example produces a scriptable object with the same scriptable API as above (with the exception that ``has_side_effects`` cannot be specified dynamically):: from apptools.appscripting.api import create_scriptable_type from traits.api import HasTraits, Int, Str class DataModel(HasTraits): foo = Str bar = Int def baz(self): pass def weeble(self) pass # Create a scriptable type based on the above. ScriptableDataModel = create_scriptable_type(DataModel, excludes=['weeble']) # Now create scriptable objects from the scriptable type. Note that each # object has the same type. obj1 = ScriptableDataModel() obj2 = ScriptableDataModel() Instead we could bypass the type and make the objects themselves scriptable as follows:: from apptools.appscripting.api import make_object_scriptable from traits.api import HasTraits, Int, Str class DataModel(HasTraits): foo = Str bar = Int def baz(self): pass def weeble(self) pass # Create unscriptable objects. obj1 = DataModel() obj2 = DataModel() # Now make the objects scriptable. Note that each object has a different # type, each a sub-type of 'DataModel'. make_object_scriptable(obj1, excludes=['weeble']) make_object_scriptable(obj2, excludes=['weeble']) With a more sophisticated design we may choose to specify the scriptable API as an interface as follows:: from apptools.appscripting.api import make_object_scriptable from traits.api import HasTraits, Int, Interface, Str class DataModel(HasTraits): foo = Str bar = Int def baz(self): pass def weeble(self) pass class IScriptableDataModel(Interface): foo = Str bar = Int def baz(self): pass # Create an unscriptable object. obj = DataModel() # Now make the object scriptable. make_object_scriptable(obj, api=IScriptableDataModel) Scripting __init__() .................... Making a type's ``__init__()`` method has advantages and disadvantages. It means that the creation of scriptable objects will be recorded in a script (along with the necessary ``import`` statements). This means that the script can be run independently of your application by the standard Python interpreter. The disadvantage is that, if you have a complex data model, with many interdependencies, then defining a complete and consistent scripting API that allows a script to run independently may prove difficult. In such cases it is better to have the application create and bind the scriptable objects itself. .. _API: api/index.html .. _example: https://svn.enthought.com/enthought/browser/AppTools/trunk/examples/appscripting/ apptools-4.5.0/docs/source/_static/0000755000076500000240000000000013547652535020537 5ustar mdickinsonstaff00000000000000apptools-4.5.0/docs/source/_static/e-logo-rev.png0000644000076500000240000000751111640354733023214 0ustar mdickinsonstaff00000000000000PNG  IHDRoi*sRGB pHYs  tIMELIDATx]ktT>z\$m ȥ!TD jV|_CWph PT\"B$$6aIf9qfB%= By}; vo`!4ChVv[12 !HGiyg):QVd8SmsQAj2~~AH3qU:?!4[a6SRu ǎ7H'1"_Qq!JbBwx$I-S^QO>AO~( HAPU)=Ojjm+ v$~ئ"33zviJn[*.\v(/E1U`Ycֿ&y3g>=x$;GS@]d1YÓo"۾X6n8o2 ,c_܊U?y" "cdL5HfFj~}Q]H錩/Oxcq'~lӕ_ ţeW\| &cLMhdȶ9-՗ $ Θڳ9i˗>xa6>#E _h2$}앿"a\l߰0/"ޑҦ.*:UQyٕ~`:oYfxu? b)<̜>җ'rYgԾ6ngeSMkm>uv" Snhj ̌ry_ݚLM01@$(]vƏ{_{#&>4l|c.8~rK05bjԈm;14*:Ο3yK|ީT\> 8nd٤B]j맻]8#&[5TEUlu#u\/kk^6t=Zo`Ӌ-,R'*EP1#EQ DfsnlOYYYҨ!${G2yZ~\pN|olӋnϯBu-\$5˘TYgNR^\8gF{@|4Ņ0ov2֊^:j)D"zM En1]WfN@wǛ뿨k B|c!>8T'JԉaZxubOW~;c%dLynظedNSt~WX\f-pO',9UI21`xĥd  ,{ER"Z G 4PLq@$#15! G}\.-2kEfV=G15Q&ph!9Ce Cvj(# 5#GX:InHJZmڞU__(h݆' H7cHκ})"Db-&`i\eU?*YJ05 D S[GabDěrqEʪ9կm"4LwtGTدr{OPۿhj?:}"i b:/7yA@eK#$t13mj51K &^w !%PSSSֆlr{s^#w4DmQI S#3a@57Q; S#:į v4yR+A&P0j/))-&Z4S.[Z2d^!j8J01-j(T!05Q)"jԌ+@vpd"'4LuyC͉cv,@A1i_qLq|s4bvGz!U !KIQD1E3[1vI $00h6FL̙dnu˞?SScw\LGaʃcf-N]y/4u: c c PM18_h>4~h޽f l%&N^>?2=iC)9v!˜j>hN'N~(aİ}Wx+' u0?1sL _/>_nH ! x9zq@bzlLؘO_6Ac6~t=F&מc2\汋rh3.婓Jx`x^_>_mqKkj+-++Y.zw3TU+qܹ~M\_:pBI" D5 JcTubd!P%+~fz*EP]6R2;/uz] g,'Nd=C^n188D,dZ}W/)~ǎ/z~*0P]g*ݐ[{s]b76 $?`[퍘JTDDKŽ t "((}qqwZΦO11fZ XSXk71E~;{GbN#"k" r@4˗mrN"srLڀ?Vh?݁nw'?0l۶`bF4]2UU ;llgL bkx'ۄ&%QU#c*B{awE|DǶBhZ-f/wIENDB`apptools-4.5.0/docs/source/_static/et.ico0000644000076500000240000002362611640354733021643 0ustar mdickinsonstaff00000000000000(f 00hvh F00( r5(H73iKD]ZZzols$jV4{H5Fg@Hg @' 9c]sE{c@Ec{bFQEPkG\ 1}m( @_-!p3%7)&[B<]VU~okWU#XW$iU$zu5{j6Yv1GWw"X f$I qJ QJ[JډJc4jJ5{J 1FJ qGI Q#W6 Q$j#XA${#h1IQ$j1< s4{A a5S Q}A\1J7|xp``?`8(0`W,"4(%p3%v>1IGF~QFja_~wtdFwG܆ h즋" ij""!zi"""!g""""ۆ"""""F얋""""""Gzޥ""""""Hi """"" ih s"""""  c"""""! R""""!|2"""!|{2""!|2"1"!5 y@y`"!""" 2""""G s"""""H b"""" i R""""!yR""""!qh2""""0i2""""GR"!z"""""hr"""r"""" """""FB""""#93"""""GB"""""62"""""hp"""""%""""" ht"""""#|"""""!"""""k """""""""""Y s"""""""""8 b"""""""&R"""""%2"""#|2"#k"Y?????( f-t1!j/#i1$n3$j2&r4&s4&s5&u5&]0'`1'^2'u5'v5'v6'x6';+(X2(_2(r4(u6(w7(x7(z7(y8(z8({8(P0)s7)z8){8)|9):*c5,i9-A1.R4.E3/W7/u=1x?2A97>;:{F:F?>{I>mH?DBAMDB~ODJHHOMLhSOQPPPQQSRRUTTaW\YXx_[c[]]]^^^b_^___dddpghhhmhkjijjjrksmnnnrpopppsssuttwwwyyy~{}}}t@4LhO\b$8PraKx_ *?VNdcpI 0M}Wfm:1n{|wFvu2!5yziAUSyxj?]GRryxj>E-BYyzk7l`, %3Jg~Z+DaT)9hzC&6Ooe" [q. *>;'#QX( <H/s=^( @m,6#>%D&K'O(R)P) Y+ q0!2%"`."8'#g1#4($l2%p3%q3%q4%0'&a0&r4&t5&u5&v5&e2'p4't4't5'u5'u6'v6'w6'x6'/)(r6(v6(x7(Y3)z8)v8*l6+2--m:-v;-w>1543Q93v@3lA7vC8;:9wF:><;N?<VA=?>>@??zK@AAAoJAFFF~SHbNIKKKnQKLLLrTMONNPNNZPQQQSRRVVUdXVXWWaWaX[ZZwa\d_^e^i_```iabbbedceedhhhphqh}mipijjjlkkplrlwmoooqpopppxqwrsssyvuwwwyyy{{}}}~~~ŻjjybO =iٙWa"HuihZ!.SWZ% 9bݫXxaBmnczq2 !*NwܐVr-% 3]wt\#%XzF' Dz:  EIv- N}I`')uHYH޾Hn65S|HJ' 9dH<%"DpHs0!.SDe(! ;g9pQ("GuM8b> .Uo0@m~1% KL  *Nwג7%%?4%% 3XжP& +_l,$ =g^/#AT($ ! 1>  )[4 #Ck,1{R(%+_f|xp``?`8(0`<5"=$H&/"S)m-*" -# X+ s0!b."o1"+%#h0#+%$l2$u3$p3%q3%q4%r4%+&&8)&V.&g2&r4&t4&t5&u5&.('O/'t5'u5'v5'u6'v6'q6(v7(y7(o7)}9),+*V2*g4*x9*:.,x;,/..70.C2.z=/211u=1l=2z@3666X?9:::}E:><<PB>~I>AAALAlKCODEEERFIIIUIUJKKK`OKMMMZPRRR\TS^TUUU_WaWYYYs_Zt`[`^^__^g_a``bbaccceeeofhggiiillksknmmpppxptsruuuxvxxxyyy}{y}z{{{|}}}~}~¿ȏՎV)IzEl~zEzEtgzE{=?bzEM+$0LqzE9!$ 8YzE5$CdzEc/$0LtzEW$$ :ZzEG$!Ce|?>$$0Ovo*R}u7$:Zc5 ?ac&!CeD $*JmN$$0R\%$ 4Rxf,! .eF <]v=)6u7!$EeږM'#(Tc-#0Ot׽a+ !AQ$# 8Yvh=$$6jB$ 1!!%Ty7!!!"A`&$6jQ&"%T@$Ay7!$6j`&!%SQ'$A?????apptools-4.5.0/docs/source/_static/default.css0000644000076500000240000003231311640354733022666 0ustar mdickinsonstaff00000000000000/** * Sphinx Doc Design */ body { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; font-size: 100%; background-color: #333333; color: #000; margin: 0; padding: 0; } /* :::: LAYOUT :::: */ div.document { background-color: #24326e; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 230px; } div.body { background-color: white; padding: 0 20px 30px 20px; } div.sphinxsidebarwrapper { padding: 10px 5px 0 10px; } div.sphinxsidebar { float: left; width: 230px; margin-left: -100%; font-size: 90%; } p.logo { text-align: center; } div.clearer { clear: both; } div.footer { color: #fff; width: 100%; padding: 9px 0 9px 0; text-align: center; font-size: 75%; } div.footer a { color: #fff; text-decoration: underline; } div.related { background-color: #24326e; color: #fff; width: 100%; height: 30px; line-height: 30px; font-size: 90%; } div.related h3 { display: none; } div.related ul { margin: 0; padding: 0 0 0 10px; list-style: none; } div.related li { display: inline; } div.related li.right { float: right; margin-right: 5px; } div.related a { color: white; } /* ::: TOC :::: */ div.sphinxsidebar h3 { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; color: #acafb3; font-size: 1.4em; font-weight: normal; margin: 0; padding: 0; } div.sphinxsidebar h4 { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; color: #acafb3; font-size: 1.3em; font-weight: normal; margin: 5px 0 0 0; padding: 0; } div.sphinxsidebar p { color: white; } div.sphinxsidebar p.topless { margin: 5px 10px 10px 10px; } div.sphinxsidebar ul { margin: 10px; padding: 0; list-style: none; color: white; } div.sphinxsidebar ul ul, div.sphinxsidebar ul.want-points { margin-left: 20px; list-style: square; } div.sphinxsidebar ul ul { margin-top: 0; margin-bottom: 0; } div.sphinxsidebar a { color: #fff; } div.sphinxsidebar form { margin-top: 10px; } div.sphinxsidebar input { border: 1px solid #9bbde2; font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; font-size: 1em; } /* :::: MODULE CLOUD :::: */ div.modulecloud { margin: -5px 10px 5px 10px; padding: 10px; line-height: 160%; border: 1px solid #666666; background-color: #dddddd; } div.modulecloud a { padding: 0 5px 0 5px; } /* :::: SEARCH :::: */ ul.search { margin: 10px 0 0 20px; padding: 0; } ul.search li { padding: 5px 0 5px 20px; background-image: url(file.png); background-repeat: no-repeat; background-position: 0 7px; } ul.search li a { font-weight: bold; } ul.search li div.context { color: #666; margin: 2px 0 0 30px; text-align: left; } ul.keywordmatches li.goodmatch a { font-weight: bold; } /* :::: COMMON FORM STYLES :::: */ div.actions { padding: 5px 10px 5px 10px; border-top: 1px solid #598ec0; border-bottom: 1px solid #598ec0; background-color: #9bbde2; } form dl { color: #333; } form dt { clear: both; float: left; min-width: 110px; margin-right: 10px; padding-top: 2px; } input#homepage { display: none; } div.error { margin: 5px 20px 0 0; padding: 5px; border: 1px solid #db7d46; font-weight: bold; } /* :::: INLINE COMMENTS :::: */ div.inlinecomments { position: absolute; right: 20px; } div.inlinecomments a.bubble { display: block; float: right; background-image: url(style/comment.png); background-repeat: no-repeat; width: 25px; height: 25px; text-align: center; padding-top: 3px; font-size: 0.9em; line-height: 14px; font-weight: bold; color: black; } div.inlinecomments a.bubble span { display: none; } div.inlinecomments a.emptybubble { background-image: url(style/nocomment.png); } div.inlinecomments a.bubble:hover { background-image: url(style/hovercomment.png); text-decoration: none; color: #598ec0; } div.inlinecomments div.comments { float: right; margin: 25px 5px 0 0; max-width: 50em; min-width: 30em; border: 1px solid #598ec0; background-color: #9bbde2; z-index: 150; } div#comments { border: 1px solid #598ec0; margin-top: 20px; } div#comments div.nocomments { padding: 10px; font-weight: bold; } div.inlinecomments div.comments h3, div#comments h3 { margin: 0; padding: 0; background-color: #598ec0; color: white; border: none; padding: 3px; } div.inlinecomments div.comments div.actions { padding: 4px; margin: 0; border-top: none; } div#comments div.comment { margin: 10px; border: 1px solid #598ec0; } div.inlinecomments div.comment h4, div.commentwindow div.comment h4, div#comments div.comment h4 { margin: 10px 0 0 0; background-color: #2eabb0; color: white; border: none; padding: 1px 4px 1px 4px; } div#comments div.comment h4 { margin: 0; } div#comments div.comment h4 a { color: #9bbde2; } div.inlinecomments div.comment div.text, div.commentwindow div.comment div.text, div#comments div.comment div.text { margin: -5px 0 -5px 0; padding: 0 10px 0 10px; } div.inlinecomments div.comment div.meta, div.commentwindow div.comment div.meta, div#comments div.comment div.meta { text-align: right; padding: 2px 10px 2px 0; font-size: 95%; color: #598ec0; border-top: 1px solid #598ec0; background-color: #9bbde2; } div.commentwindow { position: absolute; width: 500px; border: 1px solid #598ec0; background-color: #9bbde2; display: none; z-index: 130; } div.commentwindow h3 { margin: 0; background-color: #598ec0; color: white; border: none; padding: 5px; font-size: 1.5em; cursor: pointer; } div.commentwindow div.actions { margin: 10px -10px 0 -10px; padding: 4px 10px 4px 10px; color: #598ec0; } div.commentwindow div.actions input { border: 1px solid #598ec0; background-color: white; color: #073d61; cursor: pointer; } div.commentwindow div.form { padding: 0 10px 0 10px; } div.commentwindow div.form input, div.commentwindow div.form textarea { border: 1px solid #598ec0; background-color: white; color: black; } div.commentwindow div.error { margin: 10px 5px 10px 5px; background-color: #fff2b0; display: none; } div.commentwindow div.form textarea { width: 99%; } div.commentwindow div.preview { margin: 10px 0 10px 0; background-color: ##9bbde2; padding: 0 1px 1px 25px; } div.commentwindow div.preview h4 { margin: 0 0 -5px -20px; padding: 4px 0 0 4px; color: white; font-size: 1.3em; } div.commentwindow div.preview div.comment { background-color: #f2fbfd; } div.commentwindow div.preview div.comment h4 { margin: 10px 0 0 0!important; padding: 1px 4px 1px 4px!important; font-size: 1.2em; } /* :::: SUGGEST CHANGES :::: */ div#suggest-changes-box input, div#suggest-changes-box textarea { border: 1px solid #666; background-color: white; color: black; } div#suggest-changes-box textarea { width: 99%; height: 400px; } /* :::: PREVIEW :::: */ div.preview { background-image: url(style/preview.png); padding: 0 20px 20px 20px; margin-bottom: 30px; } /* :::: INDEX PAGE :::: */ table.contentstable { width: 90%; } table.contentstable p.biglink { line-height: 150%; } a.biglink { font-size: 1.3em; } span.linkdescr { font-style: italic; padding-top: 5px; font-size: 90%; } /* :::: INDEX STYLES :::: */ table.indextable td { text-align: left; vertical-align: top; } table.indextable dl, table.indextable dd { margin-top: 0; margin-bottom: 0; } table.indextable tr.pcap { height: 10px; } table.indextable tr.cap { margin-top: 10px; background-color: #dddddd; } img.toggler { margin-right: 3px; margin-top: 3px; cursor: pointer; } form.pfform { margin: 10px 0 20px 0; } /* :::: GLOBAL STYLES :::: */ .docwarning { background-color: #fff2b0; padding: 10px; margin: 0 -20px 0 -20px; border-bottom: 1px solid #db7d46; } p.subhead { font-weight: bold; margin-top: 20px; } a { color: #24326e; text-decoration: none; } a:hover { text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; background-color: #dddddd; font-weight: normal; color: #073d61; border-bottom: 1px solid #666; margin: 20px -20px 10px -20px; padding: 3px 0 3px 10px; } div.body h1 { margin-top: 0; font-size: 200%; } div.body h2 { font-size: 160%; } div.body h3 { font-size: 140%; } div.body h4 { font-size: 120%; } div.body h5 { font-size: 110%; } div.body h6 { font-size: 100%; } a.headerlink { color: #edaa1e; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; visibility: hidden; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink { visibility: visible; } a.headerlink:hover { background-color: #edaa1e; color: white; } div.body p, div.body dd, div.body li { text-align: left; line-height: 130%; } div.body p.caption { text-align: inherit; } div.body td { text-align: left; } ul.fakelist { list-style: none; margin: 10px 0 10px 20px; padding: 0; } .field-list ul { padding-left: 1em; } .first { margin-top: 0 !important; } /* "Footnotes" heading */ p.rubric { margin-top: 30px; font-weight: bold; } /* "Topics" */ div.topic { background-color: #ddd; border: 1px solid #666; padding: 0 7px 0 7px; margin: 10px 0 10px 0; } p.topic-title { font-size: 1.1em; font-weight: bold; margin-top: 10px; } /* Admonitions */ div.admonition { margin-top: 10px; margin-bottom: 10px; padding: 7px; } div.admonition dt { font-weight: bold; } div.admonition dl { margin-bottom: 0; } div.admonition p { display: inline; } div.seealso { background-color: #fff2b0; border: 1px solid #edaa1e; } div.warning { background-color: #fff2b0; border: 1px solid ##db7d46; } div.note { background-color: #eee; border: 1px solid #666; } p.admonition-title { margin: 0px 10px 5px 0px; font-weight: bold; display: inline; } p.admonition-title:after { content: ":"; } div.body p.centered { text-align: center; margin-top: 25px; } table.docutils { border: 0; } table.docutils td, table.docutils th { padding: 1px 8px 1px 0; border-top: 0; border-left: 0; border-right: 0; border-bottom: 1px solid #a9a6a2; } table.field-list td, table.field-list th { border: 0 !important; } table.footnote td, table.footnote th { border: 0 !important; } .field-list ul { margin: 0; padding-left: 1em; } .field-list p { margin: 0; } dl { margin-bottom: 15px; clear: both; } dd p { margin-top: 0px; } dd ul, dd table { margin-bottom: 10px; } dd { margin-top: 3px; margin-bottom: 10px; margin-left: 30px; } .refcount { color: #24326e; } dt:target, .highlight { background-color: #edaa1e1; } dl.glossary dt { font-weight: bold; font-size: 1.1em; } th { text-align: left; padding-right: 5px; } pre { padding: 5px; background-color: #e6f3ff; color: #333; border: 1px solid #24326e; border-left: none; border-right: none; overflow: auto; } td.linenos pre { padding: 5px 0px; border: 0; background-color: transparent; color: #aaa; } table.highlighttable { margin-left: 0.5em; } table.highlighttable td { padding: 0 0.5em 0 0.5em; } tt { background-color: #ddd; padding: 0 1px 0 1px; font-size: 0.95em; } tt.descname { background-color: transparent; font-weight: bold; font-size: 1.2em; } tt.descclassname { background-color: transparent; } tt.xref, a tt { background-color: transparent; font-weight: bold; } .footnote:target { background-color: #fff2b0 } h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { background-color: transparent; } .optional { font-size: 1.3em; } .versionmodified { font-style: italic; } form.comment { margin: 0; padding: 10px 30px 10px 30px; background-color: #ddd; } form.comment h3 { background-color: #598ec0; color: white; margin: -10px -30px 10px -30px; padding: 5px; font-size: 1.4em; } form.comment input, form.comment textarea { border: 1px solid #ddd; padding: 2px; font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; font-size: 100%; } form.comment input[type="text"] { width: 240px; } form.comment textarea { width: 100%; height: 200px; margin-bottom: 10px; } .system-message { background-color: #edaa1e; padding: 5px; border: 3px solid red; } /* :::: PRINT :::: */ @media print { div.document, div.documentwrapper, div.bodywrapper { margin: 0; width : 100%; } div.sphinxsidebar, div.related, div.footer, div#comments div.new-comment-box, #top-link { display: none; } } apptools-4.5.0/docs/source/undo/0000755000076500000240000000000013547652535020056 5ustar mdickinsonstaff00000000000000apptools-4.5.0/docs/source/undo/Introduction.rst0000644000076500000240000002356511640354733023273 0ustar mdickinsonstaff00000000000000Undo Framework ============== The Undo Framework is a component of the Enthought Tool Suite that provides developers with an API that implements the standard pattern for do/undo/redo commands. The framework is completely configurable. Alternate implementations of all major components can be provided if necessary. Framework Concepts ------------------ The following are the concepts supported by the framework. - Command A command is an application defined operation that can be done (i.e. executed), undone (i.e. reverted) and redone (i.e. repeated). A command operates on some data and maintains sufficient state to allow it to revert or repeat a change to the data. Commands may be merged so that potentially long sequences of similar commands (e.g. to add a character to some text) can be collapsed into a single command (e.g. to add a word to some text). - Macro A macro is a sequence of commands that is treated as a single command when being undone or redone. - Command Stack A command is done by pushing it onto a command stack. The last command can be undone and redone by calling appropriate command stack methods. It is also possible to move the stack's position to any point and the command stack will ensure that commands are undone or redone as required. A command stack maintains a *clean* state which is updated as commands are done and undone. It may be explicitly set, for example when the data being manipulated by the commands is saved to disk. Canned PyFace actions are provided as wrappers around command stack methods to implement common menu items. - Undo Manager An undo manager is responsible for one or more command stacks and maintains a reference to the currently active stack. It provides convenience undo and redo methods that operate on the currently active stack. An undo manager ensures that each command execution is allocated a unique sequence number, irrespective of which command stack it is pushed to. Using this it is possible to synchronise multiple command stacks and restore them to a particular point in time. An undo manager will generate an event whenever the clean state of the active stack changes. This can be used to maintain some sort of GUI status indicator to tell the user that their data has been modified since it was last saved. Typically an application will have one undo manager and one undo stack for each data type that can be edited. However this is not a requirement: how the command stack's in particular are organised and linked (with the user manager's sequence number) can need careful thought so as not to confuse the user - particularly in a plugin based application that may have many editors. To support this typical usage the PyFace ``Workbench`` class has an ``undo_manager`` trait and the PyFace ``Editor`` class has a ``command_stack`` trait. Both are lazy loaded so can be completely ignored if they are not used. API Overview ------------ This section gives a brief overview of the various classes implemented in the framework. The complete API_ documentation is available as endo generated HTML. The example_ application demonstrates all the major features of the framework. UndoManager ........... The ``UndoManager`` class is the default implementation of the ``IUndoManager`` interface. ``active_stack`` This trait is a reference to the currently active command stack and may be None. Typically it is set when some sort of editor becomes active. ``active_stack_clean`` This boolean trait reflects the clean state of the currently active command stack. It is intended to support a "document modified" indicator in the GUI. It is maintained by the undo manager. ``stack_updated`` This event is fired when the index of a command stack is changed. A reference to the stack is passed as an argument to the event and may not be the currently active stack. ``undo_name`` This Unicode trait is the name of the command that can be undone, and will be empty if there is no such command. It is maintained by the undo manager. ``redo_name`` This Unicode trait is the name of the command that can be redone, and will be empty if there is no such command. It is maintained by the undo manager. ``sequence_nr`` This integer trait is the sequence number of the next command to be executed. It is incremented immediately before a command's ``do()`` method is called. A particular sequence number identifies the state of all command stacks handled by the undo manager and allows those stacks to be set to the point they were at at a particular point in time. In other words, the sequence number allows otherwise independent command stacks to be synchronised. ``undo()`` This method calls the ``undo()`` method of the last command on the active command stack. ``redo()`` This method calls the ``redo()`` method of the last undone command on the active command stack. CommandStack ............ The ``CommandStack`` class is the default implementation of the ``ICommandStack`` interface. ``clean`` This boolean traits reflects the clean state of the command stack. Its value changes as commands are executed, undone and redone. It may also be explicitly set to mark the current stack position as being clean (when data is saved to disk for example). ``undo_name`` This Unicode trait is the name of the command that can be undone, and will be empty if there is no such command. It is maintained by the command stack. ``redo_name`` This Unicode trait is the name of the command that can be redone, and will be empty if there is no such command. It is maintained by the command stack. ``undo_manager`` This trait is a reference to the undo manager that manages the command stack. ``push(command)`` This method executes the given command by calling its ``do()`` method. Any value returned by ``do()`` is returned by ``push()``. If the command couldn't be merged with the previous one then it is saved on the command stack. ``undo(sequence_nr=0)`` This method undoes the last command. If a sequence number is given then all commands are undone up to an including the sequence number. ``redo(sequence_nr=0)`` This method redoes the last command and returns any result. If a sequence number is given then all commands are redone up to an including the sequence number and any result of the last of these is returned. ``clear()`` This method clears the command stack, without undoing or redoing any commands, and leaves the stack in a clean state. It is typically used when all changes to the data have been abandoned. ``begin_macro(name)`` This method begins a macro by creating an empty command with the given name. The commands passed to all subsequent calls to ``push()`` will be contained in the macro until the next call to ``end_macro()``. Macros may be nested. The command stack is disabled (ie. nothing can be undone or redone) while a macro is being created (ie. while there is an outstanding ``end_macro()`` call). ``end_macro()`` This method ends the current macro. ICommand ........ The ``ICommand`` interface defines the interface that must be implemented by any undoable/redoable command. ``data`` This optional trait is a reference to the data object that the command operates on. It is not used by the framework itself. ``name`` This Unicode trait is the name of the command as it will appear in any GUI element (e.g. in the text of an undo and redo menu entry). It may include ``&`` to indicate a keyboard shortcut which will be automatically removed whenever it is inappropriate. ``__init__(*args)`` If the command takes arguments then the command must ensure that deep copies should be made if appropriate. ``do()`` This method is called by a command stack to execute the command and to return any result. The command must save any state necessary for the ``undo()`` and ``redo()`` methods to work. It is guaranteed that this will only ever be called once and that it will be called before any call to ``undo()`` or ``redo()``. ``undo()`` This method is called by a command stack to undo the command. ``redo()`` This method is called by a command stack to redo the command and to return any result. ``merge(other)`` This method is called by the command stack to try and merge the ``other`` command with this one. True should be returned if the commands were merged. If the commands are merged then ``other`` will not be placed on the command stack. A subsequent undo or redo of this modified command must have the same effect as the two original commands. AbstractCommand ............... ``AbstractCommand`` is an abstract base class that implements the ``ICommand`` interface. It provides a default implementation of the ``merge()`` method. CommandAction ............. The ``CommandAction`` class is a sub-class of the PyFace ``Action`` class that is used to wrap commands. ``command`` This callable trait must be set to a factory that will return an object that implements ``ICommand``. It will be called when the action is invoked and the object created pushed onto the command stack. ``command_stack`` This instance trait must be set to the command stack that commands invoked by the action are pushed to. ``data`` This optional trait is a reference to the data object that will be passed to the ``command`` factory when it is called. UndoAction .......... The ``UndoAction`` class is a canned PyFace action that undoes the last command of the active command stack. RedoAction .......... The ``RedoAction`` class is a canned PyFace action that redoes the last command undone of the active command stack. .. _API: api/index.html .. _example: https://svn.enthought.com/enthought/browser/AppTools/trunk/examples/undo/ apptools-4.5.0/docs/source/permissions/0000755000076500000240000000000013547652535021464 5ustar mdickinsonstaff00000000000000apptools-4.5.0/docs/source/permissions/ApplicationAPI.rst0000644000076500000240000002530611640354733025010 0ustar mdickinsonstaff00000000000000Application API =============== This section provides an overview of the part of the ETS Permissions Framework API used by application developers. The `Permissions Framework example`_ demonstrates the API in use. An application typically uses the API to do the following: - define permissions - apply permissions - user authentication - getting and setting user data - integrate management actions. Defining Permissions -------------------- A permission is the object that determines the user's access to a part of an application. While it is possible to apply the same permission to more than one part of an application, it is generally a bad idea to do so as it makes it difficult to separate them at a later date. A permission has an id and a human readable description. Permission ids must be unique. By convention a dotted notation is used for ids to give them a structure. Ids should at least be given an application or plugin specific prefix to ensure their uniqueness. Conventionally all an applications permissions are defined in a single ``permissions.py`` module. The following is an extract of the example's ``permissions.py`` module:: from apptools.permissions.api import Permission # Add a new person. NewPersonPerm = Permission(id='ets.permissions.example.person.new', description=u"Add a new person") # Update a person's age. UpdatePersonAgePerm = Permission(id='ets.permissions.example.person.age.update', description=u"Update a person's age") # View or update a person's salary. PersonSalaryPerm = Permission(id='ets.permissions.example.person.salary', description=u"View or update a person's salary") Applying Permissions -------------------- Permissions are applied to different parts of an applications GUI. When the user has been granted a permission then the corresponding part of the GUI is displayed normally. When the user is denied a permission then the corresponding part of the GUI is disabled or completely hidden. Permissions can be applied to TraitsUI view items and to any object which can be wrapped in a ``SecureProxy``. TraitsUI View Items ................... Items in TraitsUI views have ``enabled_when`` and ``visible_when`` traits that are evaluated to determine if the item should be enabled or visible respectively. These are used to apply permissions by storing the relevant permissions in the model so that they are available to the view. The ``enabled_when`` and ``visible_when`` traits then simply reference the permission's ``granted`` trait. The ``granted`` trait automatically reflects whether or not the user currently has the corresponding permission. In order for the view to be correctly updated when the user's permissions change (ie. when they become authenticated) the view must use the ``SecureHandler`` handler. This handler is a simple sub-class of the standard Traits ``Handler`` class. The following extract from the example shows a default view of the ``Person`` object that enables the ``age`` item when the user has the ``UpdatePersonAgePerm`` permission and shows the ``salary`` item when the user has the ``PersonSalaryPerm`` permission:: from apptools.permissions.api import SecureHandler from traits.api import HasTraits, Int, Unicode from traitsui.api import Item, View from permissions import UpdatePersonAgePerm, PersonSalaryPerm class Person(HasTraits): """A simple example of an object model""" # Name. name = Unicode # Age in years. age = Int # Salary. salary = Int # Define the default view with permissions attached. age_perm = UpdatePersonAgePerm salary_perm = PersonSalaryPerm traits_view = View( Item(name='name'), Item(name='age', enabled_when='object.age_perm.granted'), Item(name='salary', visible_when='object.salary_perm.granted'), handler=SecureHandler) Wrapping in a SecureProxy ......................... Any object can have permissions applied by wrapping it in a ``SecureProxy`` object. An adapter is used that manages the enabled and visible states of the proxied object according to the current user's permissions. Otherwise the proxy behaves just like the object being proxied. Adapters are included for the following types of object: - PyFace actions - PyFace widgets **FIXME:** TODO - Qt widgets - wx widgets See `Writing SecureProxy Adapters`_ for a description of how to write adapters for other types of objects. The following extract from the example shows the wrapping of a standard PyFace action and the application of the ``NewPersonPerm`` permission:: from apptools.permissions.api import SecureProxy from permissions import NewPersonPerm ... def _new_person_action_default(self): """Trait initializer.""" # Create the action and secure it with the appropriate permission. act = Action(name='New Person', on_perform=self._new_person) act = SecureProxy(act, permissions=[NewPersonPerm]) return act A ``SecureProxy`` also accepts a ``show`` argument that, when set to ``False``, hides the object when it becomes disabled. Authenticating the User ----------------------- The user manager supports the concept of the current user and is responsible for authenticating the user (and subsequently unauthorising the user if required). The code fragment to authenticate the current user is:: from apptools.permissions.api import get_permissions_manager get_permissions_Manager().user_manager.authenticate_user() Unauthorising the current user is done using the ``unauthenticate_user()`` method. As a convenience two PyFace actions, called ``LoginAction`` and ``LogoutAction``, are provided that wrap these two methods. As a further convenience a PyFace menu manager, called ``UserMenuManager``, is provided that contains all the user and management actions (see below) in the permissions framework. This is used by the example. The user menu, login and logout actions can be imported from ``apptools.permissions.action.api``. Getting and Setting User Data ----------------------------- The user manager has a ``user`` trait that is an object that implements the ``IUser`` interface. It is only valid once the user has been authenticated. The ``IUser`` interface has a ``blob`` trait that holds any binary data (as a Python string). The data will be read when the user is authenticated. The data will be written whenever it is changed. Integrating Management Actions ------------------------------ Both policy and user managers can provide actions that provide access to various management functions. Both have a ``management_actions`` trait that is a list of PyFace actions that invoke appropriate dialogs that allow the user to manage the policy and the user population appropriately. User managers also have a ``user_actions`` trait that is a list of PyFace actions that invoke appropriate dialogs that allow the user to manage themselves. For example, the default user manager provides an action that allows a user to change their password. The default policy manager provides actions that allows roles to be defined in terms of sets of permissions, and allows users to be assigned one or more roles. The default user manager provides actions that allows users to be added, modified and deleted. A user manager that integrates with an enterprise's secure directory service may not provide any management actions. All management actions have appropriate permissions attached to them. Writing SecureProxy Adapters ---------------------------- ``SecureProxy`` will automatically handle most of the object types you will want to apply permissions to. However it is possible to implement additional adapters to support other object types. To do this you need to implement a sub-class of ``AdapterBase`` and register it. Adapters tend to be one of two styles according to how the object's enabled and visible states are changed. If the states are changed via attributes (typically Traits based objects) then the adapter will cause a proxy to be created for the object. If the states are changed via methods (typically toolkit widgets) then the adapter will probably modify the object itself. We will refer to these two styles as wrapping adapters and patching adapters respectively. The following gives a brief overview of the ``AdapterBase`` class: ``proxied`` This instance attribute is a reference to the original object. ``register_adapter(adapter, type, type, ...)`` This is a class method that is used to register your adapter and one or more object types that it handles. ``adapt()`` This is a method that should be reimplemented by patching adapters. (The default implementation will cause a proxy to be created for wrapping adapters.) This is where any patching of the ``proxied`` attribute is done. The object returned will be returned by ``SecureProxy()`` and would normally be the patched object - but can be any object. ``setattr(name, value)`` This method should be reimplemented by wrapping adapters to intercept the setting of relevant attributes of the ``proxied`` object. The default implementation should be used as the fallback for irrelevant attributes. ``get_enabled()`` This method must be reimplemented to return the current enabled state. ``set_enabled(value)`` This method must be reimplemented to set the enabled state to the given value. ``update_enabled(value)`` This method is called by your adapter to set the desired value of the enabled state. The actual state set will depend on the current user's permissions. ``get_visible()`` This method must be reimplemented to return the current visible state. ``set_visible(value)`` This method must be reimplemented to set the visible state to the given value. ``update_visible(value)`` This method is called by your adapter to set the desired value of the visible state. The actual state set will depend on the current user's permissions. The ``AdapterBase`` class is defined in `adapter_base.py`_. The `PyFace action adapter`_ is an example of a wrapping adapter. The `PyQt widget adapter`_ is an example of a patching adapter. .. _`Permissions Framework example`: https://svn.enthought.com/enthought/browser/AppTools/trunk/examples/permissions/application/ .. _`adapter_base.py`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/adapter_base.py .. _`PyFace action adapter`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/adapters/pyface_action.py .. _`PyQt widget adapter`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/adapters/qt4_widget.py apptools-4.5.0/docs/source/permissions/DefaultUserManagerDataAPI.rst0000644000076500000240000000725711640354733027062 0ustar mdickinsonstaff00000000000000Default User Manager Data API ============================= This section provides an overview of the part of the ETS Permissions Framework API used by developers who want to store a user database in a more secure location (eg. a remote server) than that provided by the default implementation. The API is defined by the default user manager which uses password based authorisation. If this API isn't sufficiently flexible, or if another method of authorisation is used (biometrics for example) then an alternative user manager should be implemented. The API is fully defined by the `IUserDatabase interface`_. This allows user databases to be implemented that extend the `IUser interface`_ and store additional user related data. If the user database is being persisted in secure storage (eg. a remote RDBMS) then this could be used to store sensitive data (eg. passwords for external systems) that shouldn't be stored as ordinary preferences. In most cases there will be no requirement to store additional user related data than that defined by ``IUser`` so the supplied `UserDatabase implementation`_ (which provides all the GUI code required to implement the `IUserDatabase interface`_) can be used. The `UserDatabase implementation`_ delegates the access to the user database to an object implementing the `IUserStorage interface`_. The default implementation of this interface stores the user database as a pickle in a local file. Overview of IUserStorage ------------------------ The `IUserStorage interface`_ defines a number of methods that must be implemented to read and write to the user database. The methods are designed to be implemented using simple SQL statements. In the event of an error a method must raise the ``UserStorageError`` exception. The string representation of the exception is used as an error message that is displayed to the user. Overview of IUserDatabase ------------------------- The `IUserDatabase interface`_ defines a set of ``Bool`` traits, all beginning with ``can_``, that describe the capabilities of a particular implementation. For example, the ``can_add_user`` trait is set by an implementation if it supports the ability to add a new user to the database. Each of these capability traits has a corresponding method which has the same name except for the ``can_`` prefix. The method only needs to be implemented if the corresponding traits is ``True``. The method, for example ``add_user()`` is called by the user manager to implement the capability. The interface has two other methods. The ``bootstrapping()`` method is called by the user manager to determine if the database is bootstrapping. Typically this is when the database is empty and no users have yet been defined. The permissions framework treats this situation as a special case and is able to relax the enforcement of permissions to allow users and permissions to be initially defined. The ``user_factory()`` method is called by the user manager to create a new user object, ie. an object that implements the `IUser interface`_. This allows an implementation to extend the `IUser interface`_ and store additional user related data in the object if the ``blob`` trait proves insufficient. .. _`IUser interface`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/i_user.py .. _`IUserDatabase interface`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/default/i_user_database.py .. _`IUserStorage interface`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/default/i_user_storage.py .. _`UserDatabase implementation`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/default/user_database.py apptools-4.5.0/docs/source/permissions/DefaultPolicyManagerDataAPI.rst0000644000076500000240000000244511640354733027375 0ustar mdickinsonstaff00000000000000Default Policy Manager Data API =============================== This section provides an overview of the part of the ETS Permissions Framework API used by developers who want to store a policy manager's persistent data in a more secure location (eg. a remote server) than that provided by the default implementation. The API is defined by the default policy manager which uses roles to make it easier to assign permissions to users. If this API isn't sufficiently flexible, or if roles are inappropriate, then an alternative policy manager should be implemented. The API is fully defined by the `IPolicyStorage interface`_. The default implementation of this interface stores the policy database as a pickle in a local file. Overview of IPolicyStorage -------------------------- The `IPolicyStorage interface`_ defines a number of methods that must be implemented to read and write to the policy database. The methods are designed to be implemented using simple SQL statements. In the event of an error a method must raise the ``PolicyStorageError`` exception. The string representation of the exception is used as an error message that is displayed to the user. .. _`IPolicyStorage interface`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/default/i_policy_storage.py apptools-4.5.0/docs/source/permissions/Introduction.rst0000644000076500000240000002104711640354733024672 0ustar mdickinsonstaff00000000000000Permissions Framework - Introduction ==================================== The Permissions Framework is a component of the Enthought Tool Suite that provides developers with the facility to limit access to parts of an application unless the user is appropriately authorised. In other words it enables and disables different parts of the GUI according to the identity of the user. The framework includes an API to allow it to be integrated with an organisation's existing security infrastructure, for example to look users up in a corporate LDAP directory. The framework is completely configurable. Alternate implementations of all major components can be provided if necessary. The default implementations provide a simple local filesystem user database and allows roles to be defined and assigned to users. The framework **does not** provide any facility for protecting access to data. It is not possible to implement such protection in Python and using the file security provided by a typical operating system. Framework Concepts ------------------ The following are the concepts supported by the framework. - Permission A permission is the basic tool that a developer uses to specify that access to a part of the application should be restricted. If the current user has the permission then access is granted. A permission may be attached to a PyFace action, to an item of a TraitsUI view, or to a GUI toolkit specific widget. When the user is denied access, the corresponding GUI control is disabled or completely hidden. - User Each application has a current user who is either *authorised* or *unauthorised*. In order to become authorised a user must identify themselves and authenticate that identity. An arbitrary piece of data (called a blob) can be associated with an authorised user which (with user manager support) can be stored securely. This might be used, for example, to store sensitive user preferences, or to implement a roaming profile. - User Manager The user manager is responsible for authorising the current user and, therefore, defines how that is done. It also provides information about the user population to the policy manager. It may also, optionally, provide the ability to manage the user population (eg. add or delete users). The user manager must either maintain a persistent record of the user population, or interface with an external user database or directory service. The default user manager uses password based authorisation. The user manager persists its data in a user database. The default user manager provides an API so that different implementations of the user database can be used (for example to store the data in an RDBMS, or to integrate with an existing directory service). A default user database is provided that pickles the data in a local file. - Policy Manager The policy manager is responsible for assigning permissions to users and for determining the permissions assigned to the current user. To do this it must maintain a persistent record of those assignments. The default policy manager supplied with the framework uses roles to make it easier for an administrator to manage the relationships between permissions and users. A role is defined as a named set of permissions, and a user may have one or more roles assigned to them. The policy manager persists its data in a policy database. The default policy manager provides an API so that different implementations of the policy database can be used (for example to store the data in an RDBMS). A default policy database is provided that pickles the data in a local file. - Permissions Manager The permissions manager is a singleton object used to get and set the current policy and user managers. Framework APIs -------------- The APIs provided by the permissions framework can be split into the following groups. - `Application API`_ This part of the API is used by application developers. - `Policy Manager API`_ This is the interface that an alternative policy manager must implement. The need to implement an alternative is expected to be very rare and so the API isn't covered further. See the definition of the IPolicyManager interface for the details. - `Default Policy Manager Data API`_ This part of the API is used by developers to store the policy's persistent data in a more secure location (eg. on a remote server) than that provided by the default implementation. - `User Manager API`_ This is the interface that an alternative user manager must implement. The need to implement an alternative is expected to be very rare and so the API isn't covered further. See the definition of the IUserManager interface for the details. - `Default User Manager Data API`_ This part of the API is used by developers to store the user database in a more secure location (eg. on a remote server) than that provided by the default implementation. The complete API_ documentation is available as endo generated HTML. What Do I Need to Reimplement? ------------------------------ The architecture of the permissions framework comprises several layers, each of which can reimplemented to meet the requirements of a particular environment. Hopefully the following questions and answers will clarify what needs to be reimplemented depending on your environment. Q: Do you want to use roles to group permissions and assign them to users? A: If yes then use the supplied PolicyManager, otherwise provide your own IPolicyManager implementation. Q: Do you want users to be authenticated using a password? A: If yes then use the supplied UserManager, otherwise provide your own IUserManager implementation. Q: Does the IUser interface allow you to store all the user specific information you need? A: If yes then use the supplied UserDatabase, otherwise provide your own IUserDatabase implementation. Q: Do you want to store your user accounts as pickled data in a local file? A: If yes then use the supplied default, otherwise provide UserDatabase with your own IUserStorage implementation. Q: Do you want to store your policy data (ie. roles and role assignments) as pickled data in a local file? A: If yes then use the supplied default, otherwise provide PolicyManager with your own IPolicyStorage implementation. Deploying Alternative Managers ------------------------------ The permissions framework will first try to import the different managers from the ``apptools.permissions.external`` namespace. The default managers are only used if no alternative was found. Therefore, alternative managers should be deployed as an egg containing that namespace. Specifically the framework looks for the following classes: ``PolicyManager`` from ``apptools.permissions.external.policy_manager`` ``PolicyStorage`` from ``apptools.permissions.external.policy_storage`` ``UserDatabase`` from ``apptools.permissions.external.user_database`` ``UserManager`` from ``apptools.permissions.external.user_manager`` ``UserStorage`` from ``apptools.permissions.external.user_storage`` The example server is such a package that provides PolicyStorage and UserStorage implementations that use an XML-RPC based server to provide remote (and consequently more secure) policy and user databases. Using the Default Storage Implementations ----------------------------------------- The default policy and user managers both (again by default) persist their data as pickles in local files called ``ets_perms_policydb`` and ``ets_perms_userdb`` respectively. By default these are stored in the application's home directory (ie. that returned by ``ETSConfig.application_home``). Note that this directory is normally in the user's own directory structure whereas it needs to be available to all users of the application. If the ``ETS_PERMS_DATA_DIR`` environment variable is set then its value is used instead. The directory must be writeable by all users of the application. It should be restated that the default implementations do *not* provide secure access to the permissions and user data. They are useful in a cooperative environment and as working examples. .. _API: api/index.html .. _`Application API`: ApplicationAPI.html .. _`Policy Manager API`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/i_policy_manager.py .. _`Default Policy Manager Data API`: DefaultPolicyManagerDataAPI.html .. _`User Manager API`: https://svn.enthought.com/enthought/browser/AppTools/trunk/enthought/permissions/i_user_manager.py .. _`Default User Manager Data API`: DefaultUserManagerDataAPI.html apptools-4.5.0/docs/source/selection/0000755000076500000240000000000013547652535021076 5ustar mdickinsonstaff00000000000000apptools-4.5.0/docs/source/selection/selection.rst0000644000076500000240000001633612473127616023620 0ustar mdickinsonstaff00000000000000.. _selection_service: The selection service ===================== It is quite common in GUI applications to have a UI element displaying a collection of items that a user can select ("selection providers"), while other parts of the application must react to changes in the selection ("selection listeners"). Ideally, the listeners would not have a direct dependency on the UI object. This is especially important in extensible `envisage`_ applications, where a plugin might need to react to a selection change, but we do not want to expose the internal organization of the application to external developers. This package defines a selection service that manages the communication between providers and listener. The :class:`~.SelectionService` object -------------------------------------- The :class:`~.SelectionService` object is the central manager that handles the communication between selection providers and listener. :ref:`Selection providers ` are components that wish to publish information about their current selection for public consumption. They register to a selection service instance when they first have a selection available (e.g., when the UI showing a list of selectable items is initialized), and un-register as soon as the selection is not available anymore (e.g., the UI is destroyed when the windows is closed). :ref:`Selection listeners ` can query the selection service to get the current selection published by a provider, using the provider unique ID. The service acts as a broker between providers and listeners, making sure that they are notified when the :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.selection` event is fired. .. _selection_providers: Selection providers ------------------- Any object can become a selection provider by implementing the :class:`~apptools.selection.i_selection_provider.ISelectionProvider` interface, and registering to the selection service. Selection providers must provide a unique ID :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.provider_id`, which is used by listeners to request its current selection. Whenever its selection changes, providers fire a :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.selection` event. The content of the event is an instance implementing :class:`~.ISelection` that contains information about the selected items. For example, a :class:`~.ListSelection` object contains a list of selected items, and their indices. Selection providers can also be queried directly about their current selection using the :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.get_selection` method, and can be requested to change their selection to a new one with the :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.set_selection` method. Registration ~~~~~~~~~~~~ Selection providers publish their selection by registering to the selection service using the :attr:`~apptools.selection.selection_service.SelectionService.add_selection_provider` method. When the selection is no longer available, selection providers should un-register through :attr:`~apptools.selection.selection_service.SelectionService.remove_selection_provider`. Typically, selection providers are UI objects showing a list or tree of items, they register as soon as the UI component is initialized, and un-register when the UI component disappears (e.g., because their window has been closed). In more complex applications, the registration could be done by a controller object instead. .. _selection_listeners: Selection listeners ------------------- Selection listeners request information regarding the current selection of a selection provider given their provider ID. The :class:`~.SelectionService` supports two distinct use cases: 1) Passively listening to selection changes: listener connect to a specific provider and are notified when the provider's selection changes. 2) Actively querying a provider for its current selection: the selection service can be used to query a provider using its unique ID. Passive listening ~~~~~~~~~~~~~~~~~ Listeners connect to the selection events for a given provider using the :attr:`~apptools.selection.selection_service.SelectionService.connect_selection_listener` method. They need to provide the unique ID of the provider, and a function (or callable) that is called to send the event. This callback function takes one argument, an implementation of the :class:`~.ISelection` that represents the selection. It is possible for a listener to connect to a provider ID before it is registered. As soon as the provider is registered, the listener will receive a notification containing the provider's initial selection. To disconnect a listener use the methods :attr:`~apptools.selection.selection_service.SelectionService.disconnect_selection_listener`. Active querying ~~~~~~~~~~~~~~~ In other instances, an element of the application only needs the current selection at a specific time. For example, a toolbar button could open dialog representing a user action based on what is currently selected in the active editor. The :attr:`~apptools.selection.selection_service.SelectionService.get_selection` method calls the corresponding method on the provider with the given ID and returns an :class:`~.ISelection` instance. Setting a selection ~~~~~~~~~~~~~~~~~~~ Finally, it is possible to request a provider to set its selection to a given set of objects with :attr:`~apptools.selection.selection_service.SelectionService.set_selection`. The main use case for this method is multiple views of the same list of objects, which need to keep their selection synchronized. If the items specified in the arguments are not available in the provider, a :class:`~apptools.selection.errors.ProviderNotRegisteredError` is raised, unless the optional keyword argument :attr:`ignore_missing` is set to ``True``. API Reference ------------- :mod:`apptools.selection` Package ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Users of the :mod:`apptools.selection` package can access the objects that are part of the public API through the convenience :mod:`apptools.selection.api`. :mod:`~apptools.selection.selection_service` Module ''''''''''''''''''''''''''''''''''''''''''''''''''' .. automodule:: apptools.selection.selection_service :members: :undoc-members: :show-inheritance: :mod:`~apptools.selection.i_selection_provider` Module '''''''''''''''''''''''''''''''''''''''''''''''''''''' .. automodule:: apptools.selection.i_selection_provider :members: :undoc-members: :show-inheritance: :mod:`~apptools.selection.is_selection` Module '''''''''''''''''''''''''''''''''''''''''''''' .. automodule:: apptools.selection.i_selection :members: :undoc-members: :show-inheritance: :mod:`~apptools.selection.list_selection` Module '''''''''''''''''''''''''''''''''''''''''''''''' .. automodule:: apptools.selection.list_selection :members: :undoc-members: :show-inheritance: :mod:`~apptools.selection.errors` Module '''''''''''''''''''''''''''''''''''''''''''''''' .. automodule:: apptools.selection.errors :members: :undoc-members: :show-inheritance: .. _envisage: http://docs.enthought.com/envisage/ apptools-4.5.0/image_LICENSE.txt0000644000076500000240000000165711640354733017646 0ustar mdickinsonstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: Project License File ---------------------------------------------------------------------------- GV: Gael Varoquaux BSD-like LICENSE.txt Enthought BSD-like LICENSE.txt Nuvola LGPL image_LICENSE_Nuvola.txt OOo LGPL image_LICENSE_OOo.txt Unless stated in this file, icons are work of Enthought, and are released under BSD-like license. Files and original authors: ---------------------------------------------------------------- apptools/help: help_action.png | GV apptools/naming/ui/images (and examples/naming/images): closed_folder.png | GV document.png | GV open_folder.png | GV apptools-4.5.0/CHANGES.txt0000644000076500000240000000271113547646152016471 0ustar mdickinsonstaff00000000000000Apptools CHANGELOG ================== Version 4.5.0 ~~~~~~~~~~~~~ Released : 10 October 2019 * Add missing `long_description_content_type` field in setup. (#108) * Remove use of `2to3`. (#90) * Use etstool for CI tasks. Setup travis macos and appveyor CI. (#92) * Temporarily change cwd when running tests. (#104) * Update broken imports. (#95) * Add `six` to requirements. (#101) * Remove one more use of the deprecated `set` method. (#103) * Use `trait_set` instead of the now deprecated `set` method. (#82) * Address one more numpy deprecation warning. (#100) * Address numpy deprecation warnings. (#83) * Test the package on Python 3.5, 3.6 on CI. (#78) * Fix mismatched pyface and traitsui requirements. (#73) * Drop support for Python 2.6. (#63) * Fix `state_pickler.dump` on Python 2. (#61) * Fix a few spelling mistakes in documentation. (#87) Version 4.4.0 ~~~~~~~~~~~~~ * Apptools now works with Python-3.x. (#54) * Travis-ci support with testing on Python 2.6, 2.7 and 3.4. (#55) Change summary since 4.2.1 ~~~~~~~~~~~~~~~~~~~~~~~~~~ Enhancements * Apptools now have a changelog! * Preferences system defaults to utf-8 encoded string with ConfigObj providing better support for unicode in the PreferenceHelper (#41, #45). * Added a traitsified backport of Python 3's lru_cache (#39). * Added PyTables support to the io submodule (#19, #20, and #24 through #34). * Added a SelectionService for managing selections within an application (#15, #16, #17, #23). apptools-4.5.0/setup.py0000644000076500000240000001147613547652264016403 0ustar mdickinsonstaff00000000000000# Copyright (c) 2008-2015 by Enthought, Inc. # All rights reserved. import os import re import subprocess from setuptools import setup, find_packages MAJOR = 4 MINOR = 5 MICRO = 0 IS_RELEASED = True VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO) # Return the git revision as a string def git_version(): def _minimal_ext_cmd(cmd): # construct minimal environment env = {} for k in ['SYSTEMROOT', 'PATH']: v = os.environ.get(k) if v is not None: env[k] = v # LANGUAGE is used on win32 env['LANGUAGE'] = 'C' env['LANG'] = 'C' env['LC_ALL'] = 'C' out = subprocess.Popen( cmd, stdout=subprocess.PIPE, env=env, ).communicate()[0] return out try: out = _minimal_ext_cmd(['git', 'describe', '--tags']) except OSError: out = '' git_description = out.strip().decode('ascii') expr = r'.*?\-(?P\d+)-g(?P[a-fA-F0-9]+)' match = re.match(expr, git_description) if match is None: git_revision, git_count = 'Unknown', '0' else: git_revision, git_count = match.group('hash'), match.group('count') return git_revision, git_count def write_version_py(filename='apptools/_version.py'): template = """\ # THIS FILE IS GENERATED FROM APPTOOLS SETUP.PY version = '{version}' full_version = '{full_version}' git_revision = '{git_revision}' is_released = {is_released} if not is_released: version = full_version """ # Adding the git rev number needs to be done inside # write_version_py(), otherwise the import of apptools._version messes # up the build under Python 3. fullversion = VERSION if os.path.exists('.git'): git_rev, dev_num = git_version() elif os.path.exists('apptools/_version.py'): # must be a source distribution, use existing version file try: from apptools._version import git_revision as git_rev from apptools._version import full_version as full_v except ImportError: raise ImportError("Unable to import git_revision. Try removing " "apptools/_version.py and the build directory " "before building.") match = re.match(r'.*?\.dev(?P\d+)', full_v) if match is None: dev_num = '0' else: dev_num = match.group('dev_num') else: git_rev = 'Unknown' dev_num = '0' if not IS_RELEASED: fullversion += '.dev{0}'.format(dev_num) with open(filename, "wt") as fp: fp.write(template.format(version=VERSION, full_version=fullversion, git_revision=git_rev, is_released=IS_RELEASED)) if __name__ == "__main__": write_version_py() from apptools import __version__, __requires__ setup(name='apptools', version=__version__, author='Enthought, Inc.', author_email='info@enthought.com', maintainer='ETS Developers', maintainer_email='enthought-dev@enthought.com', url='https://docs.enthought.com/apptools', download_url=('https://www.github.com/enthought/apptools'), classifiers=[c.strip() for c in """\ Development Status :: 5 - Production/Stable Intended Audience :: Developers Intended Audience :: Science/Research License :: OSI Approved :: BSD License Operating System :: MacOS Operating System :: Microsoft :: Windows Operating System :: OS Independent Operating System :: POSIX Operating System :: Unix Programming Language :: Python Topic :: Scientific/Engineering Topic :: Software Development Topic :: Software Development :: Libraries """.splitlines() if len(c.strip()) > 0], description='application tools', long_description=open('README.rst').read(), long_description_content_type="text/x-rst", include_package_data=True, package_data={'apptools': ['help/help_plugin/*.ini', 'help/help_plugin/action/images/*.png', 'logger/plugin/*.ini', 'logger/plugin/view/images/*.png', 'naming/ui/images/*.png', 'preferences/tests/*.ini' ] }, install_requires=__requires__, license='BSD', packages=find_packages(), platforms=["Windows", "Linux", "Mac OS-X", "Unix", "Solaris"], zip_safe=False, ) apptools-4.5.0/examples/0000755000076500000240000000000013547652535016477 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/preferences/0000755000076500000240000000000013547652535021000 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/preferences/example.ini0000644000076500000240000000011611640354733023121 0ustar mdickinsonstaff00000000000000[acme] width = 99 height = 600 [acme.workbench] bgcolor = red fgcolor = blue apptools-4.5.0/examples/preferences/preferences_manager.py0000644000076500000240000000556111640354733025343 0ustar mdickinsonstaff00000000000000""" An example of using the preferences manager. """ # Enthought library imports. from traits.api import Color, Int, Float, Str from traitsui.api import View # Local imports. from apptools.preferences.api import Preferences, PreferencesHelper from apptools.preferences.api import get_default_preferences from apptools.preferences.api import set_default_preferences from apptools.preferences.ui.api import PreferencesManager, PreferencesPage # Create a preferences collection from a file and make it the default root # preferences node for all preferences helpers etc. set_default_preferences(Preferences(filename='example.ini')) class AcmePreferencesPage(PreferencesPage): """ A preference page for the Acme preferences. """ #### 'IPreferencesPage' interface ######################################### # The page's category (e.g. 'General/Appearence'). The empty string means # that this is a top-level page. category = '' # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = '' # The page name (this is what is shown in the preferences dialog. name = 'Acme' # The path to the preferences node that contains our preferences. preferences_path = 'acme' #### Preferences ########################################################## width = Int(800) height = Int(600) #### Traits UI views ###################################################### view = View('width', 'height') class AcmeWorkbenchPreferencesPage(PreferencesPage): """ A preference page for the Acme workbench preferences. """ #### 'IPreferencesPage' interface ######################################### # The page's category (e.g. 'General/Appearence'). The empty string means # that this is a top-level page. category = 'Acme' # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = '' # The page name (this is what is shown in the preferences dialog. name = 'Workbench' # The path to the preferences node that contains our preferences. preferences_path = 'acme' #### Preferences ########################################################## bgcolor = Color fgcolor = Color #### Traits UI views ###################################################### view = View('bgcolor', 'fgcolor') # Entry point. if __name__ == '__main__': # Create a manager with some pages. preferences_manager = PreferencesManager( pages = [ AcmePreferencesPage(), AcmeWorkbenchPreferencesPage() ] ) # Show the UI... preferences_manager.configure_traits() # Save the preferences... get_default_preferences().flush() #### EOF ###################################################################### apptools-4.5.0/examples/appscripting/0000755000076500000240000000000013547652535021202 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/appscripting/actions.py0000644000076500000240000001640211640354733023206 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, Instance, Str, Unicode from pyface.action.api import Action from pyface.workbench.api import WorkbenchWindow # Local imports. from model import Label class BoundAction(Action): """An action with a bound object. The action is automatically disabled if the bound object is None.""" #### 'BoundAction' interface ############################################## # The bound object. obj = Any # The optional trait on obj that we are synch'ed with. trait_name = Str ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """Initialise the object.""" super(BoundAction, self).__init__(**traits) # Fake an obj change to set the initial state. self._obj_changed(None, self.obj) ########################################################################### # Traits handlers. ########################################################################### def _obj_changed(self, old, new): """Invoked when the bound object changes.""" if old is not None: if self.trait_name: # Ignore any changes to the old object. old.on_trait_change(self._trait_changed, self.trait_name, remove=True) enabled = False if new is not None: if self.trait_name: # Check for any changes on the new object. new.on_trait_change(self._trait_changed, self.trait_name) # Get the current state. if getattr(new, self.trait_name): enabled = True else: enabled = True self.enabled = enabled def _trait_changed(self, new): """Invoked when the trait on the bound object changes.""" self.enabled = new class BoundWorkbenchAction(BoundAction): """A bound action whose object is being edited in a workbench editor. The action is automatically rebound when the active editor changes.""" #### 'BoundWorkbenchAction' interface ##################################### # The type of the object that we will be enabled for. If it is None then # we will be enabled for all types. trait_type = Any # The workbench window containing the action. window = Instance(WorkbenchWindow) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """Initialise the object.""" super(BoundWorkbenchAction, self).__init__(**traits) self.window.on_trait_change(self._editor_changed, 'active_editor') # Fake an editor change to set the initial state. self._editor_changed(self.window.active_editor) ########################################################################### # Traits handlers. ########################################################################### def _editor_changed(self, new): """Invoked when the active editor changes.""" obj = None if new is not None: if self.trait_type is None: obj = new.obj elif isinstance(new.obj, self.trait_type): obj = new.obj self.obj = obj class LabelAction(BoundWorkbenchAction): """ The LabelAction class is the base class for all actions that operate on a Label. """ #### 'BoundWorkbenchAction' interface ##################################### # The type of the object that we will be enabled for. trait_type = Label #### 'BoundAction' interface ############################################## # The bound object. obj = Instance(Label) class LabelIncrementSizeAction(LabelAction): """ The LabelIncrementSizeAction class is a action that increases the size of a label's text. """ #### 'Action' interface ################################################### # The name of the action. name = Unicode("&Increment size") ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): self.obj.increment_size(1) class LabelDecrementSizeAction(LabelAction): """ The LabelDecrementSizeAction class is a action that decreases the size of a label's text. """ #### 'Action' interface ################################################### # The name of the action. name = Unicode("&Decrement size") ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): self.obj.decrement_size(1) class LabelNormalFontAction(LabelAction): """ The LabelNormalFontAction class is a action that sets a normal font for a label's text. """ #### 'Action' interface ################################################### # The name of the action. name = Unicode("&Normal font") ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): self.obj.style = 'normal' class LabelBoldFontAction(LabelAction): """ The LabelNormalFontAction class is a action that sets a bold font for a label's text. """ #### 'Action' interface ################################################### # The name of the action. name = Unicode("&Bold font") ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): self.obj.style = 'bold' class LabelItalicFontAction(LabelAction): """ The LabelNormalFontAction class is a action that sets an italic font for a label's text. """ #### 'Action' interface ################################################### # The name of the action. name = Unicode("&Italic font") ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): self.obj.style = 'italic' apptools-4.5.0/examples/appscripting/model.py0000644000076500000240000000364011640354733022646 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Enum, HasTraits, Int, Unicode from apptools.appscripting.api import scriptable, Scriptable class Label(HasTraits): """ The Label class implements the data model for a label. """ #### 'Label' interface #################################################### # The name. name = Unicode # The size in points. size = Int(18) # The style. style = Scriptable(Enum('normal', 'bold', 'italic')) ########################################################################### # 'Label' interface. ########################################################################### @scriptable def __init__(self, **traits): """ Initialise the object. We only implement this so that it can be decorated and so the script manager knows how to recreate it. """ super(Label, self).__init__(**traits) @scriptable def increment_size(self, by): """ Increment the current font size. This demonstrates a scriptable method. """ self.size += by @scriptable def decrement_size(self, by): """ decrement the current font size. This demonstrates a scriptable method. """ self.size -= by apptools-4.5.0/examples/appscripting/example_editor_manager.py0000644000076500000240000001030511640354733026235 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.etsconfig.api import ETSConfig from pyface.workbench.api import Editor, EditorManager class _wxLabelEditor(Editor): """ _wxLabelEditor is the wx implementation of a label editor. """ def create_control(self, parent): import wx w = wx.TextCtrl(parent,style=wx.TE_RICH2) style = w.GetDefaultStyle() style.SetAlignment(wx.TEXT_ALIGNMENT_CENTER) w.SetDefaultStyle(style) self._set_text(w) self._set_size_and_style(w) self.obj.on_trait_change(self._update_text, 'text') self.obj.on_trait_change(self._update_size, 'size') self.obj.on_trait_change(self._update_style, 'style') return w def _name_default(self): return self.obj.text def _update_text(self): self._set_text(self.control) def _set_text(self, w): w.SetValue("") w.WriteText("%s(%d points, %s)" % (self.obj.text, self.obj.size, self.obj.style)) def _update_size(self): self._set_size_and_style(self.control) def _update_style(self): self._set_size_and_style(self.control) def _set_size_and_style(self, w): import wx if self.obj.style == 'normal': style, weight = wx.NORMAL, wx.NORMAL elif self.obj.style == 'italic': style, weight = wx.ITALIC, wx.NORMAL elif self.obj.style == 'bold': style, weight = wx.NORMAL, wx.BOLD else: raise NotImplementedError, 'style "%s" not supported' % self.obj.style f = wx.Font(self.obj.size, wx.ROMAN, style, weight, False) style = wx.TextAttr("BLACK",wx.NullColour,f) w.SetDefaultStyle(style) self._set_text(w) class _PyQt4LabelEditor(Editor): """ _PyQt4LabelEditor is the PyQt implementation of a label editor. """ def create_control(self, parent): from PyQt4 import QtCore, QtGui w = QtGui.QLabel(parent) w.setAlignment(QtCore.Qt.AlignCenter) self._set_text(w) self._set_size(w) self._set_style(w) self.obj.on_trait_change(self._update_text, 'text') self.obj.on_trait_change(self._update_size, 'size') self.obj.on_trait_change(self._update_style, 'style') return w def _name_default(self): return self.obj.text def _update_text(self): self._set_text(self.control) def _set_text(self, w): w.setText("%s\n(%d points, %s)" % (self.obj.text, self.obj.size, self.obj.style)) def _update_size(self): self._set_size(self.control) def _set_size(self, w): f = w.font() f.setPointSize(self.obj.size) w.setFont(f) self._set_text(w) def _update_style(self): self._set_style(self.control) def _set_style(self, w): f = w.font() f.setBold(self.obj.style == 'bold') f.setItalic(self.obj.style == 'italic') w.setFont(f) self._set_text(w) class ExampleEditorManager(EditorManager): """ The ExampleEditorManager class creates the example editors. """ def create_editor(self, window, obj, kind): # Create the toolkit specific editor. tk_name = ETSConfig.toolkit if tk_name == 'wx': ed = _wxLabelEditor(window=window, obj=obj) elif tk_name == 'qt4': ed = _PyQt4LabelEditor(window=window, obj=obj) else: raise NotImplementedError, "unsupported toolkit: %s" % tk_name return ed #### EOF ###################################################################### apptools-4.5.0/examples/appscripting/example.py0000644000076500000240000000517511640354733023206 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import logging # Enthought library imports. from pyface.api import GUI, YES from pyface.workbench.api import Workbench # Local imports. from example_script_window import ExampleScriptWindow from model import Label # Log to stderr. logging.getLogger().addHandler(logging.StreamHandler()) logging.getLogger().setLevel(logging.DEBUG) class ExampleScript(Workbench): """ The ExampleScript class is a workbench that creates ExampleScriptWindow windows. """ #### 'Workbench' interface ################################################ # The factory (in this case simply a class) that is used to create # workbench windows. window_factory = ExampleScriptWindow ########################################################################### # Private interface. ########################################################################### def _exiting_changed(self, event): """ Called when the workbench is exiting. """ if self.active_window.confirm('Ok to exit?') != YES: event.veto = True return def main(argv): """ A simple example of using the the application scripting framework in a workbench. """ # Create the GUI. gui = GUI() # Create the workbench. workbench = ExampleScript(state_location=gui.state_location) window = workbench.create_window(position=(300, 300), size=(400, 300)) window.open() # Create some objects to edit. # FIXME v3: The need to explicitly set the style to its default value is # due to a bug in the implementation of Scriptable. label = Label(text="Label", style='normal') label2 = Label(text="Label2", style='normal') # Edit the objects. window.edit(label) window.edit(label2) # Start the GUI event loop. gui.start_event_loop() return if __name__ == '__main__': import sys; main(sys.argv) #### EOF ###################################################################### apptools-4.5.0/examples/appscripting/example_script_window.py0000644000076500000240000001010611640354733026147 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action, Group, MenuManager from pyface.workbench.api import WorkbenchWindow from pyface.workbench.action.api import MenuBarManager, \ ToolBarManager from traits.api import Instance, on_trait_change from apptools.appscripting.api import get_script_manager from apptools.appscripting.action.api import StartRecordingAction, \ StopRecordingAction # Local imports. from example_editor_manager import ExampleEditorManager from actions import LabelIncrementSizeAction, LabelDecrementSizeAction, \ LabelNormalFontAction, LabelBoldFontAction, LabelItalicFontAction class ExampleScriptWindow(WorkbenchWindow): """ The ExampleScriptWindow class is a workbench window that contains example editors that demonstrate the use of the application scripting framework. """ #### Private interface #################################################### # The action that exits the application. _exit_action = Instance(Action) # The File menu. _file_menu = Instance(MenuManager) # The Label menu. _label_menu = Instance(MenuManager) # The Scripts menu. _scripts_menu = Instance(MenuManager) ########################################################################### # Private interface. ########################################################################### #### Trait initialisers ################################################### def __file_menu_default(self): """ Trait initialiser. """ return MenuManager(self._exit_action, name="&File") def __label_menu_default(self): """ Trait initialiser. """ size_group = Group(LabelIncrementSizeAction(window=self), LabelDecrementSizeAction(window=self)) normal = LabelNormalFontAction(window=self, id='normal', style='radio', checked=True) bold = LabelBoldFontAction(window=self, id='bold', style='radio') italic = LabelItalicFontAction(window=self, id='italic', style='radio') style_group = Group(normal, bold, italic, id='style') return MenuManager(size_group, style_group, name="&Label") def __scripts_menu_default(self): """ Trait initialiser. """ # ZZZ: This is temporary until we put the script into a view. get_script_manager().on_trait_event(self._on_script_updated, 'script_updated') return MenuManager(StartRecordingAction(), StopRecordingAction(), name="&Scripts") def __exit_action_default(self): """ Trait initialiser. """ return Action(name="E&xit", on_perform=self.workbench.exit) def _editor_manager_default(self): """ Trait initialiser. """ return ExampleEditorManager() def _menu_bar_manager_default(self): """ Trait initialiser. """ return MenuBarManager(self._file_menu, self._label_menu, self._scripts_menu, window=self) def _tool_bar_manager_default(self): """ Trait initialiser. """ return ToolBarManager(self._exit_action, show_tool_names=False) # ZZZ: This is temporary until we put the script into a view. def _on_script_updated(self, script_manager): script = script_manager.script if script: print script, else: print "Script empty" #### EOF ###################################################################### apptools-4.5.0/examples/undo/0000755000076500000240000000000013547652535017444 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/undo/model.py0000644000076500000240000000266711640354733021120 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Enum, HasTraits, Int, Unicode class Label(HasTraits): """The Label class implements the data model for a label.""" #### 'Label' interface #################################################### # The name. name = Unicode # The size in points. size = Int(18) # The style. style = Enum('normal', 'bold', 'italic') ########################################################################### # 'Label' interface. ########################################################################### def increment_size(self, by): """Increment the current font size.""" self.size += by def decrement_size(self, by): """Decrement the current font size.""" self.size -= by apptools-4.5.0/examples/undo/example_editor_manager.py0000644000076500000240000001027012317502217024472 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.etsconfig.api import ETSConfig from pyface.workbench.api import Editor, EditorManager class _wxLabelEditor(Editor): """ _wxLabelEditor is the wx implementation of a label editor. """ def create_control(self, parent): import wx w = wx.TextCtrl(parent,style=wx.TE_RICH2) style = w.GetDefaultStyle() style.SetAlignment(wx.TEXT_ALIGNMENT_CENTER) w.SetDefaultStyle(style) self._set_text(w) self._set_size_and_style(w) self.obj.on_trait_change(self._update_text, 'text') self.obj.on_trait_change(self._update_size, 'size') self.obj.on_trait_change(self._update_style, 'style') return w def _name_default(self): return self.obj.text def _update_text(self): self._set_text(self.control) def _set_text(self, w): w.SetValue("") w.WriteText("%s(%d points, %s)" % (self.obj.text, self.obj.size, self.obj.style)) def _update_size(self): self._set_size_and_style(self.control) def _update_style(self): self._set_size_and_style(self.control) def _set_size_and_style(self, w): import wx if self.obj.style == 'normal': style, weight = wx.NORMAL, wx.NORMAL elif self.obj.style == 'italic': style, weight = wx.ITALIC, wx.NORMAL elif self.obj.style == 'bold': style, weight = wx.NORMAL, wx.BOLD else: raise NotImplementedError, 'style "%s" not supported' % self.obj.style f = wx.Font(self.obj.size, wx.ROMAN, style, weight, False) style = wx.TextAttr("BLACK",wx.NullColour,f) w.SetDefaultStyle(style) self._set_text(w) class _PyQt4LabelEditor(Editor): """ _PyQt4LabelEditor is the PyQt implementation of a label editor. """ def create_control(self, parent): from pyface.qt import QtCore, QtGui w = QtGui.QLabel(parent) w.setAlignment(QtCore.Qt.AlignCenter) self._set_text(w) self._set_size(w) self._set_style(w) self.obj.on_trait_change(self._update_text, 'text') self.obj.on_trait_change(self._update_size, 'size') self.obj.on_trait_change(self._update_style, 'style') return w def _name_default(self): return self.obj.text def _update_text(self): self._set_text(self.control) def _set_text(self, w): w.setText("%s\n(%d points, %s)" % (self.obj.text, self.obj.size, self.obj.style)) def _update_size(self): self._set_size(self.control) def _set_size(self, w): f = w.font() f.setPointSize(self.obj.size) w.setFont(f) self._set_text(w) def _update_style(self): self._set_style(self.control) def _set_style(self, w): f = w.font() f.setBold(self.obj.style == 'bold') f.setItalic(self.obj.style == 'italic') w.setFont(f) self._set_text(w) class ExampleEditorManager(EditorManager): """ The ExampleEditorManager class creates the example editors. """ def create_editor(self, window, obj, kind): # Create the toolkit specific editor. tk_name = ETSConfig.toolkit if tk_name == 'wx': ed = _wxLabelEditor(window=window, obj=obj) elif tk_name == 'qt4': ed = _PyQt4LabelEditor(window=window, obj=obj) else: raise NotImplementedError, "unsupported toolkit: %s" % tk_name return ed #### EOF ###################################################################### apptools-4.5.0/examples/undo/example_undo_window.py0000644000076500000240000001212212317502217024044 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action, Group, MenuManager from pyface.workbench.api import WorkbenchWindow from pyface.workbench.action.api import MenuBarManager, ToolBarManager from traits.api import Instance from apptools.undo.action.api import CommandAction, RedoAction, UndoAction # Local imports. from example_editor_manager import ExampleEditorManager from commands import LabelIncrementSizeCommand, LabelDecrementSizeCommand, \ LabelNormalFontCommand, LabelBoldFontCommand, LabelItalicFontCommand class ExampleUndoWindow(WorkbenchWindow): """ The ExampleUndoWindow class is a workbench window that contains example editors that demonstrate the use of the undo framework. """ #### Private interface #################################################### # The action that exits the application. _exit_action = Instance(Action) # The File menu. _file_menu = Instance(MenuManager) # The Label menu. _label_menu = Instance(MenuManager) # The Undo menu. _undo_menu = Instance(MenuManager) ########################################################################### # Private interface. ########################################################################### #### Trait initialisers ################################################### def __file_menu_default(self): """ Trait initialiser. """ return MenuManager(self._exit_action, name="&File") def __undo_menu_default(self): """ Trait initialiser. """ undo_manager = self.workbench.undo_manager undo_action = UndoAction(undo_manager=undo_manager) redo_action = RedoAction(undo_manager=undo_manager) return MenuManager(undo_action, redo_action, name="&Undo") def __label_menu_default(self): """ Trait initialiser. """ size_group = Group(CommandAction(command=LabelIncrementSizeCommand), CommandAction(command=LabelDecrementSizeCommand)) normal = CommandAction(id='normal', command=LabelNormalFontCommand, style='radio', checked=True) bold = CommandAction(id='bold', command=LabelBoldFontCommand, style='radio') italic = CommandAction(id='italic', command=LabelItalicFontCommand, style='radio') style_group = Group(normal, bold, italic, id='style') return MenuManager(size_group, style_group, name="&Label") def __exit_action_default(self): """ Trait initialiser. """ return Action(name="E&xit", on_perform=self.workbench.exit) def _editor_manager_default(self): """ Trait initialiser. """ return ExampleEditorManager() def _menu_bar_manager_default(self): """ Trait initialiser. """ return MenuBarManager(self._file_menu, self._label_menu, self._undo_menu, window=self) def _tool_bar_manager_default(self): """ Trait initialiser. """ return ToolBarManager(self._exit_action, show_tool_names=False) def _active_editor_changed(self, old, new): """ Trait handler. """ # Tell the undo manager about the new command stack. if old is not None: old.command_stack.undo_manager.active_stack = None if new is not None: new.command_stack.undo_manager.active_stack = new.command_stack # Walk the label editor menu. for grp in self._label_menu.groups: for itm in grp.items: action = itm.action # Enable the action and set the command stack and data if there # is a new editor. if new is not None: action.enabled = True action.command_stack = new.command_stack action.data = new.obj # FIXME v3: We should just be able to check the menu option # corresponding to the style trait - but that doesn't seem # to uncheck the other options in the group. Even then the # first switch to another editor doesn't update the menus # (though subsequent ones do). if grp.id == 'style': action.checked = (action.data.style == action.id) else: action.enabled = False #### EOF ###################################################################### apptools-4.5.0/examples/undo/example.py0000644000076500000240000000464011640354733021444 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import logging # Enthought library imports. from pyface.api import GUI, YES from pyface.workbench.api import Workbench # Local imports. from example_undo_window import ExampleUndoWindow from model import Label # Log to stderr. logging.getLogger().addHandler(logging.StreamHandler()) logging.getLogger().setLevel(logging.DEBUG) class ExampleUndo(Workbench): """ The ExampleUndo class is a workbench that creates ExampleUndoWindow windows. """ #### 'Workbench' interface ################################################ # The factory (in this case simply a class) that is used to create # workbench windows. window_factory = ExampleUndoWindow ########################################################################### # Private interface. ########################################################################### def _exiting_changed(self, event): """ Called when the workbench is exiting. """ if self.active_window.confirm('Ok to exit?') != YES: event.veto = True return def main(argv): """ A simple example of using the the undo framework in a workbench. """ # Create the GUI. gui = GUI() # Create the workbench. workbench = ExampleUndo(state_location=gui.state_location) window = workbench.create_window(position=(300, 300), size=(400, 300)) window.open() # Create some objects to edit. label = Label(text="Label") label2 = Label(text="Label2") # Edit the objects. window.edit(label) window.edit(label2) # Start the GUI event loop. gui.start_event_loop() return if __name__ == '__main__': import sys; main(sys.argv) #### EOF ###################################################################### apptools-4.5.0/examples/undo/commands.py0000644000076500000240000001353511640354733021615 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Instance, Int, Unicode from apptools.undo.api import AbstractCommand # Local imports. from model import Label class LabelIncrementSizeCommand(AbstractCommand): """ The LabelIncrementSizeCommand class is a command that increases the size of a label's text. This command will merge multiple increments togther. """ #### 'ICommand' interface ################################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Unicode("&Increment size") #### Private interface #################################################### _incremented_by = Int ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): self.data.increment_size(1) self._incremented_by = 1 def merge(self, other): # We can merge if the other command is the same type (or a sub-type). if isinstance(other, type(self)): self._incremented_by += 1 merged = True else: merged = False return merged def redo(self): self.data.increment_size(self._incremented_by) def undo(self): self.data.decrement_size(self._incremented_by) class LabelDecrementSizeCommand(AbstractCommand): """ The LabelDecrementSizeCommand class is a command that decreases the size of a label's text. This command will merge multiple decrements togther. """ #### 'ICommand' interface ################################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Unicode("&Decrement size") #### Private interface #################################################### _decremented_by = Int ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): self.data.decrement_size(1) self._decremented_by = 1 def merge(self, other): # We can merge if the other command is the same type (or a sub-type). if isinstance(other, type(self)): self._decremented_by += 1 merged = True else: merged = False return merged def redo(self): self.data.decrement_size(self._decremented_by) def undo(self): self.data.increment_size(self._decremented_by) class LabelNormalFontCommand(AbstractCommand): """ The LabelNormalFontCommand class is a command that sets a normal font for a label's text. """ #### 'ICommand' interface ################################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Unicode("&Normal font") ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): # Save the old value. self._saved = self.data.style # Calling redo() is a convenient way to update the model now that the # old value is saved. self.redo() def redo(self): self.data.style = 'normal' def undo(self): self.data.style = self._saved class LabelBoldFontCommand(AbstractCommand): """ The LabelNormalFontCommand class is a command that sets a bold font for a label's text. """ #### 'ICommand' interface ############################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Unicode("&Bold font") ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): # Save the old value. self._saved = self.data.style # Calling redo() is a convenient way to update the model now that the # old value is saved. self.redo() def redo(self): self.data.style = 'bold' def undo(self): self.data.style = self._saved class LabelItalicFontCommand(AbstractCommand): """ The LabelNormalFontCommand class is a command that sets an italic font for a label's text. """ #### 'ICommand' interface ################################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Unicode("&Italic font") ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): # Save the old value. self._saved = self.data.style # Calling redo() is a convenient way to update the model now that the # old value is saved. self.redo() def redo(self): self.data.style = 'italic' def undo(self): self.data.style = self._saved apptools-4.5.0/examples/permissions/0000755000076500000240000000000013547652535021052 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/permissions/server/0000755000076500000240000000000013547652535022360 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/permissions/server/server.py0000755000076500000240000004253211640354733024240 0ustar mdickinsonstaff00000000000000#!/usr/bin/env python #------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import errno import logging import os import shelve import SimpleXMLRPCServer import socket import sys import time # Log to stderr. logger = logging.getLogger() logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.INFO) # The default IP address to listen on. DEFAULT_ADDR = socket.gethostbyname(socket.gethostname()) # The default port to listen on. DEFAULT_PORT = 3800 # The default data directory. DEFAULT_DATADIR = os.path.expanduser('~/.ets_perms_server') # The session timeout in seconds. SESSION_TIMEOUT = 90 * 60 class ServerImplementation(object): """This is a container for all the functions implemented by the server.""" def __init__(self, data_dir, insecure, local_user_db): """Initialise the object.""" self._data_dir = data_dir self._insecure = insecure self._local_user_db = local_user_db # Make sure we can call _close() at any time. self._keys = self._roles = self._assignments = self._blobs = \ self._users = None # Make sure the data directory exists. if not os.path.isdir(self._data_dir): os.mkdir(self._data_dir) # Load the data. self._keys = self._open_shelf('keys') self._roles = self._open_shelf('roles') self._assignments = self._open_shelf('assignments') self._blobs = self._open_shelf('blobs') if self._local_user_db: self._users = self._open_shelf('users') # Remove any expired session keys. now = time.time() keys = self._keys.keys() for k in keys: name, perm_ids, used_at = self._keys[k] if now - used_at >= SESSION_TIMEOUT: logger.info("Expiring session key for user %s" % name) del self._keys[k] self._sync(self._keys) def _close(self): """Close all the databases.""" if self._keys is not None: self._keys.close() self._keys = None if self._roles is not None: self._roles.close() self._roles = None if self._assignments is not None: self._assignments.close() self._assignments = None if self._blobs is not None: self._blobs.close() self._blobs = None if self._users is not None: if self._local_user_db: self._users.close() self._users = None def _open_shelf(self, name): """Open a shelf.""" logger.info("Loading %s" % name) fname = os.path.join(self._data_dir, name) try: return shelve.open(fname, writeback=True) except Exception, e: logger.error("Unable to open %s: %s" % (fname, e)) sys.exit(1) @staticmethod def _sync(shelf): try: shelf.sync() except Exception, e: logger.error("Unable to sync:" % e) Exception("An error occurred on the permissions server.") def _session_data(self, key): """Validate the session key and return the user name and list of permission ids.""" # Get the session details. try: session_name, perm_ids, used_at = self._keys[key] except KeyError: # Force the timeout test to fail. used_at = -SESSION_TIMEOUT # See if the session should be timed out. now = time.time() if now - used_at >= SESSION_TIMEOUT: try: del self._keys[key] except KeyError: pass self._sync(self._keys) raise Exception("Your session has timed out. Please login again.") # Update when the session was last used. self._keys[key] = session_name, perm_ids, now self._sync(self._keys) return session_name, perm_ids def _check_user(self, key, name): """Check that the session is current and the session user matches the given user and return the permission ids.""" session_name, perm_ids = self._session_data(key) if session_name != name: raise Exception("You do not have the appropriate authority.") return perm_ids def _check_authorisation(self, key, perm_id): """Check the user has the given permission.""" # Handle the easy cases first. if self._insecure or self.is_empty_policy(): return _, perm_ids = self._session_data(key) if perm_id not in perm_ids: raise Exception("You do not have the appropriate authority.") def _check_policy_authorisation(self, key): """Check that a policy management action is authorised.""" self._check_authorisation(key, 'ets.permissions.manage_policy') def _check_users_authorisation(self, key): """Check that a users management action is authorised.""" self._check_authorisation(key, 'ets.permissions.manage_users') def capabilities(self): """Return a list of capabilities that the implementation supports. The full list is 'user_password', 'user_add', 'user_modify', 'user_delete'. """ caps = ['user_password'] if self._local_user_db: caps.extend(['user_add', 'user_modify', 'user_delete']) return caps def add_user(self, name, description, password, key=None): """Add a new user.""" self._check_users_authorisation(key) if self._local_user_db: if self._users.has_key(name): raise Exception("The user \"%s\" already exists." % name) self._users[name] = (description, password) self._sync(self._users) else: raise Exception("Adding a user isn't supported.") # Return a non-None value. return True def authenticate_user(self, name, password): """Return the tuple of the user name, description, and blob if the user was successfully authenticated.""" if self._local_user_db: try: description, pword = self._users[name] except KeyError: raise Exception("The name or password is invalid.") if password != pword: raise Exception("The name or password is invalid.") else: # FIXME raise Exception("Authenticating a user isn't yet supported.") # Create the session key. The only reason for using a human readable # string is to make the test client easier to use. We only make # limited attempts at creating a unique key. for i in range(5): key = '' for ch in os.urandom(16): key += '%02x' % ord(ch) if not self._keys.has_key(key): break else: # Something is seriously wrong if we get here. msg = "Unable to create unique session key." logger.error(msg) raise Exception(msg) # Get the user's permissions. perm_ids = [] for r in self._assignments.get(name, []): _, perms = self._roles[r] perm_ids.extend(perms) # Save the session data. self._keys[key] = name, perm_ids, time.time() return key, name, description, self._blobs.get(name, {}) def matching_users(self, name, key=None): """Return the full name and description of all the users that match the given name.""" self._check_users_authorisation(key) if self._local_user_db: # Get any user that starts with the name. users = [(full_name, description) for full_name, (description, _) in self._users.items() if full_name.startswith(name)] users = sorted(users) else: # FIXME raise Exception("Searching for users isn't yet supported.") return users def modify_user(self, name, description, password, key=None): """Update the description and password for the given user.""" self._check_users_authorisation(key) if self._local_user_db: if not self._users.has_key(name): raise Exception("The user \"%s\" does not exist." % name) self._users[name] = (description, password) self._sync(self._users) else: raise Exception("Modifying a user isn't supported.") # Return a non-None value. return True def delete_user(self, name, key=None): """Delete a user.""" self._check_users_authorisation(key) if self._local_user_db: try: del self._users[name] except KeyError: raise UserStorageError("The user \"%s\" does not exist." % name) self._sync(self._users) else: raise Exception("Deleting a user isn't supported.") # Return a non-None value. return True def unauthenticate_user(self, key=None): """Unauthenticate the given user (ie. identified by the session key). """ if not self._local_user_db: # FIXME: LDAP may or may not need anything here. raise Exception("Unauthenticating a user isn't yet supported.") # Invalidate any session key: if key is not None: try: del self._keys[key] except KeyError: pass self._sync(self._keys) # Return a non-None value. return True def update_blob(self, name, blob, key=None): """Update the blob for the given user.""" self._check_user(key, name) self._blobs[name] = blob self._sync(self._blobs) # Return a non-None value. return True def update_password(self, name, password, key=None): """Update the password for the given user.""" self._check_user(key, name) if not self._local_user_db: try: description, _ = self._users[name] except KeyError: raise Exception("The user \"%s\" does not exist." % name) self._users[name] = (description, password) self._sync(self._users) else: # FIXME raise Exception("Updating a user password isn't yet supported.") # Return a non-None value. return True def add_role(self, name, description, perm_ids, key=None): """Add a new role.""" self._check_policy_authorisation(key) if self._roles.has_key(name): raise Exception("The role \"%s\" already exists." % name) self._roles[name] = (description, perm_ids) self._sync(self._roles) # Return a non-None value. return True def all_roles(self, key=None): """Return a list of all roles.""" self._check_policy_authorisation(key) return [(name, description) for name, (description, _) in self._roles.items()] def delete_role(self, name, key=None): """Delete a role.""" self._check_policy_authorisation(key) if not self._roles.has_key(name): raise Exception("The role \"%s\" does not exist." % name) del self._roles[name] # Remove the role from any users who have it. for user, role_names in self._assignments.items(): try: role_names.remove(name) except ValueError: continue self._assignments[user] = role_names self._sync(self._roles) self._sync(self._assignments) # Return a non-None value. return True def get_assignment(self, user_name, key=None): """Return the details of the assignment for the given user name.""" self._check_policy_authorisation(key) try: role_names = self._assignments[user_name] except KeyError: return '', [] return user_name, role_names def get_policy(self, name, key=None): """Return the details of the policy for the given user name.""" return name, self._check_user(key, name) def is_empty_policy(self): """Return True if there is no useful data.""" empty = (len(self._roles) == 0 or len(self._assignments) == 0) # Include the users as well if the database is local. if self._local_user_db and len(self._users) == 0: empty = True return empty def matching_roles(self, name, key=None): """Return the full name, description and permissions of all the roles that match the given name.""" self._check_policy_authorisation(key) # Return any role that starts with the name. roles = [(full_name, description, perm_ids) for full_name, (description, perm_ids) in self._roles.items() if full_name.startswith(name)] return sorted(roles) def modify_role(self, name, description, perm_ids, key=None): """Update an existing role.""" self._check_policy_authorisation(key) if not self._roles.has_key(name): raise Exception("The role \"%s\" does not exist." % name) self._roles[name] = (description, perm_ids) self._sync(self._roles) # Return a non-None value. return True def set_assignment(self, user_name, role_names, key=None): """Save the roles assigned to a user.""" self._check_policy_authorisation(key) if len(role_names) == 0: # Delete the user, but don't worry if there is no current # assignment. try: del self._assignments[user_name] except KeyError: pass else: self._assignments[user_name] = role_names self._sync(self._assignments) # Return a non-None value. return True class RPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer): """A thin wrapper around SimpleXMLRPCServer that handles its initialisation.""" def __init__(self, addr=DEFAULT_ADDR, port=DEFAULT_PORT, data_dir=DEFAULT_DATADIR, insecure=False, local_user_db=False): """Initialise the object.""" SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, (addr, port)) self._impl = ServerImplementation(data_dir, insecure, local_user_db) self.register_instance(self._impl) def server_close(self): """Reimplemented to tidy up the implementation.""" SimpleXMLRPCServer.SimpleXMLRPCServer.server_close(self) self._impl._close() if __name__ == '__main__': # Parse the command line. import optparse p = optparse.OptionParser(description="This is an XML-RPC server that " "provides user, role and permissions data to a user and policy " "manager that is part of the ETS Permissions Framework.") p.add_option('--data-dir', default=DEFAULT_DATADIR, dest='data_dir', metavar="DIR", help="the server's data directory [default: %s]" % DEFAULT_DATADIR) p.add_option('--insecure', action='store_true', default=False, dest='insecure', help="don't require a session key for data changes") p.add_option('--ip-address', default=DEFAULT_ADDR, dest='addr', help="the IP address to listen on [default: %s]" % DEFAULT_ADDR) p.add_option('--local-user-db', action='store_true', default=False, dest='local_user_db', help="use a local user database instead of an LDAP directory") p.add_option('--port', type='int', default=DEFAULT_PORT, dest='port', help="the TCP port to listen on [default: %d]" % DEFAULT_PORT) opts, args = p.parse_args() if args: p.error("unexpected additional arguments: %s" % " ".join(args)) # We need a decent RNG for session keys. if not opts.insecure: try: os.urandom(1) except AttributeError, NotImplementedError: sys.stderr.write("os.urandom() isn't implemented so the --insecure flag must be used\n") sys.exit(1) # FIXME: Add LDAP support. if not opts.local_user_db: sys.stderr.write("Until LDAP support is implemented use the --local-user-db flag\n") sys.exit(1) # Create and start the server. server = RPCServer(addr=opts.addr, port=opts.port, data_dir=opts.data_dir, insecure=opts.insecure, local_user_db=opts.local_user_db) if opts.insecure: logger.warn("Server starting in insecure mode") else: logger.info("Server starting") try: try: server.serve_forever() except KeyboardInterrupt: pass else: raise finally: server.server_close() logger.info("Server terminated") apptools-4.5.0/examples/permissions/server/setup.py0000644000076500000240000000073611640354733024067 0ustar mdickinsonstaff00000000000000# Major package imports. from setuptools import setup, find_packages setup( name = 'PermissionsServer', version = '1.0', author = 'Riverbank Computing Limited', author_email = 'info@riverbankcomputing.com', license = 'BSD', zip_safe = True, packages = find_packages(), include_package_data = True, namespace_packages = [ 'enthought' ], ) apptools-4.5.0/examples/permissions/server/enthought/0000755000076500000240000000000013547652535024365 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/permissions/server/enthought/__init__.py0000644000076500000240000000000011640354733026453 0ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/permissions/server/enthought/permissions/0000755000076500000240000000000013547652535026740 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/permissions/server/enthought/permissions/__init__.py0000644000076500000240000000046311640354733031043 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. #------------------------------------------------------------------------------ try: __import__('pkg_resources').declare_namespace(__name__) except: pass apptools-4.5.0/examples/permissions/server/enthought/permissions/external/0000755000076500000240000000000013547652535030562 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/permissions/server/enthought/permissions/external/user_storage.py0000644000076500000240000001362613422276145033635 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import errno import socket # Enthought library imports. from apptools.permissions.default.api import IUserStorage, UserStorageError from traits.api import HasTraits, provides, List, Str # Local imports. from proxy_server import ProxyServer @provides(IUserStorage) class UserStorage(HasTraits): """This implements a user database accessed via XML RPC.""" #### 'IUserStorage' interface ############################################# capabilities = List(Str) ########################################################################### # 'IUserStorage' interface. ########################################################################### def add_user(self, name, description, password): """Add a new user.""" try: ProxyServer.add_user(name, description, password, ProxyServer.key) except Exception, e: raise UserStorageError(ProxyServer.error(e)) def authenticate_user(self, name, password): """Return the tuple of the user name, description, and blob if the user was successfully authenticated.""" try: key, name, description, blob = ProxyServer.authenticate_user(name, password) # We don't save the cache because we should be about to read the # real permission ids and we do it then. ProxyServer.cache = description, blob, [] except Exception, e: # See if we couldn't connect to the server. if not isinstance(e, socket.error): raise UserStorageError(ProxyServer.error(e)) err, _ = e.args if err != errno.ECONNREFUSED: raise UserStorageError(ProxyServer.error(e)) try: ok = ProxyServer.read_cache() except Exception, e: raise UserStorageError(str(e)) if not ok: raise UserStorageError(ProxyServer.error(e)) # We are in "disconnect" mode. key = None description, blob, _ = ProxyServer.cache ProxyServer.key = key return name, description, blob def delete_user(self, name): """Delete a new user.""" try: ProxyServer.delete_user(name, ProxyServer.key) except Exception, e: raise UserStorageError(ProxyServer.error(e)) def is_empty(self): """See if the database is empty.""" # We leave it to the policy storage to answer this question. return False def matching_users(self, name): """Return the full name and description of all the users that match the given name.""" try: return ProxyServer.matching_users(name, ProxyServer.key) except Exception, e: raise UserStorageError(ProxyServer.error(e)) def modify_user(self, name, description, password): """Update the description and password for the given user.""" try: ProxyServer.modify_user(name, description, password, ProxyServer.key) except Exception, e: raise UserStorageError(ProxyServer.error(e)) def unauthenticate_user(self, user): """Unauthenticate the given user.""" if ProxyServer.key is None: ok = True else: try: ok = ProxyServer.unauthenticate_user(ProxyServer.key) except Exception, e: raise UserStorageError(ProxyServer.error(e)) if ok: ProxyServer.key = '' ProxyServer.cache = None return ok def update_blob(self, name, blob): """Update the blob for the given user.""" # Update the cache. description, _, perm_ids = ProxyServer.cache ProxyServer.cache = description, blob, perm_ids if ProxyServer.key is None: # Write the cache and tell the user about any errors. ProxyServer.write_cache() else: try: ProxyServer.update_blob(name, blob, ProxyServer.key) except Exception, e: raise UserStorageError(ProxyServer.error(e)) # Write the cache but ignore any errors. try: ProxyServer.write_cache() except: pass def update_password(self, name, password): """Update the password for the given user.""" # If the remote server disappeared after the capabilities were read but # before the user was authenticated then we could get here. if ProxyServer.key is None: raise UserStorageError("It is not possible to change password " "when disconnected from the permissions server.") try: ProxyServer.update_password(name, password, ProxyServer.key) except Exception, e: raise UserStorageError(ProxyServer.error(e)) ########################################################################### # Trait handlers. ########################################################################### def _capabilities_default(self): """Return the storage capabilities.""" try: caps = ProxyServer.capabilities() except: caps = [] return caps apptools-4.5.0/examples/permissions/server/enthought/permissions/external/policy_storage.py0000644000076500000240000000770413422276145034156 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from apptools.permissions.default.api import IPolicyStorage, \ PolicyStorageError from traits.api import HasTraits, provides # Local imports. from proxy_server import ProxyServer @provides(IPolicyStorage) class PolicyStorage(HasTraits): """This implements a policy database accessed via XML RPC.""" ########################################################################### # 'IPolicyStorage' interface. ########################################################################### def add_role(self, name, description, perm_ids): """Add a new role.""" try: ProxyServer.add_role(name, description, perm_ids, ProxyServer.key) except Exception, e: raise PolicyStorageError(ProxyServer.error(e)) def all_roles(self): """Return a list of all roles.""" try: return ProxyServer.all_roles(ProxyServer.key) except Exception, e: raise PolicyStorageError(ProxyServer.error(e)) def delete_role(self, name): """Delete a role.""" try: ProxyServer.delete_role(name, ProxyServer.key) except Exception, e: raise PolicyStorageError(ProxyServer.error(e)) def get_assignment(self, user_name): """Return the details of the assignment for the given user name.""" try: return ProxyServer.get_assignment(user_name, ProxyServer.key) except Exception, e: raise PolicyStorageError(ProxyServer.error(e)) def get_policy(self, name): """Return the details of the policy for the given user name.""" description, blob, perm_ids = ProxyServer.cache if ProxyServer.key is not None: try: name, perm_ids = ProxyServer.get_policy(name, ProxyServer.key) except Exception, e: raise PolicyStorageError(ProxyServer.error(e)) # Save the permissions ids in the persistent cache. ProxyServer.cache = description, blob, perm_ids try: ProxyServer.write_cache() except: pass return name, perm_ids def is_empty(self): """See if the database is empty.""" try: return ProxyServer.is_empty_policy() except Exception, e: raise PolicyStorageError(ProxyServer.error(e)) def matching_roles(self, name): """Return the full name, description and permissions of all the roles that match the given name.""" try: return ProxyServer.matching_roles(name, ProxyServer.key) except Exception, e: raise PolicyStorageError(ProxyServer.error(e)) def modify_role(self, name, description, perm_ids): """Update the description and permissions for the given role.""" try: ProxyServer.modify_role(name, description, perm_ids, ProxyServer.key) except Exception, e: raise PolicyStorageError(ProxyServer.error(e)) def set_assignment(self, user_name, role_names): """Save the roles assigned to a user.""" try: ProxyServer.set_assignment(user_name, role_names, ProxyServer.key) except Exception, e: raise PolicyStorageError(ProxyServer.error(e)) apptools-4.5.0/examples/permissions/server/enthought/permissions/external/__init__.py0000644000076500000240000000046311640354733032665 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. #------------------------------------------------------------------------------ try: __import__('pkg_resources').declare_namespace(__name__) except: pass apptools-4.5.0/examples/permissions/server/enthought/permissions/external/proxy_server.py0000644000076500000240000001056111640354733033675 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import cPickle as pickle import errno import os import socket import xmlrpclib # Enthought library imports. from traits.etsconfig.api import ETSConfig from pyface.api import confirm, YES # The default IP address to connect to. DEFAULT_ADDR = socket.gethostbyname(socket.gethostname()) # The default port to connect to. DEFAULT_PORT = 3800 class ProxyServer(xmlrpclib.ServerProxy): """This is a thin wrapper around xmlrpclib.ServerProxy that handles the server address and error reporting.""" # The name of the user data cache file. _cache_file = os.path.join(ETSConfig.application_home, 'ets_perms_user_cache') def __init__(self): """Initialise the object. The server address and TCP/IP port are taken from the ETS_PERMS_SERVER environment variable if set.""" self._server = os.environ.get('ETS_PERMS_SERVER', '%s:%d' % (DEFAULT_ADDR, DEFAULT_PORT)) xmlrpclib.ServerProxy.__init__(self, uri='http://%s' % self._server) # The session key. It is an empty string if the user is not # authenticated and None if the user is in "disconnected" mode. self.key = '' # The user data cache. This is a tuple of the user description, blob # and list of permission ids when the session key is not an empty # string. self.cache = None def write_cache(self): """Write the user cache to persistent storage.""" f = open(self._cache_file, 'w') pickle.dump(self.cache, f) f.close() def read_cache(self): """Read the user cache from persistent storage. Returns False if there was no cache to read.""" try: f = open(self._cache_file, 'r') try: if confirm(None, "It was not possible to connect to the " "permissions server. Instead you can use the settings " "used when you last logged in from this system. If " "you do this then any changes made to settings " "normally held on the permissions server will be lost " "when you next login successfully.\n\nDo you want to " "use the saved settings?") != YES: raise Exception("") try: self.cache = pickle.load(f) except: raise Exception("Unable to read %s." % self._cache_file) finally: f.close() except IOError, e: if e.errno == errno.ENOENT: # There is no cache file. return False raise Exception("Unable to open %s: %s." % (self._cache_file, e)) return True def error(self, e): """Return a user friendly string describing the given exception.""" if isinstance(e, socket.error): err, msg = e.args if err == errno.ECONNREFUSED: emsg = "Unable to connect to permissions server at %s." % self._server else: emsg = "Socket error to permissions server at %s." % self._server elif isinstance(e, xmlrpclib.Fault): # Extract the text of the exception. If we don't recognise the # format then display the lot. tail = e.faultString.find(':') if tail < 0: emsg = "Unexpected exception from permissions server at %s: %s\n" % (self._server, e.faultString) else: emsg = e.faultString[tail + 1:] else: # Let any existing error handling deal with it. raise e return emsg # Create a singleton. ProxyServer = ProxyServer() apptools-4.5.0/examples/permissions/server/test_client.py0000755000076500000240000001673611640354733025256 0ustar mdickinsonstaff00000000000000#!/usr/bin/env python #------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import errno import optparse import socket import sys import xmlrpclib # The default IP address to connect to. DEFAULT_ADDR = socket.gethostbyname(socket.gethostname()) # The default port to connect to. DEFAULT_PORT = 3800 def check_blob(opts): """Check the blob was specified with the --blob flag and return the value. """ if not opts.blob: raise Exception("the --blob flag to specify a blob") return opts.blob def check_name(opts): """Check the name was specified with the --name flag and return the value. """ if not opts.name: raise Exception("the --name flag to specify a name") return opts.name def check_password(opts): """Check the password was specified with the --password flag and return the value.""" if not opts.password: raise Exception("the --password flag to specify a password") return opts.password def add_role(opts): """Add a new role.""" return [check_name(opts), opts.description, opts.permissions, opts.key] def add_user(opts): """Add a new user.""" return [check_name(opts), opts.description, check_password(opts), opts.key] def all_roles(opts): """Return all roles.""" return [opts.key] def authenticate_user(opts): """Authenticate a user and return their details.""" return [check_name(opts), check_password(opts)] def delete_role(opts): """Delete a role.""" return [check_name(opts), opts.key] def delete_user(opts): """Delete a user.""" return [check_name(opts), opts.key] def capabilities(opts): """Get the capabilities of the server.""" return [] def matching_roles(opts): """Get the roles that match a name.""" return [opts.name, opts.key] def matching_users(opts): """Get the users that match a name.""" return [opts.name, opts.key] def get_assignment(opts): """Get the details of a user's assignment.""" return [check_name(opts), opts.key] def get_policy(opts): """Get the details of a user's policy.""" return [check_name(opts), opts.key] def is_empty_policy(opts): """Check if there is any policy data.""" return [] def set_assignment(opts): """Save the details of a user's assignment.""" return [check_name(opts), opts.roles, opts.key] def unauthenticate_user(opts): """Unauthenticate a user.""" return [opts.key] def modify_role(opts): """Modify an existing role.""" return [check_name(opts), opts.description, opts.permissions, opts.key] def modify_user(opts): """Modify an existing user.""" return [check_name(opts), opts.description, check_password(opts), opts.key] def update_blob(opts): """Update the blob for a user.""" return [check_name(opts), check_blob(opts)] def update_password(opts): """Update the password for a user.""" return [check_name(opts), check_password(opts)] # The list of actions that can be invoked from the command line. actions = [ add_role, add_user, all_roles, authenticate_user, capabilities, delete_role, delete_user, get_assignment, get_policy, is_empty_policy, matching_roles, matching_users, modify_role, modify_user, set_assignment, unauthenticate_user, update_blob, update_password, ] def store_list(option, opt_str, value, p): """An option parser callback that converts space separated words into a list of strings.""" setattr(p.values, option.dest, value.split(' ')) # Parse the command line. p = optparse.OptionParser(usage="%prog [options] action", description="This " "is a client used to test the XML-RPC server. The following actions " "are supported: %s" % ', '.join([a.func_name for a in actions])) p.add_option('--ip-address', default=DEFAULT_ADDR, dest='addr', help="the IP address to connect to [default: %s]" % DEFAULT_ADDR) p.add_option('-b', '--blob', dest='blob', help="a blob string (used by update_blob)") p.add_option('-d', '--description', default='', dest='description', help="a description (used by add_role, add_user, modify_role, " "modify_user)") p.add_option('-k', '--key', default='', dest='key', help="the session key returned by login (used by add_role, add_user, " "all_roles, delete_role, delete_user, get_assignment, " "get_policy, modify_role, modify_user, set_assignment, " "unauthenticate_user)") p.add_option('-n', '--name', default='', dest='name', help="a name (used by add_role, add_user, authenticate_user, " "delete_role, delete_user, get_assignment, get_policy, " "matching_roles, matching_users, modify_role, modify_user, " "set_assignment, update_blob, update_user)") p.add_option('--permissions', action='callback', callback=store_list, default=[], dest='permissions', type='string', help="a space separated list of permission names (used by add_role, " "modify_role)") p.add_option('-p', '--password', dest='password', help="a password (used by add_user, authenticate_user, modify_user " "update_password)") p.add_option('--port', type='int', default=DEFAULT_PORT, dest='port', help="the TCP port to connect to [default: %d]" % DEFAULT_PORT) p.add_option('--roles', action='callback', callback=store_list, default=[], dest='roles', type='string', help="a space separated list of role names (used by set_assignment)") p.add_option('-v', '--verbose', action='store_true', default=False, dest='verbose', help="display the progress of the RPC") opts, args = p.parse_args() if len(args) != 1: p.error("exactly one action must be given") for action in actions: if action.func_name == args[0]: break else: p.error("unknown action: %s" % args[0]) # Get the action's arguments. try: action_args = action(opts) except Exception, e: sys.stderr.write("The %s action requires %s\n" % (action.func_name, e)) sys.exit(2) # Create the proxy. proxy = xmlrpclib.ServerProxy(uri='http://%s:%d' % (opts.addr, opts.port), verbose=opts.verbose) try: result = getattr(proxy, action.func_name)(*action_args) except socket.error, e: err, msg = e.args if err == errno.ECONNREFUSED: sys.stderr.write("Unable to connect to permissions server at %s:%d\n" % (opts.addr, opts.port)) else: sys.stderr.write("socket error: %s\n" % msg) sys.exit(1) except xmlrpclib.Fault, e: # Extract the text of the exception. If we don't recognise the format then # display the lot. tail = e.faultString.find(':') if tail < 0: msg = e.faultString else: msg = e.faultString[tail + 1:] print "The call raised an exception: %s" % msg sys.exit(0) # Show the result. print "Result:", result apptools-4.5.0/examples/permissions/application/0000755000076500000240000000000013547652535023355 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/permissions/application/person.py0000644000076500000240000000227511640354733025232 0ustar mdickinsonstaff00000000000000"""A simple example of an object model.""" # Enthought library imports. from apptools.permissions.api import SecureHandler from traits.api import HasTraits, Int, Unicode from traitsui.api import Item, View # Local imports. from permissions import UpdatePersonAgePerm, PersonSalaryPerm class Person(HasTraits): """A simple example of an object model""" # Name. name = Unicode # Age in years. age = Int # Salary. salary = Int # Define the default view with permissions attached. age_perm = UpdatePersonAgePerm salary_perm = PersonSalaryPerm traits_view = View( Item(name='name'), Item(name='age', enabled_when='object.age_perm.granted'), Item(name='salary', visible_when='object.salary_perm.granted'), handler=SecureHandler) ########################################################################### # 'object' interface. ########################################################################### def __str__(self): """Return an informal string representation of the object.""" return self.name #### EOF ###################################################################### apptools-4.5.0/examples/permissions/application/workbench_window.py0000644000076500000240000001065211640354733027273 0ustar mdickinsonstaff00000000000000"""The workbench window for the permissions framework example.""" # Enthought library imports. from pyface.action.api import Action, Group, MenuManager from pyface.workbench.api import EditorManager, WorkbenchWindow from pyface.workbench.api import Perspective, PerspectiveItem from pyface.workbench.action.api import MenuBarManager from pyface.workbench.action.api import ToolBarManager from pyface.workbench.action.api import ViewMenuManager from apptools.permissions.api import SecureProxy from apptools.permissions.action.api import UserMenuManager from traits.api import Callable, HasTraits, List, Instance # Local imports. from permissions import NewPersonPerm from person import Person from toolkit_editor import ToolkitEditor class ExampleEditorManager(EditorManager): """An editor manager that supports the editor memento protocol.""" ####################################################################### # 'IEditorManager' interface. ####################################################################### def create_editor(self, window, obj, kind): """Reimplemented to create an editor appropriate to the object being edited. """ if isinstance(obj, HasTraits): # The superclass handles Traits objects. editor = super(ExampleEditorManager, self).create_editor(window, obj, kind) else: # Assume it is handled by a toolkit specific editor. editor = ToolkitEditor(window=window, obj=obj) return editor class ExampleWorkbenchWindow(WorkbenchWindow): """A simple example of using the workbench window.""" #### 'WorkbenchWindow' interface ########################################## # The available perspectives. perspectives = [ Perspective( name = 'Foo', contents = [ PerspectiveItem(id='Black', position='bottom'), PerspectiveItem(id='Debug', position='left') ] ), Perspective( name = 'Bar', contents = [ PerspectiveItem(id='Debug', position='left') ] ) ] #### Private interface #################################################### # The Exit action. _exit_action = Instance(Action) # The New Person action. _new_person_action = Instance(Action) ########################################################################### # 'ApplicationWindow' interface. ########################################################################### def _editor_manager_default(self): """ Trait initializer. Here we return the replacement editor manager. """ return ExampleEditorManager() def _menu_bar_manager_default(self): """Trait initializer.""" file_menu = MenuManager(self._new_person_action, self._exit_action, name='&File', id='FileMenu') view_menu = ViewMenuManager(name='&View', id='ViewMenu', window=self) user_menu = UserMenuManager(id='UserMenu', window=self) return MenuBarManager(file_menu, view_menu, user_menu, window=self) def _tool_bar_manager_default(self): """Trait initializer.""" return ToolBarManager(self._exit_action, show_tool_names=False) ########################################################################### # 'WorkbenchWindow' interface. ########################################################################### def _views_default(self): """Trait initializer.""" from secured_debug_view import SecuredDebugView return [SecuredDebugView(window=self)] ########################################################################### # Private interface. ########################################################################### def __exit_action_default(self): """Trait initializer.""" return Action(name='E&xit', on_perform=self.workbench.exit) def __new_person_action_default(self): """Trait initializer.""" # Create the action and secure it with the appropriate permission. act = Action(name='New Person', on_perform=self._new_person) act = SecureProxy(act, permissions=[NewPersonPerm]) return act def _new_person(self): """Create a new person.""" self.workbench.edit(Person(name='New', age=100)) #### EOF ###################################################################### apptools-4.5.0/examples/permissions/application/toolkit_editor.py0000644000076500000240000000352711640354733026760 0ustar mdickinsonstaff00000000000000"""An editor implemented by a toolkit specific widget. This demonstrates the ability to apply permissions to toolkit specific widgets. """ # Enthought library imports. from traits.etsconfig.api import ETSConfig from apptools.permissions.api import SecureProxy from pyface.workbench.api import Editor # Local imports. from permissions import EnableWidgetPerm class ToolkitEditor(Editor): """A workbench editor that displays string with a permission attached to the toolkit widget. """ def create_control(self, parent): """Create the toolkit specific control.""" tk = ETSConfig.toolkit if tk == 'wx': import wx control = wx.StaticText(parent, -1, self.obj, style=wx.ALIGN_CENTER) elif tk == 'qt4': from PyQt4 import QtCore, QtGui control = QtGui.QLabel(self.obj, parent) control.setAlignment(QtCore.Qt.AlignHCenter) else: raise ValueError, "unsupported toolkit: %s" % tk # Return the wrapped control. return SecureProxy(control, [EnableWidgetPerm]) def destroy_control(self): """Create the toolkit specific control.""" tk = ETSConfig.toolkit if tk == 'wx': self.control.Destroy() elif tk == 'qt4': self.control.setParent(None) def _name_default(self): """Show the toolkit in the editor name.""" return str(self) ########################################################################### # 'object' interface. ########################################################################### def __str__(self): """Return an informal string representation of the object.""" return ETSConfig.toolkit + " Editor" #### EOF ###################################################################### apptools-4.5.0/examples/permissions/application/secured_debug_view.py0000644000076500000240000000146211640354733027553 0ustar mdickinsonstaff00000000000000"""The secured DebugView for the permissions framework example.""" # Enthought library imports. from pyface.workbench.debug.api import DebugView from apptools.permissions.api import SecureProxy # Local imports. from permissions import DebugViewPerm class SecuredDebugView(DebugView): """A secured DebugView.""" ########################################################################### # 'IWorkbenchPart' interface. ########################################################################### def create_control(self, parent): """Reimplemented to secure the created control.""" control = DebugView.create_control(self, parent) return SecureProxy(control, permissions=[DebugViewPerm]) #### EOF ###################################################################### apptools-4.5.0/examples/permissions/application/example.py0000644000076500000240000000431311640354733025352 0ustar mdickinsonstaff00000000000000"""A simple example of using the permissions framework.""" # Standard library imports. import logging # Enthought library imports. from pyface.api import GUI, YES from pyface.workbench.api import Workbench # Local imports. from workbench_window import ExampleWorkbenchWindow from person import Person # Log to stderr. logger = logging.getLogger() logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG) class ExampleWorkbench(Workbench): """A simple example of using the workbench.""" #### 'Workbench' interface ################################################ # The factory (in this case simply a class) that is used to create # workbench windows. window_factory = ExampleWorkbenchWindow ########################################################################### # Private interface. ########################################################################### def _exiting_changed(self, event): """Called when the workbench is exiting.""" if self.active_window.confirm('Ok to exit?') != YES: event.veto = True def main(argv): """A simple example of using the workbench.""" # Create the GUI. gui = GUI() # Create the workbench. # # fixme: I wouldn't really want to specify the state location here. # Ideally this would be part of the GUI's as DOMs idea, and the state # location would be an attribute picked up from the DOM hierarchy. This # would also be the mechanism for doing 'confirm' etc... Let the request # bubble up the DOM until somebody handles it. workbench = ExampleWorkbench(state_location=gui.state_location) # Create the workbench window. window = workbench.create_window(position=(300, 300), size=(800, 600)) window.open() # This will cause a TraitsUI editor to be created. window.edit(Person(name='fred', age=42, salary=50000)) # This will cause a toolkit specific editor to be created. window.edit("This text is implemented by a toolkit specific widget.") # Start the GUI event loop. gui.start_event_loop() if __name__ == '__main__': import sys; main(sys.argv) #### EOF ###################################################################### apptools-4.5.0/examples/permissions/application/permissions.py0000644000076500000240000000157111640354733026275 0ustar mdickinsonstaff00000000000000"""The definitions of all the permissions used in the example.""" from apptools.permissions.api import Permission # Access to the Debug view. DebugViewPerm = Permission(id='ets.permissions.example.debug.view', description=u"Use the debug view") # Add a new person. NewPersonPerm = Permission(id='ets.permissions.example.person.new', description=u"Add a new person") # Update a person's age. UpdatePersonAgePerm = Permission(id='ets.permissions.example.person.age.update', description=u"Update a person's age") # View or update a person's salary. PersonSalaryPerm = Permission(id='ets.permissions.example.person.salary', description=u"View or update a person's salary") # Enable the example toolkit specific widget. EnableWidgetPerm = Permission(id='ets.permissions.example.widget', description=u"Enable the example toolkit specific widget") apptools-4.5.0/examples/naming/0000755000076500000240000000000013547652535017750 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/naming/images/0000755000076500000240000000000013547652535021215 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/naming/images/document.png0000644000076500000240000000051511640354733023531 0ustar mdickinsonstaff00000000000000PNG  IHDRabKGD pHYs  tIME8`&BIDAT8˭=n0H[!!l"A!QsL$^Xj,{|3m1 Z;騬8xt/5ǒ4MW(w=ιURW/P&AH৞"h@ ? qs1 31#~I0r ܊@2x/!W؀C@>ӿ>  q2N1H; h(#+Xُ[{`W]h3j?5ge`bv;5\ۇ p/wyW8D H~aՎ )@C?$a/ o~%H3_ 4~<?°7b? ? `Qǁ@ x3î? V z+~lalH!PHw :t @۫ D@L cĠ&D@?y(8^ `DC @L3(0IENDB`apptools-4.5.0/examples/naming/images/closed_folder.png0000644000076500000240000000102511640354733024514 0ustar mdickinsonstaff00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxbd```, ־Rc@15址 YH'^{ "(5h_d@~aÚU @@Pkn#. 2 ~+}  o{S,f+@A x?00D3 #aj'@[v + 5>Bbh H(j;`ÀBFpXq R@ 6( ]qD q? P%K-` H @l@,D@|,chvaPb 4r@2s^IENDB`apptools-4.5.0/examples/naming/images/image_LICENSE.txt0000644000076500000240000000067111640354733024175 0ustar mdickinsonstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: GV: Gael Varoquaux: BSD-like Unless stated in this file, icons are work of enthought, and are released under BSD-like license. Files and orginal authors: ---------------------------------------------------------------- closed_folder.png | GV document.png | GV open_folder.png | GV apptools-4.5.0/examples/naming/explorer.py0000644000076500000240000000605011640354733022152 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A naming system explorer. """ # Standard library imports. import os, sys # FIXME: The below won't work in an egg-based distribution. The real question # is why it was here in the first place. ## Put the Enthought library on the Python path. #sys.path.append(os.path.abspath(r'..\..\..')) # Enthought library imports. from apptools.naming.api import Binding, Context, ContextAdapterFactory from apptools.naming.api import PyFSContext from apptools.naming.adapter import * from apptools.naming.ui.explorer import Explorer from pyface.api import GUI from traits.api import TraitDict, TraitList from apptools.type_manager import TypeManager from traits.util.resource import find_resource # Application entry point. if __name__ == '__main__': # Create the GUI (this does NOT start the GUI event loop). gui = GUI() # Create a type manager to manage context adapters. type_manager = TypeManager() # Add some interesting context adapters. # # Trait dictionaries. type_manager.register_type_adapters( ContextAdapterFactory( adaptee_class=TraitDict, adapter_class=TraitDictContextAdapter, ), TraitDict ) # Trait lists. type_manager.register_type_adapters( ContextAdapterFactory( adaptee_class=TraitList, adapter_class=TraitListContextAdapter, ), TraitList ) # Python dictionaries. type_manager.register_type_adapters( ContextAdapterFactory( adaptee_class=dict, adapter_class=DictContextAdapter, ), dict ) # Python lists. type_manager.register_type_adapters( ContextAdapterFactory( adaptee_class=list, adapter_class=ListContextAdapter, ), list ) # Python objects. type_manager.register_type_adapters( InstanceContextAdapterFactory(), object ) # Get the path to the data directory data_path = os.path.join('examples','naming','data') full_path = find_resource('AppTools', data_path, alt_path='data', return_path=True) # Create the root context. root = PyFSContext(path=full_path) root.environment[Context.TYPE_MANAGER] = type_manager # Create and open the main window. window = Explorer(root=Binding(name='Root', obj=root)) window.open() # Start the GUI event loop. gui.start_event_loop() ##### EOF ##################################################################### apptools-4.5.0/examples/naming/simple.py0000644000076500000240000000251211640354733021602 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A simple naming example. """ # Standard library imports. import os, sys # Enthought library imports. from apptools.naming.api import Context, InitialContext # Application entry point. if __name__ == '__main__': # Set up the naming environment. klass_name = "apptools.naming.InitialContextFactory" klass_name = "apptools.naming.PyFSInitialContextFactory" environment = {Context.INITIAL_CONTEXT_FACTORY : klass_name} # Create an initial context. context = InitialContext(environment) context.path = os.getcwd() print 'Context', context, context.path print 'Names', context.list_names('') ##### EOF ##################################################################### apptools-4.5.0/examples/naming/data/0000755000076500000240000000000013547652535020661 5ustar mdickinsonstaff00000000000000apptools-4.5.0/examples/naming/data/foo.txt0000644000076500000240000000001511640354733022170 0ustar mdickinsonstaff00000000000000Hello World! apptools-4.5.0/image_LICENSE_OOo.txt0000644000076500000240000005761311640354733020425 0ustar mdickinsonstaff00000000000000GNU Lesser General Public License Table of Contents * GNU Lesser General Public License * Preamble * Terms and Conditions for Copying, Distribution and Modification GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 (The master copy of this license lives on the GNU website.) Copyright (C) 1991, 1999 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: * a) The modified work must itself be a software library. * b) You must cause the files modified to carry prominent notices * stating that you changed the files and the date of any change. * c) You must cause the whole of the work to be licensed at no charge * to all third parties under the terms of this License. * d) If a facility in the modified Library refers to a function or a * table of data to be supplied by an application program that uses * the facility, other than as an argument passed when the facility is * invoked, then you must make a good faith effort to ensure that, in * the event an application does not supply such function or table, * the facility still operates, and performs whatever part of its * purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: * a) Accompany the work with the complete corresponding * machine-readable source code for the Library including whatever * changes were used in the work (which must be distributed under * Sections 1 and 2 above); and, if the work is an executable linked * with the Library, with the complete machine-readable "work that * uses the Library", as object code and/or source code, so that the * user can modify the Library and then relink to produce a modified * executable containing the modified Library. (It is understood that * the user who changes the contents of definitions files in the * Library will not necessarily be able to recompile the application * to use the modified definitions.) * b) Use a suitable shared library mechanism for linking with the * Library. A suitable mechanism is one that (1) uses at run time a * copy of the library already present on the user's computer system, * rather than copying library functions into the executable, and (2) * will operate properly with a modified version of the library, if * the user installs one, as long as the modified version is * interface-compatible with the version that the work was made with. * c) Accompany the work with a written offer, valid for at least * three years, to give the same user the materials specified in * Subsection 6a, above, for a charge no more than the cost of * performing this distribution. * d) If distribution of the work is made by offering access to copy * from a designated place, offer equivalent access to copy the above * specified materials from the same place. * e) Verify that the user has already received a copy of these * materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: * a) Accompany the combined library with a copy of the same work * based on the Library, uncombined with any other library facilities. * This must be distributed under the terms of the Sections above. * b) Give prominent notice with the combined library of the fact that * part of it is a work based on the Library, and explaining where to * find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS apptools-4.5.0/setup.cfg0000644000076500000240000000004613547652535016502 0ustar mdickinsonstaff00000000000000[egg_info] tag_build = tag_date = 0 apptools-4.5.0/apptools.egg-info/0000755000076500000240000000000013547652535020214 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools.egg-info/PKG-INFO0000644000076500000240000001045413547652534021314 0ustar mdickinsonstaff00000000000000Metadata-Version: 2.1 Name: apptools Version: 4.5.0 Summary: application tools Home-page: https://docs.enthought.com/apptools Author: Enthought, Inc. Author-email: info@enthought.com Maintainer: ETS Developers Maintainer-email: enthought-dev@enthought.com License: BSD Download-URL: https://www.github.com/enthought/apptools Description: =========================== apptools: application tools =========================== .. image:: https://api.travis-ci.org/enthought/apptools.png?branch=master :target: https://travis-ci.org/enthought/apptools :alt: Build status .. image:: http://codecov.io/github/enthought/apptools/coverage.svg?branch=master :target: http://codecov.io/github/enthought/apptools?branch=master :alt: Coverage report Documentation: http://docs.enthought.com/apptools Source Code: http://www.github.com/enthought/apptools The apptools project includes a set of packages that Enthought has found useful in creating a number of applications. They implement functionality that is commonly needed by many applications - **apptools.appscripting**: Framework for scripting applications. - **apptools.help**: Provides a plugin for displaying documents and examples and running demos in Envisage Workbench applications. - **apptools.io**: Provides an abstraction for files and folders in a file system. - **apptools.logger**: Convenience functions for creating logging handlers - **apptools.naming**: Manages naming contexts, supporting non-string data types and scoped preferences - **apptools.permissions**: Supports limiting access to parts of an application unless the user is appropriately authorised (not full-blown security). - **apptools.persistence**: Supports pickling the state of a Python object to a dictionary, which can then be flexibly applied in restoring the state of the object. - **apptools.preferences**: Manages application preferences. - **apptools.selection**: Manages the communication between providers and listener of selected items in an application. - **apptools.scripting**: A framework for automatic recording of Python scripts. - **apptools.sweet_pickle**: Handles class-level versioning, to support loading of saved data that exist over several generations of internal class structures. - **apptools.template**: Supports creating templatizable object hierarchies. - **apptools.type_manager**: Manages type extensions, including factories to generate adapters, and hooks for methods and functions. - **apptools.undo**: Supports undoing and scripting application commands. Prerequisites ------------- All packages in apptools require: * `traits `_ The `apptools.preferences` package requires: * `configobj `_ Many of the packages provide optional user interfaces using Pyface and Traitsui. In additon, many of the packages are designed to work with the Envisage plug-in system, althought most can be used independently: * `envisage `_ * `pyface `_ * `traitsui `_ Platform: Windows Platform: Linux Platform: Mac OS-X Platform: Unix Platform: Solaris Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Description-Content-Type: text/x-rst apptools-4.5.0/apptools.egg-info/not-zip-safe0000644000076500000240000000000113547652534022441 0ustar mdickinsonstaff00000000000000 apptools-4.5.0/apptools.egg-info/SOURCES.txt0000644000076500000240000003572713547652534022115 0ustar mdickinsonstaff00000000000000CHANGES.txt LICENSE.txt MANIFEST.in README.rst TODO.txt image_LICENSE.txt image_LICENSE_OOo.txt setup.py apptools/__init__.py apptools/_version.py apptools.egg-info/PKG-INFO apptools.egg-info/SOURCES.txt apptools.egg-info/dependency_links.txt apptools.egg-info/not-zip-safe apptools.egg-info/requires.txt apptools.egg-info/top_level.txt apptools/appscripting/__init__.py apptools/appscripting/api.py apptools/appscripting/bind_event.py apptools/appscripting/i_bind_event.py apptools/appscripting/i_script_manager.py apptools/appscripting/lazy_namespace.py apptools/appscripting/package_globals.py apptools/appscripting/script_manager.py apptools/appscripting/scriptable.py apptools/appscripting/scriptable_type.py apptools/appscripting/action/__init__.py apptools/appscripting/action/api.py apptools/appscripting/action/start_recording_action.py apptools/appscripting/action/stop_recording_action.py apptools/help/__init__.py apptools/help/help_plugin/__init__.py apptools/help/help_plugin/api.py apptools/help/help_plugin/examples_preferences.py apptools/help/help_plugin/help_code.py apptools/help/help_plugin/help_doc.py apptools/help/help_plugin/help_plugin.py apptools/help/help_plugin/help_submenu_manager.py apptools/help/help_plugin/i_help_code.py apptools/help/help_plugin/i_help_doc.py apptools/help/help_plugin/preferences.ini apptools/help/help_plugin/preferences_pages.py apptools/help/help_plugin/action/__init__.py apptools/help/help_plugin/action/demo_action.py apptools/help/help_plugin/action/doc_action.py apptools/help/help_plugin/action/example_action.py apptools/help/help_plugin/action/load_url_action.py apptools/help/help_plugin/action/util.py apptools/help/help_plugin/action/images/document.png apptools/help/help_plugin/action/images/python_run.png apptools/io/__init__.py apptools/io/api.py apptools/io/file.py apptools/io/h5/__init__.py apptools/io/h5/dict_node.py apptools/io/h5/file.py apptools/io/h5/table_node.py apptools/io/h5/utils.py apptools/io/h5/tests/__init__.py apptools/io/h5/tests/test_dict_node.py apptools/io/h5/tests/test_file.py apptools/io/h5/tests/test_table_node.py apptools/io/h5/tests/utils.py apptools/io/tests/file_test_case.py apptools/io/tests/folder_test_case.py apptools/logger/__init__.py apptools/logger/api.py apptools/logger/custom_excepthook.py apptools/logger/filtering_handler.py apptools/logger/log_point.py apptools/logger/log_queue_handler.py apptools/logger/logger.py apptools/logger/null_handler.py apptools/logger/ring_buffer.py apptools/logger/util.py apptools/logger/agent/__init__.py apptools/logger/agent/attachments.py apptools/logger/agent/quality_agent_mailer.py apptools/logger/agent/quality_agent_view.py apptools/logger/agent/tests/__init__.py apptools/logger/agent/tests/test_attachments.py apptools/logger/plugin/__init__.py apptools/logger/plugin/logger_plugin.py apptools/logger/plugin/logger_preferences.py apptools/logger/plugin/logger_service.py apptools/logger/plugin/preferences.ini apptools/logger/plugin/tests/__init__.py apptools/logger/plugin/tests/test_logger_service.py apptools/logger/plugin/view/__init__.py apptools/logger/plugin/view/logger_preferences_page.py apptools/logger/plugin/view/logger_view.py apptools/logger/plugin/view/images/crit_error.png apptools/logger/plugin/view/images/debug.png apptools/logger/plugin/view/images/error.png apptools/logger/plugin/view/images/info.png apptools/logger/plugin/view/images/warning.png apptools/lru_cache/__init__.py apptools/lru_cache/lru_cache.py apptools/lru_cache/tests/__init__.py apptools/lru_cache/tests/test_lru_cache.py apptools/naming/__init__.py apptools/naming/address.py apptools/naming/api.py apptools/naming/binding.py apptools/naming/context.py apptools/naming/context_adapter.py apptools/naming/context_adapter_factory.py apptools/naming/dir_context.py apptools/naming/dynamic_context.py apptools/naming/exception.py apptools/naming/initial_context.py apptools/naming/initial_context_factory.py apptools/naming/naming_event.py apptools/naming/naming_manager.py apptools/naming/object_factory.py apptools/naming/object_serializer.py apptools/naming/py_context.py apptools/naming/py_object_factory.py apptools/naming/pyfs_context.py apptools/naming/pyfs_context_factory.py apptools/naming/pyfs_initial_context_factory.py apptools/naming/pyfs_object_factory.py apptools/naming/pyfs_state_factory.py apptools/naming/reference.py apptools/naming/referenceable.py apptools/naming/referenceable_state_factory.py apptools/naming/state_factory.py apptools/naming/unique_name.py apptools/naming/adapter/__init__.py apptools/naming/adapter/api.py apptools/naming/adapter/dict_context_adapter.py apptools/naming/adapter/dict_context_adapter_factory.py apptools/naming/adapter/instance_context_adapter.py apptools/naming/adapter/instance_context_adapter_factory.py apptools/naming/adapter/list_context_adapter.py apptools/naming/adapter/list_context_adapter_factory.py apptools/naming/adapter/trait_dict_context_adapter.py apptools/naming/adapter/trait_dict_context_adapter_factory.py apptools/naming/adapter/trait_list_context_adapter.py apptools/naming/adapter/trait_list_context_adapter_factory.py apptools/naming/adapter/tuple_context_adapter.py apptools/naming/adapter/tuple_context_adapter_factory.py apptools/naming/tests/context_test_case.py apptools/naming/tests/dir_context_test_case.py apptools/naming/tests/py_context_test_case.py apptools/naming/tests/pyfs_context_test_case.py apptools/naming/trait_defs/__init__.py apptools/naming/trait_defs/api.py apptools/naming/trait_defs/naming_traits.py apptools/naming/ui/__init__.py apptools/naming/ui/api.py apptools/naming/ui/context_monitor.py apptools/naming/ui/context_node_type.py apptools/naming/ui/explorer.py apptools/naming/ui/naming_node_manager.py apptools/naming/ui/naming_tree.py apptools/naming/ui/naming_tree_model.py apptools/naming/ui/object_node_type.py apptools/naming/ui/images/closed_folder.png apptools/naming/ui/images/document.png apptools/naming/ui/images/open_folder.png apptools/permissions/__init__.py apptools/permissions/adapter_base.py apptools/permissions/api.py apptools/permissions/i_policy_manager.py apptools/permissions/i_user.py apptools/permissions/i_user_manager.py apptools/permissions/package_globals.py apptools/permissions/permission.py apptools/permissions/permissions_manager.py apptools/permissions/secure_proxy.py apptools/permissions/action/__init__.py apptools/permissions/action/api.py apptools/permissions/action/login_action.py apptools/permissions/action/logout_action.py apptools/permissions/action/user_menu_manager.py apptools/permissions/adapters/__init__.py apptools/permissions/adapters/pyface_action.py apptools/permissions/adapters/qt4_widget.py apptools/permissions/adapters/wx_window.py apptools/permissions/default/__init__.py apptools/permissions/default/api.py apptools/permissions/default/i_policy_storage.py apptools/permissions/default/i_user_database.py apptools/permissions/default/i_user_storage.py apptools/permissions/default/persistent.py apptools/permissions/default/policy_data.py apptools/permissions/default/policy_manager.py apptools/permissions/default/policy_storage.py apptools/permissions/default/role_assignment.py apptools/permissions/default/role_definition.py apptools/permissions/default/select_role.py apptools/permissions/default/select_user.py apptools/permissions/default/user_database.py apptools/permissions/default/user_manager.py apptools/permissions/default/user_storage.py apptools/persistence/__init__.py apptools/persistence/file_path.py apptools/persistence/project_loader.py apptools/persistence/spickle.py apptools/persistence/state_pickler.py apptools/persistence/updater.py apptools/persistence/version_registry.py apptools/persistence/versioned_unpickler.py apptools/persistence/tests/__init__.py apptools/persistence/tests/test_file_path.py apptools/persistence/tests/test_spickle.py apptools/persistence/tests/test_state_pickler.py apptools/persistence/tests/test_version_registry.py apptools/preferences/__init__.py apptools/preferences/api.py apptools/preferences/i_preferences.py apptools/preferences/package_globals.py apptools/preferences/preference_binding.py apptools/preferences/preferences.py apptools/preferences/preferences_helper.py apptools/preferences/scoped_preferences.py apptools/preferences/tests/__init__.py apptools/preferences/tests/example.ini apptools/preferences/tests/preference_binding_test_case.py apptools/preferences/tests/preferences_helper_test_case.py apptools/preferences/tests/preferences_test_case.py apptools/preferences/tests/py_config_example.ini apptools/preferences/tests/py_config_example_2.ini apptools/preferences/tests/py_config_file.py apptools/preferences/tests/py_config_file_test_case.py apptools/preferences/tests/scoped_preferences_test_case.py apptools/preferences/ui/__init__.py apptools/preferences/ui/api.py apptools/preferences/ui/i_preferences_page.py apptools/preferences/ui/preferences_manager.py apptools/preferences/ui/preferences_node.py apptools/preferences/ui/preferences_page.py apptools/preferences/ui/tree_item.py apptools/preferences/ui/widget_editor.py apptools/scripting/__init__.py apptools/scripting/api.py apptools/scripting/package_globals.py apptools/scripting/recordable.py apptools/scripting/recorder.py apptools/scripting/recorder_with_ui.py apptools/scripting/util.py apptools/scripting/tests/test_recorder.py apptools/selection/__init__.py apptools/selection/api.py apptools/selection/errors.py apptools/selection/i_selection.py apptools/selection/i_selection_provider.py apptools/selection/list_selection.py apptools/selection/selection_service.py apptools/selection/tests/__init__.py apptools/selection/tests/test_list_selection.py apptools/selection/tests/test_selection_service.py apptools/sweet_pickle/__init__.py apptools/sweet_pickle/global_registry.py apptools/sweet_pickle/placeholder.py apptools/sweet_pickle/updater.py apptools/sweet_pickle/versioned_unpickler.py apptools/sweet_pickle/tests/__init__.py apptools/sweet_pickle/tests/class_mapping_classes.py apptools/sweet_pickle/tests/class_mapping_test_case.py apptools/sweet_pickle/tests/global_registry_test_case.py apptools/sweet_pickle/tests/state_function_classes.py apptools/sweet_pickle/tests/state_function_test_case.py apptools/sweet_pickle/tests/two_stage_unpickler_test_case.py apptools/sweet_pickle/tests/updater_test_case.py apptools/template/__init__.py apptools/template/api.py apptools/template/imutable_template.py apptools/template/itemplate.py apptools/template/itemplate_choice.py apptools/template/itemplate_data_context.py apptools/template/itemplate_data_name_item.py apptools/template/itemplate_data_source.py apptools/template/mutable_template.py apptools/template/template_choice.py apptools/template/template_data_name.py apptools/template/template_data_names.py apptools/template/template_impl.py apptools/template/template_traits.py apptools/template/impl/__init__.py apptools/template/impl/any_context_data_name_item.py apptools/template/impl/any_data_name_item.py apptools/template/impl/api.py apptools/template/impl/context_data_name_item.py apptools/template/impl/helper.py apptools/template/impl/i_context_adapter.py apptools/template/impl/template_data_context.py apptools/template/impl/template_data_source.py apptools/template/impl/value_data_name_item.py apptools/template/impl/value_nd_data_name_item.py apptools/template/test/__init__.py apptools/template/test/enable_editor.py apptools/template/test/scatter_plot.py apptools/template/test/scatter_plot_2.py apptools/template/test/scatter_plot_nm.py apptools/template/test/template_view.py apptools/type_manager/__init__.py apptools/type_manager/abstract_adapter_factory.py apptools/type_manager/abstract_factory.py apptools/type_manager/abstract_type_system.py apptools/type_manager/adaptable.py apptools/type_manager/adapter.py apptools/type_manager/adapter_factory.py apptools/type_manager/adapter_manager.py apptools/type_manager/api.py apptools/type_manager/factory.py apptools/type_manager/hook.py apptools/type_manager/python_type_system.py apptools/type_manager/type_manager.py apptools/type_manager/util.py apptools/type_manager/tests/type_manager_test_case.py apptools/type_registry/__init__.py apptools/type_registry/api.py apptools/type_registry/type_registry.py apptools/type_registry/tests/__init__.py apptools/type_registry/tests/dummies.py apptools/type_registry/tests/test_lazy_registry.py apptools/type_registry/tests/test_type_registry.py apptools/undo/__init__.py apptools/undo/abstract_command.py apptools/undo/api.py apptools/undo/command_stack.py apptools/undo/i_command.py apptools/undo/i_command_stack.py apptools/undo/i_undo_manager.py apptools/undo/undo_manager.py apptools/undo/action/__init__.py apptools/undo/action/abstract_command_stack_action.py apptools/undo/action/api.py apptools/undo/action/command_action.py apptools/undo/action/redo_action.py apptools/undo/action/undo_action.py apptools/undo/tests/__init__.py apptools/undo/tests/test_command_stack.py apptools/undo/tests/testing_commands.py docs/Makefile docs/source/conf.py docs/source/index.rst docs/source/_static/default.css docs/source/_static/e-logo-rev.png docs/source/_static/et.ico docs/source/appscripting/Introduction.rst docs/source/permissions/ApplicationAPI.rst docs/source/permissions/DefaultPolicyManagerDataAPI.rst docs/source/permissions/DefaultUserManagerDataAPI.rst docs/source/permissions/Introduction.rst docs/source/preferences/Preferences.rst docs/source/preferences/PreferencesInEnvisage.rst docs/source/scripting/introduction.rst docs/source/selection/selection.rst docs/source/undo/Introduction.rst examples/appscripting/actions.py examples/appscripting/example.py examples/appscripting/example_editor_manager.py examples/appscripting/example_script_window.py examples/appscripting/model.py examples/naming/explorer.py examples/naming/simple.py examples/naming/data/foo.txt examples/naming/images/closed_folder.png examples/naming/images/document.png examples/naming/images/image_LICENSE.txt examples/naming/images/open_folder.png examples/permissions/application/example.py examples/permissions/application/permissions.py examples/permissions/application/person.py examples/permissions/application/secured_debug_view.py examples/permissions/application/toolkit_editor.py examples/permissions/application/workbench_window.py examples/permissions/server/server.py examples/permissions/server/setup.py examples/permissions/server/test_client.py examples/permissions/server/enthought/__init__.py examples/permissions/server/enthought/permissions/__init__.py examples/permissions/server/enthought/permissions/external/__init__.py examples/permissions/server/enthought/permissions/external/policy_storage.py examples/permissions/server/enthought/permissions/external/proxy_server.py examples/permissions/server/enthought/permissions/external/user_storage.py examples/preferences/example.ini examples/preferences/preferences_manager.py examples/undo/commands.py examples/undo/example.py examples/undo/example_editor_manager.py examples/undo/example_undo_window.py examples/undo/model.py integrationtests/persistence/test_persistence.py integrationtests/persistence/update1.py integrationtests/persistence/update2.py integrationtests/persistence/update3.pyapptools-4.5.0/apptools.egg-info/requires.txt0000644000076500000240000000002713547652534022612 0ustar mdickinsonstaff00000000000000configobj six traitsui apptools-4.5.0/apptools.egg-info/top_level.txt0000644000076500000240000000001113547652534022735 0ustar mdickinsonstaff00000000000000apptools apptools-4.5.0/apptools.egg-info/dependency_links.txt0000644000076500000240000000000113547652534024261 0ustar mdickinsonstaff00000000000000 apptools-4.5.0/README.rst0000644000076500000240000000537013422276145016344 0ustar mdickinsonstaff00000000000000=========================== apptools: application tools =========================== .. image:: https://api.travis-ci.org/enthought/apptools.png?branch=master :target: https://travis-ci.org/enthought/apptools :alt: Build status .. image:: http://codecov.io/github/enthought/apptools/coverage.svg?branch=master :target: http://codecov.io/github/enthought/apptools?branch=master :alt: Coverage report Documentation: http://docs.enthought.com/apptools Source Code: http://www.github.com/enthought/apptools The apptools project includes a set of packages that Enthought has found useful in creating a number of applications. They implement functionality that is commonly needed by many applications - **apptools.appscripting**: Framework for scripting applications. - **apptools.help**: Provides a plugin for displaying documents and examples and running demos in Envisage Workbench applications. - **apptools.io**: Provides an abstraction for files and folders in a file system. - **apptools.logger**: Convenience functions for creating logging handlers - **apptools.naming**: Manages naming contexts, supporting non-string data types and scoped preferences - **apptools.permissions**: Supports limiting access to parts of an application unless the user is appropriately authorised (not full-blown security). - **apptools.persistence**: Supports pickling the state of a Python object to a dictionary, which can then be flexibly applied in restoring the state of the object. - **apptools.preferences**: Manages application preferences. - **apptools.selection**: Manages the communication between providers and listener of selected items in an application. - **apptools.scripting**: A framework for automatic recording of Python scripts. - **apptools.sweet_pickle**: Handles class-level versioning, to support loading of saved data that exist over several generations of internal class structures. - **apptools.template**: Supports creating templatizable object hierarchies. - **apptools.type_manager**: Manages type extensions, including factories to generate adapters, and hooks for methods and functions. - **apptools.undo**: Supports undoing and scripting application commands. Prerequisites ------------- All packages in apptools require: * `traits `_ The `apptools.preferences` package requires: * `configobj `_ Many of the packages provide optional user interfaces using Pyface and Traitsui. In additon, many of the packages are designed to work with the Envisage plug-in system, althought most can be used independently: * `envisage `_ * `pyface `_ * `traitsui `_ apptools-4.5.0/LICENSE.txt0000644000076500000240000000312011640354733016467 0ustar mdickinsonstaff00000000000000This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. Copyright (c) 2006, Enthought, Inc. 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 Enthought, Inc. 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. apptools-4.5.0/apptools/0000755000076500000240000000000013547652535016522 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/preferences/0000755000076500000240000000000013547652535021023 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/preferences/preferences_helper.py0000644000076500000240000001371313506341351025224 0ustar mdickinsonstaff00000000000000""" An object that can be initialized from a preferences node. """ # Standard library imports. import logging # Enthought library imports. from traits.api import HasTraits, Instance, Str, Unicode # Local imports. from .i_preferences import IPreferences from .package_globals import get_default_preferences # Logging. logger = logging.getLogger(__name__) class PreferencesHelper(HasTraits): """ An object that can be initialized from a preferences node. """ #### 'PreferencesHelper' interface ######################################## # The preferences node used by the helper. If this trait is not set then # the package-global default preferences node is used. # # fixme: This introduces a 'sneaky' global reference to the preferences # node! preferences = Instance(IPreferences) # The path to the preference node that contains the preferences that we # use to initialize instances of this class. preferences_path = Str ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Constructor. """ super(PreferencesHelper, self).__init__(**traits) # Initialize the object's traits from the preferences node. if self.preferences: self._initialize(self.preferences) return ########################################################################### # Private interface. ########################################################################### #### Trait initializers ################################################### def _preferences_default(self): """ Trait initializer. """ # If no specific preferences node is set then we use the package-wide # global node. return get_default_preferences() #### Trait change handlers ################################################ def _anytrait_changed(self, trait_name, old, new): """ Static trait change handler. """ # If we were the one that set the trait (because the underlying # preferences node changed) then do nothing. if self.preferences and self._is_preference_trait(trait_name): self.preferences.set('%s.%s' % (self._get_path(), trait_name), new) return def _preferences_changed(self, old, new): """ Static trait change handler. """ # Stop listening to the old preferences node. if old is not None: old.remove_preferences_listener( self._preferences_changed_listener, self._get_path() ) if new is not None: # Initialize with the new preferences node (this also adds a # listener for preferences being changed in the new node). self._initialize(new, notify=True) return #### Other observer pattern listeners ##################################### def _preferences_changed_listener(self, node, key, old, new): """ Listener called when a preference value is changed. """ if key in self.trait_names(): setattr(self, key, self._get_value(key, new)) return #### Methods ############################################################## def _get_path(self): """ Return the path to our preferences node. """ if len(self.preferences_path) > 0: path = self.preferences_path else: path = getattr(self, 'PREFERENCES_PATH', None) if path is None: raise SystemError('no preferences path, %s' % self) else: logger.warn('DEPRECATED: use "preferences_path" %s' % self) return path def _get_value(self, trait_name, value): """ Get the actual value to set. This method makes sure that any required work is done to convert the preference value from a string. Str traits or those with the metadata 'is_str=True' will just be passed the string itself. """ trait = self.trait(trait_name) handler = trait.handler # If the trait type is 'Str' or Unicode then we just take the raw value. if isinstance(handler, (Str, Unicode)) or trait.is_str: pass # Otherwise, we eval it! else: try: value = eval(value) # If the eval fails then there is probably a syntax error, but # we will let the handler validation throw the exception. except: pass if handler.validate is not None: # Any traits have a validator of None. validated = handler.validate(self, trait_name, value) else: validated = value return validated def _initialize(self, preferences, notify=False): """ Initialize the object's traits from the preferences node. """ path = self._get_path() keys = preferences.keys(path) traits_to_set = {} for trait_name in self.trait_names(): if trait_name in keys: key = '%s.%s' % (path, trait_name) value = self._get_value(trait_name, preferences.get(key)) traits_to_set[trait_name] = value self.trait_set(trait_change_notify=notify, **traits_to_set) # Listen for changes to the node's preferences. preferences.add_preferences_listener( self._preferences_changed_listener, path ) return # fixme: Pretty much duplicated in 'PreferencesPage' (except for the # class name of course!). def _is_preference_trait(self, trait_name): """ Return True if a trait represents a preference value. """ if trait_name.startswith('_') or trait_name.endswith('_') \ or trait_name in PreferencesHelper.class_traits(): return False return True #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/ui/0000755000076500000240000000000013547652535021440 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/preferences/ui/widget_editor.py0000644000076500000240000000573313506341351024635 0ustar mdickinsonstaff00000000000000""" An instance editor that allows total control over widget creation. """ # Enthought library imports. from traits.etsconfig.api import ETSConfig from traits.api import Any from traitsui.api import EditorFactory # fixme: We need to import the 'Editor' class from the appropriate toolkit. exec('from traitsui.%s.editor import Editor' % ETSConfig.toolkit) class _WidgetEditor(Editor): """ An instance editor that allows total control over widget creation. """ #### '_WidgetEditor' interface ############################################ # The toolkit-specific parent of the editor. parent = Any ########################################################################### # '_WidgetEditor' interface. ########################################################################### def init(self, parent): """ Initialize the editor. """ self.parent = parent # fixme: What if there are no pages?!? page = self.object.pages[0] # Create the editor's control. self.control = page.create_control(parent) # Listen for the page being changed. self.object.on_trait_change(self._on_page_changed, 'selected_page') return def dispose(self): """ Dispose of the editor. """ page = self.object.selected_page page.destroy_control() return def update_editor(self): """ Update the editor. """ pass ########################################################################### # Private interface. ########################################################################### def _on_page_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ if old is not None: old.destroy_control() if new is not None: self.control = new.create_control(self.parent) return class WidgetEditor(EditorFactory): """ A factory widget editors. """ ########################################################################### # 'object' interface. ########################################################################### def __call__ (self, *args, **traits): """ Call the object. """ return self.trait_set(**traits) ########################################################################### # 'EditorFactory' interface. ########################################################################### def simple_editor(self, ui, object, name, description, parent): """ Create a simple editor. """ editor = _WidgetEditor( parent, factory = self, ui = ui, object = object, name = name, description = description ) return editor custom_editor = simple_editor text_editor = simple_editor readonly_editor = simple_editor #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/ui/tree_item.py0000644000076500000240000001010111640354733023747 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A generic base-class for items in a tree data structure. An example:- root = TreeItem(data='Root') fruit = TreeItem(data='Fruit') fruit.append(TreeItem(data='Apple', allows_children=False)) fruit.append(TreeItem(data='Orange', allows_children=False)) fruit.append(TreeItem(data='Pear', allows_children=False)) root.append(fruit) veg = TreeItem(data='Veg') veg.append(TreeItem(data='Carrot', allows_children=False)) veg.append(TreeItem(data='Cauliflower', allows_children=False)) veg.append(TreeItem(data='Sprout', allows_children=False)) root.append(veg) """ # Enthought library imports. from traits.api import Any, Bool, HasTraits, Instance, List, Property class TreeItem(HasTraits): """ A generic base-class for items in a tree data structure. """ #### 'TreeItem' interface ################################################# # Does this item allow children? allows_children = Bool(True) # The item's children. children = List(Instance('TreeItem')) # Arbitrary data associated with the item. data = Any # Does the item have any children? has_children = Property(Bool) # The item's parent. parent = Instance('TreeItem') ########################################################################### # 'object' interface. ########################################################################### def __str__(self): """ Returns the informal string representation of the object. """ if self.data is None: s = '' else: s = str(self.data) return s ########################################################################### # 'TreeItem' interface. ########################################################################### #### Properties ########################################################### # has_children def _get_has_children(self): """ True iff the item has children. """ return len(self.children) != 0 #### Methods ############################################################## def append(self, child): """ Appends a child to this item. This removes the child from its current parent (if it has one). """ return self.insert(len(self.children), child) def insert(self, index, child): """ Inserts a child into this item at the specified index. This removes the child from its current parent (if it has one). """ if child.parent is not None: child.parent.remove(child) child.parent = self self.children.insert(index, child) return child def remove(self, child): """ Removes a child from this item. """ child.parent = None self.children.remove(child) return child def insert_before(self, before, child): """ Inserts a child into this item before the specified item. This removes the child from its current parent (if it has one). """ index = self.children.index(before) self.insert(index, child) return (index, child) def insert_after(self, after, child): """ Inserts a child into this item after the specified item. This removes the child from its current parent (if it has one). """ index = self.children.index(after) self.insert(index + 1, child) return (index, child) #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/ui/preferences_page.py0000644000076500000240000001015613547637361025312 0ustar mdickinsonstaff00000000000000""" A page in a preferences dialog. """ # Enthought library imports. from apptools.preferences.api import PreferencesHelper from traits.api import Any, Dict, Str, provides # Local imports. from .i_preferences_page import IPreferencesPage @provides(IPreferencesPage) class PreferencesPage(PreferencesHelper): """ A page in a preferences dialog. """ #### 'IPreferencesPage' interface ######################################### # The page's category (e.g. 'General/Appearance'). The empty string means # that this is a top-level page. category = Str # DEPRECATED: The help_id was never fully implemented, and it's been # over two years (now 4/2009). The original goal was for the the Help # button to automatically appear and connect to a help page with a # help_id. Not removing the trait right now to avoid breaking code # that may be checking for this. # # Use PreferencesManager.show_help and trait show_help metadata instead. help_id = Str # The page name (this is what is shown in the preferences dialog. name = Str #### Private interface #################################################### # The traits UI that represents the page. _ui = Any # A dictionary containing the traits that have been changed since the # last call to 'apply'. _changed = Dict ########################################################################### # 'IPreferencesPage' interface. ########################################################################### def apply(self): """ Apply the page's preferences. """ path = self._get_path() for trait_name, value in self._changed.items(): if self._is_preference_trait(trait_name): self.preferences.set('%s.%s' % (path, trait_name), value) self._changed.clear() return # fixme: We would like to be able to have the following API so that # developers are not forced into using traits UI for their preferences # pages, but at the moment I can't work out how to do it! ## def create_control(self, parent): ## """ Create the toolkit-specific control that represents the page. """ ## if self._ui is None: ## self._ui = self.edit_traits(parent=parent, kind='subpanel') ## return self._ui.control ## def destroy_control(self): ## """ Destroy the toolkit-specific control that represents the page. """ ## if self._ui is not None: ## self._ui.dispose() ## self._ui = None ## return ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ def _anytrait_changed(self, trait_name, old, new): """ Static trait change handler. This is an important override! In the base-class when a trait is changed the preferences node is updated too. Here, we stop that from happening and just make a note of what changes have been made. The preferences node gets updated when the 'apply' method is called. """ # If the trait was a list or dict '_items' trait then just treat it as # if the entire list or dict was changed. if trait_name.endswith('_items'): trait_name = trait_name[:-6] if self._is_preference_trait(trait_name): self._changed[trait_name] = getattr(self, trait_name) elif self._is_preference_trait(trait_name): self._changed[trait_name] = new return # fixme: Pretty much duplicated in 'PreferencesHelper' (except for the # class name of course!). def _is_preference_trait(self, trait_name): """ Return True if a trait represents a preference value. """ if trait_name.startswith('_') or trait_name.endswith('_') \ or trait_name in PreferencesPage.class_traits(): return False return True #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/ui/__init__.py0000644000076500000240000000010411640354733023533 0ustar mdickinsonstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. apptools-4.5.0/apptools/preferences/ui/preferences_manager.py0000644000076500000240000002253513547637361026014 0ustar mdickinsonstaff00000000000000""" The preferences manager. """ # Enthought library imports. from traits.api import HasTraits, Instance, List, Property, \ Any, Bool, Dict from traitsui.api import Handler, HSplit, Item, TreeEditor from traitsui.api import TreeNode, View, HTMLEditor from traitsui.menu import Action # Local imports. from .preferences_node import PreferencesNode from .preferences_page import PreferencesPage # fixme: This is part of the attempt to allow developers to use non-Traits UI # preferences pages. It doesn't work yet! ##from widget_editor import WidgetEditor # A tree editor for preferences nodes. tree_editor = TreeEditor( nodes = [ TreeNode( node_for = [PreferencesNode], auto_open = False, children = 'children', label = 'name', rename = False, copy = False, delete = False, insert = False, menu = None, ), ], editable = False, hide_root = True, selected = 'selected_node', show_icons = False ) class PreferencesHelpWindow(HasTraits): """ Container class to present a view with string info. """ def traits_view(self): """ Default view to show for this class. """ args = [] kw_args = {'title' : 'Preferences Page Help', 'buttons' : ['OK'], 'width' : 800, 'height' : 800, 'resizable' : True, 'id' : 'apptools.preferences.ui.preferences_manager.help'} to_show = {} for name, trait_obj in self.traits().items(): if name != 'trait_added' and name != 'trait_modified': to_show[name] = trait_obj.help for name in to_show: args.append(Item(name, style='readonly', editor=HTMLEditor() )) view = View(*args, **kw_args) return view class PreferencesManagerHandler(Handler): """ The traits UI handler for the preferences manager. """ model = Instance(HasTraits) ########################################################################### # 'Handler' interface. ########################################################################### def apply(self, info): """ Handle the **Apply** button being clicked. """ info.object.apply() return def init(self, info): """ Initialize the controls of a user interface. """ # Select the first node in the tree (if there is one). self._select_first_node(info) return super(PreferencesManagerHandler, self).init(info) def close(self, info, is_ok): """ Close a dialog-based user interface. """ if is_ok: info.object.apply() return super(PreferencesManagerHandler, self).close(info, is_ok) def preferences_help(self, info): """ Custom preferences help panel. The Traits help doesn't work.""" current_page = self.model.selected_page to_show = {} for trait_name, trait_obj in current_page.traits().items(): if hasattr(trait_obj, 'show_help') and trait_obj.show_help: to_show[trait_name] = trait_obj.help help_obj = PreferencesHelpWindow(**to_show) help_obj.edit_traits(kind='livemodal') return ########################################################################### # Private interface. ########################################################################### def _select_first_node(self, info): """ Select the first node in the tree (if there is one). """ root = info.object.root if len(root.children) > 0: node = root.children[0] info.object.selected_page = node.page return class PreferencesManager(HasTraits): """ The preferences manager. """ # All of the preferences pages known to the manager. pages = List(PreferencesPage) # The root of the preferences node tree. root = Property(Instance(PreferencesNode)) # The preferences node currently selected in the tree. selected_node = Instance(PreferencesNode) # The preferences associated with the currently selected preferences node. selected_page = Instance(PreferencesPage) # Should the custom Info button be shown? If this is True, then an # Info button is shown that pops up a trait view with an HTML entry # for each trait of the *selected_page* with the metadata 'show_help' # set to True. show_help = Bool(False) # Should the Apply button be shown? show_apply = Bool(False) #### Traits UI views ###################################################### def traits_view(self): """ Default traits view for this class. """ help_action = Action(name = 'Info', action = 'preferences_help') buttons = ['OK', 'Cancel'] if self.show_apply: buttons = ['Apply'] + buttons if self.show_help: buttons = [help_action] + buttons # A tree editor for preferences nodes. tree_editor = TreeEditor( nodes = [ TreeNode( node_for = [PreferencesNode], auto_open = False, children = 'children', label = 'name', rename = False, copy = False, delete = False, insert = False, menu = None, ), ], on_select = self._selection_changed, editable = False, hide_root = True, selected = 'selected_node', show_icons = False ) view = View( HSplit( Item( name = 'root', editor = tree_editor, show_label = False, width = 250, ), Item( name = 'selected_page', #editor = WidgetEditor(), show_label = False, width = 450, style = 'custom', ), ), buttons = buttons, handler = PreferencesManagerHandler(model=self), resizable = True, title = 'Preferences', width = .3, height = .3, kind = 'modal' ) self.selected_page = self.pages[0] return view ########################################################################### # 'PreferencesManager' interface. ########################################################################### #### Trait properties ##################################################### def _get_root(self): """ Property getter. """ # Sort the pages by the length of their category path. This makes it # easy for us to create the preference hierarchy as we know that all of # a node's ancestors will have already been created. def sort_key(a): # We have the guard because if the category is the empty string # then split will still return a list containing one item (and not # the empty list). if len(a.category) == 0: len_a = 0 else: len_a = len(a.category.split('/')) return len_a self.pages.sort(key=sort_key) # Create a corresponding preference node hierarchy (the root of the # hierachy is NOT displayed in the preference dialog). # # fixme: Currently we have to create a dummy page for the root node # event though the root does not get shown in the tree! root_page = PreferencesPage(name='Root', preferences_path='root') root = PreferencesNode(page=root_page) for page in self.pages: # Get the page's parent node. parent = self._get_parent(root, page) # Add a child node representing the page. parent.append(PreferencesNode(page=page)) return root #### Trait change handlers ################################################ def _selection_changed(self, new_selection): self.selected_node = new_selection def _selected_node_changed(self, new): """ Static trait change handler. """ if self.selected_node: self.selected_page = self.selected_node.page return #### Methods ############################################################## def apply(self): """ Apply all changes made in the manager. """ for page in self.pages: page.apply() return ########################################################################### # Private interface. ########################################################################### def _get_parent(self, root, page): """ Return the page's parent preference node. """ parent = root if len(page.category) > 0: components = page.category.split('/') for component in components: parent = parent.lookup(component) return parent #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/ui/api.py0000644000076500000240000000022413547637361022561 0ustar mdickinsonstaff00000000000000from .i_preferences_page import IPreferencesPage from .preferences_manager import PreferencesManager from .preferences_page import PreferencesPage apptools-4.5.0/apptools/preferences/ui/preferences_node.py0000644000076500000240000000611613547637361025324 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for a node in a preferences dialog. """ from __future__ import print_function # Enthought library imports. from traits.api import Delegate, Instance, Str # Local imports. from .i_preferences_page import IPreferencesPage from .tree_item import TreeItem class PreferencesNode(TreeItem): """ Abstract base class for a node in a preferences dialog. A preferences node has a name and an image which are used to represent the node in a preferences dialog (usually in the form of a tree). """ #### 'PreferenceNode' interface ########################################### # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = Delegate('page') # The page name (this is what is shown in the preferences dialog. name = Delegate('page') # The page that we are a node for. page = Instance(IPreferencesPage) ########################################################################### # 'object' interface. ########################################################################### def __str__(self): """ Returns the string representation of the item. """ if self.page is None: s = 'root' else: s = self.page.name return s __repr__ = __str__ ########################################################################### # 'PreferencesNode' interface. ########################################################################### def create_page(self, parent): """ Creates the preference page for this node. """ return self.page.create_control(parent) def lookup(self, name): """ Returns the child of this node with the specified Id. Returns None if no such child exists. """ for node in self.children: if node.name == name: break else: node = None return node ########################################################################### # Debugging interface. ########################################################################### def dump(self, indent=''): """ Pretty-print the node to stdout. """ print(indent, 'Node', str(self)) for child in self.children: child.dump(indent+' ') return #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/ui/i_preferences_page.py0000644000076500000240000000227611640354733025615 0ustar mdickinsonstaff00000000000000""" The interface for pages in a preferences dialog. """ # Enthought library imports. from traits.api import Interface, Str class IPreferencesPage(Interface): """ The interface for pages in a preferences dialog. """ # The page's category (e.g. 'General/Appearence'). The empty string means # that this is a top-level page. category = Str # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = Str # The page name (this is what is shown in the preferences dialog). name = Str def apply(self): """ Apply the page's preferences. """ # fixme: We would like to be able to have the following API so that # developers are not forced into using traits UI for their preferences # pages, but at the moment I can't work out how to do it! ## def create_control(self, parent): ## """ Create the toolkit-specific control that represents the page. """ ## def destroy_control(self, parent): ## """ Destroy the toolkit-specific control that represents the page. """ #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/preference_binding.py0000644000076500000240000001350413547637361025210 0ustar mdickinsonstaff00000000000000""" A binding between a trait on an object and a preference value. """ # Enthought library imports. from traits.api import Any, HasTraits, Instance, Str, Undefined from traits.api import Unicode # Third-party librart imports. import six # Local imports. from .i_preferences import IPreferences from .package_globals import get_default_preferences class PreferenceBinding(HasTraits): """ A binding between a trait on an object and a preference value. """ #### 'PreferenceBinding' interface ######################################## # The object that we are binding the preference to. obj = Any # The preferences node used by the binding. If this trait is not set then # the package-global default preferences node is used (and if that is not # set then the binding won't work ;^) preferences = Instance(IPreferences) # The path to the preference value. preference_path = Str # The name of the trait that we are binding the preference to. trait_name = Str ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Constructor. """ super(PreferenceBinding, self).__init__(**traits) # Initialize the object's trait from the preference value. self._set_trait(notify=False) # Wire-up trait change handlers etc. self._initialize() return ########################################################################### # 'PreferenceBinding' interface. ########################################################################### #### Trait initializers ################################################### def _preferences_default(self): """ Trait initializer. """ return get_default_preferences() ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ def _on_trait_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ self.preferences.set(self.preference_path, new) return #### Other observer pattern listeners ##################################### def _preferences_listener(self, node, key, old, new): """ Listener called when a preference value is changed. """ components = self.preference_path.split('.') if key == components[-1]: self._set_trait() return #### Methods ############################################################## # fixme: This method is mostly duplicated in 'PreferencesHelper' (the only # difference is the line that gets the handler). def _get_value(self, trait_name, value): """ Get the actual value to set. This method makes sure that any required work is done to convert the preference value from a string. """ handler = self.obj.trait(trait_name).handler # If the trait type is 'Str' then we just take the raw value. if type(handler) is Str: pass # If the trait type is 'Unicode' then we convert the raw value. elif type(handler) is Unicode: value = six.text_type(value) # Otherwise, we eval it! else: try: value = eval(value) # If the eval fails then there is probably a syntax error, but # we will let the handler validation throw the exception. except: pass return handler.validate(self, trait_name, value) def _initialize(self): """ Wire-up trait change handlers etc. """ # Listen for the object's trait being changed. self.obj.on_trait_change(self._on_trait_changed, self.trait_name) # Listen for the preference value being changed. components = self.preference_path.split('.') node = '.'.join(components[:-1]) self.preferences.add_preferences_listener( self._preferences_listener, node ) return def _set_trait(self, notify=True): """ Set the object's trait to the value of the preference. """ value = self.preferences.get(self.preference_path, Undefined) if value is not Undefined: trait_value = self._get_value(self.trait_name, value) traits = {self.trait_name : trait_value} self.obj.trait_set(trait_change_notify=notify, **traits) return # Factory function for creating bindings. def bind_preference(obj, trait_name, preference_path, preferences=None): """ Create a new preference binding. """ # This may seem a bit wierd, but we manually build up a dictionary of # the traits that need to be set at the time the 'PreferenceBinding' # instance is created. # # This is because we only want to set the 'preferences' trait iff one # is explicitly specified. If we passed it in with the default argument # value of 'None' then it counts as 'setting' the trait which prevents # the binding instance from defaulting to the package-global preferences. # Also, if we try to set the 'preferences' trait *after* construction time # then it is too late as the binding initialization is done in the # constructor (we could of course split that out, which may be the 'right' # way to do it ;^). traits = { 'obj' : obj, 'trait_name' : trait_name, 'preference_path' : preference_path } if preferences is not None: traits['preferences'] = preferences return PreferenceBinding(**traits) #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/i_preferences.py0000644000076500000240000001107711640354733024203 0ustar mdickinsonstaff00000000000000""" The interface for a node in a preferences hierarchy. """ # Enthought library imports. from traits.api import Instance, Interface, Str class IPreferences(Interface): """ The interface for a node in a preferences hierarchy. """ # The absolute path to this node from the root node (the empty string if # this node *is* the root node). path = Str # The parent node (None if this node *is* the root node). parent = Instance('IPreferences') # The name of the node relative to its parent (the empty string if this # node *is* the root node). name = Str #### Methods where 'path' refers to a preference #### def get(self, path, default=None, inherit=False): """ Get the value of the preference at the specified path. If no value exists for the path (or any part of the path does not exist) then return the default value. Preference values are *always* returned as strings. e.g:: preferences.set('acme.ui.bgcolor', 'blue') preferences.get('acme.ui.bgcolor') -> 'blue' preferences.set('acme.ui.width', 100) preferences.get('acme.ui.width') -> '100' preferences.set('acme.ui.visible', True) preferences.get('acme.ui.visible') -> 'True' If 'inherit' is True then we allow 'inherited' preference values. e.g. If we are looking up:: 'acme.ui.widget.bgcolor' and it does not exist then we will also try:: 'acme.ui.bgcolor' 'acme.bgcolor' 'bgcolor' Raise a 'ValueError' exception if the path is the empty string. """ def remove(self, path): """ Remove the preference at the specified path. Does nothing if no value exists for the path (or any part of the path does not exist. Raise a 'ValueError' exception if the path is the empty string. e.g.:: preferences.remove('acme.ui.bgcolor') """ def set(self, path, value): """ Set the value of the preference at the specified path. Any missing nodes are created automatically. Primitive Python types can be set, but preferences are *always* stored and returned as strings. e.g:: preferences.set('acme.ui.bgcolor', 'blue') preferences.get('acme.ui.bgcolor') -> 'blue' preferences.set('acme.ui.width', 100) preferences.get('acme.ui.width') -> '100' preferences.set('acme.ui.visible', True) preferences.get('acme.ui.visible') -> 'True' Raise a 'ValueError' exception if the path is the empty string. """ #### Methods where 'path' refers to a node #### def clear(self, path=''): """ Remove all preference from the node at the specified path. If the path is the empty string (the default) then remove the preferences in *this* node. This does not affect any of the node's children. e.g. To clear the preferences out of a node directly:: preferences.clear() Or to clear the preferences of a node at a given path:: preferences.clear('acme.ui') """ def keys(self, path=''): """ Return the preference keys of the node at the specified path. If the path is the empty string (the default) then return the preference keys of *this* node. e.g:: keys = preferences.keys('acme.ui') """ def node(self, path=''): """ Return the node at the specified path. If the path is the empty string (the default) then return *this* node. Any missing nodes are created automatically. e.g:: node = preferences.node('acme.ui') bgcolor = node.get('bgcolor') """ def node_exists(self, path=''): """ Return True if the node at the specified path exists If the path is the empty string (the default) then return True. e.g:: exists = preferences.exists('acme.ui') """ def node_names(self, path=''): """ Return the names of the children of the node at the specified path. If the path is the empty string (the default) then return the names of the children of *this* node. e.g:: names = preferences.node_names('acme.ui') """ #### Persistence methods #### def flush(self): """ Force any changes in the node to the backing store. This includes any changes to the node's descendants. """ #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/scoped_preferences.py0000644000076500000240000003402313422276145025224 0ustar mdickinsonstaff00000000000000""" A preferences node that adds the notion of preferences scopes. """ from __future__ import print_function # Standard library imports. from os.path import join # Enthought library imports. from traits.etsconfig.api import ETSConfig from traits.api import List, Str, Undefined # Local imports. from .i_preferences import IPreferences from .preferences import Preferences class ScopedPreferences(Preferences): """ A preferences node that adds the notion of preferences scopes. Scopes provide a way to access preferences in a precedence order, usually depending on where they came from, for example from the command-line, or set by the user in a preferences file, or the defaults (set by the developer). By default, this class provides two scopes - 'application' which is persistent and 'default' which is not. Path names passed to 'ScopedPreferences' nodes can be either:: a) a preference path as used in a standard 'Preferences' node, e.g:: 'acme.widget.bgcolor'. In this case the operation either takes place in the primary scope (for operations such as 'set' etc), or on all scopes in precedence order (for operations such as 'get' etc). or b) a preference path that refers to a specific scope e.g:: 'default/acme.widget.bgcolor' In this case the operation takes place *only* in the specified scope. There is one drawback to this scheme. If you want to access a scope node itself via the 'clear', 'keys', 'node', 'node_exists' or 'node_names' methods then you have to append a trailing '/' to the path. Without that, the node would try to perform the operation in the primary scope. e.g. To get the names of the children of the 'application' scope, use:: scoped.node_names('application/') If you did this:: scoped.node_names('application') Then the node would get the primary scope and try to find its child node called 'application'. Of course you can just get the scope via:: application_scope = scoped.get_scope('application') and then call whatever methods you like on it - which is definitely more intentional and is highly recommended:: application_scope.node_names() """ #### 'ScopedPreferences' interface ######################################## # The file that the application scope preferences are stored in. # # Defaults to:- # # os.path.join(ETSConfig.application_home, 'preferences.ini') application_preferences_filename = Str # The scopes (in the order that they should be searched when looking up # preferences). # # By default, this class provides two scopes - 'application' which is # persistent and 'default' which is not. scopes = List(IPreferences) # The name of the 'primary' scope. # # This is the scope that operations take place in if no scope is specified # in a given path (for the 'get' operation, if no scope is specified the # operation takes place in *all* scopes in order of precedence). If this is # the empty string (the default) then the primary scope is the first scope # in the 'scopes' list. primary_scope_name = Str ########################################################################### # 'IPreferences' protocol. ########################################################################### #### Methods where 'path' refers to a preference #### def get(self, path, default=None, inherit=False): """ Get the value of the preference at the specified path. """ if len(path) == 0: raise ValueError('empty path') # If the path contains a specific scope then lookup the preference in # just that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, try each scope in turn (i.e. in order of precedence). else: nodes = self.scopes # Try all nodes first (without inheritance even if specified). value = self._get(path, Undefined, nodes, inherit=False) if value is Undefined: if inherit: value = self._get(path, default, nodes, inherit=True) else: value = default return value def remove(self, path): """ Remove the preference at the specified path. """ if len(path) == 0: raise ValueError('empty path') # If the path contains a specific scope then remove the preference from # just that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, remove the preference from the primary scope. else: node = self._get_primary_scope() node.remove(path) return def set(self, path, value): """ Set the value of the preference at the specified path. """ if len(path) == 0: raise ValueError('empty path') # If the path contains a specific scope then set the value in that # scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, set the value in the primary scope. else: node = self._get_primary_scope() node.set(path, value) return #### Methods where 'path' refers to a node #### def clear(self, path=''): """ Remove all preference from the node at the specified path. """ # If the path contains a specific scope then remove the preferences # from a node in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, remove the preferences from a node in the primary scope. else: node = self._get_primary_scope() return node.clear(path) def keys(self, path=''): """ Return the preference keys of the node at the specified path. """ # If the path contains a specific scope then get the keys of the node # in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, merge the keys of the node in all scopes. else: nodes = self.scopes keys = set() for node in nodes: keys.update(node.node(path).keys()) return list(keys) def node(self, path=''): """ Return the node at the specified path. """ if len(path) == 0: node = self else: # If the path contains a specific scope then we get the node that # scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, get the node from the primary scope. else: node = self._get_primary_scope() node = node.node(path) return node def node_exists(self, path=''): """ Return True if the node at the specified path exists. """ # If the path contains a specific scope then look for the node in that # scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, look for the node in the primary scope. else: node = self._get_primary_scope() return node.node_exists(path) def node_names(self, path=''): """ Return the names of the children of the node at the specified path. """ # If the path contains a specific scope then get the names of the # children of the node in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, merge the names of the children of the node in all scopes. else: nodes = self.scopes names = set() for node in nodes: names.update(node.node(path).node_names()) return list(names) ########################################################################### # 'Preferences' protocol. ########################################################################### #### Listener methods #### def add_preferences_listener(self, listener, path=''): """ Add a listener for changes to a node's preferences. """ # If the path contains a specific scope then add a preferences listener # to the node in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, add a preferences listener to the node in all scopes. else: nodes = self.scopes for node in nodes: node.add_preferences_listener(listener, path) return def remove_preferences_listener(self, listener, path=''): """ Remove a listener for changes to a node's preferences. """ # If the path contains a specific scope then remove a preferences # listener from the node in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, remove a preferences listener from the node in all scopes. else: nodes = self.scopes for node in nodes: node.remove_preferences_listener(listener, path) return #### Persistence methods #### def load(self, file_or_filename=None): """ Load preferences from a file. This loads the preferences into the primary scope. fixme: I'm not sure it is worth providing an implentation here. I think it would be better to encourage people to explicitly reference a particular scope. """ if file_or_filename is None and len(self.filename) > 0: file_or_filename = self.filename node = self._get_primary_scope() node.load(file_or_filename) return def save(self, file_or_filename=None): """ Save the node's preferences to a file. This asks each scope in turn to save its preferences. If a file or filename is specified then it is only passed to the primary scope. """ if file_or_filename is None and len(self.filename) > 0: file_or_filename = self.filename self._get_primary_scope().save(file_or_filename) for scope in self.scopes: if scope is not self._get_primary_scope(): scope.save() return ########################################################################### # 'ScopedPreferences' protocol. ########################################################################### def _application_preferences_filename_default(self): """ Trait initializer. """ return join(ETSConfig.application_home, 'preferences.ini') # fixme: In hindsight, I don't think this class should have provided # default scopes. This should have been an 'abstract' class that could # be subclassed by classes providing specific scopes. def _scopes_default(self): """ Trait initializer. """ scopes = [ Preferences( name = 'application', filename = self.application_preferences_filename ), Preferences(name='default') ] return scopes def get_scope(self, scope_name): """ Return the scope with the specified name. Return None if no such scope exists. """ for scope in self.scopes: if scope_name == scope.name: break else: scope = None return scope ########################################################################### # Private protocol. ########################################################################### def _get(self, path, default, nodes, inherit): """ Get a preference from a list of nodes. """ for node in nodes: value = node.get(path, Undefined, inherit) if value is not Undefined: break else: value = default return value def _get_scope(self, scope_name): """ Return the scope with the specified name. Raise a 'ValueError' is no such scope exists. """ scope = self.get_scope(scope_name) if scope is None: raise ValueError('no such scope %s' % scope_name) return scope def _get_primary_scope(self): """ Return the primary scope. By default, this is the first scope. """ if len(self.primary_scope_name) > 0: scope = self._get_scope(self.primary_scope_name) else: scope = self.scopes[0] return scope def _path_contains_scope(self, path): """ Return True if the path contains a scope component. """ return '/' in path def _parse_path(self, path): """ 'Parse' the path into two parts, the scope name and the rest! """ components = path.split('/') return components[0], '/'.join(components[1:]) ########################################################################### # Debugging interface. ########################################################################### def dump(self, indent=''): """ Dump the preferences hierarchy to stdout. """ if indent == '': print() print(indent, 'Node(%s)' % self.name, self._preferences) indent += ' ' for child in self.scopes: child.dump(indent) return #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/tests/0000755000076500000240000000000013547652535022165 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/preferences/tests/example.ini0000644000076500000240000000031013422276152024300 0ustar mdickinsonstaff00000000000000[acme.ui] bgcolor = blue width = 50 ratio = 1.0 visible = True description = 'acme ui' offsets = "[1, 2, 3, 4]" names = "['joe', 'fred', 'jane']" [acme.ui.splash_screen] image = splash fgcolor = red apptools-4.5.0/apptools/preferences/tests/preferences_test_case.py0000644000076500000240000004535413547637361027105 0ustar mdickinsonstaff00000000000000""" Tests for preferences nodes. """ # Standard library imports. import os, tempfile, unittest from os.path import join # Major package imports. from pkg_resources import resource_filename # Enthought library imports. from apptools.preferences.api import Preferences from traits.api import HasTraits, Int, Str # This module's package. PKG = 'apptools.preferences.tests' class PreferencesTestCase(unittest.TestCase): """ Tests for preferences nodes. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.preferences = Preferences() # The filename of the example preferences file. self.example = resource_filename(PKG, 'example.ini') # A temporary directory that can safely be written to. self.tmpdir = tempfile.mkdtemp() def tearDown(self): """ Called immediately after each test method has been called. """ # Remove the temporary directory. os.removedirs(self.tmpdir) return ########################################################################### # Tests. ########################################################################### def test_package_global_default_preferences(self): """ package global default preferences """ from apptools.preferences.api import get_default_preferences from apptools.preferences.api import set_default_preferences set_default_preferences(self.preferences) self.assertEqual(self.preferences, get_default_preferences()) return def test_get_and_set_str(self): """ get and set str """ p = self.preferences # Set a string preference. p.set('acme.ui.bgcolor', 'blue') self.assertEqual('blue', p.get('acme.ui.bgcolor')) return def test_get_and_set_int(self): """ get and set int """ p = self.preferences # Note that we can pass an actual 'int' to 'set', but the preference # manager *always* returns preference values as strings. p.set('acme.ui.width', 50) self.assertEqual('50', p.get('acme.ui.width')) return def test_get_and_set_float(self): """ get and set float """ p = self.preferences # Note that we can pass an actual 'flaot' to 'set', but the preference # manager *always* returns preference values as strings. p.set('acme.ui.ratio', 1.0) self.assertEqual('1.0', p.get('acme.ui.ratio')) return def test_get_and_set_bool(self): """ get and set bool """ p = self.preferences # Note that we can pass an actual 'bool' to 'set', but the preference # manager *always* returns preference values as strings. p.set('acme.ui.visible', True) self.assertEqual('True', p.get('acme.ui.visible')) return def test_get_and_set_list_of_str(self): """ get and set list of str """ p = self.preferences # Note that we can pass an actual 'int' to 'set', but the preference # manager *always* returns preference values as strings. p.set('acme.ui.names', ['fred', 'wilma', 'barney']) self.assertEqual("['fred', 'wilma', 'barney']", p.get('acme.ui.names')) return def test_get_and_set_list_of_int(self): """ get and set list of int """ p = self.preferences # Note that we can pass an actual 'int' to 'set', but the preference # manager *always* returns preference values as strings. p.set('acme.ui.offsets', [1, 2, 3]) self.assertEqual('[1, 2, 3]', p.get('acme.ui.offsets')) return def test_empty_path(self): """ empty path """ p = self.preferences self.assertRaises(ValueError, p.get, '') self.assertRaises(ValueError, p.remove, '') self.assertRaises(ValueError, p.set, '', 'a value') return def test_default_values(self): """ default values """ p = self.preferences # Try non-existent names to get the default-default! self.assertEqual(None, p.get('bogus')) self.assertEqual(None, p.get('acme.bogus')) self.assertEqual(None, p.get('acme.ui.bogus')) # Try non-existent names to get the specified default. self.assertEqual('a value', p.get('bogus', 'a value')) self.assertEqual('a value', p.get('acme.bogus', 'a value')) self.assertEqual('a value', p.get('acme.ui.bogus', 'a value')) return def test_keys(self): """ keys """ p = self.preferences # It should be empty to start with! self.assertEqual([], list(p.keys())) # Set some preferences in the node. p.set('a', '1') p.set('b', '2') p.set('c', '3') keys = sorted(p.keys()) self.assertEqual(['a', 'b', 'c'], keys) # Set some preferences in a child node. p.set('acme.a', '1') p.set('acme.b', '2') p.set('acme.c', '3') keys = sorted(p.keys('acme')) self.assertEqual(['a', 'b', 'c'], keys) # And, just to be sure, in a child of the child node ;^) p.set('acme.ui.a', '1') p.set('acme.ui.b', '2') p.set('acme.ui.c', '3') keys = sorted(p.keys('acme.ui')) self.assertEqual(['a', 'b', 'c'], keys) # Test keys of a non-existent node. self.assertEqual([], p.keys('bogus')) self.assertEqual([], p.keys('bogus.blargle')) self.assertEqual([], p.keys('bogus.blargle.foogle')) return def test_node(self): """ node """ p = self.preferences # Try an empty path. self.assertEqual(p, p.node()) # Try a simple path. node = p.node('acme') self.assertNotEqual(None, node) self.assertEqual('acme', node.name) self.assertEqual('acme', node.path) self.assertEqual(p, node.parent) # Make sure we get the same node each time we ask for it! self.assertEqual(node, p.node('acme')) # Try a nested path. node = p.node('acme.ui') self.assertNotEqual(None, node) self.assertEqual('ui', node.name) self.assertEqual('acme.ui', node.path) self.assertEqual(p.node('acme'), node.parent) # And just to be sure, a really nested path. node = p.node('acme.ui.splash_screen') self.assertNotEqual(None, node) self.assertEqual('splash_screen', node.name) self.assertEqual('acme.ui.splash_screen', node.path) self.assertEqual(p.node('acme.ui'), node.parent) return def test_node_exists(self): """ node exists """ p = self.preferences self.assertEqual(True, p.node_exists()) self.assertEqual(False, p.node_exists('acme')) p.node('acme') self.assertEqual(True, p.node_exists('acme')) return def test_node_names(self): """ node names """ p = self.preferences # It should be empty to start with! self.assertEqual([], p.node_names()) # Add some nodes. p.node('a') p.node('b') p.node('c') names = sorted(p.node_names()) self.assertEqual(['a', 'b', 'c'], names) # Creatd some nodes in a child node. p.node('acme.a') p.node('acme.b') p.node('acme.c') names = sorted(p.node_names('acme')) self.assertEqual(['a', 'b', 'c'], names) # And, just to be sure, in a child of the child node ;^) p.node('acme.ui.a') p.node('acme.ui.b') p.node('acme.ui.c') names = sorted(p.node_names('acme.ui')) self.assertEqual(['a', 'b', 'c'], names) # Test keys of a non-existent node. self.assertEqual([], p.node_names('bogus')) self.assertEqual([], p.node_names('bogus.blargle')) self.assertEqual([], p.node_names('bogus.blargle.foogle')) return def test_clear(self): """ clear """ p = self.preferences # Set some values. p.set('acme.ui.bgcolor', 'blue') self.assertEqual('blue', p.get('acme.ui.bgcolor')) p.set('acme.ui.width', 100) self.assertEqual('100', p.get('acme.ui.width')) # Clear all preferences from the node. p.clear('acme.ui') self.assertEqual(None, p.get('acme.ui.bgcolor')) self.assertEqual(None, p.get('acme.ui.width')) self.assertEqual(0, len(p.keys('acme.ui'))) return def test_remove(self): """ remove """ p = self.preferences # Set a value. p.set('acme.ui.bgcolor', 'blue') self.assertEqual('blue', p.get('acme.ui.bgcolor')) # Remove it. p.remove('acme.ui.bgcolor') self.assertEqual(None, p.get('acme.ui.bgcolor')) # Make sure we can't remove nodes! p.remove('acme.ui') self.assertEqual(True, p.node_exists('acme.ui')) return def test_flush(self): """ flush """ p = self.preferences # A temporary .ini file for this test. tmp = join(self.tmpdir, 'tmp.ini') # This could be set in the constructor of course, its just here we # want to use the instance declared in 'setUp'. p.filename = tmp try: # Load the preferences from an 'ini' file. p.load(self.example) # Flush it. p.flush() # Load it into a new node. p = Preferences() p.load(tmp) # Make sure it was all loaded! self.assertEqual('blue', p.get('acme.ui.bgcolor')) self.assertEqual('50', p.get('acme.ui.width')) self.assertEqual('1.0', p.get('acme.ui.ratio')) self.assertEqual('True', p.get('acme.ui.visible')) self.assertEqual('acme ui', p.get('acme.ui.description')) self.assertEqual('[1, 2, 3, 4]', p.get('acme.ui.offsets')) self.assertEqual("['joe', 'fred', 'jane']", p.get('acme.ui.names')) self.assertEqual('splash', p.get('acme.ui.splash_screen.image')) self.assertEqual('red', p.get('acme.ui.splash_screen.fgcolor')) finally: # Clean up! os.remove(tmp) return def test_load(self): """ load """ p = self.preferences # Load the preferences from an 'ini' file. p.load(self.example) # Make sure it was all loaded! self.assertEqual('blue', p.get('acme.ui.bgcolor')) self.assertEqual('50', p.get('acme.ui.width')) self.assertEqual('1.0', p.get('acme.ui.ratio')) self.assertEqual('True', p.get('acme.ui.visible')) self.assertEqual('acme ui', p.get('acme.ui.description')) self.assertEqual('[1, 2, 3, 4]', p.get('acme.ui.offsets')) self.assertEqual("['joe', 'fred', 'jane']", p.get('acme.ui.names')) self.assertEqual('splash', p.get('acme.ui.splash_screen.image')) self.assertEqual('red', p.get('acme.ui.splash_screen.fgcolor')) return def test_load_with_filename_trait_set(self): """ load with filename trait set """ p = self.preferences p.filename = self.example # Load the preferences from an 'ini' file. p.load() # Make sure it was all loaded! self.assertEqual('blue', p.get('acme.ui.bgcolor')) self.assertEqual('50', p.get('acme.ui.width')) self.assertEqual('1.0', p.get('acme.ui.ratio')) self.assertEqual('True', p.get('acme.ui.visible')) self.assertEqual('acme ui', p.get('acme.ui.description')) self.assertEqual('[1, 2, 3, 4]', p.get('acme.ui.offsets')) self.assertEqual("['joe', 'fred', 'jane']", p.get('acme.ui.names')) self.assertEqual('splash', p.get('acme.ui.splash_screen.image')) self.assertEqual('red', p.get('acme.ui.splash_screen.fgcolor')) p = self.preferences # Load the preferences from an 'ini' file. p.load(self.example) # Make sure it was all loaded! self.assertEqual('blue', p.get('acme.ui.bgcolor')) self.assertEqual('50', p.get('acme.ui.width')) self.assertEqual('1.0', p.get('acme.ui.ratio')) self.assertEqual('True', p.get('acme.ui.visible')) self.assertEqual('acme ui', p.get('acme.ui.description')) self.assertEqual('[1, 2, 3, 4]', p.get('acme.ui.offsets')) self.assertEqual("['joe', 'fred', 'jane']", p.get('acme.ui.names')) self.assertEqual('splash', p.get('acme.ui.splash_screen.image')) self.assertEqual('red', p.get('acme.ui.splash_screen.fgcolor')) return def test_save(self): """ save """ p = self.preferences # Load the preferences from an 'ini' file. p.load(self.example) # Make sure it was all loaded! self.assertEqual('blue', p.get('acme.ui.bgcolor')) self.assertEqual('50', p.get('acme.ui.width')) self.assertEqual('1.0', p.get('acme.ui.ratio')) self.assertEqual('True', p.get('acme.ui.visible')) self.assertEqual('acme ui', p.get('acme.ui.description')) self.assertEqual('[1, 2, 3, 4]', p.get('acme.ui.offsets')) self.assertEqual("['joe', 'fred', 'jane']", p.get('acme.ui.names')) self.assertEqual('splash', p.get('acme.ui.splash_screen.image')) self.assertEqual('red', p.get('acme.ui.splash_screen.fgcolor')) # Make a change. p.set('acme.ui.bgcolor', 'yellow') # Save it to another file. tmp = join(self.tmpdir, 'tmp.ini') p.save(tmp) try: # Load it into a new node. p = Preferences() p.load(tmp) # Make sure it was all loaded! self.assertEqual('yellow', p.get('acme.ui.bgcolor')) self.assertEqual('50', p.get('acme.ui.width')) self.assertEqual('1.0', p.get('acme.ui.ratio')) self.assertEqual('True', p.get('acme.ui.visible')) self.assertEqual('acme ui', p.get('acme.ui.description')) self.assertEqual('[1, 2, 3, 4]', p.get('acme.ui.offsets')) self.assertEqual("['joe', 'fred', 'jane']", p.get('acme.ui.names')) self.assertEqual('splash', p.get('acme.ui.splash_screen.image')) self.assertEqual('red', p.get('acme.ui.splash_screen.fgcolor')) finally: # Clean up! os.remove(tmp) return def SKIPtest_dump(self): """ dump """ # This make look like a weird test, since we don't ever actually check # anything, but it is useful for people to see the structure of a # preferences hierarchy. p = self.preferences # Load the preferences from an 'ini' file. p.load(self.example) p.dump() return def test_get_inherited(self): """ get inherited """ p = self.preferences # Set a string preference. p.set('bgcolor', 'red') p.set('acme.bgcolor', 'green') p.set('acme.ui.bgcolor', 'blue') self.assertEqual('blue', p.get('acme.ui.bgcolor', inherit=True)) # Now remove the 'lowest' layer. p.remove('acme.ui.bgcolor') self.assertEqual('green', p.get('acme.ui.bgcolor', inherit=True)) # And the next one. p.remove('acme.bgcolor') self.assertEqual('red', p.get('acme.ui.bgcolor', inherit=True)) # And the last one. p.remove('bgcolor') self.assertEqual(None, p.get('acme.ui.bgcolor', inherit=True)) return def test_add_listener(self): """ add listener """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new return # Add a listener. p.add_preferences_listener(listener, 'acme.ui') # Set a value and make sure the listener was called. p.set('acme.ui.bgcolor', 'blue') self.assertEqual(p.node('acme.ui'), listener.node) self.assertEqual('bgcolor', listener.key) self.assertEqual(None, listener.old) self.assertEqual('blue', listener.new) # Set it to another value to make sure we get the 'old' value # correctly. p.set('acme.ui.bgcolor', 'red') self.assertEqual(p.node('acme.ui'), listener.node) self.assertEqual('bgcolor', listener.key) self.assertEqual('blue', listener.old) self.assertEqual('red', listener.new) return def test_remove_listener(self): """ remove listener """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new return # Add a listener. p.add_preferences_listener(listener, 'acme.ui') # Set a value and make sure the listener was called. p.set('acme.ui.bgcolor', 'blue') self.assertEqual(p.node('acme.ui'), listener.node) self.assertEqual('bgcolor', listener.key) self.assertEqual(None, listener.old) self.assertEqual('blue', listener.new) # Remove the listener. p.remove_preferences_listener(listener, 'acme.ui') # Set a value and make sure the listener was *not* called. listener.node = None p.set('acme.ui.bgcolor', 'blue') self.assertEqual(None, listener.node) return def test_set_with_same_value(self): """ set with same value """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new return # Add a listener. p.add_preferences_listener(listener, 'acme.ui') # Set a value and make sure the listener was called. p.set('acme.ui.bgcolor', 'blue') self.assertEqual(p.node('acme.ui'), listener.node) self.assertEqual('bgcolor', listener.key) self.assertEqual(None, listener.old) self.assertEqual('blue', listener.new) # Clear out the listener. listener.node = None # Set the same value and make sure the listener *doesn't* get called. p.set('acme.ui.bgcolor', 'blue') self.assertEqual(None, listener.node) return # Entry point for stand-alone testing. if __name__ == '__main__': unittest.main() #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/tests/scoped_preferences_test_case.py0000644000076500000240000003246113547637361030435 0ustar mdickinsonstaff00000000000000""" Tests for scoped preferences. """ # Standard library imports. import os, tempfile, unittest from os.path import join # Major package imports. from pkg_resources import resource_filename # Enthought library imports. from apptools.preferences.api import Preferences, ScopedPreferences # Local imports. from .preferences_test_case import PreferencesTestCase # This module's package. PKG = 'apptools.preferences.tests' class ScopedPreferencesTestCase(PreferencesTestCase): """ Tests for the scoped preferences. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.preferences = ScopedPreferences() # The filename of the example preferences file. self.example = resource_filename(PKG, 'example.ini') # A temporary directory that can safely be written to. self.tmpdir = tempfile.mkdtemp() return def tearDown(self): """ Called immediately after each test method has been called. """ # Remove the temporary directory. os.removedirs(self.tmpdir) return ########################################################################### # Tests overridden from 'PreferencesTestCase'. ########################################################################### def test_node(self): """ node """ p = self.preferences # Try an empty path. self.assertEqual(p, p.node()) # Try a simple path. node = p.node('acme') self.assertNotEqual(None, node) self.assertEqual('acme', node.name) self.assertEqual('acme', node.path) self.assertEqual(p.node('application/'), node.parent) # Make sure we get the same node each time we ask for it! self.assertEqual(node, p.node('acme')) # Try a nested path. node = p.node('acme.ui') self.assertNotEqual(None, node) self.assertEqual('ui', node.name) self.assertEqual('acme.ui', node.path) self.assertEqual(p.node('application/acme'), node.parent) # And just to be sure, a really nested path. node = p.node('acme.ui.splash_screen') self.assertNotEqual(None, node) self.assertEqual('splash_screen', node.name) self.assertEqual('acme.ui.splash_screen', node.path) self.assertEqual(p.node('application/acme.ui'), node.parent) return def test_save(self): """ save """ p = self.preferences # Get the application scope. application = p.node('application/') tmp = join(self.tmpdir, 'test.ini') application.filename = tmp # Set a value. p.set('acme.ui.bgcolor', 'red') # Save all scopes. p.save() # Make sure a file was written. self.assertEqual(True, os.path.exists(tmp)) # Load the 'ini' file into a new preferences node and make sure the # preference is in there. p = Preferences() p.load(tmp) self.assertEqual('red', p.get('acme.ui.bgcolor')) # Cleanup. os.remove(tmp) return ########################################################################### # Tests. ########################################################################### def test_ability_to_specify_primary_scope(self): preferences = ScopedPreferences( scopes = [ Preferences(name='a'), Preferences(name='b'), Preferences(name='c') ], primary_scope_name = 'b' ) # This should set the prefrrence in the primary scope. preferences.set('acme.foo', 'bar') # Look it up specifically in the primary scope. self.assertEqual('bar', preferences.get('b/acme.foo')) return def test_builtin_scopes(self): """ builtin scopes """ p = self.preferences # Make sure the default built-in scopes get created. self.assertEqual(True, p.node_exists('application/')) self.assertEqual(True, p.node_exists('default/')) return def test_get_and_set_in_specific_scope(self): """ get and set in specific scope """ p = self.preferences # Set a preference and make sure we can get it again! p.set('default/acme.ui.bgcolor', 'red') self.assertEqual('red', p.get('default/acme.ui.bgcolor')) return def test_clear_in_specific_scope(self): """ clear in specific scope """ p = self.preferences # Set a value in both the application and default scopes. p.set('application/acme.ui.bgcolor', 'red') p.set('default/acme.ui.bgcolor', 'yellow') # Make sure when we look it up we get the one in first scope in the # lookup order. self.assertEqual('red', p.get('acme.ui.bgcolor')) # Now clear out the application scope. p.clear('application/acme.ui') self.assertEqual(0, len(p.keys('application/acme.ui'))) # We should now get the value from the default scope. self.assertEqual('yellow', p.get('acme.ui.bgcolor')) return def test_remove_in_specific_scope(self): """ remove in specific scope """ p = self.preferences # Set a value in both the application and default scopes. p.set('application/acme.ui.bgcolor', 'red') p.set('default/acme.ui.bgcolor', 'yellow') # Make sure when we look it up we get the one in first scope in the # lookup order. self.assertEqual('red', p.get('acme.ui.bgcolor')) # Now remove it from the application scope. p.remove('application/acme.ui.bgcolor') # We should now get the value from the default scope. self.assertEqual('yellow', p.get('acme.ui.bgcolor')) return def test_keys_in_specific_scope(self): """ keys in specific scope """ p = self.preferences # It should be empty to start with! self.assertEqual([], p.keys('default/')) # Set some preferences in the node. p.set('default/a', '1') p.set('default/b', '2') p.set('default/c', '3') keys = p.keys('default/') keys.sort() self.assertEqual(['a', 'b', 'c'], keys) # Set some preferences in a child node. p.set('default/acme.a', '1') p.set('default/acme.b', '2') p.set('default/acme.c', '3') keys = p.keys('default/acme') keys.sort() self.assertEqual(['a', 'b', 'c'], keys) # And, just to be sure, in a child of the child node ;^) p.set('default/acme.ui.a', '1') p.set('default/acme.ui.b', '2') p.set('default/acme.ui.c', '3') keys = p.keys('default/acme.ui') keys.sort() self.assertEqual(['a', 'b', 'c'], keys) return def test_node_in_specific_scope(self): """ node in specific scope """ p = self.preferences # Try an empty path. self.assertEqual(p, p.node()) # Try a simple path. node = p.node('default/acme') self.assertNotEqual(None, node) self.assertEqual('acme', node.name) self.assertEqual('acme', node.path) self.assertEqual(p.node('default/'), node.parent) # Make sure we get the same node each time we ask for it! self.assertEqual(node, p.node('default/acme')) # Try a nested path. node = p.node('default/acme.ui') self.assertNotEqual(None, node) self.assertEqual('ui', node.name) self.assertEqual('acme.ui', node.path) self.assertEqual(p.node('default/acme'), node.parent) # And just to be sure, a really nested path. node = p.node('default/acme.ui.splash_screen') self.assertNotEqual(None, node) self.assertEqual('splash_screen', node.name) self.assertEqual('acme.ui.splash_screen', node.path) self.assertEqual(p.node('default/acme.ui'), node.parent) return def test_node_exists_in_specific_scope(self): """ node exists """ p = self.preferences self.assertEqual(True, p.node_exists()) self.assertEqual(False, p.node_exists('default/acme')) p.node('default/acme') self.assertEqual(True, p.node_exists('default/acme')) return def test_node_names_in_specific_scope(self): """ node names in specific scope """ p = self.preferences # It should be empty to start with! self.assertEqual([], p.node_names('default/')) # Create some nodes. p.node('default/a') p.node('default/b') p.node('default/c') names = p.node_names('default/') names.sort() self.assertEqual(['a', 'b', 'c'], names) # Creatd some nodes in a child node. p.node('default/acme.a') p.node('default/acme.b') p.node('default/acme.c') names = p.node_names('default/acme') names.sort() self.assertEqual(['a', 'b', 'c'], names) # And, just to be sure, in a child of the child node ;^) p.node('default/acme.ui.a') p.node('default/acme.ui.b') p.node('default/acme.ui.c') names = p.node_names('default/acme.ui') names.sort() self.assertEqual(['a', 'b', 'c'], names) return def test_default_lookup_order(self): """ default lookup order """ p = self.preferences # Set a value in both the application and default scopes. p.set('application/acme.ui.bgcolor', 'red') p.set('default/acme.ui.bgcolor', 'yellow') # Make sure when we look it up we get the one in first scope in the # lookup order. self.assertEqual('red', p.get('acme.ui.bgcolor')) # But we can still get at each scope individually. self.assertEqual('red', p.get('application/acme.ui.bgcolor')) self.assertEqual('yellow', p.get('default/acme.ui.bgcolor')) return def test_lookup_order(self): """ lookup order """ p = self.preferences p.lookup_order = ['default', 'application'] # Set a value in both the application and default scopes. p.set('application/acme.ui.bgcolor', 'red') p.set('default/acme.ui.bgcolor', 'yellow') # Make sure when we look it up we get the one in first scope in the # lookup order. self.assertEqual('red', p.get('acme.ui.bgcolor')) # But we can still get at each scope individually. self.assertEqual('red', p.get('application/acme.ui.bgcolor')) self.assertEqual('yellow', p.get('default/acme.ui.bgcolor')) return def test_add_listener_in_specific_scope(self): """ add listener in specific scope. """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new return # Add a listener. p.add_preferences_listener(listener, 'default/acme.ui') # Set a value and make sure the listener was called. p.set('default/acme.ui.bgcolor', 'blue') self.assertEqual(p.node('default/acme.ui'), listener.node) self.assertEqual('bgcolor', listener.key) self.assertEqual(None, listener.old) self.assertEqual('blue', listener.new) # Set it to another value to make sure we get the 'old' value # correctly. p.set('default/acme.ui.bgcolor', 'red') self.assertEqual(p.node('default/acme.ui'), listener.node) self.assertEqual('bgcolor', listener.key) self.assertEqual('blue', listener.old) self.assertEqual('red', listener.new) return def test_remove_listener_in_specific_scope(self): """ remove listener in specific scope. """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new return # Add a listener. p.add_preferences_listener(listener, 'default/acme.ui') # Set a value and make sure the listener was called. p.set('default/acme.ui.bgcolor', 'blue') self.assertEqual(p.node('default/acme.ui'), listener.node) self.assertEqual('bgcolor', listener.key) self.assertEqual(None, listener.old) self.assertEqual('blue', listener.new) # Remove the listener. p.remove_preferences_listener(listener, 'default/acme.ui') # Set a value and make sure the listener was *not* called. listener.node = None p.set('default/acme.ui.bgcolor', 'blue') self.assertEqual(None, listener.node) return def test_non_existent_scope(self): """ non existent scope """ p = self.preferences self.assertRaises(ValueError, p.get, 'bogus/acme.ui.bgcolor') return # Entry point for stand-alone testing. if __name__ == '__main__': unittest.main() #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/tests/preference_binding_test_case.py0000644000076500000240000002542611640354733030401 0ustar mdickinsonstaff00000000000000""" Tests for preference bindings. """ # Standard library imports. import os, tempfile, unittest from os.path import join # Major package imports. from pkg_resources import resource_filename # Enthought library imports. from apptools.preferences.api import Preferences, PreferenceBinding from apptools.preferences.api import ScopedPreferences, bind_preference from apptools.preferences.api import set_default_preferences from traits.api import Bool, HasTraits, Int, Float, Str # This module's package. PKG = 'apptools.preferences.tests' def listener(obj, trait_name, old, new): """ A useful trait change handler for testing! """ listener.obj = obj listener.trait_name = trait_name listener.old = old listener.new = new return class PreferenceBindingTestCase(unittest.TestCase): """ Tests for preference bindings. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.preferences = set_default_preferences(Preferences()) # The filename of the example preferences file. self.example = resource_filename(PKG, 'example.ini') return def tearDown(self): """ Called immediately after each test method has been called. """ return ########################################################################### # Tests. ########################################################################### def test_preference_binding(self): """ preference binding """ p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool acme_ui = AcmeUI() acme_ui.on_trait_change(listener) # Make some bindings. bind_preference(acme_ui, 'bgcolor', 'acme.ui.bgcolor') bind_preference(acme_ui, 'width', 'acme.ui.width') bind_preference(acme_ui, 'ratio', 'acme.ui.ratio') bind_preference(acme_ui, 'visible', 'acme.ui.visible') # Make sure the object was initialized properly. self.assertEqual('blue', acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertEqual(True, acme_ui.visible) # Make sure we can set the preference via the helper... acme_ui.bgcolor = 'yellow' self.assertEqual('yellow', p.get('acme.ui.bgcolor')) self.assertEqual('yellow', acme_ui.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(acme_ui, listener.obj) self.assertEqual('bgcolor', listener.trait_name) self.assertEqual('blue', listener.old) self.assertEqual('yellow', listener.new) # Make sure we can set the preference via the preferences node... p.set('acme.ui.bgcolor', 'red') self.assertEqual('red', p.get('acme.ui.bgcolor')) self.assertEqual('red', acme_ui.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(acme_ui, listener.obj) self.assertEqual('bgcolor', listener.trait_name) self.assertEqual('yellow', listener.old) self.assertEqual('red', listener.new) # Make sure we can set a non-string preference via the helper... acme_ui.ratio = 0.5 self.assertEqual('0.5', p.get('acme.ui.ratio')) self.assertEqual(0.5, acme_ui.ratio) # Make sure we can set a non-string preference via the node... p.set('acme.ui.ratio', '0.75') self.assertEqual('0.75', p.get('acme.ui.ratio')) self.assertEqual(0.75, acme_ui.ratio) return def test_default_values(self): """ instance scope preferences path """ p = self.preferences class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str('blue') width = Int(50) ratio = Float(1.0) visible = Bool(True) acme_ui = AcmeUI() # Make some bindings. bind_preference(acme_ui, 'bgcolor', 'acme.ui.bgcolor') bind_preference(acme_ui, 'width', 'acme.ui.width') bind_preference(acme_ui, 'ratio', 'acme.ui.ratio') bind_preference(acme_ui, 'visible', 'acme.ui.visible') # Make sure the helper was initialized properly. self.assertEqual('blue', acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertEqual(True, acme_ui.visible) return def test_load_and_save(self): """ load and save """ p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str('red') width = Int(60) ratio = Float(2.0) visible = Bool(False) acme_ui = AcmeUI() # Make some bindings. bind_preference(acme_ui, 'bgcolor', 'acme.ui.bgcolor') bind_preference(acme_ui, 'width', 'acme.ui.width') bind_preference(acme_ui, 'ratio', 'acme.ui.ratio') bind_preference(acme_ui, 'visible', 'acme.ui.visible') # Make sure the helper was initialized properly (with the values in # the loaded .ini file *not* the trait defaults!). self.assertEqual('blue', acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertEqual(True, acme_ui.visible) # Make a change to one of the preference values. p.set('acme.ui.bgcolor', 'yellow') self.assertEqual('yellow', acme_ui.bgcolor) self.assertEqual('yellow', p.get('acme.ui.bgcolor')) # Save the preferences to a different file. tmpdir = tempfile.mkdtemp() tmp = join(tmpdir, 'tmp.ini') p.save(tmp) # Load the preferences again from that file. p = set_default_preferences(Preferences()) p.load(tmp) acme_ui = AcmeUI() # Make some bindings. bind_preference(acme_ui, 'bgcolor', 'acme.ui.bgcolor') bind_preference(acme_ui, 'width', 'acme.ui.width') bind_preference(acme_ui, 'ratio', 'acme.ui.ratio') bind_preference(acme_ui, 'visible', 'acme.ui.visible') # Make sure the helper was initialized properly (with the values in # the .ini file *not* the trait defaults!). self.assertEqual('yellow', acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertEqual(True, acme_ui.visible) # Clean up! os.remove(tmp) os.removedirs(tmpdir) return def test_explicit_preferences(self): """ explicit preferences """ p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool acme_ui = AcmeUI() acme_ui.on_trait_change(listener) # Create an empty preferences node and use that in some of the # bindings! preferences = Preferences() # Make some bindings. bind_preference(acme_ui, 'bgcolor', 'acme.ui.bgcolor', preferences) bind_preference(acme_ui, 'width', 'acme.ui.width') bind_preference(acme_ui, 'ratio', 'acme.ui.ratio', preferences) bind_preference(acme_ui, 'visible', 'acme.ui.visible') # Make sure the object was initialized properly. self.assertEqual('', acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(0.0, acme_ui.ratio) self.assertEqual(True, acme_ui.visible) return def test_nested_set_in_trait_change_handler(self): """ nested set in trait change handler """ p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool def _width_changed(self, trait_name, old, new): """ Static trait change handler. """ self.ratio = 3.0 return acme_ui = AcmeUI() acme_ui.on_trait_change(listener) # Make some bindings. bind_preference(acme_ui, 'bgcolor', 'acme.ui.bgcolor') bind_preference(acme_ui, 'width', 'acme.ui.width') bind_preference(acme_ui, 'ratio', 'acme.ui.ratio') bind_preference(acme_ui, 'visible', 'acme.ui.visible') # Make sure the object was initialized properly. self.assertEqual('blue', acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertEqual(True, acme_ui.visible) # Change the width via the preferences node. This should cause the # ratio to get set via the static trait change handler on the helper. p.set('acme.ui.width', 42) self.assertEqual(42, acme_ui.width) self.assertEqual('42', p.get('acme.ui.width')) # Did the ratio get changed? self.assertEqual(3.0, acme_ui.ratio) self.assertEqual('3.0', p.get('acme.ui.ratio')) return def test_trait_name_different_to_preference_name(self): p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The test here is to have a different name for the trait than the # preference value (which is 'bgcolor'). color = Str acme_ui = AcmeUI() acme_ui.on_trait_change(listener) # Make some bindings. bind_preference(acme_ui, 'color', 'acme.ui.bgcolor') # Make sure the object was initialized properly. self.assertEqual('blue', acme_ui.color) # Change the width via the preferences node. p.set('acme.ui.bgcolor', 'red') self.assertEqual('color', listener.trait_name) self.assertEqual('blue', listener.old) self.assertEqual('red', listener.new) return # Entry point for stand-alone testing. if __name__ == '__main__': unittest.main() #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/tests/py_config_file.py0000644000076500000240000001761513547637361025525 0ustar mdickinsonstaff00000000000000""" A Python based configuration file with hierarchical sections. """ from __future__ import print_function import six class PyConfigFile(dict): """ A Python based configuration file with hierarchical sections. """ ########################################################################### # 'object' interface. ########################################################################### def __init__(self, file_or_filename=None): """ Constructor. If 'file_or_filename' is specified it will be loaded immediately. It can be either:- a) a filename b) a file-like object that must be open for reading """ # A dictionary containing one namespace instance for each root of the # config hierarchy (see the '_Namespace' class for more details). # # e.g. If the following sections have been loaded:- # # [acme.foo] # ... # [acme.bar] # ... # [tds] # ... # [tds.baz] # ... # # Then the dictionary will contain:- # # {'acme' : , 'tds' : } # self._namespaces = {} if file_or_filename is not None: self.load(file_or_filename) return ########################################################################### # 'PyConfigFile' interface. ########################################################################### def load(self, file_or_filename): """ Load the configuration from a file. 'file_or_filename' can be either:- a) a filename b) a file-like object that must be open for reading """ # Get an open file to read from. f = self._get_file(file_or_filename) section_name = None for line in f: stripped = line.strip() # Is this line a section header? # # If so then parse the preceding section (if there is one) and # start collecting the body of the new section. if stripped.startswith('[') and stripped.endswith(']'): if section_name is not None: self._parse_section(section_name, section_body) section_name = stripped[1:-1] section_body = '' # Otherwise, this is *not* a section header so add the line to the # body of the current section. If there is no current section then # we simply ignore it! else: if section_name is not None: section_body += line # Parse the last section in the file. if section_name is not None: self._parse_section(section_name, section_body) f.close() return def save(self, file_or_filename): """ Save the configuration to a file. 'file_or_filename' can be either:- a) a filename b) a file-like object that must be open for writing """ f = self._get_file(file_or_filename, 'w') for section_name, section_data in self.items(): self._write_section(f, section_name, section_data) f.close() return ########################################################################### # Private interface. ########################################################################### def _get_file(self, file_or_filename, mode='r'): """ Return an open file object from a file or a filename. The mode is only used if a filename is specified. """ if isinstance(file_or_filename, six.string_types): f = open(file_or_filename, mode) else: f = file_or_filename return f def _get_namespace(self, section_name): """ Return the namespace that represents the section. """ components = section_name.split('.') namespace = self._namespaces.setdefault(components[0], _Namespace()) for component in components[1:]: namespace = getattr(namespace, component) return namespace def _parse_section(self, section_name, section_body): """ Parse a section. In this implementation, we don't actually 'parse' anything - we just execute the body of the section as Python code ;^) """ # If this is the first time that we have come across the section then # start with an empty dictionary for its contents. Otherwise, we will # update its existing contents. section = self.setdefault(section_name, {}) # Execute the Python code in the section dictionary. # # We use 'self._namespaces' as the globals for the code execution so # that config values can refer to other config values using familiar # Python syntax (see the '_Namespace' class for more details). # # e.g. # # [acme.foo] # bar = 1 # baz = 99 # # [acme.blargle] # blitzel = acme.foo.bar + acme.foo.baz exec(section_body, self._namespaces, section) # The '__builtins__' dictionary gets added to 'self._namespaces' as # by the call to 'exec'. However, we want 'self._namespaces' to only # contain '_Namespace' instances, so we do the cleanup here. del self._namespaces['__builtins__'] # Get the section's corresponding node in the 'dotted' namespace and # update it with the config values. namespace = self._get_namespace(section_name) namespace.__dict__.update(section) return def _write_section(self, f, section_name, section_data): """ Write a section to a file. """ f.write('[%s]\n' % section_name) for name, value in section_data.items(): f.write('%s = %s\n' % (name, repr(value))) f.write('\n') return ########################################################################### # Debugging interface. ########################################################################### def _pretty_print_namespaces(self): """ Pretty print the 'dotted' namespaces. """ for name, value in self._namespaces.items(): print('Namespace:', name) value.pretty_print(' ') return ############################################################################### # Internal use only. ############################################################################### class _Namespace(object): """ An object that represents a node in a dotted namespace. We build up a dotted namespace so that config values can refer to other config values using familiar Python syntax. e.g. [acme.foo] bar = 1 baz = 99 [acme.blargle] blitzel = acme.foo.bar + acme.foo.baz """ ########################################################################### # 'object' interface. ########################################################################### def __getattr__(self, name): """ Return the attribute with the specified name. """ # This looks a little weird, but we are simply creating the next level # in the namespace hierarchy 'on-demand'. namespace = self.__dict__[name] = _Namespace() return namespace ########################################################################### # Debugging interface. ########################################################################### def pretty_print(self, indent=''): """ Pretty print the namespace. """ for name, value in self.__dict__.items(): if isinstance(value, _Namespace): print(indent, 'Namespace:', name) value.pretty_print(indent + ' ') else: print(indent, name, ':', value) return #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/tests/py_config_example.ini0000644000076500000240000000137711640354733026355 0ustar mdickinsonstaff00000000000000[acme.ui] bgcolor = "blue" width = 50 ratio = 1.0 visible = True foo = { 'a' : 1, 'b' : 2 } bar = [ 1, 2, 3, 4 ] baz = ( 1, 'a', 6, 4 ) [acme.ui.splash_screen] image = "splash" fgcolor = "red" # You can also reference a previous setting as in the following example, but # note that if you *write* these settings back out again, then any reference # to another setting is lost - just the literal value gets written. # # e.g. The following section would be written as:- # # [acme.ui.other] # fred = "red" # wilma = 100 # [acme.ui.other] fred = acme.ui.splash_screen.fgcolor wilma = acme.ui.foo['a'] + 99 # To show that not every section needs to be from the same root! [tds.foogle] joe = 90 # A non-dotted section name. [simples] animal = "meerkat"apptools-4.5.0/apptools/preferences/tests/__init__.py0000644000076500000240000000010411640354733024260 0ustar mdickinsonstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. apptools-4.5.0/apptools/preferences/tests/preferences_helper_test_case.py0000644000076500000240000004451513422276145030431 0ustar mdickinsonstaff00000000000000""" Tests for the preferences helper. """ # Standard library imports. import time import unittest # Major package imports. from pkg_resources import resource_filename # Enthought library imports. from apptools.preferences.api import Preferences, PreferencesHelper from apptools.preferences.api import ScopedPreferences from apptools.preferences.api import set_default_preferences from traits.api import Any, Bool, HasTraits, Int, Float, List, Str, Unicode def width_listener(obj, trait_name, old, new): width_listener.obj = obj width_listener.trait_name = trait_name width_listener.old = old width_listener.new = new return def bgcolor_listener(obj, trait_name, old, new): bgcolor_listener.obj = obj bgcolor_listener.trait_name = trait_name bgcolor_listener.old = old bgcolor_listener.new = new return # This module's package. PKG = 'apptools.preferences.tests' class PreferencesHelperTestCase(unittest.TestCase): """ Tests for the preferences helper. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.preferences = set_default_preferences(Preferences()) # The filename of the example preferences file. self.example = resource_filename(PKG, 'example.ini') return def tearDown(self): """ Called immediately after each test method has been called. """ return ########################################################################### # Tests. ########################################################################### def test_class_scope_preferences_path(self): """ class scope preferences path """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = 'acme.ui' # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Unicode offsets = List(Int) names = List(Str) helper = AcmeUIPreferencesHelper() helper.on_trait_change(bgcolor_listener) # Make sure the helper was initialized properly. self.assertEqual('blue', helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertEqual(True, helper.visible) self.assertEqual(u'acme ui', helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(['joe', 'fred', 'jane'], helper.names) # Make sure we can set the preference via the helper... helper.bgcolor = 'yellow' self.assertEqual('yellow', p.get('acme.ui.bgcolor')) self.assertEqual('yellow', helper.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual('bgcolor', bgcolor_listener.trait_name) self.assertEqual('blue', bgcolor_listener.old) self.assertEqual('yellow', bgcolor_listener.new) # Make sure we can set the preference via the preferences node... p.set('acme.ui.bgcolor', 'red') self.assertEqual('red', p.get('acme.ui.bgcolor')) self.assertEqual('red', helper.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual('bgcolor', bgcolor_listener.trait_name) self.assertEqual('yellow', bgcolor_listener.old) self.assertEqual('red', bgcolor_listener.new) return def test_instance_scope_preferences_path(self): """ instance scope preferences path """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Unicode offsets = List(Int) names = List(Str) helper = AcmeUIPreferencesHelper(preferences_path='acme.ui') helper.on_trait_change(bgcolor_listener) # Make sure the helper was initialized properly. self.assertEqual('blue', helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertEqual(True, helper.visible) self.assertEqual(u'acme ui', helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(['joe', 'fred', 'jane'], helper.names) # Make sure we can set the preference via the helper... helper.bgcolor = 'yellow' self.assertEqual('yellow', p.get('acme.ui.bgcolor')) self.assertEqual('yellow', helper.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual('bgcolor', bgcolor_listener.trait_name) self.assertEqual('blue', bgcolor_listener.old) self.assertEqual('yellow', bgcolor_listener.new) # Make sure we can set the preference via the preferences node... p.set('acme.ui.bgcolor', 'red') self.assertEqual('red', p.get('acme.ui.bgcolor')) self.assertEqual('red', helper.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual('bgcolor', bgcolor_listener.trait_name) self.assertEqual('yellow', bgcolor_listener.old) self.assertEqual('red', bgcolor_listener.new) return def test_default_values(self): """ default values """ p = self.preferences class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = 'acme.ui' # The traits that we want to initialize from preferences. bgcolor = Str('blue') width = Int(50) ratio = Float(1.0) visible = Bool(True) description = Unicode(u'description') offsets = List(Int, [1, 2, 3, 4]) names = List(Str, ['joe', 'fred', 'jane']) helper = AcmeUIPreferencesHelper() # Make sure the helper was initialized properly. self.assertEqual('blue', helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertEqual(True, helper.visible) self.assertEqual(u'description', helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(['joe', 'fred', 'jane'], helper.names) return def test_real_unicode_values(self): """ Test with real life unicode values """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = 'acme.ui' # The traits that we want to initialize from preferences. bgcolor = Str('blue') width = Int(50) ratio = Float(1.0) visible = Bool(True) description = Unicode(u'') offsets = List(Int, [1, 2, 3, 4]) names = List(Str, ['joe', 'fred', 'jane']) helper = AcmeUIPreferencesHelper() first_unicode_str = u'U\xdc\xf2ser' first_utf8_str = 'U\xc3\x9c\xc3\xb2ser' original_description = helper.description helper.description = first_unicode_str self.assertEqual(first_unicode_str, helper.description) second_unicode_str = u'caf\xe9' second_utf8_str = 'caf\xc3\xa9' helper.description = second_unicode_str self.assertEqual(second_unicode_str, helper.description) self.assertEqual(second_unicode_str, p.get('acme.ui.description')) p.save(self.example) p.load(self.example) self.assertEqual(second_unicode_str, p.get('acme.ui.description')) self.assertEqual(u'True', p.get('acme.ui.visible')) self.assertEqual(True, helper.visible) # reset the original description and save the example file helper.description = original_description p.save(self.example) def test_no_preferences_path(self): """ no preferences path """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Unicode offsets = List(Int) names = List(Str) # Cannot create a helper with a preferences path. self.assertRaises(SystemError, AcmeUIPreferencesHelper) return def test_sync_trait(self): """ sync trait """ class Widget(HasTraits): """ A widget! """ background_color = Str w = Widget() w.on_trait_change(bgcolor_listener) p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = 'acme.ui' # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Unicode offsets = List(Int) names = List(Str) helper = AcmeUIPreferencesHelper() helper.sync_trait('bgcolor', w, 'background_color') # Make sure the helper was initialized properly. self.assertEqual('blue', helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertEqual(True, helper.visible) self.assertEqual(u'acme ui', helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(['joe', 'fred', 'jane'], helper.names) self.assertEqual('blue', w.background_color) # Make sure we can set the preference via the helper... helper.bgcolor = 'yellow' self.assertEqual('yellow', p.get('acme.ui.bgcolor')) self.assertEqual('yellow', helper.bgcolor) self.assertEqual('yellow', w.background_color) # ... and that the correct trait change event was fired. self.assertEqual(w, bgcolor_listener.obj) self.assertEqual('background_color', bgcolor_listener.trait_name) self.assertEqual('blue', bgcolor_listener.old) self.assertEqual('yellow', bgcolor_listener.new) # Make sure we can set the preference via the preferences node... p.set('acme.ui.bgcolor', 'red') self.assertEqual('red', p.get('acme.ui.bgcolor')) self.assertEqual('red', helper.bgcolor) self.assertEqual('red', w.background_color) # ... and that the correct trait change event was fired. self.assertEqual(w, bgcolor_listener.obj) self.assertEqual('background_color', bgcolor_listener.trait_name) self.assertEqual('yellow', bgcolor_listener.old) self.assertEqual('red', bgcolor_listener.new) return def test_scoped_preferences(self): """ scoped preferences """ p = set_default_preferences(ScopedPreferences()) # Set a preference value in the default scope. p.set('default/acme.ui.bgcolor', 'blue') class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = 'acme.ui' # The traits that we want to initialize from preferences. bgcolor = Str # A trait for a preference that does not exist yet. name = Str helper = AcmeUIPreferencesHelper() # Make sure the trait is set! self.assertEqual('blue', helper.bgcolor) # And that the non-existent trait gets the default value. self.assertEqual('', helper.name) return def test_preference_not_in_file(self): """ preference not in file """ class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = 'acme.ui' # A trait that has no corresponding value in the file. title = Str('Acme') helper = AcmeUIPreferencesHelper() # Make sure the trait is set! self.assertEqual('Acme', helper.title) # Set a new value. helper.title = 'Acme Plus' # Make sure the trait is set! self.assertEqual('Acme Plus', helper.title) self.assertEqual('Acme Plus', self.preferences.get('acme.ui.title')) return def test_preferences_node_changed(self): """ preferences node changed """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = 'acme.ui' # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Unicode offsets = List(Int) names = List(Str) helper = AcmeUIPreferencesHelper() # We only listen to some of the traits so the testing is easier. helper.on_trait_change(width_listener, ['width']) helper.on_trait_change(bgcolor_listener, ['bgcolor']) # Create a new preference node. p1 = Preferences() p1.load(self.example) p1.set('acme.ui.bgcolor', 'red') p1.set('acme.ui.width', 40) # Set the new preferences helper.preferences = p1 # Test event handling. self.assertEqual(helper, width_listener.obj) self.assertEqual('width', width_listener.trait_name) self.assertEqual(50, width_listener.old) self.assertEqual(40, width_listener.new) self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual('bgcolor', bgcolor_listener.trait_name) self.assertEqual('blue', bgcolor_listener.old) self.assertEqual('red', bgcolor_listener.new) # Test re-initialization. self.assertEqual(helper.bgcolor, 'red') self.assertEqual(helper.width, 40) # Test event handling. p1.set('acme.ui.bgcolor', 'black') self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual('bgcolor', bgcolor_listener.trait_name) self.assertEqual('red', bgcolor_listener.old) self.assertEqual('black', bgcolor_listener.new) # This should not trigger any new changes since we are setting values # on the old preferences node. p.set('acme.ui.bgcolor', 'white') self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual('bgcolor', bgcolor_listener.trait_name) self.assertEqual('red', bgcolor_listener.old) self.assertEqual('black', bgcolor_listener.new) return def test_nested_set_in_trait_change_handler(self): """ nested set in trait change handler """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Unicode offsets = List(Int) names = List(Str) # When the width changes, change the ratio. def _width_changed(self, trait_name, old, new): """ Static trait change handler. """ self.ratio = 3.0 return helper = AcmeUIPreferencesHelper(preferences_path='acme.ui') # Make sure the helper was initialized properly. self.assertEqual('blue', helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertEqual(True, helper.visible) self.assertEqual(u'acme ui', helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(['joe', 'fred', 'jane'], helper.names) # Change the width via the preferences node. This should cause the # ratio to get set via the static trait change handler on the helper. p.set('acme.ui.width', 42) self.assertEqual(42, helper.width) self.assertEqual('42', p.get('acme.ui.width')) # Did the ratio get changed? self.assertEqual(3.0, helper.ratio) self.assertEqual('3.0', p.get('acme.ui.ratio')) return # fixme: No comments - nice work... I added the doc string and the 'return' # to be compatible with the rest of the module. Interns please note correct # procedure when modifying existing code. If in doubt, ask a developer. def test_unevaluated_strings(self): """ unevaluated strings """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): width = Any(is_str=True) helper = AcmeUIPreferencesHelper(preferences_path='acme.ui') self.assertEqual('50', helper.width) return # Entry point for stand-alone testing. if __name__ == '__main__': unittest.main() #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/tests/py_config_file_test_case.py0000644000076500000240000001551613547637361027555 0ustar mdickinsonstaff00000000000000""" Tests for Python-esque '.ini' files. """ # Standard library imports. import os, tempfile, unittest from os.path import join # Major package imports. from pkg_resources import resource_filename # Enthought library imports. from .py_config_file import PyConfigFile # This module's package. PKG = 'apptools.preferences.tests' class PyConfigFileTestCase(unittest.TestCase): """ Tests for Python-esque '.ini' files. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ # The filenames of the example preferences files. self.example = resource_filename(PKG, 'py_config_example.ini') self.example_2 = resource_filename(PKG, 'py_config_example_2.ini') return def tearDown(self): """ Called immediately after each test method has been called. """ return ########################################################################### # Tests. ########################################################################### def test_load_from_filename(self): """ load from filename """ config = PyConfigFile(self.example) self.assertEqual('blue', config['acme.ui']['bgcolor']) self.assertEqual(50, config['acme.ui']['width']) self.assertEqual(1.0, config['acme.ui']['ratio']) self.assertEqual(True, config['acme.ui']['visible']) self.assertEqual({'a' : 1, 'b' : 2}, config['acme.ui']['foo']) self.assertEqual([1, 2, 3, 4], config['acme.ui']['bar']) self.assertEqual((1, 'a', 6, 4), config['acme.ui']['baz']) self.assertEqual('red', config['acme.ui.other']['fred']) self.assertEqual(100, config['acme.ui.other']['wilma']) self.assertEqual(90, config['tds.foogle']['joe']) self.assertEqual("meerkat", config['simples']['animal']) return def test_load_from_file(self): """ load from file """ config = PyConfigFile(open(self.example)) self.assertEqual('blue', config['acme.ui']['bgcolor']) self.assertEqual(50, config['acme.ui']['width']) self.assertEqual(1.0, config['acme.ui']['ratio']) self.assertEqual(True, config['acme.ui']['visible']) self.assertEqual({'a' : 1, 'b' : 2}, config['acme.ui']['foo']) self.assertEqual([1, 2, 3, 4], config['acme.ui']['bar']) self.assertEqual((1, 'a', 6, 4), config['acme.ui']['baz']) self.assertEqual('red', config['acme.ui.other']['fred']) self.assertEqual(100, config['acme.ui.other']['wilma']) self.assertEqual(90, config['tds.foogle']['joe']) self.assertEqual("meerkat", config['simples']['animal']) return def test_save(self): """ save """ config = PyConfigFile(open(self.example)) self.assertEqual('blue', config['acme.ui']['bgcolor']) self.assertEqual(50, config['acme.ui']['width']) self.assertEqual(1.0, config['acme.ui']['ratio']) self.assertEqual(True, config['acme.ui']['visible']) self.assertEqual({'a' : 1, 'b' : 2}, config['acme.ui']['foo']) self.assertEqual([1, 2, 3, 4], config['acme.ui']['bar']) self.assertEqual('red', config['acme.ui.other']['fred']) self.assertEqual(100, config['acme.ui.other']['wilma']) self.assertEqual(90, config['tds.foogle']['joe']) self.assertEqual("meerkat", config['simples']['animal']) # Save the config to another file. tmpdir = tempfile.mkdtemp() tmp = join(tmpdir, 'tmp.ini') config.save(tmp) try: self.assert_(os.path.exists(tmp)) # Make sure we can read the file back in and that we get the same # values! config = PyConfigFile(open(tmp)) self.assertEqual('blue', config['acme.ui']['bgcolor']) self.assertEqual(50, config['acme.ui']['width']) self.assertEqual(1.0, config['acme.ui']['ratio']) self.assertEqual(True, config['acme.ui']['visible']) self.assertEqual({'a' : 1, 'b' : 2}, config['acme.ui']['foo']) self.assertEqual([1, 2, 3, 4], config['acme.ui']['bar']) self.assertEqual((1, 'a', 6, 4), config['acme.ui']['baz']) self.assertEqual('red', config['acme.ui.other']['fred']) self.assertEqual(100, config['acme.ui.other']['wilma']) self.assertEqual(90, config['tds.foogle']['joe']) self.assertEqual("meerkat", config['simples']['animal']) finally: # Clean up! os.remove(tmp) os.removedirs(tmpdir) return def test_load_multiple_files(self): """ load multiple files """ config = PyConfigFile(self.example) self.assertEqual('blue', config['acme.ui']['bgcolor']) self.assertEqual(50, config['acme.ui']['width']) self.assertEqual(1.0, config['acme.ui']['ratio']) self.assertEqual(True, config['acme.ui']['visible']) self.assertEqual({'a' : 1, 'b' : 2}, config['acme.ui']['foo']) self.assertEqual([1, 2, 3, 4], config['acme.ui']['bar']) self.assertEqual((1, 'a', 6, 4), config['acme.ui']['baz']) self.assertEqual('red', config['acme.ui.other']['fred']) self.assertEqual(100, config['acme.ui.other']['wilma']) self.assertEqual(90, config['tds.foogle']['joe']) self.assertEqual("meerkat", config['simples']['animal']) # Load another file. config.load(self.example_2) # Make sure we still have the unchanged values... self.assertEqual('red', config['acme.ui']['bgcolor']) self.assertEqual(50, config['acme.ui']['width']) self.assertEqual(1.0, config['acme.ui']['ratio']) self.assertEqual(True, config['acme.ui']['visible']) self.assertEqual({'a' : 1, 'b' : 2}, config['acme.ui']['foo']) self.assertEqual([1, 2, 3, 4], config['acme.ui']['bar']) self.assertEqual((1, 'a', 6, 4), config['acme.ui']['baz']) self.assertEqual('red', config['acme.ui.other']['fred']) self.assertEqual(100, config['acme.ui.other']['wilma']) self.assertEqual(90, config['tds.foogle']['joe']) self.assertEqual("meerkat", config['simples']['animal']) # ... and the values that were overwritten... self.assertEqual('red', config['acme.ui']['bgcolor']) # ... and that we have the new ones. self.assertEqual(42, config['acme.ui']['bazzle']) # ... and that the new ones can refer to the old ones! self.assertEqual(180, config['acme.ui']['blimey']) return # Entry point for stand-alone testing. if __name__ == '__main__': unittest.main() #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/tests/py_config_example_2.ini0000644000076500000240000000010311640354733026560 0ustar mdickinsonstaff00000000000000[acme.ui] bgcolor = "red" bazzle = 42 blimey = tds.foogle.joe * 2 apptools-4.5.0/apptools/preferences/preferences.py0000644000076500000240000004252413547637361023705 0ustar mdickinsonstaff00000000000000""" The default implementation of a node in a preferences hierarchy. """ from __future__ import print_function # Standard library imports. import logging, threading # Third-party library imports. import six # Enthought library imports. from traits.api import Any, Callable, Dict, HasTraits, Instance, List from traits.api import Property, Str, Undefined, provides # Local imports. from .i_preferences import IPreferences # Logging. logger = logging.getLogger(__name__) @provides(IPreferences) class Preferences(HasTraits): """ The default implementation of a node in a preferences hierarchy. """ #### 'IPreferences' interface ############################################# # The absolute path to this node from the root node (the empty string if # this node *is* the root node). path = Property(Str) # The parent node (None if this node *is* the root node). parent = Instance(IPreferences) # The name of the node relative to its parent (the empty string if this # node *is* the root node). name = Str #### 'Preferences' interface ############################################## # The default name of the file used to persist the preferences (if no # filename is passed in to the 'load' and 'save' methods, then this is # used instead). filename = Str #### Protected 'Preferences' interface #################################### # A lock to make access to the node thread-safe. # # fixme: There *should* be no need to declare this as a trait, but if we # don't then we have problems using nodes in the preferences manager UI. # It is something to do with 'cloning' the node for use in a 'modal' traits # UI... Hmmm... _lk = Any # The node's children. _children = Dict(Str, IPreferences) # The node's preferences. _preferences = Dict(Str, Any) # Listeners for changes to the node's preferences. # # The callable must take 4 arguments, e.g:: # # listener(node, key, old, new) _preferences_listeners = List(Callable) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Constructor. """ # A lock to make access to the '_children', '_preferences' and # '_preferences_listeners' traits thread-safe. self._lk = threading.Lock() # Base class constructor. super(Preferences, self).__init__(**traits) # If a filename has been specified then load the preferences from it. if len(self.filename) > 0: self.load() return ########################################################################### # 'IPreferences' interface. ########################################################################### #### Trait properties ##################################################### def _get_path(self): """ Property getter. """ names = [] node = self while node.parent is not None: names.append(node.name) node = node.parent names.reverse() return '.'.join(names) #### Methods ############################################################## #### Methods where 'path' refers to a preference #### def get(self, path, default=None, inherit=False): """ Get the value of the preference at the specified path. """ if len(path) == 0: raise ValueError('empty path') components = path.split('.') # If there is only one component in the path then the operation takes # place in this node. if len(components) == 1: value = self._get(path, Undefined) # Otherwise, find the next node and pass the rest of the path to that. else: node = self._get_child(components[0]) if node is not None: value = node.get('.'.join(components[1:]), Undefined) else: value = Undefined # If inherited values are allowed then try those as well. # # e.g. 'acme.ui.widget.bgcolor' # 'acme.ui.bgcolor' # 'acme.bgcolor' # 'bgcolor' while inherit and value is Undefined and len(components) > 1: # Remove the penultimate component... # # e.g. 'acme.ui.widget.bgcolor' -> 'acme.ui.bgcolor' del components[-2] # ... and try that. value = self.get('.'.join(components), default=Undefined) if value is Undefined: value = default return value def remove(self, path): """ Remove the preference at the specified path. """ if len(path) == 0: raise ValueError('empty path') components = path.split('.') # If there is only one component in the path then the operation takes # place in this node. if len(components) == 1: self._remove(path) # Otherwise, find the next node and pass the rest of the path to that. else: node = self._get_child(components[0]) if node is not None: node.remove('.'.join(components[1:])) return def set(self, path, value): """ Set the value of the preference at the specified path. """ if len(path) == 0: raise ValueError('empty path') components = path.split('.') # If there is only one component in the path then the operation takes # place in this node. if len(components) == 1: self._set(path, value) # Otherwise, find the next node (creating it if it doesn't exist) # and pass the rest of the path to that. else: node = self._node(components[0]) node.set('.'.join(components[1:]), value) return #### Methods where 'path' refers to a node #### def clear(self, path=''): """ Remove all preferences from the node at the specified path. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: self._clear() # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split('.') node = self._get_child(components[0]) if node is not None: node.clear('.'.join(components[1:])) return def keys(self, path=''): """ Return the preference keys of the node at the specified path. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: keys = self._keys() # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split('.') node = self._get_child(components[0]) if node is not None: keys = node.keys('.'.join(components[1:])) else: keys = [] return keys def node(self, path=''): """ Return the node at the specified path. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: node = self # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split('.') node = self._node(components[0]) node = node.node('.'.join(components[1:])) return node def node_exists(self, path=''): """ Return True if the node at the specified path exists. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: exists = True # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split('.') node = self._get_child(components[0]) if node is not None: exists = node.node_exists('.'.join(components[1:])) else: exists = False return exists def node_names(self, path=''): """ Return the names of the children of the node at the specified path. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: names = self._node_names() # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split('.') node = self._get_child(components[0]) if node is not None: names = node.node_names('.'.join(components[1:])) else: names = [] return names #### Persistence methods #### def flush(self): """ Force any changes in the node to the backing store. This includes any changes to the node's descendants. """ self.save() return ########################################################################### # 'Preferences' interface. ########################################################################### #### Listener methods #### def add_preferences_listener(self, listener, path=''): """ Add a listener for changes to a node's preferences. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: names = self._add_preferences_listener(listener) # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split('.') node = self._node(components[0]) node.add_preferences_listener(listener, '.'.join(components[1:])) return def remove_preferences_listener(self, listener, path=''): """ Remove a listener for changes to a node's preferences. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: names = self._remove_preferences_listener(listener) # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split('.') node = self._node(components[0]) node.remove_preferences_listener(listener,'.'.join(components[1:])) return #### Persistence methods #### def load(self, file_or_filename=None): """ Load preferences from a file. This is a *merge* operation i.e. the contents of the file are added to the node. This implementation uses 'ConfigObj' files. """ if file_or_filename is None: file_or_filename = self.filename logger.debug('loading preferences from <%s>', file_or_filename) # Do the import here so that we don't make 'ConfigObj' a requirement # if preferences aren't ever persisted (or a derived class chooses to # use a different persistence mechanism). from configobj import ConfigObj config_obj = ConfigObj(file_or_filename, encoding='utf-8') # 'name' is the section name, 'value' is a dictionary containing the # name/value pairs in the section (the actual preferences ;^). for name, value in config_obj.items(): # Create/get the node from the section name. components = name.split('.') node = self for component in components: node = node._node(component) # Add the contents of the section to the node. self._add_dictionary_to_node(node, value) return def save(self, file_or_filename=None): """ Save the node's preferences to a file. This implementation uses 'ConfigObj' files. """ if file_or_filename is None: file_or_filename = self.filename # If no file or filename is specified then don't save the preferences! if len(file_or_filename) > 0: # Do the import here so that we don't make 'ConfigObj' a # requirement if preferences aren't ever persisted (or a derived # class chooses to use a different persistence mechanism). from configobj import ConfigObj logger.debug('saving preferences to <%s>', file_or_filename) config_obj = ConfigObj(file_or_filename, encoding='utf-8') self._add_node_to_dictionary(self, config_obj) config_obj.write() return ########################################################################### # Protected 'Preferences' interface. # # These are the only methods that should access the protected '_children' # and '_preferences' traits. This helps make it easy to subclass this class # to create other implementations (all the subclass has to do is to # implement these protected methods). # ########################################################################### def _add_dictionary_to_node(self, node, dictionary): """ Add the contents of a dictionary to a node's preferences. """ self._lk.acquire() node._preferences.update(dictionary) self._lk.release() return def _add_node_to_dictionary(self, node, dictionary): """ Add a node's preferences to a dictionary. """ # This method never manipulates the '_preferences' trait directly. # Instead it does eveything via the other protected methods and hence # doesn't need to grab the lock. if len(node._keys()) > 0: dictionary[node.path] = {} for key in node._keys(): dictionary[node.path][key] = node._get(key) for name in node._node_names(): self._add_node_to_dictionary(node._get_child(name), dictionary) return def _add_preferences_listener(self, listener): """ Add a listener for changes to thisnode's preferences. """ self._lk.acquire() self._preferences_listeners.append(listener) self._lk.release() return def _clear(self): """ Remove all preferences from this node. """ self._lk.acquire() self._preferences.clear() self._lk.release() return def _create_child(self, name): """ Create a child of this node with the specified name. """ self._lk.acquire() child = self._children[name] = Preferences(name=name, parent=self) self._lk.release() return child def _get(self, key, default=None): """ Get the value of a preference in this node. """ self._lk.acquire() value = self._preferences.get(key, default) self._lk.release() return value def _get_child(self, name): """ Return the child of this node with the specified name. Return None if no such child exists. """ self._lk.acquire() child = self._children.get(name) self._lk.release() return child def _keys(self): """ Return the preference keys of this node. """ self._lk.acquire() keys = list(self._preferences.keys()) self._lk.release() return keys def _node(self, name): """ Return the child of this node with the specified name. Create the child node if it does not exist. """ node = self._get_child(name) if node is None: node = self._create_child(name) return node def _node_names(self): """ Return the names of the children of this node. """ self._lk.acquire() node_names = list(self._children.keys()) self._lk.release() return node_names def _remove(self, name): """ Remove a preference value from this node. """ self._lk.acquire() if name in self._preferences: del self._preferences[name] self._lk.release() return def _remove_preferences_listener(self, listener): """ Remove a listener for changes to the node's preferences. """ self._lk.acquire() if listener in self._preferences_listeners: self._preferences_listeners.remove(listener) self._lk.release() return def _set(self, key, value): """ Set the value of a preference in this node. """ # everything must be unicode encoded so that ConfigObj configuration # can properly serialize the data. Python str are supposed to be ASCII # encoded. value = six.text_type(value) self._lk.acquire() old = self._preferences.get(key) self._preferences[key] = value # If the value is unchanged then don't call the listeners! if old == value: listeners = [] else: listeners = self._preferences_listeners[:] self._lk.release() for listener in listeners: listener(self, key, old, value) return ########################################################################### # Debugging interface. ########################################################################### def dump(self, indent=''): """ Dump the preferences hierarchy to stdout. """ if indent == '': print() print(indent, 'Node(%s)' % self.name, self._preferences) indent += ' ' for child in self._children.values(): child.dump(indent) return #### EOF ###################################################################### apptools-4.5.0/apptools/preferences/__init__.py0000644000076500000240000000025211640354733023122 0ustar mdickinsonstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. """ Manages application preferences. Part of the AppTools project of the Enthought Tool Suite """ apptools-4.5.0/apptools/preferences/api.py0000644000076500000240000000050313422276145022133 0ustar mdickinsonstaff00000000000000from .i_preferences import IPreferences from .package_globals import get_default_preferences, set_default_preferences from .preferences import Preferences from .preference_binding import PreferenceBinding, bind_preference from .preferences_helper import PreferencesHelper from .scoped_preferences import ScopedPreferences apptools-4.5.0/apptools/preferences/package_globals.py0000644000076500000240000000137413013627532024463 0ustar mdickinsonstaff00000000000000""" Package-scope globals. The default preferences node is currently used by 'PreferencesHelper' and 'PreferencesBinding' instances if no specific preferences node is set. This makes it easy for them to access the root node of an application-wide preferences hierarchy. """ # The default preferences node. _default_preferences = None def get_default_preferences(): """ Get the default preferences node. """ return _default_preferences def set_default_preferences(default_preferences): """ Set the default preferences node. """ global _default_preferences _default_preferences = default_preferences # For convenience. return _default_preferences #### EOF ###################################################################### apptools-4.5.0/apptools/scripting/0000755000076500000240000000000013547652535020524 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/scripting/util.py0000644000076500000240000000266313422276145022051 0ustar mdickinsonstaff00000000000000"""Simple utility functions provided by the scripting API. """ # Author: Prabhu Ramachandran # Copyright (c) 2008, Prabhu Ramachandran # License: BSD Style. from .recorder import Recorder from .recorder_with_ui import RecorderWithUI from .package_globals import get_recorder, set_recorder ################################################################################ # Utility functions. ################################################################################ def start_recording(object, ui=True, **kw): """Convenience function to start recording. Returns the recorder. Parameters: ----------- object : object to record. ui : bool specifying if a UI is to be shown or not kw : Keyword arguments to pass to the register function of the recorder. """ if ui: r = RecorderWithUI(root=object) r.edit_traits(kind='live') else: r = Recorder() # Set the global recorder. set_recorder(r) r.recording = True r.register(object, **kw) return r def stop_recording(object, save=True): """Stop recording the object. If `save` is `True`, this will pop up a UI to ask where to save the script. """ recorder = get_recorder() recorder.unregister(object) recorder.recording = False # Set the global recorder back to None set_recorder(None) # Save the script. if save: recorder.ui_save() apptools-4.5.0/apptools/scripting/recordable.py0000644000076500000240000000314613422276145023173 0ustar mdickinsonstaff00000000000000""" Decorator to mark functions and methods as recordable. """ # Author: Prabhu Ramachandran # Copyright (c) 2008, Prabhu Ramachandran and Enthought, Inc. # License: BSD Style. from .package_globals import get_recorder # Guard to ensure that only the outermost recordable call is recorded # and nested calls ignored. _outermost_call = True def recordable(func): """A decorator that wraps a function into one that is recordable. This will record the function only if the global recorder has been set via a `set_recorder` function call. This is almost entirely copied from the apptools.appscripting.scriptable.scriptable decorator. """ def _wrapper(*args, **kw): """A wrapper returned to replace the decorated function.""" global _outermost_call # Boolean to specify if the method was recorded or not. record = False if _outermost_call: # Get the recorder. rec = get_recorder() if rec is not None: _outermost_call = False # Record the method if recorder is available. record = True try: result = rec.record_function(func, args, kw) finally: _outermost_call = True if not record: # If the method was not recorded, just call it. result = func(*args, **kw) return result # Mimic the actual function. _wrapper.__name__ = func.__name__ _wrapper.__doc__ = func.__doc__ _wrapper.__dict__.update(func.__dict__) return _wrapper apptools-4.5.0/apptools/scripting/recorder.py0000644000076500000240000006401713547637361022713 0ustar mdickinsonstaff00000000000000""" Code to support recording to a readable and executable Python script. TODO: - Support for dictionaries? """ # Author: Prabhu Ramachandran # Copyright (c) 2008-2015, Enthought, Inc. # License: BSD Style. import warnings import six import six.moves.builtins from traits.api import (HasTraits, List, Str, Dict, Bool, Unicode, Property, Int, Instance) from traits.util.camel_case import camel_case_to_python ################################################################################ # `_RegistryData` class. ################################################################################ class _RegistryData(HasTraits): # Object's script ID script_id = Property(Str) # Path to object in object hierarchy. path = Property(Str) # Parent data for this object if any. parent_data = Instance('_RegistryData', allow_none=True) # The name of the trait on the parent which is this object. trait_name_on_parent = Str('') # List of traits we are listening for on this object. names = List(Str) # Nested recordable instances on the object. sub_recordables = List(Str) # List of traits that are lists. list_names = List(Str) _script_id = Str('') ###################################################################### # Non-public interface. ###################################################################### def _get_path(self): pdata = self.parent_data path = '' if pdata is not None: pid = pdata.script_id ppath = pdata.path tnop = self.trait_name_on_parent if '[' in tnop: # If the object is a nested object through an iterator, # we instantiate it and don't refer to it through the # path, this makes scripting convenient. if len(ppath) == 0: path = pid + '.' + tnop else: path = ppath + '.' + tnop else: path = ppath + '.' + tnop return path def _get_script_id(self): sid = self._script_id if len(sid) == 0: pdata = self.parent_data sid = pdata.script_id + '.' + self.trait_name_on_parent return sid def _set_script_id(self, id): self._script_id = id ################################################################################ # `RecorderError` class. ################################################################################ class RecorderError(Exception): pass ################################################################################ # `Recorder` class. ################################################################################ class Recorder(HasTraits): # The lines of code recorded. lines = List(Str) # Are we recording or not? recording = Bool(False, desc='if script recording is enabled or not') # The Python script we have recorded so far. This is just a # convenience trait for the `get_code()` method. script = Property(Unicode) ######################################## # Private traits. # Dict used to store information on objects registered. It stores a # unique name for the object and its path in the object hierarchy # traversed. _registry = Dict # Reverse registry with keys as script_id and object as value. _reverse_registry = Dict # A mapping to generate unique names for objects. The key is the # name used (which is something derived from the class name of the # object) and the value is an integer describing the number of times # that variable name has been used earlier. _name_map = Dict(Str, Int) # A list of special reserved script IDs. This is handy when you # want a particular object to have an easy to read script ID and not # the default one based on its class name. This leads to slightly # easier to read scripts. _special_ids = List # What are the known names in the script? By known names we mean # names which are actually bound to objects. _known_ids = List(Str) # The known types in the namespace. _known_types = List(Str) # A guard to check if we are currently in a recorded function call, # in which case we don't want to do any recording. _in_function = Bool(False) ###################################################################### # `Recorder` interface. ###################################################################### def record(self, code): """Record a string to be stored to the output file. Parameters: ----------- code - A string of text. """ if self.recording and not self._in_function: lines = self.lines # Analyze the code and add extra code if needed. self._analyze_code(code) # Add the code. lines.append(code) def register(self, object, parent=None, trait_name_on_parent='', ignore=None, known=False, script_id=None): """Register an object with the recorder. This sets up the object for recording. By default all traits (except those starting and ending with '_') are recorded. For attributes that are themselves recordable, one may mark traits with a 'record' metadata as follows: - If metadata `record=False` is set, the nested object will not be recorded. - If `record=True`, then that object is also recorded if it is not `None`. If the object is a list or dict that is marked with `record=True`, the list is itself not listened to for changes but all its contents are registered. If the `object` has a trait named `recorder` then this recorder instance will be set to it if possible. Parameters: ----------- object : Instance(HasTraits) The object to register in the registry. parent : Instance(HasTraits) An optional parent object in which `object` is contained trait_name_on_parent : str An optional trait name of the `object` in the `parent`. ignore : list(str) An optional list of trait names on the `object` to be ignored. known : bool Optional specification if the `object` id is known on the interpreter. This is needed if you are manually injecting code to define/create an object. script_id : str Optionally specify a script_id to use for this object. It is not guaranteed that this ID will be used since it may already be in use. """ registry = self._registry # Do nothing if the object is already registered. if object in registry: return # When parent is specified the trait_name_on_parent must also be. if parent is not None: assert len(trait_name_on_parent) > 0 if ignore is None: ignore = [] if isinstance(object, HasTraits): # Always ignore these. ignore.extend(['trait_added', 'trait_modified']) sub_recordables = list(object.traits(record=True).keys()) # Find all the trait names we must ignore. ignore.extend(object.traits(record=False).keys()) # The traits to listen for. tnames = [t for t in object.trait_names() if not t.startswith('_') and not t.endswith('_') \ and t not in ignore] # Find all list traits. trts = object.traits() list_names = [] for t in tnames: tt = trts[t].trait_type if hasattr(tt, 'default_value_type') and \ tt.default_value_type == 5: list_names.append(t) else: # No traits, so we can't do much. sub_recordables = [] tnames = [] list_names = [] # Setup the registry data. # If a script id is supplied try and use it. sid = '' if script_id is not None: r_registry = self._reverse_registry while script_id in r_registry: script_id = '%s1'%script_id sid = script_id # Add the chosen id to special_id list. self._special_ids.append(sid) if parent is None: pdata = None if len(sid) == 0: sid = self._get_unique_name(object) else: pdata = self._get_registry_data(parent) tnop = trait_name_on_parent if '[' in tnop: # If the object is a nested object through an iterator, # we instantiate it and don't refer to it through the # path, this makes scripting convenient. sid = self._get_unique_name(object) # Register the object with the data. data = _RegistryData(script_id=sid, parent_data=pdata, trait_name_on_parent=trait_name_on_parent, names=tnames, sub_recordables=sub_recordables, list_names=list_names) registry[object] = data # Now get the script id of the object -- note that if sid is '' # above then the script_id is computed from that of the parent. sid = data.script_id # Setup reverse registry so we can get the object from the # script_id. self._reverse_registry[sid] = object # Record the script_id if the known argument is explicitly set to # True. if known: self._known_ids.append(sid) # Try and set the recorder attribute if necessary. if hasattr(object, 'recorder'): try: object.recorder = self except Exception as e: msg = "Cannot set 'recorder' trait of object %r: "\ "%s"%(object, e) warnings.warn(msg, warnings.RuntimeWarning) if isinstance(object, HasTraits): # Add handler for lists. for name in list_names: object.on_trait_change(self._list_items_listner, '%s_items'%name) # Register all sub-recordables. for name in sub_recordables: obj = getattr(object, name) if isinstance(obj, list): # Don't register the object itself but register its # children. for i, child in enumerate(obj): attr = '%s[%d]'%(name, i) self.register(child, parent=object, trait_name_on_parent=attr) elif obj is not None: self.register(obj, parent=object, trait_name_on_parent=name) # Listen for changes to the trait itself so the newly # assigned object can also be listened to. object.on_trait_change(self._object_changed_handler, name) # Now add listner for the object itself. object.on_trait_change(self._listner, tnames) def unregister(self, object): """Unregister the given object from the recorder. This inverts the logic of the `register(...)` method. """ registry = self._registry # Do nothing if the object isn't registered. if object not in registry: return data = registry[object] # Try and unset the recorder attribute if necessary. if hasattr(object, 'recorder'): try: object.recorder = None except Exception as e: msg = "Cannot unset 'recorder' trait of object %r:"\ "%s"%(object, e) warnings.warn(msg, warnings.RuntimeWarning) if isinstance(object, HasTraits): # Remove all list_items handlers. for name in data.list_names: object.on_trait_change(self._list_items_listner, '%s_items'%name, remove=True) # Unregister all sub-recordables. for name in data.sub_recordables: obj = getattr(object, name) if isinstance(obj, list): # Unregister the children. for i, child in enumerate(obj): self.unregister(child) elif obj is not None: self.unregister(obj) # Remove the trait handler for trait assignments. object.on_trait_change(self._object_changed_handler, name, remove=True) # Now remove listner for the object itself. object.on_trait_change(self._listner, data.names, remove=True) # Remove the object data from the registry etc. if data.script_id in self._known_ids: self._known_ids.remove(data.script_id) del self._reverse_registry[data.script_id] del registry[object] def save(self, file): """Save the recorded lines to the given file. It does not close the file. """ if six.PY3: file.write(self.get_code()) else: file.write(six.text_type(self.get_code(), encoding='utf-8')) file.flush() def record_function(self, func, args, kw): """Record a function call given the function and its arguments.""" if self.recording and not self._in_function: # Record the function name and arguments. call_str = self._function_as_string(func, args, kw) # Call the function. try: self._in_function = True result = func(*args, **kw) finally: self._in_function = False # Register the result if it is not None. if func.__name__ == '__init__': f_self = args[0] code = self._import_class_string(f_self.__class__) self.lines.append(code) return_str = self._registry.get(f_self).script_id else: return_str = self._return_as_string(result) if len(return_str) > 0: self.lines.append('%s = %s'%(return_str, call_str)) else: self.lines.append('%s'%(call_str)) else: result = func(*args, **kw) return result def ui_save(self): """Save recording to file, pop up a UI dialog to find out where and close the file when done. """ from pyface.api import FileDialog, OK wildcard = 'Python files (*.py)|*.py|' + FileDialog.WILDCARD_ALL dialog = FileDialog(title='Save Script', action='save as', wildcard=wildcard ) if dialog.open() == OK: fname = dialog.path f = open(fname, 'w') self.save(f) f.close() def clear(self): """Clears all previous recorded state and unregisters all registered objects.""" # First unregister any registered objects. registry = self._registry while len(registry) > 0: self.unregister(list(registry.keys())[0]) # Clear the various lists. self.lines[:] = [] self._registry.clear() self._known_ids[:] = [] self._name_map.clear() self._reverse_registry.clear() self._known_types[:] = [] self._special_ids[:] = [] def get_code(self): """Returns the recorded lines as a string of printable code.""" return '\n'.join(self.lines) + '\n' def is_registered(self, object): """Returns True if the given object is registered with the recorder.""" return object in self._registry def get_script_id(self, object): """Returns the script_id of a registered object. Useful when you want to manually add a record statement.""" return self._get_registry_data(object).script_id def get_object_path(self, object): """Returns the path in the object hierarchy of a registered object. Useful for debugging.""" return self._get_registry_data(object).path def write_script_id_in_namespace(self, script_id): """If a script_id is not known in the current script's namespace, this sets it using the path of the object or actually instantiating it. If this is not possible (since the script_id matches no existing object), nothing is recorded but the framework is notified that the particular script_id is available in the namespace. This is useful when you want to inject code in the namespace to create a particular object. """ if not self.recording: return known_ids = self._known_ids if script_id not in known_ids: obj = self._reverse_registry.get(script_id) # Add the ID to the known_ids. known_ids.append(script_id) if obj is not None: data = self._registry.get(obj) result = '' if len(data.path) > 0: # Record code for instantiation of object. result = '%s = %s'%(script_id, data.path) else: # This is not the best thing to do but better than # nothing. result = self._import_class_string(obj.__class__) cls = obj.__class__.__name__ mod = obj.__module__ result += '\n%s = %s()'%(script_id, cls) if len(result) > 0: self.lines.extend(result.split('\n')) ###################################################################### # Non-public interface. ###################################################################### def _get_unique_name(self, obj): """Return a unique object name (a string). Note that this does not cache the object, so if called with the same object 3 times you'll get three different names. """ cname = obj.__class__.__name__ nm = self._name_map result = '' builtin = False if cname in six.moves.builtins.__dict__: builtin = True if hasattr(obj, '__name__'): cname = obj.__name__ else: cname = camel_case_to_python(cname) special_ids = self._special_ids while len(result) == 0 or result in special_ids: if cname in nm: id = nm[cname] + 1 nm[cname] = id result = '%s%d'%(cname, id) else: nm[cname] = 0 # The first id doesn't need a number if it isn't builtin. if builtin: result = '%s0'%(cname) else: result = cname return result def _get_registry_data(self, object): """Get the data for an object from registry.""" data = self._registry.get(object) if data is None: msg = "Recorder: Can't get script_id since object %s not registered" raise RecorderError(msg%(object)) return data def _listner(self, object, name, old, new): """The listner for trait changes on an object. This is called by child listners or when any of the recordable object's traits change when recording to a script is enabled. Parameters: ----------- object : Object which has changed. name : extended name of attribute that changed. old : Old value. new : New value. """ if self.recording and not self._in_function: new_repr = repr(new) sid = self._get_registry_data(object).script_id if len(sid) == 0: msg = '%s = %r'%(name, new) else: msg = '%s.%s = %r'%(sid, name, new) if new_repr.startswith('<') and new_repr.endswith('>'): self.record('# ' + msg) else: self.record(msg) def _list_items_listner(self, object, name, old, event): """The listner for *_items on list traits of the object. """ # Set the path of registered objects in the modified list and # all their children. This is done by unregistering the object # and re-registering them. This is slow but. registry = self._registry sid = registry.get(object).script_id trait_name = name[:-6] items = getattr(object, trait_name) for (i, item) in enumerate(items): if item in registry: data = registry.get(item) tnop = data.trait_name_on_parent if len(tnop) > 0: data.trait_name_on_parent = '%s[%d]'%(trait_name, i) # Record the change. if self.recording and not self._in_function: index = event.index removed = event.removed added = event.added nr = len(removed) slice = '[%d:%d]'%(index, index + nr) na = len(added) rhs = [self._object_as_string(item) for item in added] rhs = ', '.join(rhs) obj = '%s.%s'%(sid, name[:-6]) msg = '%s%s = [%s]'%(obj, slice, rhs) self.record(msg) def _object_changed_handler(self, object, name, old, new): """Called when a child recordable object has been reassigned.""" registry = self._registry if old is not None: if old in registry: self.unregister(old) if new is not None: if new not in registry: self.register(new, parent=object, trait_name_on_parent=name) def _get_script(self): return self.get_code() def _analyze_code(self, code): """Analyze the code and return extra code if needed. """ known_ids = self._known_ids lhs = '' try: lhs = code.split()[0] except IndexError: pass if '.' in lhs: ob_name = lhs.split('.')[0] self.write_script_id_in_namespace(ob_name) def _function_as_string(self, func, args, kw): """Return a string representing the function call.""" func_name = func.__name__ func_code = func.__code__ # Even if func is really a decorated method it never shows up as # a bound or unbound method here, so we have to inspect the # argument names to figure out if this is a method or function. if func_code.co_argcount > 0 and \ func_code.co_varnames[0] == 'self': # This is a method, the first argument is bound to self. f_self = args[0] # Convert the remaining arguments to strings. argl = [self._object_as_string(arg) for arg in args[1:]] # If this is __init__ we special case it. if func_name == '__init__': # Register the object. self.register(f_self, known=True) func_name = f_self.__class__.__name__ else: sid = self._object_as_string(f_self) func_name = '%s.%s'%(sid, func_name) else: argl = [self._object_as_string(arg) for arg in args] # Convert the keyword args. kwl = ['%s=%s'%(key, self._object_as_string(value)) for key, value in kw.items()] argl.extend(kwl) # Make a string representation of the args, kw. argstr = ', '.join(argl) return '%s(%s)'%(func_name, argstr) def _is_arbitrary_object(self, object): """Return True if the object is an arbitrary non-primitive object. As done in appscripting, we assume that if the hex id of the object is in its string representation then it is an arbitrary object. """ ob_id = id(object) orepr = repr(object) hex_id = "%x"%ob_id return hex_id.upper() in orepr.upper() def _object_as_string(self, object): """Return a string representing the object. """ registry = self._registry if object in registry: # Return script id if the object is known; create the script # id on the namespace if needed before that. sid = registry.get(object).script_id base_id = sid.split('.')[0] self.write_script_id_in_namespace(base_id) return sid else: if not self._is_arbitrary_object(object): return repr(object) # If we get here, we just register the object and call ourselves # again to do the needful. self.register(object) return self._object_as_string(object) def _return_as_string(self, object): """Return a string given a returned object from a function. """ result = '' long_type = long if six.PY2 else int ignore = (float, complex, bool, int, long_type, str) if object is not None and type(object) not in ignore: # If object is not know, register it. registry = self._registry if object not in registry: self.register(object) result = registry.get(object).script_id # Since this is returned it is known on the namespace. known_ids = self._known_ids if result not in known_ids: known_ids.append(result) return result def _import_class_string(self, cls): """Import a class if needed. """ cname = cls.__name__ result = '' if cname not in six.moves.builtins.__dict__: mod = cls.__module__ typename = '%s.%s'%(mod, cname) if typename not in self._known_types: result = 'from %s import %s'%(mod, cname) self._known_types.append(typename) return result apptools-4.5.0/apptools/scripting/tests/0000755000076500000240000000000013547652535021666 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/scripting/tests/test_recorder.py0000644000076500000240000003501213422276145025074 0ustar mdickinsonstaff00000000000000""" Unit tests for the script recorder. """ # Author: Prabhu Ramachandran # Copyright (c) 2008-2015, Enthought, Inc. # License: BSD Style. import unittest from traits.api import (HasTraits, Float, Instance, Str, List, Bool, HasStrictTraits, Tuple, Range, TraitPrefixMap, Trait) from apptools.scripting.recorder import Recorder from apptools.scripting.recordable import recordable from apptools.scripting.package_globals import set_recorder ###################################################################### # Test classes. class Property(HasStrictTraits): color = Tuple(Range(0.0, 1.0), Range(0.0, 1.0), Range(0.0, 1.0)) opacity = Range(0.0, 1.0, 1.0) representation = Trait('surface', TraitPrefixMap({'surface':2, 'wireframe': 1, 'points': 0})) class Toy(HasTraits): color = Str type = Str ignore = Bool(False, record=False) class Child(HasTraits): name = Str('child') age = Float(10.0) property = Instance(Property, (), record=True) toy = Instance(Toy, record=True) friends = List(Str) @recordable def grow(self, x): """Increase age by x years.""" self.age += x self.f(1) @recordable def f(self, args): """Function f.""" return args def not_recordable(self): pass class Parent(HasTraits): children = List(Child, record=True) recorder = Instance(Recorder, record=False) class Test(HasTraits): # This should be set. recorder = Instance(HasTraits) # These should be ignored. _ignore = Bool(False) ignore_ = Bool(False) class TestRecorder(unittest.TestCase): def setUp(self): self.tape = Recorder() set_recorder(self.tape) p = Parent() c = Child() toy = Toy(color='blue', type='bunny') c.toy = toy p.children.append(c) self.p = p return def tearDown(self): self.tape.clear() set_recorder(None) return def test_unique_name(self): "Does the get_unique_id method work." class XMLUnstructuredGridWriter: pass t = XMLUnstructuredGridWriter() tape = self.tape self.assertEqual(tape._get_unique_name(t), 'xml_unstructured_grid_writer') self.assertEqual(tape._get_unique_name(t), 'xml_unstructured_grid_writer1') t = Toy() self.assertEqual(tape._get_unique_name(t), 'toy') t = (1, 2) self.assertEqual(tape._get_unique_name(t), 'tuple0') l = [1, 2] self.assertEqual(tape._get_unique_name(l), 'list0') d = {'a': 1} self.assertEqual(tape._get_unique_name(d), 'dict0') self.assertEqual(tape._get_unique_name(1), 'int0') def test_record(self): "Does recording work correctly." tape = self.tape p = self.p c = p.children[0] toy = c.toy # start recording. tape.recording = True tape.register(p) # Test if p's recorder attribute is set. self.assertEqual(tape, p.recorder) # Test script ids and object path. self.assertEqual(tape.get_script_id(p), 'parent') self.assertEqual(tape.get_object_path(p), '') self.assertEqual(tape.get_script_id(c), 'child') self.assertEqual(tape.get_object_path(c), 'parent.children[0]') self.assertEqual(tape.get_script_id(toy), 'child.toy') self.assertEqual(tape.get_object_path(toy), 'parent.children[0].toy') c.name = 'Ram' # The child should first be instantiated. self.assertEqual(tape.lines[-2], "child = parent.children[0]") # Then its trait set. self.assertEqual(tape.lines[-1], "child.name = 'Ram'") c.age = 10.5 self.assertEqual(tape.lines[-1], "child.age = 10.5") c.property.representation = 'w' self.assertEqual(tape.lines[-1], "child.property.representation = 'wireframe'") c.property.color = (1, 0, 0) self.assertEqual(tape.lines[-1], "child.property.color = (1.0, 0.0, 0.0)") toy.color = 'red' self.assertEqual(tape.lines[-1], "child.toy.color = 'red'") toy.type = 'teddy' self.assertEqual(tape.lines[-1], "child.toy.type = 'teddy'") # This trait should be ignored. toy.ignore = True self.assertEqual(tape.lines[-1], "child.toy.type = 'teddy'") # Turn of recording and test. tape.recording = False toy.type = 'rat' self.assertEqual(tape.lines[-1], "child.toy.type = 'teddy'") #print tape.script # Stop recording. n = len(tape.lines) tape.unregister(p) c.property.representation = 'points' toy.type = 'bunny' self.assertEqual(tape.lines[-1], "child.toy.type = 'teddy'") self.assertEqual(n, len(tape.lines)) # Make sure the internal data of the recorder is cleared. self.assertEqual(0, len(tape._registry)) self.assertEqual(0, len(tape._reverse_registry)) self.assertEqual(0, len(tape._known_ids)) def test_recorded_trait_replaced(self): "Does recording work right when a trait is replaced." tape = self.tape p = self.p c = p.children[0] toy = c.toy # start recording. tape.recording = True tape.register(p) # Test the original trait. toy.color = 'red' self.assertEqual(tape.lines[-1], "child.toy.color = 'red'") # Now reassign the toy. t1 = Toy(name='ball') c.toy = t1 t1.color = 'yellow' self.assertEqual(tape.lines[-1], "child.toy.color = 'yellow'") def test_clear(self): "Test the clear method." p = self.p tape = self.tape tape.register(p) tape.clear() # Everything should be unregistered. self.assertEqual(p.recorder, None) # Internal data should be wiped clean. self.assertEqual(0, len(tape._registry)) self.assertEqual(0, len(tape._reverse_registry)) self.assertEqual(0, len(tape._known_ids)) self.assertEqual(0, len(tape._name_map)) def test_create_object(self): "Is the object imported and created if unknown?" tape = self.tape tape.recording = True t = Toy() tape.register(t) t.type = 'computer' # Since the name toy is unknown, there should be a # line to create it. self.assertEqual(tape.lines[-3][-10:], "import Toy") self.assertEqual(tape.lines[-2], "toy = Toy()") self.assertEqual(tape.lines[-1], "toy.type = 'computer'") # Since this one is known, there should be no imports or # anything. t1 = Toy() tape.register(t1, known=True) t1.type = 'ball' self.assertEqual(tape.lines[-2], "toy.type = 'computer'") self.assertEqual(tape.lines[-1], "toy1.type = 'ball'") def test_list_items_changed(self): "Test if a list item is changed does the change get recorded." p = self.p tape = self.tape child = p.children[0] tape.register(p, known=True) tape.recording = True child.friends = ['Krishna', 'Ajay', 'Ali'] self.assertEqual(tape.lines[-1], "child.friends = ['Krishna', 'Ajay', 'Ali']") child.friends[1:] = ['Sam', 'Frodo'] self.assertEqual(tape.lines[-1], "child.friends[1:3] = ['Sam', 'Frodo']") child.friends[1] = 'Hari' self.assertEqual(tape.lines[-1], "child.friends[1:2] = ['Hari']") # What if we change a list where record=True. child1 = Child() tape.register(child1) p.children.append(child1) self.assertEqual(tape.lines[-1], "parent.children[1:1] = [child1]") del p.children[1] self.assertEqual(tape.lines[-1], "parent.children[1:2] = []") p.children[0] = child1 self.assertEqual(tape.lines[-1], "parent.children[0:1] = [child1]") def test_path_change_on_list(self): "Does the object path update when a list has changed?" # Test the case where we have a hierarchy and we change the # list. tape = self.tape p = self.p child1 = Child() p.children.append(child1) tape.register(p) tape.recording = True self.assertEqual(tape.get_object_path(child1), 'parent.children[1]') self.assertEqual(tape.get_script_id(child1), 'child1') del p.children[0] self.assertEqual(tape.get_object_path(child1), 'parent.children[0]') self.assertEqual(tape.get_script_id(child1), 'child1') def test_write_script_id_in_namespace(self): "Test the write_script_id_in_namespace method." tape = self.tape tape.recording = True # This should not cause an error but insert the name 'foo' in the # namespace. tape.write_script_id_in_namespace('foo') def test_recorder_and_ignored(self): "Test if recorder trait is set and private traits are ignored." t = Test() self.assertEqual(t.recorder, None) self.assertEqual(t._ignore, False) self.assertEqual(t.ignore_, False) tape = Recorder() tape.register(t) tape.recording = True self.assertEqual(t.recorder, tape) t._ignore = True t.ignore_ = True self.assertEqual(len(tape.script.strip()), 0) def test_record_function(self): "See if recordable function calls are handled correctly." # Note that the global recorder is set in setUp and removed in # tearDown. tape = self.tape c = self.p.children[0] tape.register(c) tape.recording = True # Setting the age should be recorded. c.age = 11 self.assertEqual(tape.lines[-1], "child.age = 11.0") # This should also work without problems. c.f(c.toy) self.assertEqual(tape.lines[-2], "child.age = 11.0") self.assertEqual(tape.lines[-1], 'child.toy = child.f(child.toy)') # Calling f should be recorded. c.f(1) self.assertEqual(tape.lines[-1], "child.f(1)") # This should not record the call to f or the change to the age # trait inside grow. c.grow(1) self.assertEqual(c.age, 12.0) self.assertEqual(tape.lines[-2], "child.f(1)") self.assertEqual(tape.lines[-1], "child.grow(1)") # Non-recordable functions shouldn't be. c.not_recordable() self.assertEqual(tape.lines[-1], "child.grow(1)") # Test a simple recordable function. @recordable def func(x, y): return x, y result = func(1, 2) self.assertEqual(tape.lines[-1], "tuple0 = func(1, 2)") def test_non_has_traits(self): "Can classes not using traits be handled?" tape = self.tape p = self.p c = p.children[0] class A(object): @recordable def __init__(self, x, y=1): self.x = x self.y = y @recordable def f(self, x, y): return x, y @recordable def g(self, x): return x def not_recordable(self): pass tape.register(p) tape.recording = True # Test if __init__ is recorded correctly. a = A(x=1) # Should record. a.f(1, 'asd') self.assertEqual(tape.lines[-3][-8:], "import A") self.assertEqual(tape.lines[-2], "a = A(x=1)") self.assertEqual(tape.lines[-1], "tuple0 = a.f(1, 'asd')") result = a.f(p, c) # This should instantiate the parent first, get the child from # that and then record the call itself. self.assertEqual(tape.lines[-3], "parent = Parent()") self.assertEqual(tape.lines[-2], "child = parent.children[0]") self.assertEqual(tape.lines[-1], "tuple1 = a.f(parent, child)") # This should simply refer to the child. result = a.g(c) self.assertEqual(tape.lines[-1], "child = a.g(child)") # Should do nothing. a.not_recordable() self.assertEqual(tape.lines[-1], "child = a.g(child)") # When a function is called with unknown args it should attempt # to create the objects. r = a.g(Toy()) self.assertEqual(tape.lines[-3][-10:], "import Toy") self.assertEqual(tape.lines[-2], "toy = Toy()") self.assertEqual(tape.lines[-1], "toy = a.g(toy)") def test_set_script_id(self): "Test if setting script_id at registration time works." tape = self.tape p = self.p c = p.children[0] tape.register(p, script_id='child') tape.recording = True # Ask to be called child. self.assertEqual(tape.get_script_id(p), 'child') # Register another Child. c1 = Child() tape.register(c1) # Will be child2 since child1 is taken. self.assertEqual(tape.get_script_id(c1), 'child2') # Test if recording works correctly with the changed script_id. p.children.append(c1) self.assertEqual(tape.lines[-1], "child.children[1:1] = [child2]") def test_save(self): "Test if saving tape to file works." tape = self.tape p = self.p c = p.children[0] toy = c.toy # Start recording tape.register(p) tape.recording = True toy.type = 'teddy' # Now stop. tape.recording = False tape.unregister(p) import io f = io.StringIO() tape.save(f) # Test if the file is OK. expect = ["child = parent.children[0]\n", "child.toy.type = 'teddy'\n" ] f.seek(0) lines = f.readlines() self.assertEqual(expect, lines) f.close() if __name__ == '__main__': unittest.main() apptools-4.5.0/apptools/scripting/__init__.py0000644000076500000240000000102711640354733022624 0ustar mdickinsonstaff00000000000000# Copyright (c) 2008-2011, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Prabhu Ramachandran """ Automatic script recording framework, part of the AppTools project of the Enthought Tool Suite. """ apptools-4.5.0/apptools/scripting/api.py0000644000076500000240000000064113422276145021637 0ustar mdickinsonstaff00000000000000"""Public API for the scripting package. """ # Author: Prabhu Ramachandran # Copyright (c) 2008, Prabhu Ramachandran and Enthought, Inc. # License: BSD Style. from .recorder import Recorder, RecorderError from .recordable import recordable from .package_globals import get_recorder, set_recorder from .recorder_with_ui import RecorderWithUI from .util import start_recording, stop_recording apptools-4.5.0/apptools/scripting/package_globals.py0000644000076500000240000000075511640354733024172 0ustar mdickinsonstaff00000000000000""" Globals for the scripting package. """ # Author: Prabhu Ramachandran # Copyright (c) 2008, Prabhu Ramachandran and Enthought, Inc. # License: BSD Style. # The global recorder. _recorder = None def get_recorder(): """Return the global recorder. Does not create a new one if none exists. """ global _recorder return _recorder def set_recorder(rec): """Set the global recorder instance. """ global _recorder _recorder = rec apptools-4.5.0/apptools/scripting/recorder_with_ui.py0000644000076500000240000000570013547637361024435 0ustar mdickinsonstaff00000000000000""" A Recorder subclass that presents a simple user interface. """ # Author: Prabhu Ramachandran # Copyright (c) 2008, Prabhu Ramachandran. # License: BSD Style. from traits.api import Code, Button, Int, on_trait_change, Any from traitsui.api import (View, Item, Group, HGroup, CodeEditor, spring, Handler) from .recorder import Recorder ###################################################################### # `CloseHandler` class. ###################################################################### class CloseHandler(Handler): """This class cleans up after the UI for the recorder is closed.""" def close(self, info, is_ok): """This method is invoked when the user closes the UI.""" recorder = info.object recorder.on_ui_close() return True ################################################################################ # `RecorderWithUI` class. ################################################################################ class RecorderWithUI(Recorder): """ This class represents a Recorder but with a simple user interface. """ # The code to display code = Code(editor=CodeEditor(line='current_line')) # Button to save script to file. save_script = Button('Save Script') # The current line to show, used by the editor. current_line = Int # The root object which is being recorded. root = Any ######################################## # Traits View. view = View( Group( HGroup(Item('recording', show_label=True), spring, Item('save_script', show_label=False), ), Group(Item('code', show_label=False)), ), width=600, height=360, id='apptools.scripting.recorder_with_ui', buttons=['Cancel'], resizable=True, handler=CloseHandler() ) ###################################################################### # RecorderWithUI interface. ###################################################################### def on_ui_close(self): """Called from the CloseHandler when the UI is closed. This method basically stops the recording. """ from .util import stop_recording from .package_globals import get_recorder if get_recorder() is self: stop_recording(self.root, save=False) else: self.recording = False self.unregister(self.root) ###################################################################### # Non-public interface. ###################################################################### @on_trait_change('lines[]') def _update_code(self): self.code = self.get_code() self.current_line = len(self.lines) + 1 def _save_script_fired(self): self.ui_save() apptools-4.5.0/apptools/logger/0000755000076500000240000000000013547652535020001 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/util.py0000644000076500000240000000730411640354733021323 0ustar mdickinsonstaff00000000000000""" Utility functions. fixme: I don't like random collections of utility functions! Where should this go? """ # Standard library imports. import os from os.path import basename, dirname, isdir, splitdrive, splitext from zipfile import is_zipfile, ZipFile def get_module_name(filename): """ Get the fully qualified module name for a filename. For example, if the filename is /enthought/envisage/core/core_plugin_definition.py this method would return envisage.core.core_plugin_definition """ if os.path.exists(filename): # Get the name of the module minus the '.py' module, ext = os.path.splitext(os.path.basename(filename)) # Start with the actual module name. module_path = [module] # If the directory is a Python package then add it to the module path. #return self.is_folder and '__init__.py' in os.listdir(self.path) parent = dirname(filename) while isdir(parent) and '__init__.py' in os.listdir(parent): bname = basename(parent) module_path.insert(0, splitext(bname)[0]) parent = dirname(parent) module_name = '.'.join(module_path) # If the file does not exist then it might be a zip file path. else: module_name = get_module_name_from_zip(filename) return module_name # fixme: WIP def get_module_name_from_zip(filename): # first, find the zip file in the path filepath = filename zippath = None while not is_zipfile(filepath) and \ splitdrive(filepath)[1] != '\\' \ and splitdrive(filepath)[1] != '/': filepath, tail = os.path.split(filepath) if zippath is not None: zippath = tail + '/' + zippath else: zippath = tail if not is_zipfile(filepath): return None # if the split left a preceding slash on the zippath then remove # it if zippath.startswith('\\') or zippath.startswith('/'): zippath = zippath[1:] # replace any backwards slashes with forward slashes zippath = zippath.replace('\\', '/') # Get the name of the module minus the '.py' module, ext = splitext(basename(zippath)) # Start with the actual module name. module_path = [module] # to get the module name, we walk through the zippath until we # find a parent directory that does NOT have a __init__.py file z = ZipFile(filepath) parentpath = dirname(zippath) while path_exists_in_zip(z, parentpath + '/__init__.py'): module_path.insert(0, basename(parentpath)) parentpath = dirname(parentpath) z.close() return '.'.join(module_path) # fixme: WIP def path_exists_in_zip(zfile, path): try: zfile.getinfo(path) exists = True except: exists = False return exists # fixme: WIP def is_zip_path(path): """ Returns True if the path refers to a zip file. """ filepath = path while not is_zipfile(filepath) and \ splitdrive(filepath)[1] != '\\' \ and splitdrive(filepath)[1] != '/': filepath = dirname(filepath) return is_zipfile(filepath) # fixme: WIP def get_zip_path(filename): """ Returns the path to the zip file contained in the filename. fixme: An example here would help. """ filepath = filename zippath = None while not is_zipfile(filepath) and \ splitdrive(filepath)[1] != '\\' \ and splitdrive(filepath)[1] != '/': filepath, tail = os.path.split(filepath) if zippath is not None: zippath = tail + '/' + zippath else: zippath = tail return zippath #### EOF ###################################################################### apptools-4.5.0/apptools/logger/log_point.py0000644000076500000240000000317413547637370022352 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Prints a stack trace every time it is called but does not halt execution of the application. Copied from Uche Ogbuji's blog """ # Standard library imports. import inspect # Third-party library imports. from six import StringIO def log_point(msg='\n'): stack = inspect.stack() # get rid of logPoint's part of the stack: stack = stack[1:] stack.reverse() output = StringIO() if msg: output.write(str(msg) + '\n') for stackLine in stack: frame, filename, line, funcname, lines, unknown = stackLine if filename.endswith('/unittest.py'): # unittest.py code is a boring part of the traceback continue if filename.startswith('./'): filename = filename[2:] output.write('%s:%s in %s:\n' % (filename, line, funcname)) if lines: output.write(' %s\n' % ''.join(lines)[:-1]) s = output.getvalue() return s ## EOF ################################################################## apptools-4.5.0/apptools/logger/plugin/0000755000076500000240000000000013547652535021277 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/plugin/preferences.ini0000644000076500000240000000015211640354733024266 0ustar mdickinsonstaff00000000000000[enthought.logger] level = 'Info' enable_agent = False smtp_server = '' to_address = '' from_address = '' apptools-4.5.0/apptools/logger/plugin/logger_preferences.py0000644000076500000240000000145211640354733025502 0ustar mdickinsonstaff00000000000000import logging from apptools.preferences.api import PreferencesHelper from traits.api import Bool, Str, Trait class LoggerPreferences(PreferencesHelper): """ The persistent service exposing the Logger plugin's API. """ #### Preferences ########################################################### # The log levels level = Trait('Info', {'Debug' : logging.DEBUG, 'Info' : logging.INFO, 'Warning' : logging.WARNING, 'Error' : logging.ERROR, 'Critical' : logging.CRITICAL, }, is_str = True, ) enable_agent = Bool(False) smtp_server = Str() to_address = Str() from_address = Str() # The path to the preferences node that contains the preferences. preferences_path = Str('apptools.logger') apptools-4.5.0/apptools/logger/plugin/logger_service.py0000644000076500000240000001166713547637370024663 0ustar mdickinsonstaff00000000000000# Standard library imports import logging import os import zipfile # Third-party library imports from io import BytesIO # Enthought library imports from pyface.workbench.api import View as WorkbenchView from traits.api import Any, Callable, HasTraits, Instance, List, \ Property, Undefined, on_trait_change root_logger = logging.getLogger() logger = logging.getLogger(__name__) class LoggerService(HasTraits): """ The persistent service exposing the Logger plugin's API. """ # The Envisage application. application = Any() # The logging Handler we use. handler = Any() # Our associated LoggerPreferences. preferences = Any() # The view we use. plugin_view = Instance(WorkbenchView) # Contributions from other plugins. mail_files = Property(List(Callable)) def save_preferences(self): """ Save the preferences. """ self.preferences.preferences.save() def whole_log_text(self): """ Return all of the logged data as formatted text. """ lines = [ self.handler.format(rec) for rec in self.handler.get() ] # Ensure that we end with a newline. lines.append('') text = '\n'.join(lines) return text def create_email_message(self, fromaddr, toaddrs, ccaddrs, subject, priority, include_userdata=False, stack_trace="", comments="", include_environment=True): """ Format a bug report email from the log files. """ from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText message = MIMEMultipart() message['Subject'] = "%s [priority=%s]" % (subject, priority) message['To'] = ', '.join(toaddrs) message['Cc'] = ', '.join(ccaddrs) message['From'] = fromaddr message.preamble = 'You will not see this in a MIME-aware mail ' \ 'reader.\n' message.epilogue = ' ' # To guarantee the message ends with a newline # First section is simple ASCII data ... m = [] m.append("Bug Report") m.append("==============================") m.append("") if len(comments) > 0: m.append("Comments:") m.append("========") m.append(comments) m.append("") if len(stack_trace) > 0: m.append("Stack Trace:") m.append("===========") m.append(stack_trace) m.append("") msg = MIMEText('\n'.join(m)) message.attach(msg) # Include the log file ... logtext = self.whole_log_text() msg = MIMEText(logtext) msg.add_header('Content-Disposition', 'attachment', filename='logfile.txt') message.attach(msg) # Include the environment variables ... # FIXME: ask the user, maybe? if include_environment: # Transmit the user's environment settings as well. Main purpose is # to work out the user name to help with following up on bug reports # and in future we should probably send less data. entries = [] for key, value in sorted(os.environ.items()): entries.append('%30s : %s\n' % (key, value)) msg = MIMEText(''.join(entries)) msg.add_header('Content-Disposition', 'attachment', filename='environment.txt') message.attach(msg) if include_userdata and len(self.mail_files) != 0: f = BytesIO() zf = zipfile.ZipFile(f, 'w') for mf in self.mail_files: mf(zf) zf.close() msg = MIMEApplication(f.getvalue()) msg.add_header('Content-Disposition', 'attachment', filename='userdata.zip') message.attach(msg) return message def send_bug_report(self, smtp_server, fromaddr, toaddrs, ccaddrs, message): """ Send a bug report email. """ try: import smtplib logger.debug("Connecting to: %s" % smtp_server) server = smtplib.SMTP(host=smtp_server) logger.debug("Connected: %s" % server) #server.set_debuglevel(1) server.sendmail(fromaddr, toaddrs + ccaddrs, message.as_string()) server.quit() except Exception as e: logger.exception("Problem sending error report") #### Traits stuff ######################################################### def _get_mail_files(self): return self.application.get_extensions( 'apptools.logger.plugin.mail_files') @on_trait_change('preferences.level_') def _level_changed(self, new): if (new is not None and new is not Undefined and self.handler is not None): root_logger.setLevel(self.preferences.level_) self.handler.setLevel(self.preferences.level_) apptools-4.5.0/apptools/logger/plugin/tests/0000755000076500000240000000000013547652535022441 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/plugin/tests/__init__.py0000644000076500000240000000000013547637370024540 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/plugin/tests/test_logger_service.py0000644000076500000240000000312213547637370027047 0ustar mdickinsonstaff00000000000000from email.mime.multipart import MIMEMultipart import unittest try: # On Python 3, mock is part of the standard library, from unittest import mock except ImportError: # Whereas on Python 2 it is not. import mock from apptools.logger.plugin.logger_service import LoggerService class LoggerServiceTestCase(unittest.TestCase): def test_create_email_message(self): logger_service = LoggerService() with mock.patch.object(logger_service, 'whole_log_text')\ as mocked_log_txt: mocked_log_txt.return_value = "Dummy log data" msg = logger_service.create_email_message( fromaddr='', toaddrs='', ccaddrs='', subject='', priority='' ) self.assertIsInstance(msg, MIMEMultipart) def test_create_email_message_with_user_data(self): # We used a mocked logger service which doesn't depend on the # application trait and the presence of extensions to the extension # point `apptools.logger.plugin.mail_files` class MockedLoggerService(LoggerService): def _get_mail_files(self): return [lambda zip_file : None] logger_service = MockedLoggerService() with mock.patch.object(logger_service, 'whole_log_text')\ as mocked_log_txt: mocked_log_txt.return_value = "Dummy log data" msg = logger_service.create_email_message( fromaddr='', toaddrs='', ccaddrs='', subject='', priority='', include_userdata=True, ) self.assertIsInstance(msg, MIMEMultipart) apptools-4.5.0/apptools/logger/plugin/__init__.py0000644000076500000240000000000011640354733023365 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/plugin/logger_plugin.py0000644000076500000240000000732013547637361024510 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Logger plugin. """ # Standard library imports. import logging # Enthought library imports. from envisage.api import ExtensionPoint, Plugin from apptools.logger.log_queue_handler import LogQueueHandler from traits.api import Callable, List # Local imports. from .logger_preferences import LoggerPreferences from .logger_service import LoggerService ID = 'apptools.logger' ILOGGER = ID + '.plugin.logger_service.LoggerService' class LoggerPlugin(Plugin): """ Logger plugin. """ id = ID name = 'Logger plugin' #### Extension points for this plugin ###################################### MAIL_FILES = 'apptools.logger.plugin.mail_files' mail_files = ExtensionPoint( List(Callable), id=MAIL_FILES, desc=""" This extension point allows you to contribute functions which will be called to add project files to the zip file that the user mails back with bug reports from the Quality Agent. The function will be passed a zipfile.ZipFile object. """ ) #### Contributions to extension points made by this plugin ################# PREFERENCES = 'envisage.preferences' PREFERENCES_PAGES = 'envisage.ui.workbench.preferences_pages' VIEWS = 'envisage.ui.workbench.views' preferences = List(contributes_to=PREFERENCES) preferences_pages = List(contributes_to=PREFERENCES_PAGES) views = List(contributes_to=VIEWS) def _preferences_default(self): return ['pkgfile://%s/plugin/preferences.ini' % ID] def _preferences_pages_default(self): from apptools.logger.plugin.view.logger_preferences_page import \ LoggerPreferencesPage return [LoggerPreferencesPage] def _views_default(self): return [self._logger_view_factory] #### Plugin interface ###################################################### def start(self): """ Starts the plugin. """ preferences = LoggerPreferences() service = LoggerService(application=self.application, preferences=preferences) formatter = logging.Formatter('%(levelname)s|%(asctime)s|%(message)s') handler = LogQueueHandler() handler.setLevel(preferences.level_) handler.setFormatter(formatter) root_logger = logging.getLogger() root_logger.addHandler(handler) root_logger.setLevel(preferences.level_) service.handler = handler self.application.register_service(ILOGGER, service) def stop(self): """ Stops the plugin. """ service = self.application.get_service(ILOGGER) service.save_preferences() #### LoggerPlugin private interface ######################################## def _logger_view_factory(self, **traits): from apptools.logger.plugin.view.logger_view import LoggerView service = self.application.get_service(ILOGGER) view = LoggerView(service=service, **traits) # Record the created view on the service. service.plugin_view = view return view #### EOF ###################################################################### apptools-4.5.0/apptools/logger/plugin/view/0000755000076500000240000000000013547652535022251 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/plugin/view/logger_view.py0000644000076500000240000001560311665460077025136 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Standard library imports from datetime import datetime import logging # Enthought library imports. from pyface.api import ImageResource, clipboard from pyface.workbench.api import TraitsUIView from traits.api import Button, Instance, List, Property, Str, \ cached_property, on_trait_change from traitsui.api import View, Group, Item, CodeEditor, \ TabularEditor, spring from traitsui.tabular_adapter import TabularAdapter # Local imports from apptools.logger.agent.quality_agent_view import QualityAgentView from apptools.logger.plugin import view from apptools.logger.plugin.logger_service import LoggerService # Constants _IMAGE_MAP = { logging.DEBUG: ImageResource('debug'), logging.INFO: ImageResource('info'), logging.WARNING: ImageResource('warning'), logging.ERROR: ImageResource('error'), logging.CRITICAL: ImageResource('crit_error') } class LogRecordAdapter(TabularAdapter): """ A TabularEditor adapter for logging.LogRecord objects. """ columns = [ ('Level', 'level'), ('Date', 'date'), ('Time', 'time'), ('Message', 'message') ] column_widths = [ 80, 100, 120, -1 ] level_image = Property level_text = Property(Str) date_text = Property(Str) time_text = Property(Str) message_text = Property(Str) def get_width(self, object, trait, column): return self.column_widths[column] def _get_level_image(self): return _IMAGE_MAP[self.item.levelno] def _get_level_text(self): return self.item.levelname.capitalize() def _get_date_text(self): dt = datetime.fromtimestamp(self.item.created) return dt.date().isoformat() def _get_time_text(self): dt = datetime.fromtimestamp(self.item.created) return dt.time().isoformat() def _get_message_text(self): # Just display the first line of multiline messages, like stacktraces. msg = self.item.getMessage() msgs = msg.strip().split('\n') if len(msgs) > 1: suffix = '... [double click for details]' else: suffix = '' abbrev_msg = msgs[0] + suffix return abbrev_msg class LoggerView(TraitsUIView): """ The Workbench View showing the list of log items. """ id = Str('apptools.logger.plugin.view.logger_view.LoggerView') name = Str('Logger') service = Instance(LoggerService) log_records = List(Instance(logging.LogRecord)) formatted_records = Property(Str, depends_on='log_records') activated = Instance(logging.LogRecord) activated_text = Property(Str, depends_on='activated') reset_button = Button("Reset Logs") show_button = Button("Complete Text Log") copy_button = Button("Copy Log to Clipboard") code_editor = CodeEditor(lexer='null', show_line_numbers=False) log_records_editor = TabularEditor(adapter=LogRecordAdapter(), editable=False, activated='activated') trait_view = View(Group(Item('log_records', editor=log_records_editor), Group(Item('reset_button'), spring, Item('show_button'), Item('copy_button'), orientation='horizontal', show_labels=False), show_labels=False)) ########################################################################### # LogQueueHandler view interface ########################################################################### def update(self, force=False): """ Update 'log_records' if our handler has new records or 'force' is set. """ service = self.service if service.handler.has_new_records() or force: log_records = [ rec for rec in service.handler.get() if rec.levelno >= service.preferences.level_ ] log_records.reverse() self.log_records = log_records ########################################################################### # Private interface ########################################################################### @on_trait_change('service.preferences.level_') def _update_log_records(self): self.service.handler._view = self self.update(force=True) def _reset_button_fired(self): self.service.handler.reset() self.log_records = [] def _show_button_fired(self): self.edit_traits(view=View(Item('formatted_records', editor=self.code_editor, style='readonly', show_label=False), width=800, height=600, resizable=True, buttons=[ 'OK' ], title='Complete Text Log')) def _copy_button_fired(self): clipboard.text_data = self.formatted_records @cached_property def _get_formatted_records(self): return '\n'.join([ self.service.handler.formatter.format(record) for record in self.log_records ]) def _activated_changed(self): if self.activated is None: return msg = self.activated.getMessage() if self.service.preferences.enable_agent: dialog = QualityAgentView(msg=msg, service=self.service) dialog.open() else: self.edit_traits(view=View(Item('activated_text', editor=self.code_editor, style='readonly', show_label=False), width=800, height=600, resizable=True, buttons=[ 'OK' ], title='Log Message Detail')) @cached_property def _get_activated_text(self): if self.activated is None: return '' else: return self.activated.getMessage() #### EOF ###################################################################### apptools-4.5.0/apptools/logger/plugin/view/images/0000755000076500000240000000000013547652535023516 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/plugin/view/images/info.png0000644000076500000240000000706011640354733025151 0ustar mdickinsonstaff00000000000000PNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-gAMA|Q cHRMz%u0`:o_FKIDATxdOe;c A1 1BXMg= jԛăkL* RPk -t0·<ɓF&0{甴͡4ɮ< ]BJn~r}wM z'BZBYD,*MhAQQ7 x2I2K f H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-gAMA|Q cHRMz%u0`:o_FIDATxlKh\uLn&I2 yƄ8ؚ`*āBEAЕ bhA+.FJ4h3Iit^q1)n9?QUņƖQ=#O;EXχT=VW+޺4j *``d>hD!n[ll Ǯŕڬ87`6iCΩܻ܄j]lz?* ^1+pcWwVHݣn kcz1Sg)=}~S}'**j\?BJ*ۺN?K<稝C610"8L4dOX,QHc/?^u{ ƾ0$-E\Tk.Yu3rFưoA ym}[Gc d;V @N.'3ߜxgw>z ֮69+a qҝSafq1IdBqDRF]ZUh\H _Lc%/]#ji@TPX1 HݪT.5:IENDB`apptools-4.5.0/apptools/logger/plugin/view/images/debug.png0000644000076500000240000000715411640354733025310 0ustar mdickinsonstaff00000000000000PNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-gAMA|Q cHRMz%u0`:o_FIDATxdOuysw7NdfKn"jV1eQ1)Y̢ Vm35TXCWgRrs|>}y/l-t]ٳŌc*e~«P Cu~fJ";!%<UnCkBӎ:cW Xed l, {rgMALF̓q}xPػl2ox`sg&zE/bq Qy EW; C=k|w~o+eK ʙRէãw:*`%6l L<؏˿U+SEHc" ÏWM/ ڲB+9HqîS{kJߩ߇/!obz{Mx3МDJJӓ V,GnooB*rE]AU#vzfr1 03O'< s)bf8>Ѐ/R4m Ea$B ENp+611mi˱3XPP)Ie ^.(q,u)K O5˃`dR`*I_W+5&6Ĕpi>S&m[PVV6>MYvkQRWIENDB`apptools-4.5.0/apptools/logger/plugin/view/images/error.png0000644000076500000240000000701511640354733025347 0ustar mdickinsonstaff00000000000000PNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-gAMA|Q cHRMz%u0`:o_F(IDATxdoe3|8Cڤ%W vD(%"iH ]T qaY`"I h')I#&l iikgڙv:̼3\ЦƳ7䞣D>@Z(v˧ O65jOdxj *kh%` eĉlזht  KK0o௯qmZqhX/OJ{x|j7o2J^d]"ӔWWnm%1<̲1d!4?9zD~mm`hH6ɽ~ _'Oʽc>9XW7c\_R!~ѽ{yu`Ȏt^JKd'&ȋPؠjgJīU=4D)#qؖ<.^dY6~V&jJzyR]k|hR.s| N ?"+EU cm Kco/o|MXlǡ;$%ץ^4 ~թThaO5Ǐ)NLc_/#C!fYq|X{TdXe'ucc9,B_gٽFڰW2!%"uH2{*gI:|{b~D3T*(,H$>4F1jk)R <*  ss뮛OB MhR"BEJgf?;NeġxFL_>MA`EBl[z;ZU16wEA)u4' b!V7vk_71~Scl k GVL4A/\8Jiӵu"Z綂 1>㝳tnIiR)R=C\BDVT,sZ.nT+9oz';gSL*hWx1Dd6p+|8>YJ&#]=S&%nXPܹw׻ Xbeuj PbpdVAFIENDB`apptools-4.5.0/apptools/logger/plugin/view/__init__.py0000644000076500000240000000000011640354733024337 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/plugin/view/logger_preferences_page.py0000644000076500000240000000624711640354733027457 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ import logging from apptools.preferences.ui.api import PreferencesPage from traits.api import Bool, Trait, Str from traitsui.api import EnumEditor, Group, Item, View class LoggerPreferencesPage(PreferencesPage): """ A preference page for the logger plugin. """ #### 'PreferencesPage' interface ########################################## # The page's category (e.g. 'General/Appearance'). The empty string means # that this is a top-level page. category = '' # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = '' # The page name (this is what is shown in the preferences dialog. name = 'Logger' # The path to the preferences node that contains the preferences. preferences_path = 'apptools.logger' #### Preferences ########################################################### # The log levels level = Trait('Info', {'Debug' : logging.DEBUG, 'Info' : logging.INFO, 'Warning' : logging.WARNING, 'Error' : logging.ERROR, 'Critical' : logging.CRITICAL, }, is_str = True, ) enable_agent = Bool(False) smtp_server = Str to_address = Str from_address = Str # The view used to change the plugin preferences traits_view = View( Group( Group( Item( name='level', editor=EnumEditor( values={ 'Debug' : '1:Debug', 'Info' : '2:Info', 'Warning' : '3:Warning', 'Error' : '4:Error' , 'Critical' : '5:Critical', }, ), style='simple', ), label='Logger Settings', show_border=True, ), Group(Item(name='10')), Group( Group( Group(Item(name='enable_agent', label='Enable quality agent'), show_left=False), Group(Item(name='smtp_server', label='SMTP server'), Item(name='from_address'), Item(name='to_address'), enabled_when='enable_agent==True')), label='Quality Agent Settings', show_border=True, ), ), ) #### EOF ###################################################################### apptools-4.5.0/apptools/logger/__init__.py0000644000076500000240000000050711640354733022103 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005 by Enthought, Inc. # All rights reserved. # #----------------------------------------------------------------------------- """ Convenience functions for creating logging handlers. Part of the EnthoughtBase project. """ apptools-4.5.0/apptools/logger/agent/0000755000076500000240000000000013547652535021077 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/agent/tests/0000755000076500000240000000000013547652535022241 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/agent/tests/__init__.py0000644000076500000240000000000013547637370024340 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/logger/agent/tests/test_attachments.py0000644000076500000240000000363413547637370026173 0ustar mdickinsonstaff00000000000000from email.mime.multipart import MIMEMultipart import io import os import shutil import tempfile import unittest try: # On Python 3, mock is part of the standard library, from unittest import mock except ImportError: # Whereas on Python 2 it is not. import mock from apptools.logger.agent.attachments import Attachments class AttachmentsTestCase(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.tmpdir) self.tmpfile = os.path.join(self.tmpdir, "dummy_file.txt") with io.open(self.tmpfile, 'w', encoding='utf8') as filehandle: filehandle.write(u"Dummy data in dummy file for dummies") def test_attaching_workspace(self): class DummyWorkspace(object): path = self.tmpdir class MockedApplication(object): tmpdir = self.tmpdir def get_service(self, service_id): return DummyWorkspace() attachments = Attachments( application=MockedApplication(), message=MIMEMultipart() ) attachments.package_workspace() message = attachments.message self.assertTrue(message.is_multipart()) payload = message.get_payload() self.assertEqual(len(payload), 1) def test_attaching_single_project(self): class DummySingleProject(object): location = self.tmpdir class MockedApplication(object): tmpdir = self.tmpdir def get_service(self, service_id): return DummySingleProject() attachments = Attachments( application=MockedApplication(), message=MIMEMultipart() ) attachments.package_single_project() message = attachments.message self.assertTrue(message.is_multipart()) payload = message.get_payload() self.assertEqual(len(payload), 1) apptools-4.5.0/apptools/logger/agent/__init__.py0000644000076500000240000000004211640354733023173 0ustar mdickinsonstaff00000000000000""" lib.apptools.logger.agent """ apptools-4.5.0/apptools/logger/agent/attachments.py0000644000076500000240000000535513547637370023774 0ustar mdickinsonstaff00000000000000""" Attach relevant project files. FIXME: there are no public project plugins for Envisage 3, yet. In any case, this stuff should not be hard-coded, but extensible via extension points. The code remains here because we can reuse the zip utility code in that extensible rewrite. """ import logging import os.path from email import encoders from email.mime.base import MIMEBase from traits.api import Any, HasTraits logger = logging.getLogger(__name__) class Attachments(HasTraits): application = Any() message = Any() def __init__(self, message, **traits): traits = traits.copy() traits['message'] = message super(Attachments, self).__init__(**traits) # FIXME: all of the package_*() methods refer to deprecated project plugins. def package_workspace(self): if self.application is None: pass workspace = self.application.get_service('envisage.project.IWorkspace') if workspace is not None: dir = workspace.path self._attach_directory(dir) return def package_single_project(self): if self.application is None: pass single_project = self.application.get_service('envisage.single_project.ModelService') if single_project is not None: dir = single_project.location self._attach_directory(dir) def package_any_relevant_files(self): self.package_workspace() self.package_single_project() return def _attach_directory(self, dir): relpath = os.path.basename(dir) import zipfile from io import BytesIO ctype = 'application/octet-stream' maintype, subtype = ctype.split('/', 1) msg = MIMEBase(maintype, subtype) file_object = BytesIO() zip = zipfile.ZipFile(file_object, 'w') _append_to_zip_archive(zip, dir, relpath) zip.close() msg.set_payload(file_object.getvalue()) encoders.encode_base64(msg) # Encode the payload using Base64 msg.add_header('Content-Disposition', 'attachment', filename='project.zip') self.message.attach(msg) file_object.close() def _append_to_zip_archive(zip, dir, relpath): """ Add all files in and below directory dir into zip archive""" for filename in os.listdir(dir): path = os.path.join(dir, filename) if os.path.isfile(path): name = os.path.join(relpath, filename) zip.write(path, name) logger.debug('adding %s to error report' % path) else: if filename != ".svn": # skip svn files if any subdir = os.path.join(dir, filename) _append_to_zip_archive(zip, subdir, os.path.join(relpath, filename)) return apptools-4.5.0/apptools/logger/agent/quality_agent_mailer.py0000644000076500000240000000765713547637361025667 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Standard library imports. import logging import os # Enthought library imports. from traits.util.home_directory import get_home_directory # Setup a logger for this module. logger = logging.getLogger(__name__) def create_email_message(fromaddr, toaddrs, ccaddrs, subject, priority, include_project=False, stack_trace="", comments=""): # format a message suitable to be sent to the Roundup bug tracker from email.MIMEMultipart import MIMEMultipart from email.MIMEText import MIMEText from email.MIMEBase import MIMEBase message = MIMEMultipart() message['Subject'] = "%s [priority=%s]" % (subject, priority) message['To'] = ', '.join(toaddrs) message['Cc'] = ', '.join(ccaddrs) message['From'] = fromaddr message.preamble = 'You will not see this in a MIME-aware mail reader.\n' message.epilogue = ' ' # To guarantee the message ends with a newline # First section is simple ASCII data ... m = [] m.append("Bug Report") m.append("==============================") m.append("") if len(comments) > 0: m.append("Comments:") m.append("========") m.append(comments) m.append("") if len(stack_trace) > 0: m.append("Stack Trace:") m.append("===========") m.append(stack_trace) m.append("") msg = MIMEText('\n'.join(m)) message.attach(msg) # Include the log file ... if True: try: log = os.path.join(get_home_directory(), 'envisage.log') f = open(log, 'r') entries = f.readlines() f.close() ctype = 'application/octet-stream' maintype, subtype = ctype.split('/', 1) msg = MIMEBase(maintype, subtype) msg = MIMEText(''.join(entries)) msg.add_header('Content-Disposition', 'attachment', filename='logfile.txt') message.attach(msg) except: logger.exception('Failed to include log file with message') # Include the environment variables ... if True: """ Transmit the user's environment settings as well. Main purpose is to work out the user name to help with following up on bug reports and in future we should probably send less data. """ try: entries = [] for key, value in os.environ.items(): entries.append('%30s : %s\n' % (key, value)) ctype = 'application/octet-stream' maintype, subtype = ctype.split('/', 1) msg = MIMEBase(maintype, subtype) msg = MIMEText(''.join(entries)) msg.add_header('Content-Disposition', 'attachment', filename='environment.txt') message.attach(msg) except: logger.exception('Failed to include environment variables with message') # FIXME: no project plugins exist for Envisage 3, yet, and this isn't the right # way to do it, either. See the docstring of attachments.py. # # Attach the project if requested ... # if include_project: # from attachments import Attachments # try: # attachments = Attachments(message) # attachments.package_any_relevant_files() # except: # logger.exception('Failed to include workspace files with message') return message apptools-4.5.0/apptools/logger/agent/quality_agent_view.py0000644000076500000240000002772311640354733025353 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Standard library imports. import logging # Enthought library imports. from pyface.api import Dialog from traits.api import Any, Str, Tuple # Setup a logger for this module. logger = logging.getLogger(__name__) priority_levels = ['Low', 'Medium', 'High', 'Critical'] class QualityAgentView(Dialog): size = Tuple((700, 900)) title = Str('Quality Agent') # The associated LoggerService. service = Any() msg = Str('') subject = Str('Untitled Error Report') to_address = Str() cc_address = Str('') from_address = Str() smtp_server = Str() priority = Str(priority_levels[2]) comments = Str('None') include_userdata = Any ########################################################################### # Protected 'Dialog' interface. ########################################################################### # fixme: Ideally, this should be passed in; this topic ID belongs to the # Enlib help project/plug-in. help_id = 'enlib|HID_Quality_Agent_Dlg' def _create_dialog_area(self, parent): """ Creates the main content of the dialog. """ import wx parent.SetSizeHints(minW=300, minH=575) # Add the main panel sizer = wx.BoxSizer(wx.VERTICAL) panel = wx.Panel(parent, -1) panel.SetSizer(sizer) panel.SetAutoLayout(True) # Add a descriptive label at the top ... label = wx.StaticText(panel, -1, "Send a comment or bug report ...") sizer.Add(label, 0, wx.ALL, border=5) # Add the stack trace view ... error_panel = self._create_error_panel(panel) sizer.Add(error_panel, 1, wx.ALL|wx.EXPAND|wx.CLIP_CHILDREN, border=5) # Update the layout: sizer.Fit(panel) # Add the error report view ... report_panel = self._create_report_panel(panel) sizer.Add(report_panel, 2, wx.ALL|wx.EXPAND|wx.CLIP_CHILDREN, border=5) # Update the layout: sizer.Fit(panel) return panel def _create_buttons(self, parent): """ Creates the buttons. """ import wx sizer = wx.BoxSizer(wx.HORIZONTAL) # 'Send' button. send = wx.Button(parent, wx.ID_OK, "Send") wx.EVT_BUTTON(parent, wx.ID_OK, self._on_send) sizer.Add(send) send.SetDefault() # 'Cancel' button. cancel = wx.Button(parent, wx.ID_CANCEL, "Cancel") wx.EVT_BUTTON(parent, wx.ID_CANCEL, self._wx_on_cancel) sizer.Add(cancel, 0, wx.LEFT, 10) # 'Help' button. if len(self.help_id) > 0: help = wx.Button(parent, wx.ID_HELP, "Help") wx.EVT_BUTTON(parent, wx.ID_HELP, self._wx_on_help) sizer.Add(help, 0, wx.LEFT, 10) return sizer def _on_help(self, event): """Called when the 'Help' button is pressed. """ hp = self.service.application.get_service('apptools.help.IHelp') hp.library.show_topic(self.help_id) return ### Utility methods ####################################################### def _create_error_panel(self, parent): import wx box = wx.StaticBox(parent, -1, "Message:") sizer = wx.StaticBoxSizer(box, wx.VERTICAL) # Print the stack trace label2 = wx.StaticText(parent, -1,"The following information will be included in the report:") sizer.Add(label2, 0, wx.LEFT|wx.TOP|wx.BOTTOM|wx.CLIP_CHILDREN, border=5) details = wx.TextCtrl(parent, -1, self.msg, size=(-1,75), style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL | wx.VSCROLL | wx.TE_RICH2 | wx.CLIP_CHILDREN) details.SetSizeHints(minW=-1, minH=75) # Set the font to not be proportional font = wx.Font(12, wx.MODERN, wx.NORMAL, wx.NORMAL) details.SetStyle(0, len(self.msg), wx.TextAttr(font=font)) sizer.Add(details, 1, wx.EXPAND|wx.ALL|wx.CLIP_CHILDREN, 5) return sizer def _create_report_panel(self, parent): import wx box = wx.StaticBox(parent, -1, "Report Information:") sizer = wx.StaticBoxSizer(box, wx.VERTICAL) # Add email info ... sizer.Add(self._create_email_info(parent), 0, wx.ALL|wx.EXPAND, 5) # Add priority combo: sizer.Add(self._create_priority_combo(parent), 0, wx.ALL|wx.RIGHT, 5) # Extra comments from the user: label3 = wx.StaticText(parent, -1, "Additional Comments:") sizer.Add(label3, 0, wx.LEFT|wx.TOP|wx.BOTTOM|wx.CLIP_CHILDREN, 5) comments_field = wx.TextCtrl(parent, -1, self.comments, size=(-1,75), style=wx.TE_MULTILINE | wx.TE_RICH2 | wx.CLIP_CHILDREN) comments_field.SetSizeHints(minW=-1, minH=75) font = wx.Font(12, wx.MODERN, wx.NORMAL, wx.NORMAL) comments_field.SetStyle(0, len(self.comments), wx.TextAttr(font=font)) sizer.Add(comments_field, 1, wx.ALL|wx.EXPAND|wx.CLIP_CHILDREN, 5) wx.EVT_TEXT(parent, comments_field.GetId(), self._on_comments) # Include the project combobox? if len(self.service.mail_files) > 0: sizer.Add(self._create_project_upload(parent), 0, wx.ALL, border=5) return sizer def _create_email_info(self, parent): import wx # Layout setup .. sizer = wx.FlexGridSizer(5,2,10,10) sizer.AddGrowableCol(1) title_label = wx.StaticText(parent, -1, "Subject:") sizer.Add(title_label , 0, wx.ALL|wx.ALIGN_RIGHT) title_field = wx.TextCtrl(parent, -1, self.subject, wx.Point(-1,-1)) sizer.Add(title_field, 1, wx.EXPAND|wx.ALL|wx.ALIGN_RIGHT|wx.CLIP_CHILDREN) wx.EVT_TEXT(parent, title_field.GetId(), self._on_subject) to_label = wx.StaticText(parent, -1, "To:") sizer.Add(to_label , 0, wx.ALL|wx.ALIGN_RIGHT) to_field = wx.TextCtrl(parent, -1, self.to_address) sizer.Add(to_field, 1, wx.EXPAND|wx.ALL|wx.ALIGN_RIGHT|wx.CLIP_CHILDREN) wx.EVT_TEXT(parent, to_field.GetId(), self._on_to) cc_label = wx.StaticText(parent, -1, "Cc:") sizer.Add(cc_label, 0, wx.ALL|wx.ALIGN_RIGHT) cc_field = wx.TextCtrl(parent, -1, "") sizer.Add(cc_field, 1, wx.EXPAND|wx.ALL|wx.ALIGN_RIGHT|wx.CLIP_CHILDREN) wx.EVT_TEXT(parent, cc_field.GetId(), self._on_cc) from_label = wx.StaticText(parent, -1, "From:") sizer.Add(from_label, 0, wx.ALL|wx.ALIGN_RIGHT) from_field = wx.TextCtrl(parent, -1, self.from_address) sizer.Add(from_field, 1, wx.EXPAND|wx.ALL|wx.ALIGN_RIGHT|wx.CLIP_CHILDREN) wx.EVT_TEXT(parent, from_field.GetId(), self._on_from) smtp_label = wx.StaticText(parent, -1, "SMTP Server:") sizer.Add(smtp_label, 0, wx.ALL|wx.ALIGN_RIGHT) smtp_server_field = wx.TextCtrl(parent, -1, self.smtp_server) sizer.Add(smtp_server_field, 1, wx.EXPAND|wx.ALL|wx.ALIGN_RIGHT|wx.CLIP_CHILDREN) wx.EVT_TEXT(parent, smtp_server_field.GetId(), self._on_smtp_server) return sizer def _create_priority_combo(self, parent): import wx sizer = wx.BoxSizer(wx.HORIZONTAL) label = wx.StaticText(parent, -1, "How critical is this issue?") sizer.Add(label, 0, wx.ALL, border=0) cb = wx.ComboBox(parent, -1, self.priority, wx.Point(90, 50), wx.Size(95, -1), priority_levels, wx.CB_READONLY) sizer.Add(cb, 1, wx.EXPAND|wx.LEFT|wx.CLIP_CHILDREN, border=10) wx.EVT_COMBOBOX(parent, cb.GetId(), self._on_priority) return sizer def _create_project_upload(self, parent): import wx id = wx.NewId() cb = wx.CheckBox(parent, id, "Include Workspace Files (will increase email size) ", wx.Point(65, 80), wx.Size(-1, 20), wx.NO_BORDER) wx.EVT_CHECKBOX(parent, id, self._on_project) return cb ## UI Listeners ########################################################### def _on_subject(self, event): self.subject = event.GetEventObject().GetValue() def _on_to(self, event): self.to_address = event.GetEventObject().GetValue() def _on_cc(self, event): self.cc_address = event.GetEventObject().GetValue() def _on_from(self, event): self.from_address = event.GetEventObject().GetValue() def _on_smtp_server(self, event): self.smtp_server = event.GetEventObject().GetValue() def _on_priority(self, event): self.priority = event.GetEventObject().GetStringSelection() def _on_comments(self, event): self.comments = event.GetEventObject().GetValue() def _on_project(self, event): self.include_userdata = event.Checked() cb = event.GetEventObject() if event.Checked(): cb.SetLabel("Include Workspace Files (approx. %.2f MBytes)" % self._compute_project_size()) else: cb.SetLabel("Include Workspace Files (will increase email size)") return def _on_send(self, event): import wx # Disable the Send button while we go through the possibly # time-consuming email-sending process. button = event.GetEventObject() button.Enable(0) fromaddr, toaddrs, ccaddrs = self._create_email_addresses() message = self._create_email(fromaddr, toaddrs, ccaddrs) self.service.send_bug_report(self.smtp_server, fromaddr, toaddrs, ccaddrs, message) # save the user's preferences self.service.preferences.smtp_server = self.smtp_server self.service.preferences.to_address = self.to_address self.service.preferences.from_address = self.from_address # finally we close the dialog self._wx_on_ok(event) return ## Private ################################################################ def _create_email_addresses(self): # utility function map addresses from ui into the standard format # FIXME: We should use standard To: header parsing instead of this ad # hoc whitespace-only approach. fromaddr = self.from_address if "" == fromaddr.strip(): fromaddr = "anonymous" toaddrs = self.to_address.split() ccaddrs = self.cc_address.split() return fromaddr, toaddrs, ccaddrs def _compute_project_size(self): # determine size of email in MBytes fromaddr, toaddrs, ccaddrs = self._create_email_addresses() message = self._create_email(fromaddr, toaddrs, ccaddrs) return len(message.as_string()) / (2.0**20) def _create_email(self, fromaddr, toaddrs, ccaddrs): return self.service.create_email_message( fromaddr, toaddrs, ccaddrs, self.subject, self.priority, self.include_userdata, self.msg, self.comments, ) def _to_address_default(self): return self.service.preferences.to_address def _from_address_default(self): return self.service.preferences.from_address def _smtp_server_default(self): return self.service.preferences.smtp_server ####### EOF ############################################################# apptools-4.5.0/apptools/logger/logger.py0000644000076500000240000000545313547637361021641 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Convenience functions for creating logging handlers etc. """ # Standard library imports. import logging from logging.handlers import RotatingFileHandler # Enthought library imports. from traits.util.api import deprecated # Local imports. from .log_queue_handler import LogQueueHandler # The default logging level. LEVEL = logging.DEBUG # The default formatter. FORMATTER = logging.Formatter('%(levelname)s|%(asctime)s|%(message)s') class LogFileHandler(RotatingFileHandler): """ The default log file handler. """ def __init__(self, path, maxBytes=1000000, backupCount=3, level=None, formatter=None): RotatingFileHandler.__init__( self, path, maxBytes=maxBytes, backupCount=3 ) if level is None: level = LEVEL if formatter is None: formatter = FORMATTER # Set our default formatter and log level. self.setFormatter(formatter) self.setLevel(level) @deprecated('use "LogFileHandler"') def create_log_file_handler(path, maxBytes=1000000, backupCount=3, level=None, formatter=None): """ Creates a log file handler. This is just a convenience function to make it easy to create the same kind of handlers across applications. It sets the handler's formatter to the default formatter, and its logging level to the default logging level. """ if level is None: level = LEVEL if formatter is None: formatter = FORMATTER handler = RotatingFileHandler( path, maxBytes=maxBytes, backupCount=backupCount ) handler.setFormatter(formatter) handler.setLevel(level) return handler def add_log_queue_handler(logger, level=None, formatter=None): """ Adds a queueing log handler to a logger. """ if level is None: level = LEVEL if formatter is None: formatter = FORMATTER # Add the handler to the root logger. log_queue_handler = LogQueueHandler() log_queue_handler.setLevel(level) log_queue_handler.setFormatter(formatter) logger.addHandler(log_queue_handler) return log_queue_handler #### EOF ###################################################################### apptools-4.5.0/apptools/logger/api.py0000644000076500000240000000035713547637361021131 0ustar mdickinsonstaff00000000000000from .logger import add_log_queue_handler, create_log_file_handler from .logger import FORMATTER, LEVEL, LogFileHandler from .log_point import log_point from .filtering_handler import FilteringHandler from .null_handler import NullHandler apptools-4.5.0/apptools/logger/filtering_handler.py0000644000076500000240000001374113547637361024041 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A log handler that allows filtering of messages by origin. """ # Standard library imports. from __future__ import print_function import logging, inspect, os # Local imports. # # fixme: This module was just copied over from 'envisage.core' (so # that we don't rely on Envisage here!). Where should this module go? from .util import get_module_name class FilteringHandler(logging.Handler): """ A log handler that allows filtering of messages by origin. Example ------- :: from apptools.logger.api import DebugHandler, logger handler = FilteringHandler( include = { 'envisage.core' : True }, exclude = { 'envisage.core.application' : False } ) logger.addHandler(handler) Notes ----- The boolean value specified for each name in the include and exclude dictionaries indicates whether or not the include or exclude should pertain to any sub-packages and modules. The above example includes all log messages from anything contained in, or under the 'envisage.core' package, EXCEPT for any log messages from the 'envisage.core.application' module. """ ########################################################################### # 'object' interface. ########################################################################### def __init__(self, include=None, exclude=None): """ Creates a new handler. """ # Base class constructor. logging.Handler.__init__(self) # Packages or modules to include. self.include = include is not None and include or {} # Packages or modules to exclude. self.exclude = exclude is not None and exclude or {} return ########################################################################### # 'Handler' interface. ########################################################################### def emit(self, record): """ Emits a log record. """ # Get the name of the module that the logger was called from. module_name = self._get_module_name() if len(self.include) == 0 or self._include(module_name): if len(self.exclude) == 0 or not self._exclude(module_name): self.filtered_emit(record) return ########################################################################### # 'Handler' interface. ########################################################################### def filtered_emit(self, record): """ Emits a log record if it has not been filtered. """ print(record.getMessage()) return ########################################################################### # Private interface. ########################################################################### def _get_module_name(self): """ Returns the module that the logger was actually called from. """ # fixem: Ahem.... what can I say... this gets us to the actual caller! frame = inspect.currentframe() frame = frame.f_back.f_back.f_back.f_back.f_back.f_back.f_back # This returns a tuple containing the last 5 elements of the frame # record which are:- # # - the filename # - the line number # - the function name # - the list of lines of context from the source code # - the index of the current line within that list filename, lineno, funcname, source, index = inspect.getframeinfo(frame) # The plugin definition's location is the directory containing the # module that it is defined in. self.location = os.path.dirname(filename) # We can't use 'inspect.getmodulename' here as it gets confused because # of our import hook in Envisage 8^( # # e.g. for the core plugin definition:- # # using inspect -> 'core_plugin_definition' # using ours -> 'envisage.core.core_plugin_definition' return get_module_name(filename) def _include(self, module_name): """ Is the module name in the include set? """ for item, include_children in self.include.items(): if item == module_name: include = True break elif include_children and self._is_child_of(item, module_name): include = True break else: include = False return include def _exclude(self, module_name): """ Is the module name in the exclude set? """ for item, exclude_children in self.exclude.items(): if item == module_name: exclude = True break elif exclude_children and self._is_child_of(item, module_name): exclude = True break else: exclude = False return exclude def _is_child_of(self, x, y): """ Is 'y' a child symbol of 'x'? e.g. 'foo.bar.baz' (y) is a child of 'foo.bar' (x) """ if y.startswith(x): x_atoms = x.split('.') y_atoms = y.split('.') is_child_of = y_atoms[:len(x_atoms)] == x_atoms else: is_child_of = False return is_child_of #### EOF ###################################################################### apptools-4.5.0/apptools/logger/custom_excepthook.py0000644000076500000240000000255711640354733024116 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Standard library imports. import logging from traceback import format_exception """ To catch exceptions with our own code this code needs to be added sys.excepthook = custom_excepthook """ def custom_excepthook(type, value, traceback): """ Pass on the exception to the logging system. """ msg = 'Custom - Traceback (most recent call last):\n' list = format_exception(type, value, traceback) msg = "".join(list) # Try to find the module that the exception actually came from. name = getattr(traceback.tb_frame, 'f_globals', {}).get('__name__', __name__) logger = logging.getLogger(name) logger.error(msg) return ## EOF ################################################################## apptools-4.5.0/apptools/logger/log_queue_handler.py0000644000076500000240000000446513547637361024046 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Standard library imports. from logging import Handler # Local imports. from .ring_buffer import RingBuffer class LogQueueHandler(Handler): """ Buffers up the log messages so that we can display them later. This is important on startup when log messages are generated before the ui has started. By putting them in this queue we can display them once the ui is ready. """ # The view where updates will go _view = None def __init__(self, size=1000): Handler.__init__(self) # only buffer 1000 log records self.size = size self.ring = RingBuffer(self.size) self.dirty = False return def emit(self, record): """ Actually this is more like an enqueue than an emit().""" self.ring.append(record) if self._view is not None: try: self._view.update() except Exception as e: pass self.dirty = True return def get(self): self.dirty = False try: result = self.ring.get() except Exception as msg: # we did our best and it won't cause too much damage # to just return a bogus message result = [] return result def has_new_records(self): return self.dirty def reset(self): # start over with a new empty buffer self.ring = RingBuffer(self.size) if self._view is not None: try: self._view.update() except Exception as e: pass self.dirty = True return ## EOF ################################################################## apptools-4.5.0/apptools/logger/null_handler.py0000644000076500000240000000263011640354733023012 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A null log handler. """ # Standard library imports. import logging class NullHandler(logging.Handler): """ A null log handler. This is a quick hack so that we can start to refactor the 'logger' module since it used to add actual handlers at module load time. Now we only add this handler so that people using just the ETS library and not one of our applications won't see the warning about 'no handlers'. """ ########################################################################### # 'Handler' interface. ########################################################################### def emit(self, record): """ Emits a log record. """ pass #### EOF ###################################################################### apptools-4.5.0/apptools/logger/ring_buffer.py0000644000076500000240000000315611640354733022637 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Copied from Python Cookbook. """ class RingBuffer: def __init__(self,size_max): self.max = size_max self.data = [] def append(self,x): """append an element at the end of the buffer""" self.data.append(x) if len(self.data) == self.max: self.cur = 0 self.__class__ = RingBufferFull def get(self): """ return a list of elements from the oldest to the newest""" return self.data class RingBufferFull: def __init__(self,n): raise Exception("you should use RingBuffer") def append(self,x): self.data[self.cur]=x self.cur=(self.cur+1) % self.max def get(self): return self.data[self.cur:]+self.data[:self.cur] # sample of use """x=RingBuffer(5) x.append(1); x.append(2); x.append(3); x.append(4) print x.__class__,x.get() x.append(5) print x.__class__,x.get() x.append(6) print x.data,x.get() x.append(7); x.append(8); x.append(9); x.append(10) print x.data,x.get()""" apptools-4.5.0/apptools/_version.py0000644000076500000240000000026513547652534020722 0ustar mdickinsonstaff00000000000000# THIS FILE IS GENERATED FROM APPTOOLS SETUP.PY version = '4.5.0' full_version = '4.5.0' git_revision = 'Unknown' is_released = True if not is_released: version = full_version apptools-4.5.0/apptools/type_registry/0000755000076500000240000000000013547652535021433 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/type_registry/tests/0000755000076500000240000000000013547652535022575 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/type_registry/tests/test_type_registry.py0000644000076500000240000001143612317502217027105 0ustar mdickinsonstaff00000000000000import unittest from ..type_registry import TypeRegistry from .dummies import A, B, C, D, Mixed, Abstract, Concrete, ConcreteSubclass class TestTypeRegistry(unittest.TestCase): def setUp(self): self.registry = TypeRegistry() def test_deferred_push(self): self.registry.push('dummies:A', 'A') self.registry.push('dummies:C', 'C') self.assertEqual(self.registry.lookup_by_type(A), 'A') self.assertEqual(self.registry.lookup_by_type(B), 'A') self.assertEqual(self.registry.lookup_by_type(C), 'C') self.assertRaises(KeyError, self.registry.lookup_by_type, D) def test_greedy_push(self): self.registry.push(A, 'A') self.registry.push(C, 'C') self.assertEqual(self.registry.lookup_by_type(A), 'A') self.assertEqual(self.registry.lookup_by_type(B), 'A') self.assertEqual(self.registry.lookup_by_type(C), 'C') self.assertRaises(KeyError, self.registry.lookup_by_type, D) def test_pop_by_name_from_name(self): self.registry.push('dummies:A', 'A') self.registry.pop('dummies:A') self.assertRaises(KeyError, self.registry.lookup_by_type, A) def test_pop_by_name_from_type(self): self.registry.push(A, 'A') self.registry.pop('dummies:A') self.assertRaises(KeyError, self.registry.lookup_by_type, A) def test_pop_by_type_from_name(self): self.registry.push('dummies:A', 'A') self.registry.pop(A) self.assertRaises(KeyError, self.registry.lookup_by_type, A) def test_pop_by_type_from_type(self): self.registry.push(A, 'A') self.registry.pop(A) self.assertRaises(KeyError, self.registry.lookup_by_type, A) def test_mro(self): self.registry.push('dummies:A', 'A') self.registry.push('dummies:D', 'D') self.assertEqual(self.registry.lookup_by_type(Mixed), 'A') def test_lookup_instance(self): self.registry.push(A, 'A') self.registry.push(C, 'C') self.assertEqual(self.registry.lookup(A()), 'A') self.assertEqual(self.registry.lookup(B()), 'A') self.assertEqual(self.registry.lookup(C()), 'C') self.assertRaises(KeyError, self.registry.lookup, D()) def test_lookup_all(self): self.registry.push(A, 'A') self.registry.push(C, 'C') self.assertEqual(self.registry.lookup_all(A()), ['A']) self.assertEqual(self.registry.lookup_all(B()), ['A']) self.registry.push(A, 'A2') self.assertEqual(self.registry.lookup_all(A()), ['A', 'A2']) self.assertEqual(self.registry.lookup_all(B()), ['A', 'A2']) def test_abc(self): self.registry.push_abc(Abstract, 'Abstract') self.assertEqual(self.registry.lookup_by_type(Concrete), 'Abstract') self.assertEqual(self.registry.lookup_by_type(ConcreteSubclass), 'Abstract') def test_stack_type(self): self.registry.push(A, 'A1') self.registry.push(A, 'A2') self.assertEqual(self.registry.lookup_by_type(A), 'A2') self.registry.pop(A) self.assertEqual(self.registry.lookup_by_type(A), 'A1') self.registry.pop(A) self.assertRaises(KeyError, self.registry.lookup_by_type, A) self.assertRaises(KeyError, self.registry.pop, A) self.assertRaises(KeyError, self.registry.pop, 'dummies:A') self.assertEqual(self.registry.type_map, {}) self.assertEqual(self.registry.name_map, {}) self.assertEqual(self.registry.abc_map, {}) def test_stack_mixed_type_and_name(self): self.registry.push(A, 'A1') self.registry.push('dummies:A', 'A2') self.assertEqual(self.registry.lookup_by_type(A), 'A2') self.registry.pop(A) self.assertEqual(self.registry.lookup_by_type(A), 'A1') self.registry.pop('dummies:A') self.assertRaises(KeyError, self.registry.lookup_by_type, A) self.assertRaises(KeyError, self.registry.pop, A) self.assertRaises(KeyError, self.registry.pop, 'dummies:A') self.assertEqual(self.registry.type_map, {}) self.assertEqual(self.registry.name_map, {}) self.assertEqual(self.registry.abc_map, {}) def test_stack_abc(self): self.registry.push_abc(Abstract, 'Abstract1') self.registry.push_abc(Abstract, 'Abstract2') self.assertEqual(self.registry.lookup_by_type(Concrete), 'Abstract2') self.registry.pop(Abstract) self.assertEqual(self.registry.lookup_by_type(Concrete), 'Abstract1') self.registry.pop(Abstract) self.assertRaises(KeyError, self.registry.lookup_by_type, Concrete) self.assertRaises(KeyError, self.registry.pop, Abstract) self.assertEqual(self.registry.type_map, {}) self.assertEqual(self.registry.name_map, {}) self.assertEqual(self.registry.abc_map, {}) apptools-4.5.0/apptools/type_registry/tests/__init__.py0000644000076500000240000000000012317502217024655 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/type_registry/tests/test_lazy_registry.py0000644000076500000240000000402212317502217027074 0ustar mdickinsonstaff00000000000000import unittest from ..type_registry import LazyRegistry from .dummies import A, B, C, D, Mixed, Abstract, Concrete, ConcreteSubclass class TestLazyRegistry(unittest.TestCase): def setUp(self): self.registry = LazyRegistry() # Bodge in a different importer. self.registry._import_object = 'Imported {0}'.format def test_deferred_push(self): self.registry.push('dummies:A', 'foo:A') self.registry.push('dummies:C', 'foo:C') self.assertEqual(self.registry.lookup_by_type(A), 'Imported foo:A') self.assertEqual(self.registry.lookup_by_type(B), 'Imported foo:A') self.assertEqual(self.registry.lookup_by_type(C), 'Imported foo:C') self.assertRaises(KeyError, self.registry.lookup_by_type, D) def test_greedy_push(self): self.registry.push(A, 'foo:A') self.registry.push(C, 'foo:C') self.assertEqual(self.registry.lookup_by_type(A), 'Imported foo:A') self.assertEqual(self.registry.lookup_by_type(B), 'Imported foo:A') self.assertEqual(self.registry.lookup_by_type(C), 'Imported foo:C') self.assertRaises(KeyError, self.registry.lookup_by_type, D) def test_mro(self): self.registry.push('dummies:A', 'foo:A') self.registry.push('dummies:D', 'foo:D') self.assertEqual(self.registry.lookup_by_type(Mixed), 'Imported foo:A') def test_lookup_instance(self): self.registry.push(A, 'foo:A') self.registry.push(C, 'foo:C') self.assertEqual(self.registry.lookup(A()), 'Imported foo:A') self.assertEqual(self.registry.lookup(B()), 'Imported foo:A') self.assertEqual(self.registry.lookup(C()), 'Imported foo:C') self.assertRaises(KeyError, self.registry.lookup, D()) def test_abc(self): self.registry.push_abc(Abstract, 'foo:Abstract') self.assertEqual(self.registry.lookup_by_type(Concrete), 'Imported foo:Abstract') self.assertEqual(self.registry.lookup_by_type(ConcreteSubclass), 'Imported foo:Abstract') apptools-4.5.0/apptools/type_registry/tests/dummies.py0000644000076500000240000000066413547637361024620 0ustar mdickinsonstaff00000000000000import abc import six class A(object): pass class B(A): pass class C(B): pass class D(object): pass class Mixed(A, D): pass class Abstract(six.with_metaclass(abc.ABCMeta, object)): pass class Concrete(object): pass class ConcreteSubclass(Concrete): pass for typ in (A, B, C, D, Mixed, Abstract, Concrete, ConcreteSubclass): typ.__module__ = 'dummies' Abstract.register(Concrete) apptools-4.5.0/apptools/type_registry/__init__.py0000644000076500000240000000000012317502217023513 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/type_registry/type_registry.py0000644000076500000240000002025313547637361024720 0ustar mdickinsonstaff00000000000000import six def get_mro(obj_class): """ Get a reasonable method resolution order of a class and its superclasses for both old-style and new-style classes. """ if not hasattr(obj_class, '__mro__'): # Old-style class. Mix in object to make a fake new-style class. try: obj_class = type(obj_class.__name__, (obj_class, object), {}) except TypeError: # Old-style extension type that does not descend from object. mro = [obj_class] else: mro = obj_class.__mro__[1:-1] else: mro = obj_class.__mro__ return mro def _mod_name_key(typ): """ Return a '__module__:__name__' key for a type. """ module = getattr(typ, '__module__', None) name = getattr(typ, '__name__', None) key = '{0}:{1}'.format(module, name) return key class TypeRegistry(object): """ Register objects for types. Each type maintains a stack of registered objects that can be pushed and popped. """ def __init__(self): # Map types to lists of registered objects. The last is the most # current and will be the one that is returned on lookup. self.type_map = {} # Map '__module__:__name__' strings to lists of registered objects. self.name_map = {} # Map abstract base classes to lists of registered objects. self.abc_map = {} #### TypeRegistry public interface ######################################## def push(self, typ, obj): """ Push an object onto the stack for the given type. Parameters ---------- typ : type or '__module__:__name__' string for a type obj : object The object to register. """ if isinstance(typ, six.string_types): # Check the cached types. for cls in self.type_map: if _mod_name_key(cls) == typ: self.type_map[cls].append(obj) break else: if typ not in self.name_map: self.name_map[typ] = [] self.name_map[typ].append(obj) else: if typ not in self.type_map: self.type_map[typ] = [] self.type_map[typ].append(obj) def push_abc(self, typ, obj): """ Push an object onto the stack for the given ABC. Parameters ---------- typ : abc.ABCMeta obj : object """ if typ not in self.abc_map: self.abc_map[typ] = [] self.abc_map[typ].append(obj) def pop(self, typ): """ Pop a registered object for the given type. Parameters ---------- typ : type or '__module__:__name__' string for a type Returns ------- obj : object The last registered object for the type. Raises ------ KeyError if the type is not registered. """ if isinstance(typ, six.string_types): if typ not in self.name_map: # We may have it cached in the type map. We will have to # iterate over all of the types to check. for cls in self.type_map: if _mod_name_key(cls) == typ: old = self._pop_value(self.type_map, cls) break else: raise KeyError("No registered value for {0!r}".format(typ)) else: old = self._pop_value(self.name_map, typ) else: if typ in self.type_map: old = self._pop_value(self.type_map, typ) elif typ in self.abc_map: old = self._pop_value(self.abc_map, typ) else: old = self._pop_value(self.name_map, _mod_name_key(typ)) return old def lookup(self, instance): """ Look up the registered object for the given instance. Parameters ---------- instance : object An instance of a possibly registered type. Returns ------- obj : object The registered object for the type of the instance, one of the type's superclasses, or else one of the ABCs the type implements. Raises ------ KeyError if the instance's type has not been registered. """ return self.lookup_by_type(type(instance)) def lookup_by_type(self, typ): """ Look up the registered object for a type. Parameters ---------- typ : type Returns ------- obj : object The registered object for the type, one of its superclasses, or else one of the ABCs it implements. Raises ------ KeyError if the type has not been registered. """ return self.lookup_all_by_type(typ)[-1] def lookup_all(self, instance): """ Look up all the registered objects for the given instance. Parameters ---------- instance : object An instance of a possibly registered type. Returns ------- objs : list of objects The list of registered objects for the instance. If the given instance is not registered, its superclasses are searched. If none of the superclasses are registered, search the possible ABCs. Raises ------ KeyError if the instance's type has not been registered. """ return self.lookup_all_by_type(type(instance)) def lookup_all_by_type(self, typ): """ Look up all the registered objects for a type. Parameters ---------- typ : type Returns ------- objs : list of objects The list of registered objects for the type. If the given type is not registered, its superclasses are searched. If none of the superclasses are registered, search the possible ABCs. Raises ------ KeyError if the type has not been registered. """ # If a concrete superclass is registered use it. for cls in get_mro(typ): if cls in self.type_map or self._in_name_map(cls): objs = self.type_map[cls] if objs: return objs # None of the concrete superclasses. Check the ABCs. for abstract, objs in self.abc_map.items(): if issubclass(typ, abstract) and objs: return objs # If we have reached here, the lookup failed. raise KeyError("No registered value for {0!r}".format(typ)) #### Private implementation ############################################### def _pop_value(self, mapping, key): """ Pop a value from a keyed stack in a mapping, taking care to remove the key if the stack is depleted. """ objs = mapping[key] old = objs.pop() if not objs: del mapping[key] return old def _in_name_map(self, typ): """ Check if the given type is specified in the name map. Parameters ---------- typ : type Returns ------- is_in_name_map : bool If True, the registered value will be moved over to the type map for future lookups. """ key = _mod_name_key(typ) if key in self.name_map: self.type_map[typ] = self.name_map.pop(key) return True else: return False class LazyRegistry(TypeRegistry): """ A type registry that will lazily import the registered objects. Register '__module__:__name__' strings for the lazily imported objects. These will only be imported when the matching type is looked up. The module name must be a fully-qualified absolute name with all of the parent packages specified. """ def lookup_by_type(self, typ): """ Look up the registered object for a type. """ mod_name = TypeRegistry.lookup_by_type(self, typ) return self._import_object(mod_name) def _import_object(self, mod_object): module, name = mod_object.split(':') mod = __import__(module, {}, {}, [name], 0) return getattr(mod, name) apptools-4.5.0/apptools/type_registry/api.py0000644000076500000240000000006612317502217022541 0ustar mdickinsonstaff00000000000000from .type_registry import LazyRegistry, TypeRegistry apptools-4.5.0/apptools/io/0000755000076500000240000000000013547652535017131 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/io/h5/0000755000076500000240000000000013547652535017445 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/io/h5/tests/0000755000076500000240000000000013547652535020607 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/io/h5/tests/test_dict_node.py0000644000076500000240000001374713547637361024164 0ustar mdickinsonstaff00000000000000import numpy as np from numpy.testing import raises, assert_allclose import six from ..dict_node import H5DictNode from .utils import open_h5file, temp_h5_file, temp_file NODE = '/dict_node' def test_create(): with temp_h5_file() as h5: h5dict = H5DictNode.add_to_h5file(h5, NODE) h5dict['a'] = 1 h5dict['b'] = 2 assert h5dict['a'] == 1 assert h5dict['b'] == 2 def test_is_dict_node(): with temp_h5_file() as h5: node = h5.create_dict(NODE, {}) assert H5DictNode.is_dict_node(node._h5_group) def test_is_not_dict_node(): with temp_h5_file() as h5: node = h5.create_group(NODE) assert not H5DictNode.is_dict_node(node) assert not H5DictNode.is_dict_node(node._h5_group) def test_create_with_data(): with temp_h5_file() as h5: data = {'a': 10} h5dict = H5DictNode.add_to_h5file(h5, NODE, data) assert h5dict['a'] == 10 def test_create_with_array_data(): foo = np.arange(100) bar = np.arange(150) with temp_h5_file() as h5: data = {'a': 10, 'foo': foo, 'bar': bar} h5dict = H5DictNode.add_to_h5file(h5, NODE, data) assert h5dict['a'] == 10 assert_allclose(h5dict['foo'], foo) assert_allclose(h5dict['bar'], bar) def test_load_saved_dict_node(): with temp_file() as filename: # Write data to new dict node and close. with open_h5file(filename, 'w') as h5: h5dict = H5DictNode.add_to_h5file(h5, NODE) h5dict['a'] = 1 # Read dict node and make sure the data was saved. with open_h5file(filename, mode='r+') as h5: h5dict = h5[NODE] assert h5dict['a'] == 1 # Change data for next test h5dict['a'] = 2 # Check that data is modified by the previous write. with open_h5file(filename) as h5: h5dict = h5[NODE] assert h5dict['a'] == 2 def test_load_saved_dict_node_with_array(): arr = np.arange(100) arr1 = np.arange(200) with temp_file() as filename: # Write data to new dict node and close. with open_h5file(filename, 'w') as h5: h5dict = H5DictNode.add_to_h5file(h5, NODE) h5dict['arr'] = arr # Read dict node and make sure the data was saved. with open_h5file(filename, mode='r+') as h5: h5dict = h5[NODE] assert_allclose(h5dict['arr'], arr) # Change data for next test h5dict['arr'] = arr1 h5dict['arr_old'] = arr # Check that data is modified by the previous write. with open_h5file(filename) as h5: h5dict = h5[NODE] assert_allclose(h5dict['arr'], arr1) assert_allclose(h5dict['arr_old'], arr) # Make sure that arrays come back as arrays assert isinstance(h5dict['arr'], np.ndarray) assert isinstance(h5dict['arr_old'], np.ndarray) def test_keys(): with temp_h5_file() as h5: keys = set(('hello', 'world', 'baz1')) data = dict((n, 1) for n in keys) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) assert set(h5dict.keys()) == keys def test_delete_item(): with temp_h5_file() as h5: data = dict(a=10) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) del h5dict['a'] assert 'a' not in h5dict def test_delete_array(): with temp_h5_file() as h5: data = dict(a=np.arange(10)) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) del h5dict['a'] assert 'a' not in h5dict assert 'a' not in h5[NODE] def test_auto_flush(): with temp_h5_file() as h5: data = dict(a=1) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) # Overwrite existing data, which should get written to disk. new_data = dict(b=2) h5dict.data = new_data # Load data from disk to check that data was automatically flushed. h5dict_from_disk = h5[NODE] assert 'a' not in h5dict_from_disk assert h5dict_from_disk['b'] == 2 def test_auto_flush_off(): with temp_h5_file() as h5: data = dict(a=1) h5dict = H5DictNode.add_to_h5file(h5, NODE, data, auto_flush=False) # Overwrite existing data, but don't write to disk. new_data = dict(b=2) h5dict.data = new_data # Load data from disk to check that it's unchanged. h5dict_from_disk = h5[NODE] assert h5dict_from_disk['a'] == 1 assert 'b' not in h5dict_from_disk # Manually flush, and check that data was written h5dict.flush() h5dict_from_disk = h5[NODE] assert 'a' not in h5dict_from_disk assert h5dict_from_disk['b'] == 2 @raises(KeyError) def test_undefined_key(): with temp_h5_file() as h5: data = dict(a='int') h5dict = H5DictNode.add_to_h5file(h5, NODE, data) del h5dict['b'] def test_basic_dtypes(): with temp_h5_file() as h5: data = dict(a_int=1, a_float=1.0, a_str='abc') h5dict = H5DictNode.add_to_h5file(h5, NODE, data) assert isinstance(h5dict['a_int'], int) assert isinstance(h5dict['a_float'], float) assert isinstance(h5dict['a_str'], six.string_types) def test_mixed_type_list(): with temp_h5_file() as h5: data = dict(a=[1, 1.0, 'abc']) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) for value, dtype in zip(h5dict['a'], (int, float, six.string_types)): assert isinstance(value, dtype) def test_dict(): with temp_h5_file() as h5: data = dict(a=dict(b=1, c=2)) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) sub_dict = h5dict['a'] assert sub_dict['b'] == 1 assert sub_dict['c'] == 2 @raises(AssertionError) def test_wrap_self_raises_error(): with temp_h5_file() as h5: H5DictNode.add_to_h5file(h5, NODE) node = h5[NODE] H5DictNode(node) if __name__ == '__main__': from numpy import testing testing.run_module_suite() apptools-4.5.0/apptools/io/h5/tests/test_table_node.py0000644000076500000240000000363513422276145024312 0ustar mdickinsonstaff00000000000000import numpy as np from numpy.testing import assert_allclose from pandas import DataFrame from ..table_node import H5TableNode from .utils import temp_h5_file NODE = '/table_node' def test_basics(): description = [('a', np.float64), ('b', np.float64)] with temp_h5_file() as h5: h5table = H5TableNode.add_to_h5file(h5, NODE, description) h5table.append({'a': [1, 2], 'b': [3, 4]}) assert_allclose(h5table['a'], [1, 2]) assert_allclose(h5table['b'], [3, 4]) dtype_description = np.dtype([('c', 'f4'), ('d', 'f4')]) with temp_h5_file() as h5: h5table = H5TableNode.add_to_h5file(h5, NODE, dtype_description) h5table.append({'c': [1.2, 3.4], 'd': [5.6, 7.8]}) assert_allclose(h5table['c'], [1.2, 3.4]) assert_allclose(h5table['d'], [5.6, 7.8]) assert len(repr(h5table)) > 0 def test_getitem(): description = [('a', np.float64), ('b', np.float64)] with temp_h5_file() as h5: h5table = H5TableNode.add_to_h5file(h5, NODE, description) h5table.append({'a': [1, 2], 'b': [3, 4]}) assert_allclose(h5table['a'], (1, 2)) assert_allclose(h5table[['b', 'a']], [(3, 1), (4, 2)]) def test_keys(): description = [('hello', 'int'), ('world', 'int'), ('Qux1', 'bool')] with temp_h5_file() as h5: keys = set(list(zip(*description))[0]) h5table = H5TableNode.add_to_h5file(h5, NODE, description) assert set(h5table.keys()) == keys def test_to_dataframe(): description = [('a', np.float64)] with temp_h5_file() as h5: h5table = H5TableNode.add_to_h5file(h5, NODE, description) h5table.append({'a': [1, 2, 3]}) df = h5table.to_dataframe() assert isinstance(df, DataFrame) assert_allclose(df['a'], h5table['a']) if __name__ == '__main__': from numpy import testing testing.run_module_suite() apptools-4.5.0/apptools/io/h5/tests/__init__.py0000644000076500000240000000000012473127616022700 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/io/h5/tests/utils.py0000644000076500000240000000100712473127616022311 0ustar mdickinsonstaff00000000000000from contextlib import contextmanager import tempfile import os from ..utils import open_h5file SEPARATOR = '-' * 60 @contextmanager def temp_file(suffix='', prefix='tmp', dir=None): fd, filename = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir) try: yield filename finally: os.close(fd) os.unlink(filename) @contextmanager def temp_h5_file(**kwargs): with temp_file(suffix='h5') as fn: with open_h5file(fn, mode='a', **kwargs) as h5: yield h5 apptools-4.5.0/apptools/io/h5/tests/test_file.py0000644000076500000240000004463213547637361023150 0ustar mdickinsonstaff00000000000000import os from contextlib import closing import numpy as np from numpy import testing import tables from ..file import H5File from ..dict_node import H5DictNode from ..table_node import H5TableNode from .utils import open_h5file, temp_h5_file H5_TEST_FILE = '_temp_test_filt.h5' def teardown(): try: os.remove(H5_TEST_FILE) except OSError: pass def test_reopen(): h5 = H5File(H5_TEST_FILE, mode='w') assert h5.is_open h5.close() assert not h5.is_open h5.open() assert h5.is_open h5.close() def test_open_from_pytables_object(): with closing(tables.File(H5_TEST_FILE, 'w')) as pyt_file: pyt_file.create_group('/', 'my_group') with open_h5file(pyt_file) as h5: assert '/my_group' in h5 def test_open_from_closed_pytables_object(): with closing(tables.File(H5_TEST_FILE, 'w')) as pyt_file: pyt_file.create_group('/', 'my_group') pyt_file.close() with open_h5file(pyt_file) as h5: assert '/my_group' in h5 def test_create_array_with_H5File(): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5array = h5.create_array('/array', array) # Test returned array testing.assert_allclose(h5array, array) # Test stored array testing.assert_allclose(h5['/array'], array) def test_create_array_with_H5Group(): array = np.arange(3) node_path = '/tardigrade/array' with open_h5file(H5_TEST_FILE, mode='w') as h5: group = h5.create_group('/tardigrade') h5array = group.create_array('array', array) # Test returned array testing.assert_allclose(h5array, array) # Test stored array testing.assert_allclose(h5[node_path], array) def test_getitem_failure(): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.create_array('/array', array) testing.assert_raises(NameError, h5.__getitem__, '/not_there') def test_iteritems(): node_paths = ['/foo', '/bar', '/baz'] array = np.arange(3) with open_h5file(H5_TEST_FILE, mode='w') as h5: for path in node_paths: h5.create_array(path, array) # We expect to see the root node when calling iteritems... node_paths.append('/') iter_paths = [] # 2to3 converts the iteritems blindly to items which is incorrect, # so we resort to this ugliness. items = getattr(h5, 'iteritems')() for path, node in items: iter_paths.append(path) assert set(node_paths) == set(iter_paths) def test_create_plain_array_with_H5File(): with open_h5file(H5_TEST_FILE, mode='w') as h5: h5array = h5.create_array('/array', np.arange(3), chunked=False) assert isinstance(h5array, tables.Array) assert not isinstance(h5array, tables.CArray) def test_create_plain_array_with_H5Group(): with open_h5file(H5_TEST_FILE, mode='w') as h5: h5array = h5.root.create_array('/array', np.arange(3), chunked=False) assert isinstance(h5array, tables.Array) assert not isinstance(h5array, tables.CArray) def test_create_chunked_array_with_H5File(): array = np.arange(3, dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5array = h5.create_array('/array', array, chunked=True) testing.assert_allclose(h5array, array) assert isinstance(h5array, tables.CArray) def test_create_chunked_array_with_H5Group(): array = np.arange(3, dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5array = h5.root.create_array('/array', array, chunked=True) testing.assert_allclose(h5array, array) assert isinstance(h5array, tables.CArray) def test_create_extendable_array_with_H5File(): array = np.arange(3, dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5array = h5.create_array('/array', array, extendable=True) testing.assert_allclose(h5array, array) assert isinstance(h5array, tables.EArray) def test_create_extendable_array_with_H5Group(): array = np.arange(3, dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5array = h5.root.create_array('/array', array, extendable=True) testing.assert_allclose(h5array, array) assert isinstance(h5array, tables.EArray) def test_str_and_repr(): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.create_array('/array', array) assert repr(h5) == repr(h5._h5) assert str(h5) == str(h5._h5) def test_shape_and_dtype(): array = np.ones((3, 4), dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode='w') as h5: for node, chunked in (('/array', False), ('/carray', True)): h5array = h5.create_array(node, array.shape, dtype=array.dtype, chunked=chunked) assert h5array.dtype == array.dtype assert h5array.shape == array.shape def test_shape_only_raises(): shape = (3, 4) with open_h5file(H5_TEST_FILE, mode='w') as h5: testing.assert_raises(ValueError, h5.create_array, '/array', shape) def test_create_duplicate_array_raises(): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode='w', delete_existing=False) as h5: h5.create_array('/array', array) testing.assert_raises(ValueError, h5.create_array, '/array', array) def test_delete_existing_array_with_H5File(): old_array = np.arange(3) new_array = np.ones(5) with open_h5file(H5_TEST_FILE, mode='w', delete_existing=True) as h5: h5.create_array('/array', old_array) # New array with the same node name should delete old array h5.create_array('/array', new_array) testing.assert_allclose(h5['/array'], new_array) def test_delete_existing_array_with_H5Group(): old_array = np.arange(3) new_array = np.ones(5) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.create_array('/array', old_array) # New array with the same node name should delete old array h5.root.create_array('/array', new_array, delete_existing=True) testing.assert_allclose(h5['/array'], new_array) def test_delete_existing_dict_with_H5File(): old_dict = {'a': 'Goose'} new_dict = {'b': 'Quail'} with open_h5file(H5_TEST_FILE, mode='w', delete_existing=True) as h5: h5.create_dict('/dict', old_dict) # New dict with the same node name should delete old dict h5.create_dict('/dict', new_dict) assert h5['/dict'].data == new_dict def test_delete_existing_dict_with_H5Group(): old_dict = {'a': 'Goose'} new_dict = {'b': 'Quail'} with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.create_dict('/dict', old_dict) # New dict with the same node name should delete old dict h5.root.create_dict('/dict', new_dict, delete_existing=True) assert h5['/dict'].data == new_dict def test_delete_existing_table_with_H5File(): old_description = [('Honk', 'int'), ('Wink', 'float')] new_description = [('Toot', 'float'), ('Pop', 'int')] with open_h5file(H5_TEST_FILE, mode='w', delete_existing=True) as h5: h5.create_table('/table', old_description) # New table with the same node name should delete old table h5.create_table('/table', new_description) tab = h5['/table'] tab.append({'Pop': (1,), 'Toot': (np.pi,)}) assert tab.ix[0][0] == np.pi def test_delete_existing_table_with_H5Group(): old_description = [('Honk', 'int'), ('Wink', 'float')] new_description = [('Toot', 'float'), ('Pop', 'int')] with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.create_table('/table', old_description) # New table with the same node name should delete old table h5.root.create_table('/table', new_description, delete_existing=True) tab = h5['/table'] tab.append({'Pop': (1,), 'Toot': (np.pi,)}) assert tab.ix[0][0] == np.pi def test_delete_existing_group_with_H5File(): with open_h5file(H5_TEST_FILE, mode='w', delete_existing=True) as h5: h5.create_group('/group') grp = h5['/group'] grp.attrs['test'] = 4 assert grp.attrs['test'] == 4 h5.create_group('/group') grp = h5['/group'] grp.attrs['test'] = 6 assert grp.attrs['test'] == 6 def test_delete_existing_group_with_H5Group(): with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.create_group('/group') grp = h5['/group'] grp.attrs['test'] = 4 assert grp.attrs['test'] == 4 h5.root.create_group('/group', delete_existing=True) grp = h5['/group'] grp.attrs['test'] = 6 assert grp.attrs['test'] == 6 def test_remove_group_with_H5File(): with open_h5file(H5_TEST_FILE, mode='w', delete_existing=True) as h5: h5.create_group('/group') assert '/group' in h5 h5.remove_group('/group') assert '/group' not in h5 def test_remove_group_with_H5Group(): node_path = '/waterbear/group' with open_h5file(H5_TEST_FILE, mode='w', delete_existing=True) as h5: group = h5.create_group('/waterbear') group.create_group('group') assert node_path in h5 group.remove_group('group') assert node_path not in h5 @testing.raises(ValueError) def test_remove_group_with_remove_node(): node_path = '/group' with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.create_group(node_path) h5.remove_node(node_path) # Groups should be removed w/ `remove_group` def test_remove_node_with_H5File(): with open_h5file(H5_TEST_FILE, mode='w', delete_existing=True) as h5: h5.create_array('/array', np.arange(3)) assert '/array' in h5 h5.remove_node('/array') assert '/array' not in h5 def test_remove_node_with_H5Group(): node_path = '/waterbear/array' with open_h5file(H5_TEST_FILE, mode='w', delete_existing=True) as h5: group = h5.create_group('/waterbear') h5.create_array(node_path, np.arange(3)) assert node_path in h5 group.remove_node('array') assert node_path not in h5 def test_read_mode_raises_on_nonexistent_file(): cm = open_h5file('_nonexistent_.h5', mode='r') testing.assert_raises(IOError, cm.__enter__) cm = open_h5file('_nonexistent_.h5', mode='r+') testing.assert_raises(IOError, cm.__enter__) def test_cleanup(): with open_h5file(H5_TEST_FILE, mode='w') as h5: h5_pytables = h5._h5 # This reference gets deleted on close assert not h5_pytables.isopen def test_create_group_with_H5File(): with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.create_group('/group') assert '/group' in h5 def test_create_group_with_H5Group(): with open_h5file(H5_TEST_FILE, mode='w') as h5: group = h5['/'] group.create_group('group') assert '/group' in h5 def test_split_path(): path, node = H5File.split_path('/') assert path == '/' assert node == '' path, node = H5File.split_path('/node') assert path == '/' assert node == 'node' path, node = H5File.split_path('/group/node') assert path == '/group' assert node == 'node' def test_join_path(): path = H5File.join_path('/', 'a', 'b', 'c') assert path == '/a/b/c' path = H5File.join_path('a', 'b/c') assert path == '/a/b/c' path = H5File.join_path('a', '/b', '/c') assert path == '/a/b/c' def test_auto_groups(): with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.auto_groups = True h5.create_array('/group/array', np.arange(3)) assert '/group/array' in h5 def test_auto_groups_deep(): with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.auto_groups = True h5.create_array('/group1/group2/array', np.arange(3)) assert '/group1/group2/array' in h5 def test_groups(): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5.create_array('/a/b/array', array) group_a = h5['/a'] # Check that __contains__ works for groups assert 'b' in group_a assert 'b/array' in group_a group_b = group_a['b'] # Check that __contains__ works for arrays assert 'array' in group_b testing.assert_allclose(group_b['array'], array) testing.assert_allclose(group_a['b/array'], array) def test_group_attributes(): value_1 = 'foo' value_2 = 'bar' with open_h5file(H5_TEST_FILE, mode='w') as h5: h5['/'].attrs['name'] = value_1 assert h5['/'].attrs['name'] == value_1 group = h5['/'] # Make sure changes to the attribute consistent group._h5_group._v_attrs['name'] = value_2 assert group.attrs['name'] == value_2 def test_group_properties(): with open_h5file(H5_TEST_FILE, mode='w', auto_groups=True) as h5: h5.create_array('/group1/group2/array', np.arange(3)) h5.create_array('/group1/array1', np.arange(3)) assert h5['/group1'].name == 'group1' child_names = h5['/group1'].children_names assert sorted(child_names) == sorted(['group2', 'array1']) sub_names = h5['/group1'].subgroup_names assert sub_names == ['group2'] assert h5['/group1'].root.name == '/' assert h5['/group1/group2'].root.name == '/' def test_iter_groups(): with open_h5file(H5_TEST_FILE, mode='w', auto_groups=True) as h5: h5.create_array('/group1/array', np.arange(3)) h5.create_array('/group1/subgroup/deep_array', np.arange(3)) group = h5['/group1'] assert set(n.name for n in group.iter_groups()) == set(['subgroup']) def test_mapping_interface_for_file(): with open_h5file(H5_TEST_FILE, mode='w', auto_groups=True) as h5: array = h5.create_array('/array', np.arange(3)) h5.create_array('/group/deep_array', np.arange(3)) # `deep_array` isn't a direct descendent and isn't counted. assert len(h5) == 2 assert '/group' in h5 assert '/array' in h5 testing.assert_allclose(h5['/array'], array) assert set(n.name for n in h5) == set(['array', 'group']) def test_mapping_interface_for_group(): with open_h5file(H5_TEST_FILE, mode='w', auto_groups=True) as h5: array = h5.create_array('/group1/array', np.arange(3)) h5.create_array('/group1/subgroup/deep_array', np.arange(3)) group = h5['/group1'] # `deep_array` isn't a direct descendent and isn't counted. assert len(group) == 2 assert 'subgroup' in group assert 'array' in group testing.assert_allclose(group['array'], array) assert set(n.name for n in group) == set(['array', 'subgroup']) def test_group_str_and_repr(): with open_h5file(H5_TEST_FILE, mode='w') as h5: group = h5['/'] assert str(group) == str(group._h5_group) assert repr(group) == repr(group._h5_group) def test_attribute_translation(): value_1 = (1, 2, 3) value_1_array = np.array(value_1) with open_h5file(H5_TEST_FILE, mode='w') as h5: h5['/'].attrs['name'] = value_1 assert isinstance(h5['/'].attrs['name'], np.ndarray) testing.assert_allclose(h5['/'].attrs['name'], value_1_array) def test_get_attribute(): with open_h5file(H5_TEST_FILE, mode='w') as h5: attrs = h5['/'].attrs attrs['name'] = 'hello' assert attrs.get('name') == attrs['name'] def test_del_attribute(): with open_h5file(H5_TEST_FILE, mode='w') as h5: attrs = h5['/'].attrs attrs['name'] = 'hello' del attrs['name'] assert 'name' not in attrs def test_get_attribute_default(): with open_h5file(H5_TEST_FILE, mode='w') as h5: assert h5['/'].attrs.get('missing', 'null') == 'null' def test_attribute_update(): with open_h5file(H5_TEST_FILE, mode='w') as h5: attrs = h5['/'].attrs attrs.update({'a': 1, 'b': 2}) assert attrs['a'] == 1 assert attrs['b'] == 2 attrs.update({'b': 20, 'c': 30}) assert attrs['b'] == 20 assert attrs['c'] == 30 def test_attribute_iteration_methods(): with open_h5file(H5_TEST_FILE, mode='w') as h5: attrs = h5['/'].attrs attrs['organ'] = 'gallbladder' attrs['count'] = 42 attrs['alpha'] = 0xff items = list(attrs.items()) assert all(isinstance(x, tuple) for x in items) # unfold the pairs keys, vals = [list(item) for item in zip(*items)] assert keys == list(attrs.keys()) assert vals == list(attrs.values()) # Check that __iter__ is consistent assert keys == list(iter(attrs)) assert len(attrs) == len(keys) def test_bad_node_name(): with open_h5file(H5_TEST_FILE, mode='w') as h5: testing.assert_raises(ValueError, h5.create_array, '/attrs', np.zeros(3)) def test_bad_group_name(): with open_h5file(H5_TEST_FILE, mode='w') as h5: testing.assert_raises(ValueError, h5.create_array, '/attrs/array', np.zeros(3)) def test_create_dict_with_H5File(): data = {'a': 1} with temp_h5_file() as h5: h5.create_dict('/dict', data) assert isinstance(h5['/dict'], H5DictNode) assert h5['/dict']['a'] == 1 def test_create_dict_with_H5Group(): node_path = '/bananas/dict' data = {'a': 1} with temp_h5_file() as h5: group = h5.create_group('/bananas') group.create_dict('dict', data) assert isinstance(h5[node_path], H5DictNode) assert h5[node_path]['a'] == 1 def test_create_table_with_H5File(): description = [('foo', 'int'), ('bar', 'float')] with temp_h5_file() as h5: h5.create_table('/table', description) tab = h5['/table'] assert isinstance(tab, H5TableNode) tab.append({'foo': (1,), 'bar': (np.pi,)}) assert tab.ix[0][0] == 1 assert tab.ix[0][1] == np.pi h5.remove_node('/table') assert '/table' not in h5 def test_create_table_with_H5Group(): node_path = '/rhinocerous/table' description = [('foo', 'int'), ('bar', 'float')] with temp_h5_file() as h5: group = h5.create_group('/rhinocerous') group.create_table('table', description) tab = h5[node_path] assert isinstance(tab, H5TableNode) tab.append({'foo': (1,), 'bar': (np.pi,)}) assert tab.ix[0][0] == 1 assert tab.ix[0][1] == np.pi group.remove_node('table') assert node_path not in h5 if __name__ == '__main__': testing.run_module_suite() apptools-4.5.0/apptools/io/h5/__init__.py0000644000076500000240000000000012473127616021536 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/io/h5/table_node.py0000644000076500000240000001140713547637361022116 0ustar mdickinsonstaff00000000000000import numpy as np from pandas import DataFrame import six from tables.table import Table as PyTablesTable class _TableRowAccessor(object): """ A simple object which provides read access to the rows in a Table. """ def __init__(self, h5_table): self._h5_table = h5_table def __getitem__(self, key): return self._h5_table[key] class H5TableNode(object): """ A wrapper for PyTables Table nodes. Parameters ---------- node : tables.Table instance An H5 node which is a pytables.Table or H5TableNode instance """ def __init__(self, node): # Avoid a circular import from .file import H5Attrs assert self.is_table_node(node) self._h5_table = node._h5_table if hasattr(node, '_h5_table') else node self.attrs = H5Attrs(self._h5_table._v_attrs) #-------------------------------------------------------------------------- # Creation methods #-------------------------------------------------------------------------- @classmethod def add_to_h5file(cls, h5, node_path, description, **kwargs): """ Add table node to an H5 file at the specified path. Parameters ---------- h5 : H5File The H5 file where the table node will be stored. node_path : str Path to node where data is stored (e.g. '/path/to/my_table') description : list of tuples or numpy dtype object The description of the columns in the table. This is either a list of (column name, dtype, [, shape or itemsize]) tuples or a numpy record array dtype. For more information, see the documentation for `Table` in PyTables. **kwargs : dict Additional keyword arguments to pass to pytables.File.create_table """ if isinstance(description, (tuple, list)): description = np.dtype(description) cls._create_pytables_node(h5, node_path, description, **kwargs) node = h5[node_path] return cls(node) @classmethod def is_table_node(cls, pytables_node): """ Return True if pytables_node is a pytables.Table or a H5TableNode. """ return isinstance(pytables_node, (PyTablesTable, H5TableNode)) #-------------------------------------------------------------------------- # Public interface #-------------------------------------------------------------------------- def append(self, data): """ Add some data to the table. Parameters ---------- data : dict A dictionary of column name -> values items """ rows = list(zip(*[data[name] for name in self.keys()])) self._h5_table.append(rows) def __getitem__(self, col_or_cols): """ Return one or more columns of data from the table. Parameters ---------- col_or_cols : str or list of str A single column name or a list of column names Return ------ data : ndarray An array of column data with the column order matching that of `col_or_cols`. """ if isinstance(col_or_cols, six.string_types): return self._h5_table.col(col_or_cols) column_data = [self._h5_table.col(name) for name in col_or_cols] return np.column_stack(column_data) @property def ix(self): """ Return an object which provides access to row data. """ return _TableRowAccessor(self._h5_table) def keys(self): return self._h5_table.colnames def to_dataframe(self): """ Return table data as a pandas `DataFrame`. XXX: This does not work if the table contains a multidimensional column """ # Slicing rows gives a numpy struct array, which DataFrame understands. return DataFrame(self.ix[:]) #-------------------------------------------------------------------------- # Object interface #-------------------------------------------------------------------------- def __repr__(self): return repr(self._h5_table) def __len__(self): return self._h5_table.nrows #-------------------------------------------------------------------------- # Private interface #-------------------------------------------------------------------------- def _f_remove(self): """ Implement the PyTables `Node._f_remove` method so that H5File doesn't choke when trying to remove our node. """ self._h5_table._f_remove() self._h5_table = None @classmethod def _create_pytables_node(cls, h5, node_path, description, **kwargs): path, name = h5.split_path(node_path) pyt_file = h5._h5 pyt_file.create_table(path, name, description, **kwargs) apptools-4.5.0/apptools/io/h5/file.py0000644000076500000240000003710113547637361020740 0ustar mdickinsonstaff00000000000000from collections import Mapping, MutableMapping from functools import partial import numpy as np import tables from .dict_node import H5DictNode from .table_node import H5TableNode def get_atom(dtype): """ Return a PyTables Atom for the given dtype or dtype string. """ return tables.Atom.from_dtype(np.dtype(dtype)) def iterator_length(iterator): return sum(1 for _ in iterator) def _update_wrapped_docstring(wrapped, original=None): PREAMBLE = """\ ** H5Group wrapper for H5File.{func_name}: ** Note that the first argument is a nodepath relative to the group, rather than an absolute path. Below is the original docstring: """.format(func_name=wrapped.__name__) wrapped.__doc__ = PREAMBLE + original.__doc__ return wrapped def h5_group_wrapper(original): return partial(_update_wrapped_docstring, original=original) class H5File(Mapping): """File object for HDF5 files. This class wraps PyTables to provide a cleaner, but only implements an interface for accessing arrays. Parameters ---------- filename : str or a `tables.File` instance Filename for an HDF5 file, or a PyTables `File` object. mode : str Mode to open the file: 'r' : Read-only 'w' : Write; create new file (an existing file would be deleted). 'a' : Read and write to file; create if not existing 'r+': Read and write to file; must already exist delete_existing : bool If True, an existing node will be deleted when a `create_*` method is called. Otherwise, a ValueError will be raise. auto_groups : bool If True, `create_array` will automatically create parent groups. auto_open : bool If True, open the file automatically on initialization. Otherwise, you can call `H5File.open()` explicitly after initialization. chunked : bool If True, the default behavior of `create_array` will be a chunked array (see PyTables `create_carray`). """ exists_error = ("'{}' exists in '{}'; set `delete_existing` attribute " "to True to overwrite existing calculations.") def __init__(self, filename, mode='r+', delete_existing=False, auto_groups=True, auto_open=True, h5filters=None): self.mode = mode self.delete_existing = delete_existing self.auto_groups = auto_groups if h5filters is None: self.h5filters = tables.Filters(complib='blosc', complevel=5, shuffle=True) self._h5 = None if isinstance(filename, tables.File): pyt_file = filename filename = pyt_file.filename if pyt_file.isopen: self._h5 = pyt_file self.filename = filename if auto_open: self.open() def open(self): if not self.is_open: self._h5 = tables.open_file(self.filename, mode=self.mode) def close(self): if self.is_open: self._h5.close() self._h5 = None @property def root(self): return self['/'] @property def is_open(self): return self._h5 is not None def __str__(self): return str(self._h5) def __repr__(self): return repr(self._h5) def __contains__(self, node_path): return node_path in self._h5 def __getitem__(self, node_path): try: node = self._h5.get_node(node_path) except tables.NoSuchNodeError: msg = "Node {0!r} not found in {1!r}" raise NameError(msg.format(node_path, self.filename)) return _wrap_node(node) def __iter__(self): return (_wrap_node(n) for n in self._h5.iter_nodes(where='/')) def __len__(self): return iterator_length(self) def iteritems(self, path='/'): """ Iterate over node paths and nodes of the h5 file. """ for node in self._h5.walk_nodes(where=path): node_path = node._v_pathname yield node_path, _wrap_node(node) def create_array(self, node_path, array_or_shape, dtype=None, chunked=False, extendable=False, **kwargs): """Create node to store an array. Parameters ---------- node_path : str PyTable node path; e.g. '/path/to/node'. array_or_shape : array or shape tuple Array or shape tuple for an array. If given a shape tuple, the `dtype` parameter must also specified. dtype : str or numpy.dtype Data type of array. Only necessary if `array_or_shape` is a shape. chunked : bool Controls whether the array is chunked. extendable : {None | bool} Controls whether the array is extendable. kwargs : key/value pairs Keyword args passed to PyTables `File.create_(c|e)array`. """ self._check_node(node_path) self._assert_valid_path(node_path) h5 = self._h5 if isinstance(array_or_shape, tuple): if dtype is None: msg = "`dtype` must be specified if only given array shape." raise ValueError(msg) array = None dtype = dtype shape = array_or_shape else: array = array_or_shape dtype = array.dtype.name shape = array.shape path, name = self.split_path(node_path) if extendable: shape = (0,) + shape[1:] atom = get_atom(dtype) node = h5.create_earray(path, name, atom, shape, filters=self.h5filters, **kwargs) if array is not None: node.append(array) elif chunked: atom = get_atom(dtype) node = h5.create_carray(path, name, atom, shape, filters=self.h5filters, **kwargs) if array is not None: node[:] = array else: if array is None: array = np.zeros(shape, dtype=dtype) node = h5.create_array(path, name, array, **kwargs) return node def create_group(self, group_path, **kwargs): """Create group. Parameters ---------- group_path : str PyTable group path; e.g. '/path/to/group'. kwargs : key/value pairs Keyword args passed to PyTables `File.create_group`. """ self._check_node(group_path) self._assert_valid_path(group_path) path, name = self.split_path(group_path) self._h5.create_group(path, name, **kwargs) return self[group_path] def create_dict(self, node_path, data=None, **kwargs): """ Create dict node at the specified path. Parameters ---------- node_path : str Path to node where data is stored (e.g. '/path/to/my_dict') data : dict Data for initialization, if desired. """ self._check_node(node_path) self._assert_valid_path(node_path) H5DictNode.add_to_h5file(self, node_path, data=data, **kwargs) return self[node_path] def create_table(self, node_path, description, **kwargs): """ Create table node at the specified path. Parameters ---------- node_path : str Path to node where data is stored (e.g. '/path/to/my_dict') description : dict or numpy dtype object The description of the columns in the table. This is either a dict of column name -> dtype items or a numpy record array dtype. For more information, see the documentation for Table in pytables. """ self._check_node(node_path) self._assert_valid_path(node_path) H5TableNode.add_to_h5file(self, node_path, description, **kwargs) return self[node_path] def _check_node(self, node_path): """Check if node exists and create parent groups if necessary. Either raise error or delete depending on `delete_existing` attribute. """ if self.auto_groups: path, name = self.split_path(node_path) self._create_required_groups(path) if node_path in self: if self.delete_existing: if isinstance(self[node_path], H5Group): self.remove_group(node_path, recursive=True) else: self.remove_node(node_path) else: msg = self.exists_error.format(node_path, self.filename) raise ValueError(msg) def _create_required_groups(self, path): if path not in self: parent, missing = self.split_path(path) # Call recursively to ensure that all parent groups exist. self._create_required_groups(parent) self.create_group(path) def remove_node(self, node_path): """Remove node Parameters ---------- node_path : str PyTable node path; e.g. '/path/to/node'. """ node = self[node_path] if isinstance(node, H5Group): msg = "{!r} is a group. Use `remove_group` to remove group nodes." raise ValueError(msg.format(node.pathname)) node._f_remove() def remove_group(self, group_path, **kwargs): """Remove group Parameters ---------- group_path : str PyTable group path; e.g. '/path/to/group'. """ self[group_path]._h5_group._g_remove(**kwargs) @classmethod def _assert_valid_path(self, node_path): if 'attrs' in node_path.split('/'): raise ValueError("'attrs' is an invalid node name.") @classmethod def split_path(cls, node_path): """Split node path returning the base path and node name. For example: '/path/to/node' will return '/path/to' and 'node' Parameters ---------- node_path : str PyTable node path; e.g. '/path/to/node'. """ i = node_path.rfind('/') if i == 0: return '/', node_path[1:] else: return node_path[:i], node_path[i + 1:] @classmethod def join_path(cls, *args): """Join parts of an h5 path. For example, the 3 argmuments 'path', 'to', 'node' will return '/path/to/node'. Parameters ---------- args : str Parts of path to be joined. """ path = '/'.join(part.strip('/') for part in args) if not path.startswith('/'): path = '/' + path return path class H5Attrs(MutableMapping): """ An attributes dictionary for an h5 node. This intercepts `__setitem__` so that python sequences can be converted to numpy arrays. This helps preserve the readability of our HDF5 files by other (non-python) programs. """ def __init__(self, node_attrs): self._node_attrs = node_attrs def __delitem__(self, key): del self._node_attrs[key] def __getitem__(self, key): return self._node_attrs[key] def __iter__(self): return iter(self.keys()) def __len__(self): return len(self._node_attrs._f_list()) def __setitem__(self, key, value): if isinstance(value, tuple) or isinstance(value, list): value = np.array(value) self._node_attrs[key] = value def get(self, key, default=None): return default if key not in self else self[key] def keys(self): return self._node_attrs._f_list() def values(self): return [self[k] for k in self.keys()] def items(self): return [(k, self[k]) for k in self.keys()] class H5Group(Mapping): """ A group node in an H5File. This is a thin wrapper around PyTables' Group object to expose attributes and maintain the dict interface of H5File. """ def __init__(self, pytables_group): self._h5_group = pytables_group self.attrs = H5Attrs(self._h5_group._v_attrs) def __contains__(self, node_path): return node_path in self._h5_group def __str__(self): return str(self._h5_group) def __repr__(self): return repr(self._h5_group) def __getitem__(self, node_path): parts = node_path.split('/') # PyTables stores children as attributes node = self._h5_group.__getattr__(parts[0]) node = _wrap_node(node) if len(parts) == 1: return node else: return node['/'.join(parts[1:])] def __iter__(self): return (_wrap_node(c) for c in self._h5_group) def __len__(self): return iterator_length(self) @property def pathname(self): return self._h5_group._v_pathname @property def name(self): return self._h5_group._v_name @property def filename(self): return self._h5_group._v_file.filename @property def root(self): return _wrap_node(self._h5_group._v_file.root) @property def children_names(self): return list(self._h5_group._v_children.keys()) @property def subgroup_names(self): return list(self._h5_group._v_groups.keys()) def iter_groups(self): """ Iterate over `H5Group` nodes that are children of this group. """ return (_wrap_node(g) for g in self._h5_group._v_groups.itervalues()) @h5_group_wrapper(H5File.create_group) def create_group(self, group_subpath, delete_existing=False, **kwargs): return self._delegate_to_h5file('create_group', group_subpath, delete_existing=delete_existing, **kwargs) @h5_group_wrapper(H5File.remove_group) def remove_group(self, group_subpath, **kwargs): return self._delegate_to_h5file('remove_group', group_subpath, **kwargs) @h5_group_wrapper(H5File.create_array) def create_array(self, node_subpath, array_or_shape, dtype=None, chunked=False, extendable=False, **kwargs): return self._delegate_to_h5file('create_array', node_subpath, array_or_shape, dtype=dtype, chunked=chunked, extendable=extendable, **kwargs) @h5_group_wrapper(H5File.create_table) def create_table(self, node_subpath, description, *args, **kwargs): return self._delegate_to_h5file('create_table', node_subpath, description, *args, **kwargs) @h5_group_wrapper(H5File.create_dict) def create_dict(self, node_subpath, data=None, **kwargs): return self._delegate_to_h5file('create_dict', node_subpath, data=data, **kwargs) @h5_group_wrapper(H5File.remove_node) def remove_node(self, node_subpath, **kwargs): return self._delegate_to_h5file('remove_node', node_subpath, **kwargs) def _delegate_to_h5file(self, function_name, node_subpath, *args, **kwargs): delete_existing = kwargs.pop('delete_existing', False) h5 = H5File(self._h5_group._v_file, delete_existing=delete_existing) group_path = h5.join_path(self.pathname, node_subpath) func = getattr(h5, function_name) return func(group_path, *args, **kwargs) def _wrap_node(node): """ Wrap PyTables node object, if necessary. """ if isinstance(node, tables.Group): if H5DictNode.is_dict_node(node): node = H5DictNode(node) else: node = H5Group(node) elif H5TableNode.is_table_node(node): node = H5TableNode(node) return node apptools-4.5.0/apptools/io/h5/utils.py0000644000076500000240000000127512473127616021156 0ustar mdickinsonstaff00000000000000from contextlib import contextmanager from .file import H5File @contextmanager def open_h5file(filename, mode='r+', **kwargs): """Context manager for reading an HDF5 file as an H5File object. Parameters ---------- filename : str HDF5 file name. mode : str Mode to open the file: 'r' : Read-only 'w' : Write; create new file (an existing file would be deleted). 'a' : Read and write to file; create if not existing 'r+': Read and write to file; must already exist See `H5File` for additional keyword arguments. """ h5 = H5File(filename, mode=mode, **kwargs) try: yield h5 finally: h5.close() apptools-4.5.0/apptools/io/h5/dict_node.py0000644000076500000240000001641113547637361021752 0ustar mdickinsonstaff00000000000000from contextlib import closing import json from numpy import ndarray from tables import Group as PyTablesGroup from tables.nodes import filenode #: The key name which identifies array objects in the JSON dict. ARRAY_PROXY_KEY = '__array__' NODE_KEY = 'node_name' class H5DictNode(object): """ Dictionary-like node interface. Data for the dict is stored as a JSON file in a PyTables FileNode. This allows easy storage of Python objects, such as dictionaries and lists of different data types. Note that this is implemented using a group-node assuming that arrays are valid inputs and will be stored as H5 array nodes. Parameters ---------- h5_group : H5Group instance Group node which will be used as a dictionary store. auto_flush : bool If True, write data to disk whenever the dict data is altered. Otherwise, call `flush()` explicitly to write data to disk. """ #: Name of filenode where dict data is stored. _pyobject_data_node = '_pyobject_data' def __init__(self, h5_group, auto_flush=True): assert self.is_dict_node(h5_group) h5_group = self._get_pyt_group(h5_group) self._h5_group = h5_group self.auto_flush = auto_flush # Load dict data from the file node. dict_node = getattr(h5_group, self._pyobject_data_node) with closing(filenode.open_node(dict_node)) as f: self._pyobject_data = json.loads( f.read().decode('ascii'), object_hook=self._object_hook ) #-------------------------------------------------------------------------- # Dictionary interface #-------------------------------------------------------------------------- def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value if self.auto_flush: self.flush() def __delitem__(self, key): del self.data[key] if self.auto_flush: self.flush() def __contains__(self, key): return key in self.data def keys(self): return self.data.keys() #-------------------------------------------------------------------------- # Public interface #-------------------------------------------------------------------------- @property def data(self): return self._pyobject_data @data.setter def data(self, new_data_dict): self._pyobject_data = new_data_dict if self.auto_flush: self.flush() def flush(self): """ Write buffered data to disk. """ self._remove_pyobject_node() self._write_pyobject_node() @classmethod def add_to_h5file(cls, h5, node_path, data=None, **kwargs): """ Add dict node to an H5 file at the specified path. Parameters ---------- h5 : H5File The H5 file where the dictionary data will be stored. node_path : str Path to node where data is stored (e.g. '/path/to/my_dict') data : dict Data for initialization, if desired. """ h5.create_group(node_path) group = h5[node_path] cls._create_pyobject_node(h5._h5, node_path, data=data) return cls(group, **kwargs) @classmethod def is_dict_node(cls, pytables_node): """ Return True if PyTables node looks like an H5DictNode. NOTE: That this returns False if the node is an `H5DictNode` instance, since the input node should be a normal PyTables Group node. """ # Import here to prevent circular imports from .file import H5Group if isinstance(pytables_node, H5Group): pytables_node = cls._get_pyt_group(pytables_node) if not isinstance(pytables_node, PyTablesGroup): return False return cls._pyobject_data_node in pytables_node._v_children #-------------------------------------------------------------------------- # Private interface #-------------------------------------------------------------------------- def _f_remove(self): """ This is called by H5File whenever a node is removed. All nodes in `_h5_group` will be removed. """ for name in self._h5_group._v_children.keys(): if name != self._pyobject_data_node: self._h5_group.__getattr__(name)._f_remove() # Remove the dict node self._remove_pyobject_node() # Remove the group node self._h5_group._f_remove() def _object_hook(self, dct): """ This gets passed object dictionaries by `json.load(s)` and if it finds `ARRAY_PROXY_KEY` in the object description it returns the proxied array object. """ if ARRAY_PROXY_KEY in dct: node_name = dct[NODE_KEY] return getattr(self._h5_group, node_name)[:] return dct def _remove_pyobject_node(self): node = getattr(self._h5_group, self._pyobject_data_node) node._f_remove() def _write_pyobject_node(self): pyt_file = self._h5_group._v_file node_path = self._h5_group._v_pathname self._create_pyobject_node(pyt_file, node_path, self.data) @classmethod def _create_pyobject_node(cls, pyt_file, node_path, data=None): if data is None: data = {} # Stash the array values in their own h5 nodes and return a dictionary # which is appropriate for JSON serialization. out_data = cls._handle_array_values(pyt_file, node_path, data) kwargs = dict(where=node_path, name=cls._pyobject_data_node) with closing(filenode.new_node(pyt_file, **kwargs)) as f: f.write(json.dumps(out_data).encode('ascii')) @classmethod def _get_pyt_group(self, group): if hasattr(group, '_h5_group'): group = group._h5_group return group @classmethod def _array_proxy(cls, pyt_file, group, key, array): """ Stores an array as a normal H5 node and returns the proxy object which will be serialized to JSON. `ARRAY_PROXY_KEY` marks the object dictionary as an array proxy so that `_object_hook` can recognize it. `NODE_KEY` stores the node name of the array so that `_object_hook` can load the array data when the dict node is deserialized. """ if key in group: pyt_file.remove_node(group, key) pyt_file.create_array(group, key, array) return {ARRAY_PROXY_KEY: True, NODE_KEY: key} @classmethod def _handle_array_values(cls, pyt_file, group_path, data): group = pyt_file.get_node(group_path) # Convert numpy array values to H5 array nodes. out_data = {} for key in data.keys(): value = data[key] if isinstance(value, ndarray): out_data[key] = cls._array_proxy(pyt_file, group, key, value) else: out_data[key] = value # Remove stored arrays which are no longer in the data dictionary. pyt_children = group._v_children nodes_to_remove = [] for key in pyt_children.keys(): if key not in data: nodes_to_remove.append(key) for key in nodes_to_remove: pyt_file.remove_node(group, key) return out_data apptools-4.5.0/apptools/io/tests/0000755000076500000240000000000013547652535020273 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/io/tests/file_test_case.py0000644000076500000240000001452213422276145023611 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Tests file operations. """ # Standard library imports. import os, shutil, stat, unittest # Enthought library imports. from apptools.io import File class FileTestCase(unittest.TestCase): """ Tests file operations on a local file system. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ try: shutil.rmtree('data') except: pass os.mkdir('data') return def tearDown(self): """ Called immediately after each test method has been called. """ shutil.rmtree('data') return ########################################################################### # Tests. ########################################################################### def test_properties(self): """ file properties """ # Properties of a non-existent file. f = File('data/bogus.xx') self.assert_(os.path.abspath(os.path.curdir) in f.absolute_path) self.assertEqual(f.children, None) self.assertEqual(f.ext, '.xx') self.assertEqual(f.exists, False) self.assertEqual(f.is_file, False) self.assertEqual(f.is_folder, False) self.assertEqual(f.is_package, False) self.assertEqual(f.is_readonly, False) self.assertEqual(f.mime_type, 'content/unknown') self.assertEqual(f.name, 'bogus') self.assertEqual(f.parent.path, 'data') self.assertEqual(f.path, 'data/bogus.xx') self.assert_(os.path.abspath(os.path.curdir) in f.url) self.assertEqual(str(f), 'File(%s)' % f.path) # Properties of an existing file. f = File('data/foo.txt') f.create_file() self.assert_(os.path.abspath(os.path.curdir) in f.absolute_path) self.assertEqual(f.children, None) self.assertEqual(f.ext, '.txt') self.assertEqual(f.exists, True) self.assertEqual(f.is_file, True) self.assertEqual(f.is_folder, False) self.assertEqual(f.is_package, False) self.assertEqual(f.is_readonly, False) self.assertEqual(f.mime_type, 'text/plain') self.assertEqual(f.name, 'foo') self.assertEqual(f.parent.path, 'data') self.assertEqual(f.path, 'data/foo.txt') self.assert_(os.path.abspath(os.path.curdir) in f.url) # Make it readonly. os.chmod(f.path, stat.S_IRUSR) self.assertEqual(f.is_readonly, True) # And then make it NOT readonly so that we can delete it at the end of # the test! os.chmod(f.path, stat.S_IRUSR | stat.S_IWUSR) self.assertEqual(f.is_readonly, False) return def test_copy(self): """ file copy """ content = 'print "Hello World!"\n' f = File('data/foo.txt') self.assertEqual(f.exists, False) # Create the file. f.create_file(content) self.assertEqual(f.exists, True) self.assertRaises(ValueError, f.create_file, content) self.assertEqual(f.children, None) self.assertEqual(f.ext, '.txt') self.assertEqual(f.is_file, True) self.assertEqual(f.is_folder, False) self.assertEqual(f.mime_type, 'text/plain') self.assertEqual(f.name, 'foo') self.assertEqual(f.path, 'data/foo.txt') # Copy the file. g = File('data/bar.txt') self.assertEqual(g.exists, False) f.copy(g) self.assertEqual(g.exists, True) self.assertEqual(g.children, None) self.assertEqual(g.ext, '.txt') self.assertEqual(g.is_file, True) self.assertEqual(g.is_folder, False) self.assertEqual(g.mime_type, 'text/plain') self.assertEqual(g.name, 'bar') self.assertEqual(g.path, 'data/bar.txt') # Attempt to copy a non-existent file (should do nothing). f = File('data/bogus.xx') self.assertEqual(f.exists, False) g = File('data/bogus_copy.txt') self.assertEqual(g.exists, False) f.copy(g) self.assertEqual(g.exists, False) return def test_create_file(self): """ file creation """ content = 'print "Hello World!"\n' f = File('data/foo.txt') self.assertEqual(f.exists, False) # Create the file. f.create_file(content) self.assertEqual(f.exists, True) self.assertEqual(open(f.path).read(), content) # Try to create it again. self.assertRaises(ValueError, f.create_file, content) return def test_delete(self): """ file deletion """ content = 'print "Hello World!"\n' f = File('data/foo.txt') self.assertEqual(f.exists, False) # Create the file. f.create_file(content) self.assertEqual(f.exists, True) self.assertRaises(ValueError, f.create_file, content) self.assertEqual(f.children, None) self.assertEqual(f.ext, '.txt') self.assertEqual(f.is_file, True) self.assertEqual(f.is_folder, False) self.assertEqual(f.mime_type, 'text/plain') self.assertEqual(f.name, 'foo') self.assertEqual(f.path, 'data/foo.txt') # Delete it. f.delete() self.assertEqual(f.exists, False) # Attempt to delete a non-existet file (should do nothing). f = File('data/bogus.txt') self.assertEqual(f.exists, False) f.delete() self.assertEqual(f.exists, False) return if __name__ == "__main__": unittest.main() #### EOF ###################################################################### apptools-4.5.0/apptools/io/tests/folder_test_case.py0000644000076500000240000001717413422276145024153 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Tests folder operations. """ # Standard library imports. import os, shutil, stat, unittest from os.path import join # Enthought library imports. from apptools.io import File class FolderTestCase(unittest.TestCase): """ Tests folder operations on a local file system. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ try: shutil.rmtree('data') except: pass os.mkdir('data') return def tearDown(self): """ Called immediately after each test method has been called. """ shutil.rmtree('data') return ########################################################################### # Tests. ########################################################################### def test_properties(self): """ folder properties """ # Properties of a non-existent folder. f = File('data/bogus') self.assert_(os.path.abspath(os.path.curdir) in f.absolute_path) self.assertEqual(f.children, None) self.assertEqual(f.ext, '') self.assertEqual(f.exists, False) self.assertEqual(f.is_file, False) self.assertEqual(f.is_folder, False) self.assertEqual(f.is_package, False) self.assertEqual(f.is_readonly, False) self.assertEqual(f.mime_type, 'content/unknown') self.assertEqual(f.name, 'bogus') self.assertEqual(f.parent.path, 'data') self.assertEqual(f.path, 'data/bogus') self.assert_(os.path.abspath(os.path.curdir) in f.url) self.assertEqual(str(f), 'File(%s)' % f.path) # Properties of an existing folder. f = File('data/sub') f.create_folder() self.assert_(os.path.abspath(os.path.curdir) in f.absolute_path) self.assertEqual(len(f.children), 0) self.assertEqual(f.ext, '') self.assertEqual(f.exists, True) self.assertEqual(f.is_file, False) self.assertEqual(f.is_folder, True) self.assertEqual(f.is_package, False) self.assertEqual(f.is_readonly, False) self.assertEqual(f.mime_type, 'content/unknown') self.assertEqual(f.name, 'sub') self.assertEqual(f.parent.path, 'data') self.assertEqual(f.path, 'data/sub') self.assert_(os.path.abspath(os.path.curdir) in f.url) # Make it readonly. os.chmod(f.path, stat.S_IRUSR) self.assertEqual(f.is_readonly, True) # And then make it NOT readonly so that we can delete it at the end of # the test! os.chmod(f.path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) self.assertEqual(f.is_readonly, False) # Properties of a Python package folder. f = File('data/package') f.create_folder() init = File('data/package/__init__.py') init.create_file() self.assert_(os.path.abspath(os.path.curdir) in f.absolute_path) self.assertEqual(len(f.children), 1) self.assertEqual(f.ext, '') self.assertEqual(f.exists, True) self.assertEqual(f.is_file, False) self.assertEqual(f.is_folder, True) self.assertEqual(f.is_package, True) self.assertEqual(f.is_readonly, False) self.assertEqual(f.mime_type, 'content/unknown') self.assertEqual(f.name, 'package') self.assertEqual(f.parent.path, 'data') self.assertEqual(f.path, 'data/package') self.assert_(os.path.abspath(os.path.curdir) in f.url) return def test_copy(self): """ folder copy """ f = File('data/sub') self.assertEqual(f.exists, False) # Create the folder. f.create_folder() self.assertEqual(f.exists, True) self.assertRaises(ValueError, f.create_folder) self.assertEqual(len(f.children), 0) self.assertEqual(f.ext, '') self.assertEqual(f.is_file, False) self.assertEqual(f.is_folder, True) self.assertEqual(f.mime_type, 'content/unknown') self.assertEqual(f.name, 'sub') self.assertEqual(f.path, 'data/sub') # Copy the folder. g = File('data/copy') self.assertEqual(g.exists, False) f.copy(g) self.assertEqual(g.exists, True) self.assertEqual(len(g.children), 0) self.assertEqual(g.ext, '') self.assertEqual(g.is_file, False) self.assertEqual(g.is_folder, True) self.assertEqual(g.mime_type, 'content/unknown') self.assertEqual(g.name, 'copy') self.assertEqual(g.path, 'data/copy') # Attempt to copy a non-existent folder (should do nothing). f = File('data/bogus') self.assertEqual(f.exists, False) g = File('data/bogus_copy') self.assertEqual(g.exists, False) f.copy(g) self.assertEqual(g.exists, False) return def test_create_folder(self): """ folder creation """ f = File('data/sub') self.assertEqual(f.exists, False) # Create the folder. f.create_folder() self.assertEqual(f.exists, True) parent = File('data') self.assertEqual(len(parent.children), 1) self.assertEqual(parent.children[0].path, join('data', 'sub')) # Try to create it again. self.assertRaises(ValueError, f.create_folder) return def test_create_folders(self): """ nested folder creation """ f = File('data/sub/foo') self.assertEqual(f.exists, False) # Attempt to create the folder with 'create_folder' which requires # that all intermediate folders exist. self.assertRaises(OSError, f.create_folder) # Create the folder. f.create_folders() self.assertEqual(f.exists, True) self.assertEqual(File('data/sub').exists, True) # Try to create it again. self.assertRaises(ValueError, f.create_folders) return def test_delete(self): """ folder deletion """ f = File('data/sub') self.assertEqual(f.exists, False) # Create the folder. f.create_folder() self.assertEqual(f.exists, True) self.assertRaises(ValueError, f.create_folder) self.assertEqual(len(f.children), 0) self.assertEqual(f.ext, '') self.assertEqual(f.is_file, False) self.assertEqual(f.is_folder, True) self.assertEqual(f.mime_type, 'content/unknown') self.assertEqual(f.name, 'sub') self.assertEqual(f.path, 'data/sub') # Delete it. f.delete() self.assertEqual(f.exists, False) # Attempt to delete a non-existet folder (should do nothing). f = File('data/bogus') self.assertEqual(f.exists, False) f.delete() self.assertEqual(f.exists, False) return #### EOF ###################################################################### apptools-4.5.0/apptools/io/__init__.py0000644000076500000240000000144013547637361021241 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Provides an abstraction for files and folders in a file system. Part of the AppTools project of the Enthought Tool Suite. """ from apptools.io.api import * apptools-4.5.0/apptools/io/api.py0000644000076500000240000000122313547637361020252 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from .file import File apptools-4.5.0/apptools/io/file.py0000644000076500000240000002324213422276145020414 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A representation of files and folders in a file system. """ # Standard/built-in imports. import mimetypes, os, shutil, stat # Enthought library imports. from traits.api import Bool, HasPrivateTraits, Instance, List, Property from traits.api import Str class File(HasPrivateTraits): """ A representation of files and folders in a file system. """ #### 'File' interface ##################################################### # The absolute path name of this file/folder. absolute_path = Property(Str) # The folder's children (for files this is always None). children = Property(List('File')) # The file extension (for folders this is always the empty string). # # fixme: Currently the extension includes the '.' (ie. we have '.py' and # not 'py'). This is because things like 'os.path.splitext' leave the '.' # on, but I'm not sure that this is a good idea! ext = Property(Str) # Does the file/folder exist? exists = Property(Bool) # Is this an existing file? is_file = Property(Bool) # Is this an existing folder? is_folder = Property(Bool) # Is this a Python package (ie. a folder contaning an '__init__.py' file. is_package = Property(Bool) # Is the file/folder readonly? is_readonly = Property(Bool) # The MIME type of the file (for a folder this will always be # 'context/unknown' (is that what it should be?)). mime_type = Property(Str) # The last component of the path without the extension. name = Property(Str) # The parent of this file/folder (None if it has no parent). parent = Property(Instance('File')) # The path name of this file/folder. path = Str # A URL reference to the file. url = Property(Str) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, path, **traits): """ Creates a new representation of the specified path. """ super(File, self).__init__(path=path, **traits) return def __cmp__(self, other): """ Comparison operators. """ if isinstance(other, File): return cmp(self.path, other.path) return 1 def __str__(self): """ Returns an 'informal' string representation of the object. """ return 'File(%s)' % self.path ########################################################################### # 'File' interface. ########################################################################### #### Properties ########################################################### def _get_absolute_path(self): """ Returns the absolute path of this file/folder. """ return os.path.abspath(self.path) def _get_children(self): """ Returns the folder's children. Returns None if the path does not exist or is not a folder. """ if self.is_folder: children = [] for name in os.listdir(self.path): children.append(File(os.path.join(self.path, name))) else: children = None return children def _get_exists(self): """ Returns True if the file exists, otherwise False. """ return os.path.exists(self.path) def _get_ext(self): """ Returns the file extension. """ name, ext = os.path.splitext(self.path) return ext def _get_is_file(self): """ Returns True if the path exists and is a file. """ return self.exists and os.path.isfile(self.path) def _get_is_folder(self): """ Returns True if the path exists and is a folder. """ return self.exists and os.path.isdir(self.path) def _get_is_package(self): """ Returns True if the path exists and is a Python package. """ return self.is_folder and '__init__.py' in os.listdir(self.path) def _get_is_readonly(self): """ Returns True if the file/folder is readonly, otherwise False. """ # If the File object is a folder, os.access cannot be used because it # returns True for both read-only and writable folders on Windows # systems. if self.is_folder: # Mask for the write-permission bits on the folder. If these bits # are set to zero, the folder is read-only. WRITE_MASK = 0x92 permissions = os.stat(self.path)[0] if permissions & WRITE_MASK == 0: readonly = True else: readonly = False elif self.is_file: readonly = not os.access(self.path, os.W_OK) else: readonly = False return readonly def _get_mime_type(self): """ Returns the mime-type of this file/folder. """ mime_type, encoding = mimetypes.guess_type(self.path) if mime_type is None: mime_type = "content/unknown" return mime_type def _get_name(self): """ Returns the last component of the path without the extension. """ basename = os.path.basename(self.path) name, ext = os.path.splitext(basename) return name def _get_parent(self): """ Returns the parent of this file/folder. """ return File(os.path.dirname(self.path)) def _get_url(self): """ Returns the path as a URL. """ # Strip out the leading slash on POSIX systems. return 'file:///%s' % self.absolute_path.lstrip('/') #### Methods ############################################################## def copy(self, destination): """ Copies this file/folder. """ # Allow the destination to be a string. if not isinstance(destination, File): destination = File(destination) if self.is_folder: shutil.copytree(self.path, destination.path) elif self.is_file: shutil.copyfile(self.path, destination.path) return def create_file(self, contents=''): """ Creates a file at this path. """ if self.exists: raise ValueError("file %s already exists" % self.path) f = open(self.path, 'w') f.write(contents) f.close() return def create_folder(self): """ Creates a folder at this path. All intermediate folders MUST already exist. """ if self.exists: raise ValueError("folder %s already exists" % self.path) os.mkdir(self.path) return def create_folders(self): """ Creates a folder at this path. This will attempt to create any missing intermediate folders. """ if self.exists: raise ValueError("folder %s already exists" % self.path) os.makedirs(self.path) return def create_package(self): """ Creates a package at this path. All intermediate folders/packages MUST already exist. """ if self.exists: raise ValueError("package %s already exists" % self.path) os.mkdir(self.path) # Create the '__init__.py' file that actually turns the folder into a # package! init = File(os.path.join(self.path, '__init__.py')) init.create_file() return def delete(self): """ Deletes this file/folder. Does nothing if the file/folder does not exist. """ if self.is_folder: # Try to make sure that everything in the folder is writeable. self.make_writeable() # Delete it! shutil.rmtree(self.path) elif self.is_file: # Try to make sure that the file is writeable. self.make_writeable() # Delete it! os.remove(self.path) return def make_writeable(self): """ Attempt to make the file/folder writeable. """ if self.is_folder: # Try to make sure that everything in the folder is writeable # (i.e., can be deleted!). This comes in especially handy when # deleting '.svn' directories. for path, dirnames, filenames in os.walk(self.path): for name in dirnames + filenames: filename = os.path.join(path, name) if not os.access(filename, os.W_OK): os.chmod(filename, stat.S_IWUSR) elif self.is_file: # Try to make sure that the file is writeable (i.e., can be # deleted!). if not os.access(self.path, os.W_OK): os.chmod(self.path, stat.S_IWUSR) return def move(self, destination): """ Moves this file/folder. """ # Allow the destination to be a string. if not isinstance(destination, File): destination = File(destination) # Try to make sure that everything in the directory is writeable. self.make_writeable() # Move it! shutil.move(self.path, destination.path) return #### EOF ###################################################################### apptools-4.5.0/apptools/template/0000755000076500000240000000000013547652535020335 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/template/impl/0000755000076500000240000000000013547652535021276 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/template/impl/template_data_context.py0000644000076500000240000001055613547637361026227 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A concrete implementation of the ITemplateDataContext interface intended to # be used for creating the *output_data_context* value of an # **ITemplateDataNameItem** implementation (although they are not required to # use it). # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A concrete implementation of the ITemplateDataContext interface intended to be used for creating the *output_data_context* value of an **ITemplateDataNameItem** implementation (although they are not required to use it). """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api\ import HasPrivateTraits, Dict, Str, Any, Property, provides, \ cached_property from apptools.template.itemplate_data_context \ import ITemplateDataContext, ITemplateDataContextError #------------------------------------------------------------------------------- # 'TemplateDataContext' class: #------------------------------------------------------------------------------- class TemplateDataContext ( HasPrivateTraits ): """ A concrete implementation of the ITemplateDataContext interface intended to be used for creating the *output_data_context* value of an **ITemplateDataNameItem** implementation (although they are not required to use it). """ implements( ITemplateDataContext ) #-- 'ITemplateDataContext' Interface Traits -------------------------------- # The path to this data context (does not include the 'data_context_name'): data_context_path = Str # The name of the data context: data_context_name = Str # A list of the names of the data values in this context: data_context_values = Property # List( Str ) # The list of the names of the sub-contexts of this context: data_contexts = Property # List( Str ) #-- Public Traits --------------------------------------------------------- # The data context values dictionary: values = Dict( Str, Any ) # The data contexts dictionary: contexts = Dict( Str, ITemplateDataContext ) #-- 'ITemplateDataContext' Property Implementations ------------------------ @cached_property def _get_data_context_values ( self ): values = sorted(self.values.keys()) return values @cached_property def _get_data_contexts ( self ): contexts = sorted(self.contexts.keys()) return contexts #-- 'ITemplateDataContext' Interface Implementation ------------------------ def get_data_context_value ( self, name ): """ Returns the data value with the specified *name*. Raises a **ITemplateDataContextError** if *name* is not defined as a data value in the context. Parameters ---------- name : A string specifying the name of the context data value to be returned. Returns ------- The data value associated with *name* in the context. The type of the data is application dependent. Raises **ITemplateDataContextError** if *name* is not associated with a data value in the context. """ try: return self.values[ name ] except: raise ITemplateDataContextError( "Value '%s' not found." % name ) def get_data_context ( self, name ): """ Returns the **ITemplateDataContext** value associated with the specified *name*. Raises **ITemplateDataContextError** if *name* is not defined as a data context in the context. Parameters ---------- name : A string specifying the name of the data context to be returned. Returns ------- The **ITemplateDataContext** associated with *name* in the context. Raises **ITemplateDataContextError** if *name* is not associated with a data context in the context. """ try: return self.context[ name ] except: raise ITemplateDataContextError( "Context '%s' not found." % name ) apptools-4.5.0/apptools/template/impl/__init__.py0000644000076500000240000000000111640354733023365 0ustar mdickinsonstaff00000000000000 apptools-4.5.0/apptools/template/impl/api.py0000644000076500000240000000150513547637361022422 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Publically exported symbols for the template.impl package. # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- from .any_context_data_name_item \ import AnyContextDataNameItem from .any_data_name_item \ import AnyDataNameItem from .context_data_name_item \ import ContextDataNameItem from .template_data_context \ import TemplateDataContext from .template_data_source \ import TemplateDataSource from .value_data_name_item \ import ValueDataNameItem from .value_nd_data_name_item \ import ValueNDDataNameItem, Value1DDataNameItem, Value2DDataNameItem, \ Value3DDataNameItem apptools-4.5.0/apptools/template/impl/i_context_adapter.py0000644000076500000240000001110013547637361025335 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines an adapter from an codetools.contexts.api.IContext # to an ITemplateDataContext. # # Written by: David C. Morrill # Modified by: Robert Kern # # Date: 11/16/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines an adapter from an codetools.contexts.api.IContext to an ITemplateDataContext. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Adapter, Str, List, adapts from traits.protocols.api \ import AdaptationError, adapt from codetools.contexts.api \ import IContext from apptools.template.itemplate_data_context \ import ITemplateDataContext, ITemplateDataContextError from .helper \ import path_for #------------------------------------------------------------------------------- # 'IContextAdapter' class: #------------------------------------------------------------------------------- class IContextAdapter ( Adapter ): """ Defines an adapter from an codetools.contexts.api.IContext to an ITemplateDataContext. """ adapts( IContext, ITemplateDataContext ) #-- ITemplateDataContext Interface Implementation -------------------------- # The path to this data context (does not include the 'data_context_name'): data_context_path = Str # The name of the data context: data_context_name = Str # A list of the names of the data values in this context: data_context_values = List( Str ) # The list of the names of the sub-contexts of this context: data_contexts = List( Str ) def get_data_context_value ( self, name ): """ Returns the data value with the specified *name*. Raises a **ITemplateDataContextError** if *name* is not defined as a data value in the context. Parameters ---------- name : A string specifying the name of the context data value to be returned. Returns ------- The data value associated with *name* in the context. The type of the data is application dependent. Raises **ITemplateDataContextError** if *name* is not associated with a data value in the context. """ try: if name in self.data_context_values: return self.adaptee[ name ] raise ITemplateDataContextError( "No value named '%s' found." % name ) except Exception as excp: raise ITemplateDataContextError( str( excp ) ) def get_data_context ( self, name ): """ Returns the **ITemplateDataContext** value associated with the specified *name*. Raises **ITemplateDataContextError** if *name* is not defined as a data context in the context. Parameters ---------- name : A string specifying the name of the data context to be returned. Returns ------- The **ITemplateDataContext** associated with *name* in the context. Raises **ITemplateDataContextError** if *name* is not associated with a data context in the context. """ try: if name in self.data_contexts: bdca = IContextAdapter( self.adaptee[ name ] ) bdca.data_context_path = path_for( self.data_context_path, self.data_context_name ) return bdca raise ITemplateDataContextError( "No context named '%s' found." % name ) except Exception as excp: raise ITemplateDataContextError( str( excp ) ) #-- Traits Event Handlers -------------------------------------------------- def _adaptee_changed ( self, context ): """ Handles being bound to a IContext object. """ self.data_context_name = context.name values = [] contexts = [] for name in context.keys(): value = context[ name ] try: adapt( value, IContext ) except AdaptationError: # Is not a subcontext. values.append( name ) else: # Is a subcontext. contexts.append( name ) self.data_context_values = values self.data_contexts = contexts apptools-4.5.0/apptools/template/impl/any_data_name_item.py0000644000076500000240000001736613547637361025463 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # An abstract base class implementation of the ITemplateDataNameItem interface # that looks for all specified values in its input context or optionally any of # its sub-contexts and outputs a context containing all such values found. # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ An abstract base class implementation of the ITemplateDataNameItem interface that looks for all specified values in its input context or optionally any of its sub-contexts and outputs a context containing all such values found. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import HasPrivateTraits, Instance, Property, provides from apptools.template.template_traits \ import TBool from apptools.template.itemplate_data_context \ import ITemplateDataContext from apptools.template.itemplate_data_name_item \ import ITemplateDataNameItem from apptools.template.template_impl \ import Template from .template_data_context \ import TemplateDataContext from .helper \ import path_for #------------------------------------------------------------------------------- # 'AnyDataNameItem' class: #------------------------------------------------------------------------------- class AnyDataNameItem ( Template ): """ An abstract base class implementation of the ITemplateDataNameItem interface that looks for all specified values in its input context or optionally any of its sub-contexts and outputs a context containing all such values found. """ implements ( ITemplateDataNameItem ) #-- 'ITemplateDataNameItem' Interface Implementation ----------------------- # The data context which this data name item should match against: input_data_context = Instance( ITemplateDataContext ) # The data context containing the data values and/or contexts this data # name item matches: output_data_context = Instance( ITemplateDataContext ) # The ITemplateChoice instance representing the current settings of the # data name item. This value must be read/write, and must be overridden by # sublasses. data_name_item_choice = Property # The alternative choices the user has for the data name item settings for # the current input data context. The list may be empty, in which case the # user cannot change the settings of the data name item. This value can be # read only, and must be overridden by subclasses. data_name_item_choices = Property #-- Public Traits ---------------------------------------------------------- # Should all sub-contexts be included in the search: recursive = TBool( False ) # Should included sub-contexts be flattened into a single context? flatten = TBool( False ) #-- Private Traits --------------------------------------------------------- # The current recursive setting: current_recursive = TBool( False ) # The current input data context: current_input_data_context = Property #-- Abstract Methods (Must be overridden in subclasses) -------------------- def filter ( self, name, value ): """ Returns **True** if the specified context data *name* and *value* should be included in the output context; and **False** otherwise. """ raise NotImplementedError #-- Property Implementations ----------------------------------------------- def _get_data_name_item_choice ( self ): raise NotImplementedError def _set_data_name_item_choice ( self, value ): raise NotImplementedError def _get_data_name_item_choices ( self ): raise NotImplementedError def _get_current_input_data_context ( self ): return self.input_data_context #-- Trait Event Handlers --------------------------------------------------- def _recursive_changed ( self, value ): """ Handles the primary recursive setting being changed. """ self.current_recursive = value def _input_data_context_changed ( self ): """ Handles the 'input_data_context' trait being changed. """ self.inputs_changed() #-- Private Methods -------------------------------------------------------- def inputs_changed ( self ): """ Handles any input value being changed. This method should be called by subclasses when any of their input values change. """ output_context = None input_context = self.current_input_data_context if input_context is not None: values = {} if self.current_recursive: if self.flatten: self._add_context( input_context, values ) else: self._copy_context( input_context, values ) else: self._add_values( input_context, values, '' ) if len( values ) > 0: output_context = TemplateDataContext( data_context_path = input_context.data_context_path, data_context_name = input_context.data_context_name, values = values ) self.output_data_context = output_context def _add_values ( self, input_context, values, path = '' ): """ Adds all of the matching values in the specified *input_context* to the specified *values* dictionary. """ # Filter each name/value in the current input context to see if it # should be added to the output values: filter = self.filter gdcv = input_context.get_data_context_value for name in input_context.data_context_values: value = gdcv( name ) if self.filter( name, value ): values[ path_for( path, name ) ] = value def _add_context ( self, input_context, values, path = '' ): """ Adds all of the matching values in the specified *input_context* to the specified *output_context*, and then applies itself recursively to all contexts contained in the specified *input_context*. """ # Add all of the filtered values in the specified input context: self._add_values( input_context, values, path ) # Now process all of the input context's sub-contexts: gdc = input_context.get_data_context for name in input_context.data_contexts: self._add_context( gdc( name ), values, path_for( path, input_context.data_context_name ) ) def _copy_context ( self, input_context ): """ Clone the input context so that the result only contains values and contexts which contain valid values and are not empty. """ values = {} contexts = {} # Add all of the filtered values in the specified input context: self._add_values( input_context, values ) # Now process all of the input context's sub-contexts: gdc = input_context.get_data_context for name in input_context.data_contexts: context = self._copy_context( gdc( name ) ) if context is not None: contexts[ name ] = context if (len( values ) == 0) and (len( contexts ) == 0): return None return TemplateDataContext( data_context_path = input_context.data_context_path, data_context_name = input_context.data_context_name, values = values, contexts = contexts ) apptools-4.5.0/apptools/template/impl/helper.py0000644000076500000240000000207311640354733023120 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Helper functions/classes useful for implementing various template interfaces. # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- # Returns a properly joined path name: #------------------------------------------------------------------------------- def path_for ( *names ): """ Returns a properly joined path name (i.e. the elements of the name are separated by '.'). """ return '.'.join( [ name for name in names if name != '' ] ) #------------------------------------------------------------------------------- # Parses a possible compound data context name: #------------------------------------------------------------------------------- def parse_name ( name ): """ Parses a possible compound data context name. """ return name.split( '.' ) apptools-4.5.0/apptools/template/impl/context_data_name_item.py0000644000076500000240000000653513547637361026354 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A concrete implementation of the ITemplateDataNameItem interface that looks # for a specified sub-context in its input context and outputs that as its # output context if it is found. # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A concrete implementation of the ITemplateDataNameItem interface that looks for a specified sub-context in its input context and outputs that as its output context if it is found. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from apptools.template.template_traits \ import TStr from .any_context_data_name_item \ import AnyContextDataNameItem from .helper \ import path_for, parse_name #------------------------------------------------------------------------------- # 'ContextDataNameItem' class: #------------------------------------------------------------------------------- class ContextDataNameItem ( AnyContextDataNameItem ): """ A concrete implementation of the ITemplateDataNameItem interface that looks for a specified sub-context in its input context and outputs that as its output context if it is found. """ #-- Public Traits ---------------------------------------------------------- # The name of the context to be matched: name = TStr #-- Abstract Method Implementations ---------------------------------------- def filter ( self, name, context ): """ Returns **True** if the specified *context* called *name* should be included in the output context; and **False** otherwise. """ return (name == self.name_last) #-- AnyDataNameItem Property Implementation Overrides ---------------------- def _get_data_name_item_choice ( self ): return TemplateChoice( choice_value = self.name ) def _set_data_name_item_choice ( self, value ): self.name = value.choice_value def _get_data_name_item_choices ( self ): return self._get_choices( self.input_data_context ) def _get_current_input_data_context ( self ): context = self.input_data_context for name in parse_name( self.name )[:-1]: if name not in context.data_contexts: return None context = context.get_data_context( name ) return context #-- Trait Event Handlers --------------------------------------------------- def _name_changed ( self, name ): """ Handles the 'name' trait being changed. """ self.name_last = parse_name( name )[-1] self.inputs_changed() #-- Private Methods -------------------------------------------------------- def _get_choices ( self, context, path = '' ): """ Returns all of the valid TemplateChoices for this item. """ choices = [] gdc = context.get_data_context for name in context.data_contexts: next_path = path_for( path, name ) choices.append( TemplateChoice( choice_value = next_path ) ) choices.extend( self._get_choices( gdc( name ), next_path ) ) return choices apptools-4.5.0/apptools/template/impl/template_data_source.py0000644000076500000240000000332013422276145026021 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A concrete implementation of the ITemplateDataSource interface based on the # implementation of the TemplateDataName class. # # Write by: David C. Morrill # # Date: 07/30/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A concrete implementation of the ITemplateDataSource interface based on the implementation of the TemplateDataName class. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import provides from apptools.template.template_data_name \ import TemplateDataName from apptools.template.itemplate_data_source \ import ITemplateDataSource #------------------------------------------------------------------------------- # 'TemplateDataSource' class: #------------------------------------------------------------------------------- class TemplateDataSource ( TemplateDataName ): """ A concrete implementation of the ITemplateDataSource interface based on the implementation of the TemplateDataName class. """ implements( ITemplateDataSource ) #-- ITemplateDataSource Interface Implementation --------------------------- def name_from_data_source ( self ): """ Allows the object to provide a description of the possibly optional data binding it requires. Returns ------- A **TemplateDataName** object describing the binding the data source object requires. """ return self apptools-4.5.0/apptools/template/impl/any_context_data_name_item.py0000644000076500000240000001245513547637361027221 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # An abstract base class implementation of the ITemplateDataNameItem interface # that looks for specified sub-contexts in its input context and if one match # is found, outputs that context; otherwise if more than one match is found it # outputs a context containing all matching sub-contexts found. # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ An abstract base class implementation of the ITemplateDataNameItem interface that looks for specified sub-contexts in its input context and if one match is found, outputs that context; otherwise if more than one match is found it outputs a context containing all matching sub-contexts found. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import HasPrivateTraits, Instance, Property, provides, on_trait_change from apptools.template.itemplate_data_context \ import ITemplateDataContext from apptools.template.itemplate_data_name_item \ import ITemplateDataNameItem from apptools.template.template_impl \ import Template from .template_data_context \ import TemplateDataContext #------------------------------------------------------------------------------- # 'AnyContextDataNameItem' class: #------------------------------------------------------------------------------- class AnyContextDataNameItem ( Template ): """ An abstract base class implementation of the ITemplateDataNameItem interface that looks for specified sub-contexts in its input context and if one match is found, outputs that context; otherwise if more than one match is found it outputs a context containing all matching sub-contexts found. """ implements ( ITemplateDataNameItem ) #-- 'ITemplateDataNameItem' Interface Implementation ----------------------- # The data context which this data name item should match against: input_data_context = Instance( ITemplateDataContext ) # The data context containing the data values and/or contexts this data # name item matches: output_data_context = Instance( ITemplateDataContext ) # The ITemplateChoice instance representing the current settings of the # data name item. This value must be read/write, and must be overridden by # sublasses. data_name_item_choice = Property # The alternative choices the user has for the data name item settings for # the current input data context. The list may be empty, in which case the # user cannot change the settings of the data name item. This value can be # read only, and must be overridden by subclasses. data_name_item_choices = Property #-- Private Traits --------------------------------------------------------- # The current input data context: current_input_data_context = Property #-- Partially Abstract Methods (Can be overridden in subclasses) ----------- def filter ( self, name, context ): """ Returns **True** if the specified *context* called *name* should be included in the output context; and **False** otherwise. """ return False #-- Property Implementations ----------------------------------------------- def _get_data_name_item_choice ( self ): raise NotImplementedError def _set_data_name_item_choice ( self, value ): raise NotImplementedError def _get_data_name_item_choices ( self ): raise NotImplementedError def _get_current_input_data_context ( self ): return self.input_data_context #-- Trait Event Handlers --------------------------------------------------- def _input_data_context_changed ( self ): """ Handles the 'input_data_context' trait being changed. """ self.inputs_changed() #-- Private Methods -------------------------------------------------------- def inputs_changed ( self ): """ Handles any of the input values being changed. May be called by subclasses. """ output_context = None input_context = self.input_data_context if input_context is not None: contexts = {} # Process each name/context in the input data contexts, and only add # those that match the subclass's filter to the output context: filter = self.filter gdc = input_context.get_data_context for name in input_context.data_contexts: if filter( name, gdc( name ) ): contexts[ name ] = context # If the result set is not empty, create an output context for it: n = len( contexts ) if n == 1: output_context = list(values.values())[0] elif n > 1: output_context = TemplateDataContext( data_context_path = input_context.data_context_path, data_context_name = input_context.data_context_name, contexts = contexts ) # Set the new output context: self.output_data_context = output_context apptools-4.5.0/apptools/template/impl/value_nd_data_name_item.py0000644000076500000240000000663713547637361026470 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A concrete implementation of the ITemplateDataNameItem interface that looks # for array values of a specified dimensionality in its input context or # optionally in any of its sub-contexts and outputs a context containing # only those values that it found. # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A concrete implementation of the ITemplateDataNameItem interface that looks for array values of a specified dimensionality in its input context or optionally in any of its sub-contexts and outputs a context containing only those values that it found. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from numpy \ import array from apptools.template.template_traits \ import TRange from apptools.template.template_choice \ import TemplateChoice from .any_data_name_item \ import AnyDataNameItem #------------------------------------------------------------------------------- # 'ValueNDDataNameItem' class: #------------------------------------------------------------------------------- class ValueNDDataNameItem ( AnyDataNameItem ): """ A concrete implementation of the ITemplateDataNameItem interface that looks for array values of a specified dimensionality in its input context or optionally in any of its sub-contexts and outputs a context containing only those values that it found. """ #-- Public Traits ---------------------------------------------------------- # The dimensionality of the array values to be accepted: dimensions = TRange( 0, 1000 ) #-- AnyDataNameItem Property Implementation Overrides ---------------------- def _get_data_name_item_choice ( self ): return TemplateChoice( choice_value = '%dD array' % self.dimensions ) def _set_data_name_item_choice ( self, value ): pass def _get_data_name_item_choices ( self ): return [] #-- Abstract Method Implementations ---------------------------------------- def filter ( self, name, value ): """ Returns **True** if the specified context data *name* and *value* should be included in the output context; and **False** otherwise. """ return (isinstance( value, array ) and (len( value.shape ) == self.dimensions )) #-- Trait Event Handlers --------------------------------------------------- def _name_changed ( self ): """ Handles the 'name' trait being changed. """ self.inputs_changed() #------------------------------------------------------------------------------- # Define a few common sub-classes for 1D, 2D and 3D arrays: #------------------------------------------------------------------------------- class Value1DDataNameItem ( ValueNDDataNameItem ): # Override the dimensionaly of the array values to be accepted: dimensions = 1 class Value2DDataNameItem ( ValueNDDataNameItem ): # Override the dimensionaly of the array values to be accepted: dimensions = 2 class Value3DDataNameItem ( ValueNDDataNameItem ): # Override the dimensionaly of the array values to be accepted: dimensions = 3 apptools-4.5.0/apptools/template/impl/value_data_name_item.py0000644000076500000240000001260713547637361026001 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A concrete implementation of the ITemplateDataNameItem interface that looks # for a specified named value in its input context or optionally in any of its # sub-contexts and outputs a context containing only those values that match # the specified name. # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A concrete implementation of the ITemplateDataNameItem interface that looks for a specified named value in its input context or optionally in any of its sub-contexts and outputs a context containing only those values that match the specified name. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Bool from apptools.template.template_traits \ import TStr, TBool from apptools.template.template_choice \ import TemplateChoice from apptools.template.itemplate_data_context \ import ITemplateDataContext from .any_data_name_item \ import AnyDataNameItem from .helper \ import parse_name, path_for #------------------------------------------------------------------------------- # Constants: #------------------------------------------------------------------------------- # Value used to reset to the factory settings: ResetChoice = '----' #------------------------------------------------------------------------------- # 'ValueDataNameItem' class: #------------------------------------------------------------------------------- class ValueDataNameItem ( AnyDataNameItem ): """ A concrete implementation of the ITemplateDataNameItem interface that looks for a specified named value in its input context or optionally in any of its sub-contexts and outputs a context containing only those values that match the specified name. """ #-- Public Traits ---------------------------------------------------------- # The name of the value to be matched: name = TStr # (Override) Should included sub-contexts be flattened into a single # context? flatten = True #-- Private Traits --------------------------------------------------------- # The current name of the value to be matched: current_name = TStr # The current name's last component: current_name_last = TStr # Should all possible choices be included? all_choices = Bool( True ) #-- AnyDataNameItem Property Implementation Overrides ---------------------- def _get_data_name_item_choice ( self ): return TemplateChoice( choice_value = self.current_name ) def _set_data_name_item_choice ( self, choice ): if choice.choice_value == ResetChoice: self.current_recursive = self.recursive self.current_name = self.name self.all_choices = True else: self.current_recursive = False self.current_name = choice.choice_value self.all_choices = False def _get_data_name_item_choices ( self ): context = self.input_data_context if context is None: return [] if not self.all_choices: output_context = self.output_data_context if output_context is not None: return [ TemplateChoice( choice_value = name ) for name in output_context.data_context_values ] return ([ TemplateChoice( choice_value = ResetChoice ) ] + self._get_choices( context )) def _get_current_input_data_context ( self ): context = self.input_data_context for name in parse_name( self.current_name )[:-1]: if name not in context.data_contexts: return None context = context.get_data_context( name ) return context #-- Abstract Method Implementations ---------------------------------------- def filter ( self, name, value ): """ Returns **True** if the specified context data *name* and *value* should be included in the output context; and **False** otherwise. """ return (name == self.current_name_last) #-- Trait Event Handlers --------------------------------------------------- def _name_changed ( self, name ): """ Handles the 'name' trait being changed. """ self.current_name = name def _current_name_changed ( self, name ): self.current_name_last = parse_name( name )[-1] self.inputs_changed() #-- Private Methods -------------------------------------------------------- def _get_choices ( self, context, path = '' ): """ Returns the list of available user settings choices for the specified context. """ choices = [ TemplateChoice( choice_value = path_for( path, name ) ) for name in context.data_context_values ] if self.recursive: # Now process all of the context's sub-contexts: gdc = context.get_data_context for name in context.data_contexts: choices.extend( self._get_choices( gdc( name ), path_for( path, context.data_context_name ) ) ) return choices apptools-4.5.0/apptools/template/test/0000755000076500000240000000000013547652535021314 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/template/test/enable_editor.py0000644000076500000240000000504711640354733024457 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Traits UI editor for displaying Enable Components. # # Written by: David Morrill # # Date: 07/30/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Traits UI editor for displaying Enable Components. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- import wx from traitsui.wx.editor \ import Editor from traitsui.wx.basic_editor_factory \ import BasicEditorFactory from enable.wx_backend.api \ import Window #------------------------------------------------------------------------------- # '_EnableEditor' class: #------------------------------------------------------------------------------- class _EnableEditor ( Editor ): """ Traits UI editor for displaying Enable Components. """ # Override the default value to allow the control to be resizable: scrollable = True #--------------------------------------------------------------------------- # Finishes initializing the editor by creating the underlying toolkit # widget: #--------------------------------------------------------------------------- def init ( self, parent ): """ Finishes initializing the editor by creating the underlying toolkit widget. """ self._window = Window( parent, -1, component = self.value, bg_color = ( 0.698, 0.698, 0.698, 1.0 ) ) self.control = self._window.control self.control.SetSize( wx.Size( 300, 300 ) ) self.set_tooltip() #--------------------------------------------------------------------------- # Updates the editor when the object trait changes external to the editor: #--------------------------------------------------------------------------- def update_editor ( self ): """ Updates the editor when the object trait changes externally to the editor. """ self._window.component = self.value #------------------------------------------------------------------------------- # Create the editor factory object: #------------------------------------------------------------------------------- # wxPython editor factory for Enable component editors: class EnableEditor ( BasicEditorFactory ): # The editor class to be created: klass = _EnableEditor apptools-4.5.0/apptools/template/test/__init__.py0000644000076500000240000000000111640354733023403 0ustar mdickinsonstaff00000000000000 apptools-4.5.0/apptools/template/test/scatter_plot.py0000644000076500000240000001606613547637361024402 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A simple scatter-plot template defined as a test for the template package. # # Written by: David C. Morrill # (based on the original cp.plot geo_scatter_plot.py file) # # Date: 07/30/2007 # # (c) Copy 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A simple scatter-plot template defined as a test for the template package. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import HasPrivateTraits, Undefined from traitsui.api \ import View, VGroup, Item, Label, Theme, TextEditor from traitsui.wx.themed_slider_editor \ import ThemedSliderEditor from traitsui.wx.themed_text_editor \ import ThemedTextEditor from chaco.api \ import ScatterPlot, ArrayPlotData, Plot from chaco.tools.api \ import PanTool, SimpleZoom from enable.api \ import ColorTrait from chaco.scatter_markers \ import marker_trait from apptools.template.api \ import Template, TRange, TStr, TDerived, TDataSource from apptools.template.impl.api \ import TemplateDataSource, ValueDataNameItem from .enable_editor \ import EnableEditor #------------------------------------------------------------------------------- # Trait definitions: #------------------------------------------------------------------------------- # Template color trait: TColor = ColorTrait( template = 'copy' ) #------------------------------------------------------------------------------- # 'ScatterPlot' class: #------------------------------------------------------------------------------- class ScatterPlot ( Template ): #-- Template Traits -------------------------------------------------------- # The plot index data source: index = TDataSource # The plot value data source: value = TDataSource # The title of the plot: title = TStr( 'Scatter Plot' ) # The type of marker to use. This is a mapped trait using strings as the # keys: marker = marker_trait( template = 'copy', event = 'update' ) # The pixel size of the marker (doesn't include the thickness of the # outline): marker_size = TRange( 1, 5, 1, event = 'update' ) # The thickness, in pixels, of the outline to draw around the marker. If # this is 0, no outline will be drawn. line_width = TRange( 0.0, 5.0, 1.0 ) # The fill color of the marker: color = TColor( 'red', event = 'update' ) # The color of the outline to draw around the marker outline_color = TColor( 'black', event = 'update' ) #-- Derived Traits --------------------------------------------------------- plot = TDerived # Instance( ScatterPlot ) #-- Traits UI Views -------------------------------------------------------- # The scatter plot view: template_view = View( VGroup( Item( 'title', show_label = False, style = 'readonly', editor = ThemedTextEditor( theme = Theme( '@GBB', alignment = 'center' ) ) ), Item( 'plot', show_label = False, resizable = True, editor = EnableEditor(), item_theme = Theme( '@GF5', margins = 0 ) ) ), resizable = True ) # The scatter plot options view: options_view = View( VGroup( VGroup( Label( 'Scatter Plot Options', item_theme = Theme( '@GBB', alignment = 'center' ) ), show_labels = False ), VGroup( Item( 'title', editor = TextEditor() ), Item( 'marker' ), Item( 'marker_size', editor = ThemedSliderEditor() ), Item( 'line_width', label = 'Line Width', editor = ThemedSliderEditor() ), Item( 'color', label = 'Fill Color' ), Item( 'outline_color', label = 'Outline Color' ), group_theme = Theme( '@GF5', margins = ( -5, -1 ) ), item_theme = Theme( '@G0B', margins = 0 ) ) ) ) #-- Default Values --------------------------------------------------------- def _index_default ( self ): """ Returns the default value for the 'index' trait. """ return TemplateDataSource( items = [ ValueDataNameItem( name = 'index', flatten = True ) ], description = 'Scatter Plot Index' ) def _value_default ( self ): """ Returns the default value for the 'value' trait. """ return TemplateDataSource( items = [ ValueDataNameItem( name = 'value', flatten = True ) ], description = 'Scatter Plot Value' ) #-- ITemplate Interface Implementation ------------------------------------- def activate_template ( self ): """ Converts all contained 'TDerived' objects to real objects using the template traits of the object. This method must be overridden in subclasses. Returns ------- None """ # If our data sources are still unbound, then just exit; someone must # have marked them as optional: if ((self.index.context_data is Undefined) or (self.value.context_data is Undefined)): return # Create a plot data object and give it this data: pd = ArrayPlotData() pd.set_data( 'index', self.index.context_data ) pd.set_data( 'value', self.value.context_data ) # Create the plot: self.plot = plot = Plot( pd ) plot.plot( ( 'index', 'value' ), type = 'scatter', index_sort = 'ascending', marker = self.marker, color = self.color, outline_color = self.outline_color, marker_size = self.marker_size, line_width = self.line_width, bgcolor = 'white' ) plot.trait_set( padding_left = 50, padding_right = 0, padding_top = 0, padding_bottom = 20 ) # Attach some tools to the plot: plot.tools.append( PanTool( plot, constrain_key = 'shift' ) ) zoom = SimpleZoom( component = plot, tool_mode = 'box', always_on = False ) plot.overlays.append( zoom ) #-- Trait Event Handlers --------------------------------------------------- def _update_changed ( self ): """ Handles a plot option being changed. """ self.plot = Undefined apptools-4.5.0/apptools/template/test/scatter_plot_2.py0000644000076500000240000001421113547637361024611 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A simple dual scatter-plot template defined as a test for the template # package. # # Written by: David C. Morrill # (based on the original cp.plot geo_scatter_plot.py file) # # Date: 08/01/2007 # # (c) Copy 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A simple dual scatter-plot template defined as a test for the template package. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Undefined from traitsui.api \ import View, VGroup, Item, Label, Theme, TextEditor from traitsui.wx.themed_slider_editor \ import ThemedSliderEditor from traitsui.wx.themed_text_editor \ import ThemedTextEditor from enable.api \ import ColorTrait from chaco.api \ import HPlotContainer from chaco.scatter_markers \ import marker_trait from apptools.template.api \ import Template, TRange, TStr, TInstance, TDerived from .enable_editor \ import EnableEditor from .scatter_plot \ import ScatterPlot #------------------------------------------------------------------------------- # Trait definitions: #------------------------------------------------------------------------------- # Template color trait: TColor = ColorTrait( template = 'copy' ) #------------------------------------------------------------------------------- # 'ScatterPlot2' class: #------------------------------------------------------------------------------- class ScatterPlot2 ( Template ): #-- Template Traits -------------------------------------------------------- # The title of the plot: title = TStr( 'Dual Scatter Plots' ) # The type of marker to use. This is a mapped trait using strings as the # keys: marker = marker_trait( template = 'copy', event = 'update' ) # The pixel size of the marker (doesn't include the thickness of the # outline): marker_size = TRange( 1, 5, 1, event = 'update' ) # The thickness, in pixels, of the outline to draw around the marker. If # this is 0, no outline will be drawn. line_width = TRange( 0.0, 5.0, 1.0 ) # The fill color of the marker: color = TColor( 'red', event = 'update' ) # The color of the outline to draw around the marker outline_color = TColor( 'black', event = 'update' ) # The amount of space between plots: spacing = TRange( 0.0, 20.0, 0.0 ) # The contained scatter plots: scatter_plot_1 = TInstance( ScatterPlot, () ) scatter_plot_2 = TInstance( ScatterPlot, () ) #-- Derived Traits --------------------------------------------------------- plot = TDerived #-- Traits UI Views -------------------------------------------------------- # The scatter plot view: template_view = View( VGroup( Item( 'title', show_label = False, style = 'readonly', editor = ThemedTextEditor( theme = Theme( '@GBB', alignment = 'center' ) ) ), Item( 'plot', show_label = False, resizable = True, editor = EnableEditor(), item_theme = Theme( '@GF5', margins = 0 ) ) ), resizable = True ) # The scatter plot options view: options_view = View( VGroup( VGroup( Label( 'Scatter Plot Options', item_theme = Theme( '@GBB', alignment = 'center' ) ), show_labels = False ), VGroup( Item( 'title', editor = TextEditor() ), Item( 'marker' ), Item( 'marker_size', editor = ThemedSliderEditor() ), Item( 'line_width', label = 'Line Width', editor = ThemedSliderEditor() ), Item( 'spacing', editor = ThemedSliderEditor() ), Item( 'color', label = 'Fill Color' ), Item( 'outline_color', label = 'Outline Color' ), group_theme = Theme( '@GF5', margins = ( -5, -1 ) ), item_theme = Theme( '@G0B', margins = 0 ) ) ) ) #-- ITemplate Interface Implementation ------------------------------------- def activate_template ( self ): """ Converts all contained 'TDerived' objects to real objects using the template traits of the object. This method must be overridden in subclasses. Returns ------- None """ plots = [ p for p in [ self.scatter_plot_1.plot, self.scatter_plot_2.plot ] if p is not None ] if len( plots ) == 2: self.plot = HPlotContainer( spacing = self.spacing ) self.plot.add( *plots ) elif len( plots ) == 1: self.plot = plots[0] #-- Default Values --------------------------------------------------------- def _scatter_plot_1_default ( self ): """ Returns the default value for the first scatter plot. """ result = ScatterPlot() result.index.description = 'Shared Plot Index' result.value.description += ' 1' return result def _scatter_plot_2_default ( self ): """ Returns the default value for the second scatter plot. """ result = ScatterPlot( index = self.scatter_plot_1.index ) result.value.description += ' 2' result.value.optional = True return result #-- Trait Event Handlers --------------------------------------------------- def _update_changed ( self, name, old, new ): """ Handles a plot option being changed. """ setattr( self.scatter_plot_1, name, new ) setattr( self.scatter_plot_2, name, new ) self.plot = Undefined def _spacing_changed ( self, spacing ): """ Handles the spacing between plots being changed. """ self.plot = Undefined apptools-4.5.0/apptools/template/test/template_view.py0000644000076500000240000003072013547637361024535 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A feature-based Traits UI plug-in for viewing templates. # # Written by: David C. Morrill # # Date: 07/30/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A feature-based Traits UI plug-in for viewing templates. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- import sys from os.path \ import split, splitext from pickle \ import dump, load from traits.api \ import HasPrivateTraits, Str, Instance, Any, File, Button, on_trait_change from traitsui.api \ import View, VGroup, HGroup, Tabbed, Item, InstanceEditor, Theme, Label from traitsui.wx.themed_text_editor \ import ThemedTextEditor from etsdevtools.developer.features.api \ import DropFile from pyface.api \ import FileDialog, OK from etsdevtools.developer.helper.themes \ import TButton from blockcanvas.app.utils \ import import_log_files from apptools.template.api \ import ITemplate, ITemplateDataContext, TemplateDataNames, Template from .enable_editor \ import EnableEditor #-- Adapters that might be used ------------------------------------------------ from apptools.template.impl.base_data_context_adapter \ import BaseDataContextAdapter #------------------------------------------------------------------------------- # Helper class: #------------------------------------------------------------------------------- class Message ( HasPrivateTraits ): #-- Trait Definitions ------------------------------------------------------ # The message to be displayed: message = Str #-- Traits View Definitions ------------------------------------------------ view = View( Item( 'message', style = 'readonly', show_label = False, editor = ThemedTextEditor( theme = '@GBB' ), ) ) #-- Object Overrides ------------------------------------------------------- def __init__ ( self, message = '', **traits ): traits.setdefault( 'message', message ) super( Message, self ).__init__( **traits ) # Define some standard messages: no_context = Message( 'Please provide a data context' ) no_template = Message( 'Please provide a view template' ) no_template_found = Message( 'No view templates found in Python source file' ) no_bindings = Message( 'Please resolve all required template data bindings' ) no_options = Message( 'No options are currently available' ) #------------------------------------------------------------------------------- # 'TemplateView' class: #------------------------------------------------------------------------------- class TemplateView ( HasPrivateTraits ): """ A feature-based Traits UI plug-in for viewing templates. """ #-- Public Traits ---------------------------------------------------------- # The name of the plugin: name = Str( 'Template View' ) # The data context supplying the data to be viewed: context = Instance( ITemplateDataContext, connect = 'to: data context' ) # The name of the file containing a template view: file_name = File( drop_file = DropFile( extensions = [ '.py', '.tv', '.las' ], tooltip = 'Drop a LAS data file, a saved view template ' 'or a Python source file containing a view ' 'template here.' ), connect = 'to: template view' ) # The template to view: template = Instance( ITemplate ) #-- Private Traits --------------------------------------------------------- # The name of the file to save the template in: save_file_name = File # The current data names: data_names = Instance( TemplateDataNames ) # The TemplateDataNames or Message being viewed: names_view = Any( no_context ) # The name of the most recently loaded template file: template_file_name = File # The template or message being viewed: template_view = Any( no_template ) # The options view for the currently active template: options_view = Any( no_options ) # The event fired when the user wants to save the template: save_template = Button( 'Save Template' ) #-- Traits View Definitions ------------------------------------------------ view = View( VGroup( HGroup( Item( 'file_name', show_label = False, width = 350 ), TButton( 'save_template', label = 'Save Template', enabled_when = 'template is not None' ), group_theme = Theme( '@GFB', margins = ( -7, -5 ) ) ), Tabbed( VGroup( '8', Label( 'Data Bindings', item_theme = Theme( '@GBB', alignment = 'center' ) ), Item( 'names_view', style = 'custom', resizable = True, editor = InstanceEditor(), export = 'DockWindowShell', item_theme = Theme( '@GFB', margins = ( -5, -1 ) ) ), label = 'Data Bindings', show_labels = False ), Item( 'template_view', label = 'Template View', style = 'custom', resizable = True, editor = InstanceEditor( view = 'template_view' ), export = 'DockWindowShell' ), Item( 'options_view', label = 'Template Options', style = 'custom', resizable = True, editor = InstanceEditor( view = 'options_view' ), export = 'DockWindowShell' ), id = 'tabbed', dock = 'horizontal', show_labels = False ), ), id = 'template.test.template_view.TemplateView' ) #-- Trait Event Handlers --------------------------------------------------- def _context_changed ( self, context ): """ Handles the 'context' trait being changed. """ if context is None: self.names_view = no_context elif self.template is None: self.names_view = no_template else: self._create_view() def _template_changed ( self, template ): """ Handles the 'template' trait being changed. """ if self.context is None: self.template_view = no_context else: self._create_view() def _file_name_changed ( self, file_name ): """ Handles the 'file_name' trait being changed. """ ext = splitext( file_name )[1] if ext == '.py': self._load_python_template( file_name ) elif ext == '.tv': self._load_pickled_template( file_name ) elif ext == '.las': self._load_las_file( file_name ) else: # fixme: Display an informational message here... pass def _save_template_changed ( self ): """ Handles the user clicking the 'Save Template' button. """ self._save_pickled_template() @on_trait_change( 'data_names.unresolved_data_names' ) def _on_unresolved_data_names ( self ): if len( self.data_names.unresolved_data_names ) == 0: self._create_object_view() elif not isinstance( self.template_view, Message ): self.template_view = no_bindings self.options_view = no_options @on_trait_change( 'template.template_mutated?' ) def _on_template_mutated ( self ): """ Handles a mutable template changing. """ if self.context is not None: self._create_view() #-- Private Methods -------------------------------------------------------- def _load_python_template ( self, file_name ): """ Attempts to load a template from a Python source file. """ path, name = split( file_name ) sys.path[0:0] = [ path ] try: ###values = {} module_name, ext = splitext( name ) module = __import__( module_name ) values = module.__dict__ ###execfile( file_name, values ) template = values.get( 'template' ) if template is None: templates = [] for value in values.values(): try: if (issubclass( value, Template ) and ###(value.__module__ == '__builtin__')): (value.__module__ == module_name)): templates.append( value ) except: pass for i, template in enumerate( templates ): for t in templates[ i + 1: ]: if issubclass( template, t ): break else: break else: self.template_view = no_template_found return if not isinstance( template, Template ): template = template() self.template = template self.template_file_name = file_name except Exception as excp: self.template_view = Message( str( excp ) ) # Clean up the Python path: del sys.path[0] def _load_pickled_template ( self, file_name ): """ Attempts to load a template from a pickle. """ # fixme: Implement this...load template from .tv pickle file. fh = None delete = False try: fh = open( file_name, 'rb' ) file_name = load( fh ) path, name = split( file_name ) sys.path[0:0] = [ path ] delete = True module_name, ext = splitext( name ) module = __import__( module_name ) self.template = load( fh ) self.template_file_name = file_name except Exception as excp: import traceback traceback.print_exc() self.template_view = Message( str( excp ) ) if fh is not None: fh.close() if delete: del sys.path[0] def _load_las_file ( self, file_name ): """ Creates a data context from the specified LAS file. """ try: self.context = import_log_files( file_name, 'las' ) except Exception as excp: self.names_view = Message( str( excp ) ) def _save_pickled_template ( self ): file_name = self.save_file_name or self.file_name fd = FileDialog( action = 'save as', default_path = file_name ) #wildcard = 'Template files (*.tv)|*.tv|' ) if fd.open() == OK: self.save_file_name = file_name = fd.path fh = None try: fh = open( file_name, 'wb' ) dump( self.template_file_name, fh, -1 ) dump( self.template.template_from_object(), fh, -1 ) except: # fixme: Display an informational message here... import traceback traceback.print_exc() if fh is not None: fh.close() def _create_view ( self ): """ Begins the process of creating a live view from a template and data context object. """ self.data_names = self.names_view = nv = TemplateDataNames( context = self.context, data_names = self.template.names_from_template() ) if len( nv.unresolved_data_names ) == 0: self._create_object_view() else: self.template_view = no_bindings def _create_object_view ( self ): """ Create the object view from the current template. """ self.template.object_from_template() self.template_view = self.options_view = self.template apptools-4.5.0/apptools/template/test/scatter_plot_nm.py0000644000076500000240000001637313547637361025075 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A n row x m column scatter-plot template defined as a test for the template # package. # # Written by: David C. Morrill # (based on the original cp.plot geo_scatter_plot.py file) # # Date: 08/01/2007 # # (c) Copy 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A n row x m column scatter-plot template defined as a test for the template package. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Undefined from traitsui.api \ import View, VGroup, Item, Label, Theme, TextEditor from traitsui.wx.themed_slider_editor \ import ThemedSliderEditor from traitsui.wx.themed_text_editor \ import ThemedTextEditor from enable.api \ import ColorTrait from chaco.api \ import GridPlotContainer, PlotComponent from chaco.scatter_markers \ import marker_trait from apptools.template.api \ import MutableTemplate, TRange, TStr, TList, TDerived from .enable_editor \ import EnableEditor from .scatter_plot \ import ScatterPlot #------------------------------------------------------------------------------- # Trait definitions: #------------------------------------------------------------------------------- # Template color trait: TColor = ColorTrait( template = 'copy' ) #------------------------------------------------------------------------------- # 'ScatterPlotNM' class: #------------------------------------------------------------------------------- class ScatterPlotNM ( MutableTemplate ): #-- Template Traits -------------------------------------------------------- # The title of the plot: title = TStr( 'NxM Scatter Plots' ) # The type of marker to use. This is a mapped trait using strings as the # keys: marker = marker_trait( template = 'copy', event = 'update' ) # The pixel size of the marker (doesn't include the thickness of the # outline): marker_size = TRange( 1, 5, 1, event = 'update' ) # The thickness, in pixels, of the outline to draw around the marker. If # this is 0, no outline will be drawn. line_width = TRange( 0.0, 5.0, 1.0 ) # The fill color of the marker: color = TColor( 'red', event = 'update' ) # The color of the outline to draw around the marker outline_color = TColor( 'black', event = 'update' ) # The number of rows of plots: rows = TRange( 1, 3, 1, event = 'grid' ) # The number of columns of plots: columns = TRange( 1, 5, 1, event = 'grid' ) # The contained scatter plots: scatter_plots = TList( ScatterPlot ) #-- Derived Traits --------------------------------------------------------- plot = TDerived #-- Traits UI Views -------------------------------------------------------- # The scatter plot view: template_view = View( VGroup( Item( 'title', show_label = False, style = 'readonly', editor = ThemedTextEditor( theme = Theme( '@GBB', alignment = 'center' ) ) ), Item( 'plot', show_label = False, resizable = True, editor = EnableEditor(), item_theme = Theme( '@GF5', margins = 0 ) ) ), resizable = True ) # The scatter plot options view: options_view = View( VGroup( VGroup( Label( 'Scatter Plot Options', item_theme = Theme( '@GBB', alignment = 'center' ) ), show_labels = False ), VGroup( Item( 'title', editor = TextEditor() ), Item( 'marker' ), Item( 'marker_size', editor = ThemedSliderEditor() ), Item( 'line_width', label = 'Line Width', editor = ThemedSliderEditor() ), Item( 'color', label = 'Fill Color' ), Item( 'outline_color', label = 'Outline Color' ), Item( 'rows', editor = ThemedSliderEditor() ), Item( 'columns', editor = ThemedSliderEditor() ), group_theme = Theme( '@GF5', margins = ( -5, -1 ) ), item_theme = Theme( '@G0B', margins = 0 ) ) ) ) #-- ITemplate Interface Implementation ------------------------------------- def activate_template ( self ): """ Converts all contained 'TDerived' objects to real objects using the template traits of the object. This method must be overridden in subclasses. Returns ------- None """ plots = [] i = 0 for r in range( self.rows ): row = [] for c in range( self.columns ): plot = self.scatter_plots[i].plot if plot is None: plot = PlotComponent() row.append( plot ) i += 1 plots.append( row ) self.plot = GridPlotContainer( shape = ( self.rows, self.columns ) ) self.plot.component_grid = plots #-- Default Values --------------------------------------------------------- def _scatter_plots_default ( self ): """ Returns the default value for the scatter plots list. """ plots = [] for i in range( self.rows * self.columns ): plots.append( ScatterPlot() ) self._update_plots( plots ) return plots #-- Trait Event Handlers --------------------------------------------------- def _update_changed ( self, name, old, new ): """ Handles a plot option being changed. """ for sp in self.scatter_plots: setattr( sp, name, new ) self.plot = Undefined def _grid_changed ( self ): """ Handles the grid size being changed. """ n = self.rows * self.columns plots = self.scatter_plots if n < len( plots ): self.scatter_plots = plots[:n] else: for j in range( len( plots ), n ): plots.append( ScatterPlot() ) self._update_plots( plots ) self.template_mutated = True #-- Private Methods -------------------------------------------------------- def _update_plots ( self, plots ): """ Update the data sources for all of the current plots. """ index = None i = 0 for r in range( self.rows ): for c in range( self.columns ): sp = plots[i] i += 1 desc = sp.value.description col = desc.rfind( '[' ) if col >= 0: desc = desc[:col] sp.value.description = '%s[%d,%d]' % ( desc, r, c ) sp.value.optional = True if index is None: index = sp.index index.description = 'Shared Plot Index' index.optional = True else: sp.index = index apptools-4.5.0/apptools/template/template_traits.py0000644000076500000240000000555413547637361024121 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Trait definitions useful when creating templatizable classes. # # Written by: David C. Morrill # # Date: 07/26/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Trait definitions useful when creating templatizable classes. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Instance, Int, Float, Str, List, Bool, Range, TraitType, Undefined from .itemplate_data_source \ import ITemplateDataSource #------------------------------------------------------------------------------- # Trait definitions: #------------------------------------------------------------------------------- # Data source template trait: TDataSource = Instance( ITemplateDataSource, template = 'datasource' ) # Templatizable object trait: def TInstance ( *args, **kw ): kw[ 'template' ] = 'instance' return Instance( *args, **kw ) # A list of templatizable object traits: def TList( *args, **kw ): kw[ 'template' ] = 'instance' return List( *args, **kw ) # Simple, copyable template traits: TInt = Int( template = 'copy' ) TFloat = Float( template = 'copy' ) TStr = Str( template = 'copy' ) TBool = Bool( template = 'copy' ) def TRange ( *args, **kw ): kw[ 'template' ] = 'copy' return Range( *args, **kw ) def TEnum ( *args, **kw ): kw[ 'template' ] = 'copy' return Enum( *args, **kw ) #------------------------------------------------------------------------------- # 'TDerived' trait: #------------------------------------------------------------------------------- class TDerived ( TraitType ): """ Defines a trait property for handling attributes whose value depends upon the templatizable traits of an object. Note that the implementation of the *activate_template* method is not allowed to read the value of any **TDerived** traits, it may only set them. """ # Base trait metadata: metadata = { 'template': 'derived', 'transient': True } def get ( self, object, name ): name += '_' value = object.__dict__.get( name, Undefined ) if value is not Undefined: return value object.activate_template() value = object.__dict__.get( name, Undefined ) if value is not Undefined: return value object.__dict__[ name ] = None return None def set ( self, object, name, value ): xname = name + '_' old = object.__dict__.get( xname, Undefined ) if old is not value: object.__dict__[ xname ] = value object.trait_property_changed( name, old, value ) apptools-4.5.0/apptools/template/template_data_names.py0000644000076500000240000002077013547637361024704 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the TemplateDataNames class used to manage application data source # bindings to named context data. # # Written by: David C. Morrill # # Date: 07/26/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the TemplateDataNames class used to manage application data source bindings to named context data. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import HasPrivateTraits, Instance, Int, List, Property, Delegate, \ cached_property from traitsui.api \ import View, Item, TableEditor, EnumEditor, TextEditor from traitsui.table_column \ import ObjectColumn from .template_data_name \ import TemplateDataName from .itemplate_data_context \ import ITemplateDataContext from .template_choice \ import TemplateChoice #------------------------------------------------------------------------------- # Table editor support for editing a list of TemplateDataName objects: #------------------------------------------------------------------------------- class BindingsColumn ( ObjectColumn ): # The column index: index = Int #-- ObjectColumn Method Overrides ------------------------------------------ def is_editable ( self, object ): return (self.index < len( object.data_name.items )) def get_editor ( self, object ): return EnumEditor( values = [ dnic.choice_value for dnic in object.data_name.items[ self.index ].data_name_item_choices ] + [ self.get_raw_value( object ) ] ) class ResolvedColumn ( ObjectColumn ): def get_raw_value( self, object ): if object.data_name.resolved: return '' return 'X' def get_cell_color ( self, object ): if object.data_name.resolved: return self.read_only_cell_color_ return self.cell_color_ class OptionalColumn ( ObjectColumn ): def get_raw_value( self, object ): return ' X'[ object.data_name.optional ] # Define the table editor: table_editor = TableEditor( columns_name = 'table_columns', configurable = False, auto_size = False, sortable = False, scroll_dy = 4, selection_bg_color = None ) # The standard columns: std_columns = [ ResolvedColumn( name = 'resolved', label = '?', editable = False, width = 20, horizontal_alignment = 'center', cell_color = 0xFF8080 ), OptionalColumn( name = 'optional', label = '*', editable = False, width = 20, horizontal_alignment = 'center' ), ObjectColumn( name = 'description', editor = TextEditor(), width = 0.47 ) ] #------------------------------------------------------------------------------- # 'TemplateDataNames' class: #------------------------------------------------------------------------------- class TemplateDataNames ( HasPrivateTraits ): #-- Public Traits ---------------------------------------------------------- # The data context to which bindings are made: context = Instance( ITemplateDataContext ) # The current set of data names to be bound to the context: data_names = List( TemplateDataName ) # The list of unresolved, required bindings: unresolved_data_names = Property( depends_on = 'data_names.resolved' ) # The list of optional bindings: optional_data_names = Property( depends_on = 'data_names.optional' ) # The list of unresolved optional bindings: unresolved_optional_data_names = Property( depends_on = 'data_names.[resolved,optional]' ) #-- Private Traits --------------------------------------------------------- # List of 'virtual' data names for use by table editor: virtual_data_names = List # The list of table editor columns: table_columns = Property( depends_on = 'data_names' ) # List( ObjectColumn ) #-- Traits View Definitions ------------------------------------------------ view = View( Item( 'virtual_data_names', show_label = False, style = 'custom', editor = table_editor ) ) #-- Property Implementations ----------------------------------------------- @cached_property def _get_unresolved_data_names ( self ): return [ dn for dn in self.data_names if (not dn.resolved) and (not dn.optional) ] @cached_property def _get_optional_data_names ( self ): return [ dn for dn in self.data_names if dn.optional ] @cached_property def _get_unresolved_optional_data_names ( self ): return [ dn for dn in self.data_names if (not dn.resolved) and dn.optional ] @cached_property def _get_table_columns ( self ): n = max( [ len( dn.items ) for dn in self.data_names ] ) if n == 1: return std_columns + [ BindingsColumn( name = 'value0', label = 'Name', width = 0.43 ) ] width = 0.43 / n return (std_columns + [ BindingsColumn( name = 'value%d' % i, index = i, label = 'Name %d' % ( i + 1 ), width = width ) for i in range( n ) ]) #-- Trait Event Handlers --------------------------------------------------- def _context_changed ( self, context ): for data_name in self.data_names: data_name.context = context def _data_names_changed ( self, old, new ): """ Handles the list of 'data_names' being changed. """ # Make sure that all of the names are unique: new = set( new ) # Update the old and new context links: self._update_contexts( old, new ) # Update the list of virtual names based on the new set: dns = [ VirtualDataName( data_name = dn ) for dn in new ] dns.sort( lambda l, r: cmp( l.description, r.description ) ) self.virtual_data_names = dns def _data_names_items_changed ( self, event ): # Update the old and new context links: old, new = event.old, event.new self._update_contexts( old, new ) # Update the list of virtual names based on the old and new sets: i = event.index self.virtual_data_names[ i: i + len( old ) ] = [ VirtualDataName( data_name = dn ) for dn in new ] #-- Private Methods -------------------------------------------------------- def _update_contexts ( self, old, new ): """ Updates the data context for an old and new set of data names. """ for data_name in old: data_name.context = None context = self.context for data_name in new: data_name.context = context #------------------------------------------------------------------------------- # 'VirtualDataName' class: #------------------------------------------------------------------------------- # Define the 'VirtualValue' property: def _get_virtual_data ( self, name ): return self.data_name.items[ self.trait( name ).index ].data_name_item_choice.choice_value def _set_virtual_data ( self, name, new_value ): old_value = _get_virtual_data( self, name ) if old_value != new_value: self.data_name.items[ self.trait( name ).index ].data_name_item_choice = \ TemplateChoice( choice_value = new_value ) self.trait_property_changed( name, old_value, new_value ) VirtualValue = Property( _get_virtual_data, _set_virtual_data ) class VirtualDataName ( HasPrivateTraits ): # The TemplateDataName this is a virtual copy of: data_name = Instance( TemplateDataName ) # The data name description: description = Delegate( 'data_name', modify = True ) # The 'virtual' traits of this object: value0 = VirtualValue( index = 0 ) value1 = VirtualValue( index = 1 ) value2 = VirtualValue( index = 2 ) value3 = VirtualValue( index = 3 ) value4 = VirtualValue( index = 4 ) value5 = VirtualValue( index = 5 ) apptools-4.5.0/apptools/template/mutable_template.py0000644000076500000240000000250113547637361024231 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A concrete base class that implements the IMutableTemplate interface. # # Written by: David C. Morrill # # Date: 08/01/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ A concrete base class that implements the IMutableTemplate interface. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Event, provides from .template_impl \ import Template from .imutable_template \ import IMutableTemplate #------------------------------------------------------------------------------- # 'MutableTemplate' class: #------------------------------------------------------------------------------- class MutableTemplate ( Template ): """ A concrete base class that implements the IMutableTemplate interface. """ implements( IMutableTemplate ) #-- IMutableTemplate Interface Implementation ------------------------------ # An event fired when the template mutates (i.e. changes in some way that # may affect the number of data sources it exposes, and so on): template_mutated = Event apptools-4.5.0/apptools/template/itemplate_data_context.py0000644000076500000240000000646711640354733025434 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the ITemplateDataContext interface for accessing a named collection # of data that can be bound to a templatized object when converting it to a # 'live' set of objects. # # Written by: David C. Morrill # # Date: 07/26/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the ITemplateDataContext interface for accessing a named collection of data that can be bound to a templatized object when converting it to a 'live' set of objects. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api\ import Interface, List, Str #------------------------------------------------------------------------------- # 'ITemplateDataContext' interface: #------------------------------------------------------------------------------- class ITemplateDataContext ( Interface ): """ Defines the ITemplateDataContext interface for accessing a named collection of data that can be bound to a templatized object when converting it to a 'live' set of objects. """ # The path to this data context (does not include the 'data_context_name'): data_context_path = Str # The name of the data context: data_context_name = Str # A list of the names of the data values in this context: data_context_values = List( Str ) # The list of the names of the sub-contexts of this context: data_contexts = List( Str ) def get_data_context_value ( self, name ): """ Returns the data value with the specified *name*. Raises a **ITemplateDataContextError** if *name* is not defined as a data value in the context. Parameters ---------- name : A string specifying the name of the context data value to be returned. Returns ------- The data value associated with *name* in the context. The type of the data is application dependent. Raises **ITemplateDataContextError** if *name* is not associated with a data value in the context. """ def get_data_context ( self, name ): """ Returns the **ITemplateDataContext** value associated with the specified *name*. Raises **ITemplateDataContextError** if *name* is not defined as a data context in the context. Parameters ---------- name : A string specifying the name of the data context to be returned. Returns ------- The **ITemplateDataContext** associated with *name* in the context. Raises **ITemplateDataContextError** if *name* is not associated with a data context in the context. """ #------------------------------------------------------------------------------- # 'ITemplateDataContextError' exception: #------------------------------------------------------------------------------- class ITemplateDataContextError ( Exception ): """ The exception class associated with the **ITemplateDataContext** interface. """ apptools-4.5.0/apptools/template/__init__.py0000644000076500000240000000053111640354733022434 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. #------------------------------------------------------------------------------ """ Supports creating templatizable object hierarchies. Part of the AppTools project of the Enthought Tool Suite. """ apptools-4.5.0/apptools/template/itemplate_data_source.py0000644000076500000240000000267111640354733025241 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the ITemplateDataSource interface for creating 'live' application # data sources from a templatized data source object. # # Written by: David C. Morrill # # Date: 07/26/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the ITemplateDataSource interface for creating 'live' application data sources from a templatized data source object. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Interface #------------------------------------------------------------------------------- # 'ITemplateDataSource' interface: #------------------------------------------------------------------------------- class ITemplateDataSource ( Interface ): """ Defines the ITemplateDataSource interface for creating 'live' application data sources from a templatized data source object. """ def name_from_data_source ( self ): """ Allows the object to provide a description of the possibly optional data binding it requires. Returns ------- A **TemplateDataName** object describing the binding the data source object requires. """ apptools-4.5.0/apptools/template/imutable_template.py0000644000076500000240000000242613547637361024410 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the IMutableTemplate interface used to define modifiable templates. # # Written by: David C. Morrill # # Date: 08/01/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the IMutableTemplate interface used to define modifiable templates. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Event from .itemplate \ import ITemplate #------------------------------------------------------------------------------- # 'IMutableTemplate' interface: #------------------------------------------------------------------------------- class IMutableTemplate ( ITemplate ): """ Defines the IMutableTemplate interface used to define modifiable templates. Extends the ITemplate interface by supporting the ability for the template to modify itself. """ # An event fired when the template mutates (i.e. changes in some way that # may affect the number of data sources it exposes, and so on): template_mutated = Event apptools-4.5.0/apptools/template/api.py0000644000076500000240000000215513547637361021463 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the external API for the template package. # # Written by: David C. Morrill # # Date: 07/26/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the external API for the template package. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from .template_traits \ import TDataSource, TInstance, TList, TInt, TFloat, TStr, TBool, TRange, \ TEnum, TDerived from .itemplate \ import ITemplate from .imutable_template \ import IMutableTemplate from .itemplate_data_context \ import ITemplateDataContext, ITemplateDataContextError from .template_data_name \ import TemplateDataName from .template_data_names \ import TemplateDataNames from .template_impl \ import Template from .mutable_template \ import MutableTemplate from .template_choice \ import TemplateChoice apptools-4.5.0/apptools/template/itemplate_data_name_item.py0000644000076500000240000000512413547637361025704 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the ITemplateDataNameItem interface used by elements of a # TemplateDataName. # # Written by: David C. Morrill # # Date: 07/27/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the ITemplateDataNameItem interface used by elements of a TemplateDataName. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Interface, Instance, List from .itemplate_data_context \ import ITemplateDataContext from .itemplate_choice \ import ITemplateChoice #------------------------------------------------------------------------------- # 'ITemplateDataNameItem' interface: #------------------------------------------------------------------------------- class ITemplateDataNameItem ( Interface ): """ Defines the ITemplateDataNameItem interface used by elements of a TemplateDataName. The contents of the *input_data_context* and *output_data_context* are assumed to be immutable. That is, no changes should occur to the data contexts once they have been assigned to the **ITemplateDataNameItem** traits. However, new input contexts can be assigned, in which case a new output context should be created and assigned to the output context (once it has been fully populated). Also, the *output_data_context* should be **None** if the *input_data_context* value is **None** or the object cannot match any values in the *input_data_context*. """ # The data context which this data name item should match against. This # value must be read/write. input_data_context = Instance( ITemplateDataContext ) # The data context containing the data values and/or contexts this data # name item matches. This value must be read/write. output_data_context = Instance( ITemplateDataContext ) # The ITemplateChoice instance representing the current settings of the # data name item. This value must be read/write. data_name_item_choice = Instance( ITemplateChoice ) # The alternative choices the user has for the data name item settings for # the current input data context. The list may be empty, in which case the # user cannot change the settings of the data name item. This value can be # read only. data_name_item_choices = List( ITemplateChoice ) apptools-4.5.0/apptools/template/itemplate_choice.py0000644000076500000240000000206011640354733024172 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the ITemplateChoice interface used by ITemplateDataNameItem # interface. # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the ITemplateChoice interface used by ITemplateDataNameItem interface. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Interface, Str #------------------------------------------------------------------------------- # 'ITemplateChoice' interface: #------------------------------------------------------------------------------- class ITemplateChoice ( Interface ): """ Defines the ITemplateChoice interface used by ITemplateDataNameItem interface. """ # The user interface string for this choice: choice_value = Str apptools-4.5.0/apptools/template/template_choice.py0000644000076500000240000000202013547637361024026 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # A concrete implementation of the ITemplateChoice interface that can be used # as is for simple cases, or extended for more complex ones. # # Written by: David C. Morrill # # Date: 07/29/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import HasPrivateTraits, Str, provides from .itemplate_choice \ import ITemplateChoice #------------------------------------------------------------------------------- # 'TemplateChoice' class: #------------------------------------------------------------------------------- class TemplateChoice ( HasPrivateTraits ): implements ( ITemplateChoice ) # The user interface string for this choice: choice_value = Str apptools-4.5.0/apptools/template/template_data_name.py0000644000076500000240000001046013547637361024514 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the TemplateDataName class used for binding a data source to a data # context. # # Written by: David C. Morrill # # Date: 07/27/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the TemplateDataName class used for binding a data source to a data context. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import HasPrivateTraits, Instance, Bool, Property, Undefined, \ on_trait_change, cached_property from .itemplate_data_name_item \ import ITemplateDataNameItem from .itemplate_data_context \ import ITemplateDataContext from .template_impl \ import Template from .template_traits \ import TList, TStr, TBool #------------------------------------------------------------------------------- # 'TemplateDataName' interface: #------------------------------------------------------------------------------- class TemplateDataName ( Template ): """ Defines the TemplateDataName class used for binding a data source to a data context. """ #-- Public Template Traits ------------------------------------------------- # The list of ITemplateDataNameItem's used to match the data context: items = TList( ITemplateDataNameItem ) # A description of the template data name (for use in a user interface): description = TStr # Is this binding optional? optional = TBool( False ) #-- Public Non-Template Traits --------------------------------------------- # The data context the template name is matching against currently: context = Instance( ITemplateDataContext ) # Is the binding resolved? resolved = Property( Bool, depends_on = 'items.output_data_context' ) # The actual name of the context data that the data source is bound to: context_data_name = Property # The context data the data source is bound to: context_data = Property #-- Property Implementations ----------------------------------------------- @cached_property def _get_resolved ( self ): if len( self.items ) == 0: return False context = self.items[-1].output_data_context return ((context is not None) and (len( context.data_context_values ) == 1) and (len( context.data_contexts ) == 0)) def _get_context_data_name ( self ): if self.resolved: context = self.items[-1].output_data_context path = context.data_context_path if path != '': path += '.' name = context.data_context_name if name != '': name += '.' return '%s%s%s' % ( path, name, context.data_context_values[0] ) return '' def _get_context_data ( self ): if self.resolved: context = self.items[-1].output_data_context return context.get_data_context_value( context.data_context_values[0] ) return Undefined #-- Trait Event Handlers --------------------------------------------------- def _context_changed ( self, context ): """ Resets the name whenever the context is changed. """ self._reset() @on_trait_change( ' items' ) def _on_items_changed ( self ): """ Resets the name whenever any of the name items are changed. """ self._reset() @on_trait_change( 'items:output_data_context' ) def _on_output_data_context_changed ( self, item, name, old, new ): i = self.items.index( item ) if i < (len( self.items ) - 1): self.items[ i + 1 ].input_data_context = new #-- Private Methods -------------------------------------------------------- def _reset ( self ): """ Resets the name whenever any significant change occurs. """ items = self.items n = len( items ) if n > 0: items[0].input_data_context = self.context for i in range( 0, n - 1 ): items[ i + 1 ].input_data_context = \ items[ i ].output_data_context apptools-4.5.0/apptools/template/template_impl.py0000644000076500000240000001410213547637361023541 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the Template class, which provides a default implementation of the # ITemplate interface that can be used when defining templatizable objects. # # Written by: David C. Morrill # # Date: 07/26/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the Template class, which provides a default implementation of the ITemplate interface that can be used when defining templatizable objects. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import HasPrivateTraits, Undefined, provides from .itemplate \ import ITemplate #------------------------------------------------------------------------------- # Metadata filter for extracting 'object' and 'datasource' items: #------------------------------------------------------------------------------- def instance_or_datasource ( value ): return value in ( 'instance', 'datasource' ) #------------------------------------------------------------------------------- # Metadata filter for extracting traits with no 'template' metadata: #------------------------------------------------------------------------------- def is_none ( value ): return (value is None) #------------------------------------------------------------------------------- # 'Template' class: #------------------------------------------------------------------------------- class Template ( HasPrivateTraits ): """ Defines the Template class, which provides a default implementation of the ITemplate interface that can be used when defining templatizable objects. """ implements( ITemplate ) #-- 'ITemplate' Interface Implementation ----------------------------------- def names_from_template ( self ): """ Returns a list of **TemplateDataName** objects, one for each bindable data source contained within the template. Each **TemplateDataName** object supports unresolved bindings in addition to resolved bindings. Also, a **TemplateDataName** may be marked as *optional*, meaning the if the name remains unresolved, the application can take an alternative action (e.g. omit the data from a plot, or substitute default data, etc.). Returns ------- A list of **TemplateDataName** objects, one for each bindable data source contained in the template. """ data_names = [] # Recursively propagate the request to all contained templatizable # objects: for value in self.get( template = 'instance' ).values(): if isinstance( value, list ): for v in value: data_names.extend( v.names_from_template() ) elif value is not None: data_names.extend( value.names_from_template() ) # Add the bindings for all of our local data source objects: for value in self.get( template = 'datasource' ).values(): if isinstance( value, list ): for v in value: data_names.append( v.name_from_data_source() ) elif value is not None: data_names.append( value.name_from_data_source() ) # Return the list of TemplateDataName objects we collected: return data_names def object_from_template ( self ): """ Activates the object from its template data. Returns ------- The original object. """ # Convert all our data source objects to 'live' objects: for value in self.get( template = 'datasource' ).values(): if isinstance( value, list ): for v in value: v.object_from_template() elif value is not None: value.object_from_template() # Recursively propagate the request to all contained templatizable # objects: for value in self.get( template = 'instance' ).values(): if isinstance( value, list ): for v in value: v.object_from_template() elif value is not None: value.object_from_template() # Reset all of our 'live' objects to undefined: for name in self.trait_names( template = 'derived' ): setattr( self, name, Undefined ) # Return ourselves as the new, live version of the object: return self def template_from_object ( self ): """ Returns a *templatized* version of the object that is safe for serialization. Returns ------- A new object of the same class as the original. """ # Mark all traits without 'template' metadata as 'transient': for trait in self.traits( template = is_none ).values(): trait.transient = True # Collect all of the data source and templatizable objects we contain: contents = self.get( template = instance_or_datasource ) # Convert them all to new objects in template form: for name, value in contents.items(): if isinstance( value, list ): contents[ name ] = [ v.template_from_object() for v in value ] else: contents[ name ] = value.template_from_object() # Add all of the simple values which can just be copied: contents.update( self.get( template = 'copy' ) ) # Finally return a new instance of our class containing just the # templatizable values: return self.__class__( **contents ) def activate_template ( self ): """ Converts all contained 'TDerived' objects to real objects using the template traits of the object. This method must be overridden in subclasses. Returns ------- None """ raise NotImplementedError apptools-4.5.0/apptools/template/itemplate.py0000644000076500000240000000451711640354733022671 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the ITemplate interface used to create templatizable object # hierarchies. # # Written by: David C. Morrill # # Date: 07/26/2007 # # (c) Copyright 2007 by Enthought, Inc. # #------------------------------------------------------------------------------- """ Defines the ITemplate interface used to create templatizable object hierarchies. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from traits.api \ import Interface #------------------------------------------------------------------------------- # 'ITemplate' interface: #------------------------------------------------------------------------------- class ITemplate ( Interface ): """ Defines the ITemplate interface used to create templatizable object hierarchies. """ def names_from_template ( self ): """ Returns a list of **TemplateDataName** objects, one for each bindable data source contained within the template. Each **TemplateDataName** object supports unresolved bindings in addition to resolved bindings. Also, a **TemplateDataName** may be marked as *optional*, meaning the if the name remains unresolved, the application can take an alternative action (e.g. omit the data from a plot, or substitute default data, etc.). Returns ------- A list of **TemplateDataName** objects, one for each bindable data source contained in the template. """ def object_from_template ( self ): """ Activates the object from its template data. Returns ------- The original object. """ def template_from_object ( self ): """ Returns a *templatized* version of the object that is safe for serialization. Returns ------- A new object of the same class as the original. """ def activate_template ( self ): """ Converts all contained 'TDerived' objects to real objects using the template traits of the object. Returns ------- None """ apptools-4.5.0/apptools/appscripting/0000755000076500000240000000000013547652535021225 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/appscripting/script_manager.py0000644000076500000240000006410713547637361024605 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import datetime import types import weakref # Enthought library imports. from traits.api import Any, Bool, Callable, Dict, Event, HasTraits, \ implements, Instance, Int, List, Property, Str, Unicode # Local imports. from .bind_event import BindEvent from .i_bind_event import IBindEvent from .i_script_manager import IScriptManager from .lazy_namespace import add_to_namespace, FactoryWrapper, LazyNamespace from .scriptable_type import make_object_scriptable @provides(IScriptManager) class _BoundObject(HasTraits): """The base class for any object that can be bound to a name.""" #### '_BoundObject' interface ############################################# # Set if the object was explicitly bound. explicitly_bound = Bool(True) # The name the object is bound to. name = Str # The object being bound. obj = Any class _ScriptObject(_BoundObject): """The _ScriptObject class encapsulates a scriptable object.""" #### '_BoundObject' interface ############################################# # The object being bound. obj = Property #### '_ScriptObject' interface ############################################ # The positional arguments passed to __init__ after being converted to # strings. A particular argument may be an exception if it couldn't be # converted. args = List # The keyword arguments passed to __init__ after being converted to # strings. A particular argument may be an exception if it couldn't be # converted. kwargs = Dict # The id of the object. obj_id = Int # A weak reference to the object. obj_ref = Any # The type of the scriptable object. scripted_type = Any ########################################################################### # Private interface. ########################################################################### def _get_obj(self): """The property getter.""" return self.obj_ref() class _ScriptCall(HasTraits): """ The _ScriptCall class is the base class for all script calls. """ #### '_ScriptCall' interface ############################################## # The name of the call. name = Str # The scriptable object. so = Any ########################################################################### # '_ScriptCall' interface. ########################################################################### def as_str(self, sm, so_needed): """ Return the string equivalent of the call, updated the list of needed scriptable objects if required. """ raise NotImplementedError class _ScriptTraitGet(_ScriptCall): """ The _ScriptTraitGet class encapsulates a single call to the get of a scriptable trait. """ #### '_ScriptTraitGet' interface ########################################## # Set if the getter has side effects. has_side_effects = Bool(False) # The result of the get. Keeping a reference to it means that the memory # can't get reused. result = Any ########################################################################### # '_ScriptCall' interface. ########################################################################### def as_str(self, sm, so_needed): """ Return the string equivalent of the call, updated the list of needed scriptable objects if required. """ # Ignore if it is no longer bound. if not self.so.name: return None if self.result is None: rstr = "" else: nr, _ = sm._results[id(self.result)] if nr >= 0: rstr = "r%d = " % nr else: rstr = "" # Unless getter has side effects, if the result is not needed then # don't bother including it in the script. if not self.has_side_effects and rstr == "": return None so = sm.arg_as_string(self.so, so_needed) return "%s%s.%s" % (rstr, so, self.name) class _ScriptTraitSet(_ScriptCall): """ The _ScriptTraitSet class encapsulates a single call to the set of a scriptable trait. """ #### '_ScriptTraitSet' interface ########################################## # The value the trait is set to. value = Any ########################################################################### # '_ScriptCall' interface. ########################################################################### def as_str(self, sm, so_needed): """ Return the string equivalent of the call, updated the list of needed scriptable objects if required. """ # Ignore if it is no longer bound. if not self.so.name: return None so = sm.arg_as_string(self.so, so_needed) value = sm.arg_as_string(self.value, so_needed) return "%s.%s = %s" % (so, self.name, value) class _ScriptMethod(_ScriptCall): """ The _ScriptMethod class encapsulates a single call to a scriptable method. """ #### '_ScriptMethod' interface ############################################ # The positional arguments passed to the method after being converted to # strings. A particular argument may be an exception if it couldn't be # converted. args = List # The keyword arguments passed to the method after being converted to # strings. A particular argument may be an exception if it couldn't be # converted. kwargs = Dict # The result of the method call. Keeping a reference to it means that the # memory can't get reused. result = Any ########################################################################### # '_ScriptCall' interface. ########################################################################### def as_str(self, sm, so_needed): """ Return the string equivalent of the call, updated the list of needed scriptable objects if required. """ # Ignore if it is no longer bound. if self.so and not self.so.name: return None if self.result is None: rstr = "" elif type(self.result) is type(()): rlist = [] needed = False for r in self.result: nr, _ = sm._results[id(r)] if nr >= 0: rlist.append("r%d" % nr) needed = True else: rlist.append("_") if needed: rstr = ", ".join(rlist) + " = " else: rstr = "" else: nr, _ = sm._results[id(self.result)] if nr >= 0: rstr = "r%d = " % nr else: rstr = "" if self.so: so = sm.arg_as_string(self.so, so_needed) + '.' else: so = '' args = sm.args_as_string_list(self.args, self.kwargs, so_needed) return "%s%s%s(%s)" % (rstr, so, self.name, ", ".join(args)) class _FactoryObject(_BoundObject): """ The _FactoryObject class wraps a factory that lazily creates scriptable objects. """ #### '_BoundObject' interface ############################################# # The object being bound. obj = Property #### '_FactoryObject' interface ########################################### # The optional object that defines the scripting API. api = Any # The scriptable object factory. factory = Callable # The optional attribute include list. includes = Any # The optional attribute exclude list. excludes = Any ########################################################################### # Private interface. ########################################################################### def _get_obj(self): """The property getter.""" return FactoryWrapper(factory=self.factory, api=self.api, includes=self.includes, excludes=self.excludes) class ScriptManager(HasTraits): """ The ScriptManager class is the default implementation of IScriptManager. """ #### 'IScriptManager' interface ########################################### # This event is fired whenever a scriptable object is bound or unbound. It # is intended to be used by an interactive Python shell to give the # advanced user access to the scriptable objects. If an object is created # via a factory then the event is fired when the factory is called, and not # when the factory is bound. bind_event = Event(IBindEvent) # This is set if user actions are being recorded as a script. It is # maintained by the script manager. recording = Bool(False) # This is the text of the script currently being recorded (or the last # recorded script if none is currently being recorded). It is updated # automatically as the user performs actions. script = Property(Unicode) # This event is fired when the recorded script changes. The value of the # event will be the ScriptManager instance. script_updated = Event(IScriptManager) #### Private interface #################################################### # The list of calls to scriptable calls. _calls = List(Instance(_ScriptCall)) # The dictionary of bound names. The value is the next numerical suffix # to use when the binding policy is 'auto'. _names = Dict # The dictionary of _BoundObject instances keyed by the name the object is # bound to. _namespace = Dict # The next sequential result number. _next_result_nr = Int # The results returned by previous scriptable calls. The key is the id() # of the result object. The value is a two element tuple of the sequential # result number (easier for the user to use than the id()) and the result # object itself. _results = Dict # The dictionary of _ScriptObject instances keyed by the object's id(). _so_by_id = Dict # The dictionary of _ScriptObject instances keyed by the a weak reference # to the object. _so_by_ref = Dict # The date and time when the script was recorded. _when_started = Any ########################################################################### # 'IScriptManager' interface. ########################################################################### def bind(self, obj, name=None, bind_policy='unique', api=None, includes=None, excludes=None): """ Bind obj to name and make (by default) its public methods and traits (ie. those not beginning with an underscore) scriptable. The default value of name is the type of obj with the first character forced to lower case. name may be a dotted name (eg. 'abc.def.xyz'). bind_policy determines what happens if the name is already bound. If the policy is 'auto' then a numerical suffix will be added to the name, if necessary, to make it unique. If the policy is 'unique' then an exception is raised. If the policy is 'rebind' then the previous binding is discarded. The default is 'unique' If api is given then it is a class, or a list of classes, that define the attributes that will be made scriptable. Otherwise if includes is given it is a list of names of attributes that will be made scriptable. Otherwise all the public attributes of scripted_type will be made scriptable except those in the excludes list. """ # Register the object. self.new_object(obj, obj.__class__, name=name, bind_policy=bind_policy) # Make it scriptable. make_object_scriptable(obj, api=api, includes=includes, excludes=excludes) def bind_factory(self, factory, name, bind_policy='unique', api=None, includes=None, excludes=None): """ Bind factory to name. This does the same as the bind() method except that it uses a factory that will be called later on to create the object only if the object is needed. See the documentation for bind() for a description of the remaining arguments. """ name = self._unique_name(name, bind_policy) self._namespace[name] = _FactoryObject(name=name, factory=factory, api=api, includes=includes, excludes=excludes) def run(self, script): """ Run the given script, either a string or a file-like object. """ # Initialise the namespace with all explicitly bound objects. nspace = LazyNamespace() for name, bo in self._namespace.items(): if bo.explicitly_bound: add_to_namespace(bo.obj, name, nspace) exec(script, nspace) def run_file(self, file_name): """ Run the given script file. """ f = open(file_name) self.run(f) f.close() def start_recording(self): """ Start the recording of user actions. The 'script' trait is cleared and all subsequent actions are added to 'script'. The 'recording' trait is updated appropriately. """ self._calls = [] self._next_result_nr = 0 self._results = {} self.recording = True self.script_updated = self def stop_recording(self): """ Stop the recording of user actions. The 'recording' trait is updated appropriately. """ self.recording = False ########################################################################### # 'ScriptManager' interface. ########################################################################### def record_method(self, func, args, kwargs): """ Record the call of a method of a ScriptableObject instance and return the result. This is intended to be used only by the scriptable decorator. """ if self.recording: # Record the arguments before the function has a chance to modify # them. srec = self._new_method(func, args, kwargs) result = func(*args, **kwargs) self._add_method(srec, result) self.script_updated = self else: result = func(*args, **kwargs) return result def record_trait_get(self, obj, name, result): """ Record the get of a trait of a scriptable object. This is intended to be used only by the Scriptable trait getter. """ if self.recording: side_effects = self._add_trait_get(obj, name, result) # Don't needlessly fire the event if there are no side effects. if side_effects: self.script_updated = self def record_trait_set(self, obj, name, value): """ Record the set of a trait of a scriptable object. This is intended to be used only by the Scriptable trait getter. """ if self.recording: self._add_trait_set(obj, name, value) self.script_updated = self def new_object(self, obj, scripted_type, args=None, kwargs=None, name=None, bind_policy='auto'): """ Register a scriptable object and the arguments used to create it. If no arguments were provided then assume the object is being explicitly bound. """ # The name defaults to the type name. if not name: name = scripted_type.__name__ name = name[0].lower() + name[1:] name = self._unique_name(name, bind_policy) obj_id = id(obj) obj_ref = weakref.ref(obj, self._gc_script_obj) so = _ScriptObject(name=name, obj_id=obj_id, obj_ref=obj_ref, scripted_type=scripted_type) # If we are told how to create the object then it must be implicitly # bound. if args is not None: # Convert each argument to its string representation if possible. # Doing this now avoids problems with mutable arguments. so.args = [self._scriptable_object_as_string(a) for a in args] for n, value in kwargs.items(): so.kwargs[n] = self._scriptable_object_as_string(value) so.explicitly_bound = False # Remember the scriptable object via the different access methods. self._so_by_id[obj_id] = so self._so_by_ref[obj_ref] = so self._namespace[name] = so # Note that if anything listening to this event doesn't use weak # references then the object will be kept alive. self.bind_event = BindEvent(name=name, obj=obj) @staticmethod def args_as_string_list(args, kwargs, so_needed=None): """ Return a complete argument list from sets of positional and keyword arguments. Update the optional so_needed list for those arguments that refer to a scriptable object. """ if so_needed is None: so_needed = [] all_args = [] for arg in args: s = ScriptManager.arg_as_string(arg, so_needed) all_args.append(s) for name, value in kwargs.items(): s = ScriptManager.arg_as_string(value, so_needed) all_args.append('%s=%s' % (name, s)) return all_args @staticmethod def arg_as_string(arg, so_needed): """ Return the string representation of an argument. Update the so_needed list if the argument refers to a scriptable object. Any delayed conversion exception is handled here. """ if isinstance(arg, Exception): raise arg if isinstance(arg, _ScriptObject): # Check it hasn't been unbound. if not arg.name: raise NameError("%s has been unbound but is needed by the script" % arg.obj_ref()) # Add it to the needed list if it isn't already there. if arg not in so_needed: so_needed.append(arg) arg = arg.name return arg ########################################################################### # Private interface. ########################################################################### def _new_method(self, func, args, kwargs): """ Return an object that encapsulates a call to a scriptable method. _add_method() must be called to add it to the current script. """ # Convert each argument to its string representation if possible. # Doing this now avoids problems with mutable arguments. nargs = [self._object_as_string(arg) for arg in args] if type(func) is types.FunctionType: so = None else: so = nargs[0] nargs = nargs[1:] nkwargs = {} for name, value in kwargs.items(): nkwargs[name] = self._object_as_string(value) return _ScriptMethod(name=func.__name__, so=so, args=nargs, kwargs=nkwargs) def _add_method(self, entry, result): """ Add a method call (returned by _new_method()), with it's associated result and ID, to the current script. """ self._start_script() if result is not None: # Assume that a tuple represents multiple returned values - not # necessarily a valid assumption unless we make it a rule for # scriptable functions. if type(result) is type(()): for r in result: self._save_result(r) else: self._save_result(result) entry.result = result self._calls.append(entry) def _add_trait_get(self, obj, name, result): """ Add a call to a trait getter, with it's associated result and ID, to the current script. Return True if the get had side effects. """ self._start_script() side_effects = obj.trait(name).has_side_effects if side_effects is None: side_effects = False so = self._object_as_string(obj) if result is not None: self._save_result(result) self._calls.append(_ScriptTraitGet(so=so, name=name, result=result, has_side_effects=side_effects)) return side_effects def _add_trait_set(self, obj, name, value): """ Add a call to a trait setter, with it's associated value and ID, to the current script. """ self._start_script() so = self._object_as_string(obj) value = self._object_as_string(value) self._calls.append(_ScriptTraitSet(so=so, name=name, value=value)) def _unique_name(self, name, bind_policy): """ Return a name that is guaranteed to be unique according to the bind policy. """ # See if the name is already is use. bo = self._namespace.get(name) if bo is None: self._names[name] = 1 elif bind_policy == 'auto': suff = self._names[name] self._names[name] = suff + 1 name = '%s%d' % (name, suff) elif bind_policy == 'rebind': self._unbind(bo) else: raise NameError("\"%s\" is already bound to a scriptable object" % name) return name def _unbind(self, bo): """Unbind the given bound object.""" # Tell everybody it is no longer bound. Don't bother if it is a # factory because the corresponding bound event wouldn't have been # fired. if not isinstance(bo, _FactoryObject): self.bind_event = BindEvent(name=bo.name, obj=None) # Forget about it. del self._namespace[bo.name] bo.name = '' @staticmethod def _gc_script_obj(obj_ref): """ The callback invoked when a scriptable object is garbage collected. """ # Avoid recursive imports. from .package_globals import get_script_manager sm = get_script_manager() so = sm._so_by_ref[obj_ref] if so.name: sm._unbind(so) del sm._so_by_id[so.obj_id] del sm._so_by_ref[so.obj_ref] def _start_script(self): """ Save when a script recording is started. """ if len(self._calls) == 0: self._when_started = datetime.datetime.now().strftime('%c') def _object_as_string(self, obj): """ Convert an object to a string as it will appear in a script. An exception may be returned (not raised) if there was an error in the conversion. """ obj_id = id(obj) # See if the argument is the result of a previous call. nr, _ = self._results.get(obj_id, (None, None)) if nr is not None: if nr < 0: nr = self._next_result_nr self._next_result_nr += 1 # Key on the ID of the argument (which is hashable) rather than # the argument itself (which might not be). self._results[obj_id] = (nr, obj) return "r%d" % nr return self._scriptable_object_as_string(obj) def _scriptable_object_as_string(self, obj): """ Convert an object to a string as it will appear in a script. An exception may be returned (not raised) if there was an error in the conversion. """ obj_id = id(obj) # If it is a scriptable object we return the object and convert it to a # string later when we know it is really needed. so = self._so_by_id.get(obj_id) if so is not None: return so # Use the repr result if it doesn't appear to be the generic response, # ie. it doesn't contain its own address as a hex string. s = repr(obj) if hex(obj_id) not in s: return s # We don't know how to represent the argument as a string. This is # most likely because an appropriate __init__ hasn't been made # scriptable. We don't raise an exception until the user decides to # convert the calls to a script. return ValueError("unable to create a script representation of %s" % obj) def _save_result(self, result): """ Save the result of a call to a scriptable method so that it can be recognised later. """ if id(result) not in self._results: self._results[id(result)] = (-1, result) def _get_script(self): """ Convert the current list of calls to a script. """ # Handle the trivial case. if len(self._calls) == 0: return "" # Generate the header. header = "# Script generated %s" % self._when_started # Generate the calls. so_needed = [] calls = [] for call in self._calls: s = call.as_str(self, so_needed) if s: calls.append(s) calls = "\n".join(calls) # Generate the scriptable object constructors. types_needed = [] ctors = [] for so in so_needed: if so.explicitly_bound: continue so_type = so.scripted_type args = self.args_as_string_list(so.args, so.kwargs) ctors.append("%s = %s(%s)" % (so.name, so_type.__name__, ", ".join(args))) # See if a new import is needed. if so_type not in types_needed: types_needed.append(so_type) ctors = "\n".join(ctors) # Generate the import statements. imports = [] for so_type in types_needed: imports.append("from %s import %s" % (so_type.__module__, so_type.__name__)) imports = "\n".join(imports) return "\n\n".join([header, imports, ctors, calls]) + "\n" apptools-4.5.0/apptools/appscripting/i_script_manager.py0000644000076500000240000001063613547637361025113 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, Event, Instance, Interface, Unicode # Local imports. from .i_bind_event import IBindEvent class IScriptManager(Interface): """ The script manager interface. A script manager is responsible for the recording of appropriately annotated user actions as scripts that can be executed without user intervention at a later time. Typically an application would have a single script manager. """ #### 'IScriptManager' interface ########################################### # This event is fired whenever a scriptable object is bound or unbound. It # is intended to be used by an interactive Python shell to give the # advanced user access to the scriptable objects. If an object is created # via a factory then the event is fired when the factory is called, and not # when the factory is bound. bind_event = Event(IBindEvent) # This is set if user actions are being recorded as a script. It is # maintained by the script manager. recording = Bool(False) # This is the text of the script currently being recorded (or the last # recorded script if none is currently being recorded). It is updated # automatically as the user performs actions. script = Unicode # This event is fired when the recorded script changes. The value of the # event will be the ScriptManager instance. script_updated = Event(Instance('apptools.appscripting.api.IScriptManager')) ########################################################################### # 'IScriptManager' interface. ########################################################################### def bind(self, obj, name=None, bind_policy='unique', api=None, includes=None, excludes=None): """ Bind obj to name and make (by default) its public methods and traits (ie. those not beginning with an underscore) scriptable. The default value of name is the type of obj with the first character forced to lower case. bind_policy determines what happens if the name is already bound. If the policy is 'auto' then a numerical suffix will be added to the name, if necessary, to make it unique. If the policy is 'unique' then an exception is raised. If the policy is 'rebind' then the previous binding is discarded. The default is 'unique' If api is given then it is a class, or a list of classes, that define the attributes that will be made scriptable. Otherwise if includes is given it is a list of names of attributes that will be made scriptable. Otherwise all the public attributes of scripted_type will be made scriptable except those in the excludes list. """ def bind_factory(self, factory, name, bind_policy='unique', api=None, includes=None, excludes=None): """ Bind factory to name. This does the same as the bind() method except that it uses a factory that will be called later on to create the object only if the object is needed. See the documentation for bind() for a description of the remaining arguments. """ def run(self, script): """ Run the given script, either a string or a file-like object. """ def run_file(self, file_name): """ Run the given script file. """ def start_recording(self): """ Start the recording of user actions. The 'script' trait is cleared and all subsequent actions are added to 'script'. The 'recording' trait is updated appropriately. """ def stop_recording(self): """ Stop the recording of user actions. The 'recording' trait is updated appropriately. """ apptools-4.5.0/apptools/appscripting/bind_event.py0000644000076500000240000000225313547637361023716 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, HasTraits, provides, Str # Local imports. from .i_bind_event import IBindEvent @provides(IBindEvent) class BindEvent(HasTraits): """The default implementation of the bind event interface.""" #### 'IBindEvent' interface ############################################### # This is the name being bound or unbound. name = Str # This is the object being bound to the name. It is None if the name is # being unbound. obj = Any apptools-4.5.0/apptools/appscripting/__init__.py0000644000076500000240000000101611640354733023323 0ustar mdickinsonstaff00000000000000# Copyright (c) 2005-2011, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. """ Application scripting framework, part of the AppTools project of the Enthought Tool Suite. """ apptools-4.5.0/apptools/appscripting/api.py0000644000076500000240000000174313547637361022355 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ from .scriptable_type import create_scriptable_type, make_object_scriptable from .i_bind_event import IBindEvent from .i_script_manager import IScriptManager from .package_globals import get_script_manager, set_script_manager from .script_manager import ScriptManager from .scriptable import scriptable, Scriptable apptools-4.5.0/apptools/appscripting/action/0000755000076500000240000000000013547652535022502 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/appscripting/action/stop_recording_action.py0000644000076500000240000000417711640354733027432 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Bool, Unicode # Local imports. from apptools.appscripting.package_globals import get_script_manager class StopRecordingAction(Action): """An action that stops the recording of changes to scriptable objects to a script.""" #### 'Action' interface ################################################### enabled = Bool(False) name = Unicode("Stop recording") ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Initialise the instance. """ super(StopRecordingAction, self).__init__(**traits) get_script_manager().on_trait_change(self._on_recording, 'recording') ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Perform the action. """ get_script_manager().stop_recording() ########################################################################### # Private interface. ########################################################################### def _on_recording(self, new): """ Handle a change to the script manager's recording trait. """ self.enabled = new apptools-4.5.0/apptools/appscripting/action/__init__.py0000644000076500000240000000064711640354733024611 0ustar mdickinsonstaff00000000000000# Copyright (c) 2005-2011, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. apptools-4.5.0/apptools/appscripting/action/api.py0000644000076500000240000000143013547637361023623 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ from .start_recording_action import StartRecordingAction from .stop_recording_action import StopRecordingAction apptools-4.5.0/apptools/appscripting/action/start_recording_action.py0000644000076500000240000000261511640354733027575 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Unicode # Local imports. from apptools.appscripting.package_globals import get_script_manager class StartRecordingAction(Action): """An action that starts the recording of changes to scriptable objects to a script.""" #### 'Action' interface ################################################### name = Unicode("Start recording") ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Perform the action. """ get_script_manager().start_recording() apptools-4.5.0/apptools/appscripting/package_globals.py0000644000076500000240000000231713547637361024700 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # The script manager. _script_manager = None def get_script_manager(): """Return the IScriptManager implementation, creating a ScriptManager instance if no other implementation has been set.""" global _script_manager if _script_manager is None: from .script_manager import ScriptManager _script_manager = ScriptManager() return _script_manager def set_script_manager(script_manager): """Set the IScriptManager implementation to use.""" global _script_manager _script_manager = script_manager apptools-4.5.0/apptools/appscripting/i_bind_event.py0000644000076500000240000000227011640354733024214 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, Interface, Str class IBindEvent(Interface): """The bind event interface. A corresponding instance is the value of the event fired when a scriptable object is bound or unbound to or from a name. """ #### 'IBindEvent' interface ############################################### # This is the name being bound or unbound. name = Str # This is the object being bound to the name. It is None if the name is # being unbound. obj = Any apptools-4.5.0/apptools/appscripting/lazy_namespace.py0000644000076500000240000000723213547637361024576 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, Callable, HasTraits # Local imports. from .bind_event import BindEvent from .package_globals import get_script_manager from .scriptable_type import make_object_scriptable class FactoryWrapper(HasTraits): """The FactoryWrapper class wraps a factory for an object.""" #### 'FactoryWrapper' interface ########################################### # The optional object that defines the scripting API. api = Any # The object factory. factory = Callable # The optional attribute include list. includes = Any # The optional attribute exclude list. excludes = Any ########################################################################### # 'FactoryWrapper' interface. ########################################################################### def create_scriptable_object(self, name): """Invoke the factory to create the object then make it scriptable.""" obj = self.factory() sm = get_script_manager() sm.bind_event = BindEvent(name=name, obj=obj) make_object_scriptable(obj, self.api, self.includes, self.excludes) return obj class _LazyNode(object): """The _LazyNode class implements a node in a lazy namespace that will automatically invoke a factory if one is referenced.""" def __getattribute__(self, name): value = super(_LazyNode, self).__getattribute__(name) if isinstance(value, FactoryWrapper): value = value.create_scriptable_object(name) setattr(self, name, value) return value class LazyNamespace(dict): """The LazyNamespace class implements a lazy namespace that will automatically invoke a factory if one is referenced.""" def __getitem__(self, name): value = super(LazyNamespace, self).__getitem__(name) if isinstance(value, FactoryWrapper): value = value.create_scriptable_object(name) self[name] = value return value def add_to_namespace(so, name, nspace): """Add a named scriptable object (or a factory for one) to a lazy namespace. If a name is a dotted name then intermediary nodes in the namespace are created as required.""" def save_obj(obj, name): if isinstance(nspace, LazyNamespace): nspace[name] = obj else: setattr(nspace, name, obj) parts = name.split('.') for part in parts[:-1]: if isinstance(nspace, LazyNamespace): try: next_nspace = nspace[part] except KeyError: next_nspace = _LazyNode() elif isinstance(nspace, _LazyNode): try: next_nspace = nspace[part] except KeyError: next_nspace = _LazyNode() else: raise NameError save_obj(next_nspace, part) nspace = next_nspace if not isinstance(nspace, (LazyNamespace, _LazyNode)): raise NameError save_obj(so, parts[-1]) apptools-4.5.0/apptools/appscripting/scriptable_type.py0000644000076500000240000001217613547637361024777 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import inspect import types # Enthought library imports. from traits.api import HasTraits # Local imports. from .package_globals import get_script_manager from .scriptable import scriptable, Scriptable def create_scriptable_type(scripted_type, name=None, bind_policy='auto', api=None, includes=None, excludes=None, script_init=True): """Create and return a new type based on the given scripted_type that will (by default) have its public methods and traits (ie. those not beginning with an underscore) made scriptable. name is the name that objects of this type will be bound to. It defaults to the name of scripted_type with the first character forced to lower case. It is ignored if script_init is False. bind_policy determines what happens if a name is already bound. If the policy is 'auto' then a numerical suffix will be added to the name, if necessary, to make it unique. If the policy is 'unique' then an exception is raised. If the policy is 'rebind' then the previous binding is discarded. It is ignored if script_init is False. The default is 'auto'. If api is given then it is a class, or a list of classes, that define the attributes that will be made scriptable. Otherwise if includes is given it is a list of names of attributes that will be made scriptable. Otherwise all the public attributes of scripted_type will be made scriptable except those in the excludes list. Irrespective of any other arguments, if script_init is set then the __init__() method will always be made scriptable. """ def __init__(self, *args, **kwargs): """Initialise the dynamic sub-class instance.""" get_script_manager().new_object(self, scripted_type, args, kwargs, name, bind_policy) scripted_type.__init__(self, *args, **kwargs) # See if we need to pull all attribute names from a type. if api is not None: if isinstance(api, list): src = api else: src = [api] elif includes is None: src = [scripted_type] else: names = includes src = None if src: ndict = {} for cls in src: if issubclass(cls, HasTraits): for n in cls.class_traits().keys(): if not n.startswith('_') and not n.startswith('trait'): ndict[n] = None for c in inspect.getmro(cls): if c is HasTraits: break for n in c.__dict__.keys(): if not n.startswith('_'): ndict[n] = None # Respect the excludes so long as there was no explicit API. if api is None and excludes is not None: for n in excludes: try: del ndict[n] except KeyError: pass names = list(ndict.keys()) # Create the type dictionary containing replacements for everything that # needs to be scriptable. type_dict = {} if script_init: type_dict['__init__'] = __init__ if issubclass(scripted_type, HasTraits): traits = scripted_type.class_traits() for n in names: trait = traits.get(n) if trait is not None: type_dict[n] = Scriptable(trait) for n in names: try: attr = getattr(scripted_type, n) except AttributeError: continue if type(attr) is types.MethodType: type_dict[n] = scriptable(attr) type_name = 'Scriptable(%s)' % scripted_type.__name__ return type(type_name, (scripted_type, ), type_dict) def make_object_scriptable(obj, api=None, includes=None, excludes=None): """Make (by default) an object's public methods and traits (ie. those not beginning with an underscore) scriptable. If api is given then it is a class, or a list of classes, that define the attributes that will be made scriptable. Otherwise if includes is given it is a list of names of attributes that will be made scriptable. Otherwise all the public attributes of scripted_type will be made scriptable except those in the excludes list. """ # Create the new scriptable type. new_type = create_scriptable_type(obj.__class__, api=api, includes=includes, excludes=excludes, script_init=False) # Fix the object's type to make it scriptable. obj.__class__ = new_type apptools-4.5.0/apptools/appscripting/scriptable.py0000644000076500000240000000713713547637361023737 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, Property, Undefined from traits.traits import trait_cast # Local imports. from .package_globals import get_script_manager # This is the guard that ensures that only outermost scriptable methods get # recorded. _outermost_call = True def scriptable(func): """ This is the decorator applied to functions and methods to mark them as being scriptable. """ def _scripter(*args, **kwargs): """ This is the wrapper that is returned in place of the scriptable method. """ global _outermost_call if _outermost_call: _outermost_call = False # See if there is an script manager set. sm = get_script_manager() if func.__name__ == '__init__': sm.new_object(args[0], type(args[0]), args[1:], kwargs) try: result = func(*args, **kwargs) finally: _outermost_call = True else: # Record the ordinary method. try: result = sm.record_method(func, args, kwargs) finally: _outermost_call = True else: # We aren't at the outermost call so just invoke the method. result = func(*args, **kwargs) return result # Be a good citizen. _scripter.__name__ = func.__name__ _scripter.__doc__ = func.__doc__ _scripter.__dict__.update(func.__dict__) return _scripter def _scriptable_get(obj, name): """ The getter for a scriptable trait. """ global _outermost_call saved_outermost = _outermost_call _outermost_call = False try: result = getattr(obj, '_' + name, None) if result is None: result = obj.trait(name).default finally: _outermost_call = saved_outermost if saved_outermost: get_script_manager().record_trait_get(obj, name, result) return result def _scriptable_set(obj, name, value): """ The setter for a scriptable trait. """ if _outermost_call: get_script_manager().record_trait_set(obj, name, value) _name = '_' + name old_value = getattr(obj, _name, Undefined) if old_value is not value: setattr(obj, _name, value) obj.trait_property_changed(name, old_value, value) def Scriptable(trait=Any, **metadata): """ Scriptable is a wrapper around another trait that makes it scriptable, ie. changes to its value can be recorded. If a trait is read, but the value isn't set to another scriptable trait or passed to a scriptable method then the read will not be included in the recorded script. To make sure a read is always recorded set the 'has_side_effects' argument to True. """ trait = trait_cast(trait) metadata['default'] = trait.default_value()[1] return Property(_scriptable_get, _scriptable_set, trait=trait, **metadata) apptools-4.5.0/apptools/__init__.py0000644000076500000240000000037213547637370020635 0ustar mdickinsonstaff00000000000000# Copyright (c) 2007-2014 by Enthought, Inc. # All rights reserved. try: from apptools._version import full_version as __version__ except ImportError: __version__ = 'not-built' __requires__ = [ 'configobj', 'six', 'traitsui', ] apptools-4.5.0/apptools/undo/0000755000076500000240000000000013547652535017467 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/undo/abstract_command.py0000644000076500000240000000564213547637361023351 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, HasTraits, Unicode, provides # Local imports. from .i_command import ICommand @provides(ICommand) class AbstractCommand(HasTraits): """ The AbstractCommand class is an abstract base class that implements the ICommand interface. """ #### 'ICommand' interface ################################################# # This is the data on which the command operates. data = Any # This is the name of the command as it will appear in any GUI element. It # may include '&' which will be automatically removed whenever it is # inappropriate. name = Unicode ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): """ This is called by the command stack to do the command and to return any value. The command must save any state necessary for the 'redo()' and 'undo()' methods to work. The class's __init__() must also ensure that deep copies of any arguments are made if appropriate. It is guaranteed that this will only ever be called once and that it will be called before any call to 'redo()' or 'undo()'. """ raise NotImplementedError def merge(self, other): """ This is called by the command stack to try and merge another command with this one. True is returned if the commands were merged. 'other' is the command that is about to be executed. If the commands are merged then 'other' will discarded and not placed on the command stack. A subsequent undo or redo of this modified command must have the same effect as the two original commands. """ # By default merges never happen. return False def redo(self): """ This is called by the command stack to redo the command. Any returned value will replace the value that the command stack references from the original call to 'do()' or previous call to 'redo()'. """ raise NotImplementedError def undo(self): """ This is called by the command stack to undo the command. """ raise NotImplementedError apptools-4.5.0/apptools/undo/command_stack.py0000644000076500000240000002312413547637361022646 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, HasTraits, Instance, Int, List, Property, \ Unicode, provides # Local imports. from .abstract_command import AbstractCommand from .i_command import ICommand from .i_command_stack import ICommandStack from .i_undo_manager import IUndoManager class _StackEntry(HasTraits): """ The _StackEntry class is a single entry on a command stack. """ #### '_StackEntry' interface ############################################## # Set if the entry corresponds to a clean point on the stack. clean = Bool(False) # The command instance. command = Instance(ICommand) # The sequence number of the entry. sequence_nr = Int class _MacroCommand(AbstractCommand): """ The _MacroCommand class is an internal command that handles macros. """ #### '_MacroCommand' interface ############################################ # The commands that make up this macro. macro_commands = List(Instance(ICommand)) ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): """ Invoke the command. """ # This is a dummy. return None def merge(self, other): """ Try and merge a command. """ if len(self.macro_commands) == 0: merged = False else: merged = self.macro_commands[-1].merge(other) return merged def redo(self): """ Redo the sub-commands. """ for cmd in self.macro_commands: cmd.redo() # Macros cannot return values. return None def undo(self): """ Undo the sub-commands. """ for cmd in self.macro_commands: cmd.undo() @provides(ICommandStack) class CommandStack(HasTraits): """ The CommandStack class is the default implementation of the ICommandStack interface. """ #### 'ICommandStack' interface ############################################ # This is the clean state of the stack. Its value changes as commands are # undone and redone. It can also be explicity set to mark the current # stack position as being clean (when the data is saved to disk for # example). clean = Property(Bool) # This is the name of the command that can be redone. It will be empty if # there is no command that can be redone. It is maintained by the undo # stack. redo_name = Property(Unicode) # This is the undo manager that manages this stack. undo_manager = Instance(IUndoManager) # This is the name of the command that can be undone. It will be empty if # there is no command that can be undone. It is maintained by the undo # stack. undo_name = Property(Unicode) #### Private interface #################################################### # The current index into the stack (ie. the last command that was done). _index = Int(-1) # The current macro stack. _macro_stack = List(Instance(_MacroCommand)) # The stack itself. _stack = List(Instance(_StackEntry)) ########################################################################### # 'ICommandStack' interface. ########################################################################### def begin_macro(self, name): """ This begins a macro by creating an empty command with the given 'name'. All subsequent calls to 'push()' create commands that will be children of the empty command until the next call to 'end_macro()'. Macros may be nested. The stack is disabled (ie. nothing can be undone or redone) while a macro is being created (ie. while there is an outstanding 'end_macro()' call). """ command = _MacroCommand(name=name) self.push(command) self._macro_stack.append(command) def clear(self): """ This clears the stack, without undoing or redoing any commands, and leaves the stack in a clean state. It is typically used when all changes to the data have been abandoned. """ self._index = -1 self._stack = [] self._macro_stack = [] self.undo_manager.stack_updated = self def end_macro(self): """ This ends a macro. """ try: self._macro_stack.pop() except IndexError: pass def push(self, command): """ This executes a command and saves it on the command stack so that it can be subsequently undone and redone. 'command' is an instance that implements the ICommand interface. Its 'do()' method is called to execute the command. If any value is returned by 'do()' then it is returned by 'push()'. """ # See if the command can be merged with the previous one. if len(self._macro_stack) == 0: if self._index >= 0: merged = self._stack[self._index].command.merge(command) else: merged = False else: merged = self._macro_stack[-1].merge(command) # Increment the global sequence number. if not merged: self.undo_manager.sequence_nr += 1 # Execute the command. result = command.do() # Do nothing more if the command was merged. if merged: return result # Only update the command stack if there is no current macro. if len(self._macro_stack) == 0: # Remove everything on the stack after the last command that was # done. self._index += 1 del self._stack[self._index:] # Create a new stack entry and add it to the stack. entry = _StackEntry(command=command, sequence_nr=self.undo_manager.sequence_nr) self._stack.append(entry) self.undo_manager.stack_updated = self else: # Add the command to the parent macro command. self._macro_stack[-1].macro_commands.append(command) return result def redo(self, sequence_nr=0): """ If 'sequence_nr' is 0 then the last command that was undone is redone and any result returned. Otherwise commands are redone up to and including the given 'sequence_nr' and any result of the last of these is returned. """ # Make sure a redo is valid in the current context. if self.redo_name == "": return None if sequence_nr == 0: result = self._redo_one() else: result = None while self._index + 1 < len(self._stack): if self._stack[self._index + 1].sequence_nr > sequence_nr: break result = self._redo_one() self.undo_manager.stack_updated = self return result def undo(self, sequence_nr=0): """ If 'sequence_nr' is 0 then the last command is undone. Otherwise commands are undone up to and including the given 'sequence_nr'. """ # Make sure an undo is valid in the current context. if self.undo_name == "": return if sequence_nr == 0: self._undo_one() else: while self._index >= 0: if self._stack[self._index].sequence_nr <= sequence_nr: break self._undo_one() self.undo_manager.stack_updated = self ########################################################################### # Private interface. ########################################################################### def _redo_one(self): """ Redo the command at the current index and return the result. """ self._index += 1 entry = self._stack[self._index] return entry.command.redo() def _undo_one(self): """ Undo the command at the current index. """ entry = self._stack[self._index] self._index -= 1 entry.command.undo() def _get_clean(self): """ Get the clean state of the stack. """ if self._index >= 0: clean = self._stack[self._index].clean else: clean = True return clean def _set_clean(self, clean): """ Set the clean state of the stack. """ if self._index >= 0: self._stack[self._index].clean = clean def _get_redo_name(self): """ Get the name of the redo command, if any. """ redo_name = "" if len(self._macro_stack) == 0 and self._index + 1 < len(self._stack): redo_name = self._stack[self._index + 1].command.name.replace('&', '') return redo_name def _get_undo_name(self): """ Get the name of the undo command, if any. """ undo_name = "" if len(self._macro_stack) == 0 and self._index >= 0: command = self._stack[self._index].command undo_name = command.name.replace('&', '') return undo_name apptools-4.5.0/apptools/undo/tests/0000755000076500000240000000000013547652535020631 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/undo/tests/testing_commands.py0000644000076500000240000000171713422276145024536 0ustar mdickinsonstaff00000000000000# ----------------------------------------------------------------------------- # # Copyright (c) 2015, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # ----------------------------------------------------------------------------- from traits.api import Int from apptools.undo.api import AbstractCommand class SimpleCommand(AbstractCommand): """ Simplest command possible operating on an integer. """ name = "Increment by 1" data = Int def do(self): self.redo() def redo(self): self.data += 1 def undo(self): self.data -= 1 class UnnamedCommand(SimpleCommand): name = "" apptools-4.5.0/apptools/undo/tests/test_command_stack.py0000644000076500000240000001271613547637361025054 0ustar mdickinsonstaff00000000000000# ----------------------------------------------------------------------------- # # Copyright (c) 2015, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # ----------------------------------------------------------------------------- from contextlib import contextmanager from nose.tools import assert_equal from traits.testing.unittest_tools import unittest from apptools.undo.api import CommandStack, UndoManager from apptools.undo.tests.testing_commands import SimpleCommand, UnnamedCommand class TestCommandStack(unittest.TestCase): def setUp(self): self.stack = CommandStack() undo_manager = UndoManager() self.stack.undo_manager = undo_manager self.command = SimpleCommand() # Command pushing tests --------------------------------------------------- def test_empty_command_stack(self): with assert_n_commands_pushed(self.stack, 0): pass def test_1_command_pushed(self): with assert_n_commands_pushed(self.stack, 1): self.stack.push(self.command) def test_n_command_pushed(self): n = 4 with assert_n_commands_pushed(self.stack, n): for i in range(n): self.stack.push(self.command) # Undo/Redo tests --------------------------------------------------------- def test_undo_1_command(self): with assert_n_commands_pushed_and_undone(self.stack, 1): self.stack.push(self.command) self.assertEqual(self.stack.undo_name, self.command.name) self.stack.undo() def test_undo_n_command(self): n = 4 with assert_n_commands_pushed_and_undone(self.stack, n): for i in range(n): self.stack.push(self.command) for i in range(n): self.stack.undo() def test_undo_unnamed_command(self): unnamed_command = UnnamedCommand() with assert_n_commands_pushed(self.stack, 1): self.stack.push(unnamed_command) # But the command cannot be undone because it has no name self.assertEqual(self.stack.undo_name, "") # This is a no-op self.stack.undo() def test_undo_redo_1_command(self): with assert_n_commands_pushed(self.stack, 1): self.stack.push(self.command) self.stack.undo() self.stack.redo() # Macro tests ------------------------------------------------------------- def test_define_macro(self): with assert_n_commands_pushed(self.stack, 1): add_macro(self.stack, num_commands=2) def test_undo_macro(self): with assert_n_commands_pushed_and_undone(self.stack, 1): # The 2 pushes are viewed as 1 command add_macro(self.stack, num_commands=2) self.stack.undo() # Cleanliness tests ------------------------------------------------------- def test_empty_stack_is_clean(self): self.assertTrue(self.stack.clean) def test_non_empty_stack_is_dirty(self): self.stack.push(self.command) self.assertFalse(self.stack.clean) def test_make_clean(self): # This makes it dirty by default self.stack.push(self.command) # Make the current tip of the stack clean self.stack.clean = True self.assertTrue(self.stack.clean) def test_make_dirty(self): # Start from a clean state: self.stack.push(self.command) self.stack.clean = True self.stack.clean = False self.assertFalse(self.stack.clean) def test_save_push_undo_is_clean(self): self.stack.push(self.command) self.stack.clean = True self.stack.push(self.command) self.stack.undo() self.assertTrue(self.stack.clean) def test_save_push_save_undo_is_clean(self): self.stack.push(self.command) self.stack.clean = True self.stack.push(self.command) self.stack.clean = True self.stack.undo() self.assertTrue(self.stack.clean) def test_push_undo_save_redo_is_dirty(self): self.stack.push(self.command) self.stack.undo() self.stack.clean = True self.stack.redo() self.assertFalse(self.stack.clean) def add_macro(stack, num_commands=2): command = SimpleCommand() stack.begin_macro('Increment n times') try: for i in range(num_commands): stack.push(command) finally: stack.end_macro() # Assertion helpers ----------------------------------------------------------- @contextmanager def assert_n_commands_pushed(stack, n): current_length = len(stack._stack) yield # N commands have been pushed... assert_equal(len(stack._stack), current_length+n) # ... and the state is at the tip of the stack... assert_equal(stack._index, current_length+n-1) @contextmanager def assert_n_commands_pushed_and_undone(stack, n): current_length = len(stack._stack) yield # N commands have been pushed and then reverted. The stack still # contains the commands... assert_equal(len(stack._stack), n) # ... but we are back to the initial (clean) state assert_equal(stack._index, current_length-1) apptools-4.5.0/apptools/undo/tests/__init__.py0000644000076500000240000000000013422276145022717 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/undo/__init__.py0000644000076500000240000000104211640354733021564 0ustar mdickinsonstaff00000000000000# Copyright (c) 2005-2011, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. """ Supports undoing and scripting application commands. Part of the AppTools project of the Enthought Tool Suite. """ apptools-4.5.0/apptools/undo/i_undo_manager.py0000644000076500000240000000507713547637361023021 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, Event, Instance, Int, Interface, Unicode class IUndoManager(Interface): """ The undo manager interface. An undo manager is responsible for one or more command stacks. Typically an application would have a single undo manager. """ #### 'IUndoManager' interface ############################################# # This is the currently active command stack and may be None. Typically it # is set when some sort of editor becomes active. active_stack = Instance('apptools.undo.api.ICommandStack') # This reflects the clean state of the currently active command stack. It # is intended to support a "document modified" indicator in the GUI. It is # maintained by the undo manager. active_stack_clean = Bool # This is the name of the command that can be redone. It will be empty if # there is no command that can be redone. It is maintained by the undo # manager. redo_name = Unicode # This is the sequence number of the next command to be performed. It is # incremented immediately before a command is invoked (by its 'do()' # method). sequence_nr = Int # This event is fired when the index of a command stack changes. Note that # it may not be the active stack. stack_updated = Event(Instance('apptools.undo.api.ICommandStack')) # This is the name of the command that can be undone. It will be empty if # there is no command that can be undone. It is maintained by the undo # manager. undo_name = Unicode ########################################################################### # 'IUndoManager' interface. ########################################################################### def redo(self): """ Redo the last undone command of the active command stack. """ def undo(self): """ Undo the last command of the active command stack. """ apptools-4.5.0/apptools/undo/i_command.py0000644000076500000240000000540211640354733021757 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, Interface, Unicode class ICommand(Interface): """ The command interface. The state of the data can be changed by passing an instance that implements this interface to the 'push()' method of a command stack along with any arguments. """ #### 'ICommand' interface ################################################# # This is the data on which the command operates. data = Any # This is the name of the command as it will appear in any GUI element. It # may include '&' which will be automatically removed whenever it is # inappropriate. name = Unicode ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): """ This is called by the command stack to do the command and to return any value. The command must save any state necessary for the 'redo()' and 'undo()' methods to work. The class's __init__() must also ensure that deep copies of any arguments are made if appropriate. It is guaranteed that this will only ever be called once and that it will be called before any call to 'redo()' or 'undo()'. """ def merge(self, other): """ This is called by the command stack to try and merge another command with this one. True is returned if the commands were merged. 'other' is the command that is about to be executed. If the commands are merged then 'other' will discarded and not placed on the command stack. A subsequent undo or redo of this modified command must have the same effect as the two original commands. """ def redo(self): """ This is called by the command stack to redo the command. Any returned value will replace the value that the command stack references from the original call to 'do()' or previous call to 'redo()'. """ def undo(self): """ This is called by the command stack to undo the command. """ apptools-4.5.0/apptools/undo/api.py0000644000076500000240000000160713547637361020616 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ from .abstract_command import AbstractCommand from .command_stack import CommandStack from .i_command import ICommand from .i_command_stack import ICommandStack from .i_undo_manager import IUndoManager from .undo_manager import UndoManager apptools-4.5.0/apptools/undo/action/0000755000076500000240000000000013547652535020744 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/undo/action/command_action.py0000644000076500000240000000373413547637361024300 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Any, Callable, Instance from ..i_command_stack import ICommandStack class CommandAction(Action): """ The CommandAction class is an Action class that wraps undo/redo commands. It is only useful for commands that do not take any arguments or return any result. """ #### 'CommandAction' interface ############################################ # The command to create when the action is performed. command = Callable # The command stack onto which the command will be pushed when the action # is performed. command_stack = Instance(ICommandStack) # This is the data on which the command operates. data = Any ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ This is reimplemented to push a new command instance onto the command stack. """ self.command_stack.push(self.command(data=self.data)) def _name_default(self): """ This gets the action name from the command. """ if self.command: name = self.command().name else: name = "" return name apptools-4.5.0/apptools/undo/action/undo_action.py0000644000076500000240000000321013547637361023614 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Local imports. from .abstract_command_stack_action import AbstractCommandStackAction class UndoAction(AbstractCommandStackAction): """ An action that undos the last command of the active command stack. """ ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Perform the action. """ self.undo_manager.undo() ########################################################################### # 'AbstractUndoAction' interface. ########################################################################### def _update_action(self): """ Update the state of the action. """ name = self.undo_manager.undo_name if name: name = "&Undo " + name self.enabled = True else: name = "&Undo" self.enabled = False self.name = name apptools-4.5.0/apptools/undo/action/__init__.py0000644000076500000240000000064711640354733023053 0ustar mdickinsonstaff00000000000000# Copyright (c) 2005-2011, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. apptools-4.5.0/apptools/undo/action/redo_action.py0000644000076500000240000000322713547637361023610 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Local imports. from .abstract_command_stack_action import AbstractCommandStackAction class RedoAction(AbstractCommandStackAction): """ An action that redos the last command undone of the active command stack. """ ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Perform the action. """ self.undo_manager.redo() ########################################################################### # 'AbstractUndoAction' interface. ########################################################################### def _update_action(self): """ Update the state of the action. """ name = self.undo_manager.redo_name if name: name = "&Redo " + name self.enabled = True else: name = "&Redo" self.enabled = False self.name = name apptools-4.5.0/apptools/undo/action/api.py0000644000076500000240000000141113547637361022064 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ from .command_action import CommandAction from .redo_action import RedoAction from .undo_action import UndoAction apptools-4.5.0/apptools/undo/action/abstract_command_stack_action.py0000644000076500000240000000546413547637361027352 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Instance # Local library imports from ..i_undo_manager import IUndoManager class AbstractCommandStackAction(Action): """ The abstract base class for all actions that operate on a command stack. """ #### 'AbstractCommandStackAction' interface ############################### # The undo manager. undo_manager = Instance(IUndoManager) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Initialise the instance. """ super(AbstractCommandStackAction, self).__init__(**traits) self.undo_manager.on_trait_event(self._on_stack_updated, 'stack_updated') # Update the action to initialise it. self._update_action() ########################################################################### # 'Action' interface. ########################################################################### def destroy(self): """ Called when the action is no longer required. By default this method does nothing, but this would be a great place to unhook trait listeners etc. """ self.undo_manager.on_trait_event(self._on_stack_updated, 'stack_updated', remove=True) ########################################################################### # Protected interface. ########################################################################### def _update_action(self): """ Update the state of the action. """ raise NotImplementedError ########################################################################### # Private interface. ########################################################################### def _on_stack_updated(self, stack): """ Handle changes to the state of a command stack. """ # Ignore unless it is the active stack. if stack is self.undo_manager.active_stack: self._update_action() apptools-4.5.0/apptools/undo/undo_manager.py0000644000076500000240000000760513547637361022510 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, Event, HasTraits, Instance, Int, Property, \ Unicode, provides # Local imports. from .i_undo_manager import IUndoManager @provides(IUndoManager) class UndoManager(HasTraits): """ The UndoManager class is the default implementation of the IUndoManager interface. """ #### 'IUndoManager' interface ############################################# # This is the currently active command stack and may be None. Typically it # is set when some sort of editor becomes active. active_stack = Instance('apptools.undo.api.ICommandStack') # This reflects the clean state of the currently active command stack. It # is intended to support a "document modified" indicator in the GUI. It is # maintained by the undo manager. active_stack_clean = Property(Bool) # This is the name of the command that can be redone. It will be empty if # there is no command that can be redone. It is maintained by the undo # manager. redo_name = Property(Unicode) # This is the sequence number of the next command to be performed. It is # incremented immediately before a command is invoked (by its 'do()' # method). sequence_nr = Int # This event is fired when the index of a command stack changes. The value # of the event is the stack that has changed. Note that it may not be the # active stack. stack_updated = Event # This is the name of the command that can be undone. It will be empty if # there is no command that can be undone. It is maintained by the undo # manager. undo_name = Property(Unicode) ########################################################################### # 'IUndoManager' interface. ########################################################################### def redo(self): """ Redo the last undone command of the active command stack. """ if self.active_stack is not None: self.active_stack.redo() def undo(self): """ Undo the last command of the active command stack. """ if self.active_stack is not None: self.active_stack.undo() ########################################################################### # Private interface. ########################################################################### def _active_stack_changed(self, new): """ Handle a different stack becoming active. """ # Pretend that the stack contents have changed. self.stack_updated = new def _get_active_stack_clean(self): """ Get the current clean state. """ if self.active_stack is None: active_stack_clean = True else: active_stack_clean = self.active_stack.clean return active_stack_clean def _get_redo_name(self): """ Get the current redo name. """ if self.active_stack is None: redo_name = "" else: redo_name = self.active_stack.redo_name return redo_name def _get_undo_name(self): """ Get the current undo name. """ if self.active_stack is None: undo_name = "" else: undo_name = self.active_stack.undo_name return undo_name apptools-4.5.0/apptools/undo/i_command_stack.py0000644000076500000240000000750213547637361023160 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, Instance, Interface, Unicode # Local imports. from .i_undo_manager import IUndoManager class ICommandStack(Interface): """ The command stack interface. A command stack is responsible for managing the changes to a data model and recording those changes so that they can be undone or redone. """ #### 'ICommandStack' interface ############################################ # This is the clean state of the stack. Its value changes as commands are # undone and redone. It can also be explicity set to mark the current # stack position as being clean (when the data is saved to disk for # example). clean = Bool # This is the name of the command that can be redone. It will be empty if # there is no command that can be redone. It is maintained by the undo # stack. redo_name = Unicode # This is the undo manager that manages this stack. undo_manager = Instance(IUndoManager) # This is the name of the command that can be undone. It will be empty if # there is no command that can be undone. It is maintained by the undo # stack. undo_name = Unicode ########################################################################### # 'ICommandStack' interface. ########################################################################### def begin_macro(self, name): """ This begins a macro by creating an empty command with the given 'name'. The commands passed to all subsequent calls to 'push()' will be contained in the macro until the next call to 'end_macro()'. Macros may be nested. The stack is disabled (ie. nothing can be undone or redone) while a macro is being created (ie. while there is an outstanding 'end_macro()' call). """ def clear(self): """ This clears the stack, without undoing or redoing any commands, and leaves the stack in a clean state. It is typically used when all changes to the data have been abandoned. """ def end_macro(self): """ This ends a macro. """ def push(self, command): """ This executes a command and saves it on the command stack so that it can be subsequently undone and redone. 'command' is an instance that implements the ICommand interface. Its 'do()' method is called to execute the command. If any value is returned by 'do()' then it is returned by 'push()'. The command stack will keep a reference to the result so that it can recognise it as an argument to a subsequent command (which allows a script to properly save a result needed later). """ def redo(self, sequence_nr=0): """ If 'sequence_nr' is 0 then the last command that was undone is redone and any result returned. Otherwise commands are redone up to and including the given 'sequence_nr' and any result of the last of these is returned. """ def undo(self, sequence_nr=0): """ If 'sequence_nr' is 0 then the last command is undone. Otherwise commands are undone up to and including the given 'sequence_nr'. """ apptools-4.5.0/apptools/type_manager/0000755000076500000240000000000013547652535021175 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/type_manager/abstract_type_system.py0000644000076500000240000000167611640354733026020 0ustar mdickinsonstaff00000000000000""" The abstract base class for type systems. """ # Enthought library imports. from traits.api import HasTraits class AbstractTypeSystem(HasTraits): """ The abstract base class for type systems. A type system is responsible for:- 1) Determining whether an object is of a particular type. 2) Determining the MRO of a type. See 'PythonTypeSystem' for an implementation with standard Python semantics. """ ########################################################################### # 'AbstractTypeSystem' interface. ########################################################################### def is_a(self, obj, type): """ Is an object an instance of the specified type? """ raise NotImplementedError def get_mro(self, type): """ Returns the MRO of a type. """ raise NotImplementedError #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/adapter_factory.py0000644000076500000240000000365013547637361024722 0ustar mdickinsonstaff00000000000000""" An adapter factory. """ # Enthought library imports. from traits.api import Any # Local imports. from .abstract_adapter_factory import AbstractAdapterFactory class AdapterFactory(AbstractAdapterFactory): """ An adapter factory. This implementation provides the common case where the factory adapts exactly one type of object to exactly one target type using exactly one adapter. This class requires the adapter class to have an 'adaptee' trait. The default 'Adapter' class provides exactly that. """ #### 'AdapterFactory' interface ########################################### # fixme: These trait definitions should be 'Class' but currently this only # allows old-style classes! # # The type of object that the factory can adapt. adaptee_class = Any # The adapter class (the class that adapts the adaptee to the target # class). adapter_class = Any # The target class (the class that the factory can adapt objects to). target_class = Any ########################################################################### # Protected 'AbstractAdapterFactory' interface. ########################################################################### def _can_adapt(self, adaptee, target_class, *args, **kw): """ Returns True if the factory can produce an appropriate adapter. """ can_adapt = target_class is self.target_class \ and self.type_system.is_a(adaptee, self.adaptee_class) return can_adapt def _adapt(self, adaptee, target_class, *args, **kw): """ Returns an adapter that adapts an object to the target class. This requires the adapter class to have an 'adaptee' trait. The default 'Adapter' class provides exactly that. """ return self.adapter_class(adaptee=adaptee, *args, **kw) #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/adaptable.py0000644000076500000240000000205313547637361023464 0ustar mdickinsonstaff00000000000000""" The base class for adaptable objects. """ # Enthought library imports. from traits.api import HasTraits, Instance # Local imports. from .adapter_manager import AdapterManager class Adaptable(HasTraits): """ The base class for adaptable objects. """ #### 'Adaptable' interface ################################################ # The adapter manager in effect for the object. adapter_manager = Instance(AdapterManager) ########################################################################### # 'Adaptable' interface. ########################################################################### def adapt(self, target_class, *args, **kw): """ Returns True if the factory can produce an appropriate adapter. """ if self.adapter_manager is not None: adapter = self.adapter_manager.adapt( self, target_class, *args, **kw ) else: adapter = None return adapter #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/abstract_factory.py0000644000076500000240000000412411640354733025071 0ustar mdickinsonstaff00000000000000""" Abstract base class for all object factories. """ # Standard library imports. import logging # Enthought library imports. from traits.api import HasTraits logger = logging.getLogger(__name__) class AbstractFactory(HasTraits): """ Abstract base class for all object factories. """ ########################################################################### # 'AbstractFactory' interface. ########################################################################### def create(self, target_class, *args, **kw): """ Creates an object of the specified target class. Returns None if the factory cannot produce an object of the target class. """ if self._can_create(target_class, *args, **kw): obj = self._create(target_class, *args, **kw) if obj is None: logger.warn(self._get_warning_message(target_class)) else: obj = None return obj ########################################################################### # Protected 'AbstractFactory' interface. ########################################################################### def _can_create(self, target_class, *args, **kw): """ Returns True if the factory can create objects of a class. """ return NotImplementedError def _create(self, target_class, *args, **kw): """ Creates an object of the specified target class. """ raise NotImplementedError ########################################################################### # Private interface. ########################################################################### def _get_warning_message(self, target_class): """ Returns a warning message. The warning message is used when a factory fails to create something that it said it could! """ message = "%s failed to create a %s" % ( self.__class__.__name__, target_class.__name__ ) return message #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/type_manager.py0000644000076500000240000001546313547637361024233 0ustar mdickinsonstaff00000000000000""" A type manager. """ # Enthought library imports. from traits.api import HasTraits, Instance, Property, Str # Local imports. from .abstract_type_system import AbstractTypeSystem from .adapter_manager import AdapterManager from .factory import Factory from .hook import add_pre, add_post, remove_pre, remove_post from .python_type_system import PythonTypeSystem class TypeManager(HasTraits): """ A type manager. The type manager allows for objects to be created/adapted to a particular type. """ #### 'TypeManager' interface ############################################## # The adapter manager looks after errr, all adapters. adapter_manager = Property(Instance(AdapterManager)) # The type manager's globally unique identifier (only required if you have # more than one type manager of course!). id = Str # The parent type manager. # # By default this is None, but you can use it to set up a hierarchy of # type managers. If a type manager fails to adapt or create an object of # some target class then it will give its parent a chance to do so. parent = Instance('TypeManager') # The type system used by the manager (it determines 'is_a' relationships # and type MROs etc). # # By default we use standard Python semantics. type_system = Instance(AbstractTypeSystem, PythonTypeSystem()) #### Private interface #################################################### # The adapter manager looks after errr, all adapters. _adapter_manager = Instance(AdapterManager) ########################################################################### # 'TypeManager' interface. ########################################################################### #### Properties ########################################################### def _get_adapter_manager(self): """ Returns the adapter manager. """ return self._adapter_manager #### Methods ############################################################## def object_as(self, obj, target_class, *args, **kw): """ Adapts or creates an object of the target class. Returns None if no appropriate adapter or factory is available. """ # If the object is already an instance of the target class then we # simply return it. if self.type_system.is_a(obj, target_class): result = obj # If the object is a factory that creates instances of the target class # then ask it to produce one. elif self._is_factory_for(obj, target_class, *args, **kw): result = obj.create(target_class, *args, **kw) # Otherwise, see if the object can be adapted to the target class. else: result = self._adapter_manager.adapt(obj, target_class, *args,**kw) # If this type manager couldn't do the job, then give its parent a go! if result is None and self.parent is not None: result = self.parent.object_as(obj, target_class, *args, **kw) return result # Adapters. def register_adapters(self, factory, adaptee_class): """ Registers an adapter factory. 'adaptee_class' can be either a class or the name of a class. """ self._adapter_manager.register_adapters(factory, adaptee_class) return def unregister_adapters(self, factory): """ Unregisters an adapter factory. """ self._adapter_manager.unregister_adapters(factory) return def register_instance_adapters(self, factory, obj): """ Registers an adapter factory for an individual instance. A factory can be in exactly one manager (as it uses the manager's type system). """ self._adapter_manager.register_instance_adapters(factory, obj) return def unregister_instance_adapters(self, factory, obj): """ Unregisters an adapter factory for an individual instance. A factory can be in exactly one manager (as it uses the manager's type system). """ self._adapter_manager.unregister_instance_adapters(factory, obj) return def register_type_adapters(self, factory, adaptee_class): """ Registers an adapter factory. 'adaptee_class' can be either a class or the name of a class. """ self._adapter_manager.register_type_adapters(factory, adaptee_class) return def unregister_type_adapters(self, factory): """ Unregisters an adapter factory. """ self._adapter_manager.unregister_type_adapters(factory) return # Categories. # # Currently, there is no technical reason why we have this convenience # method to add categories. However, it may well turn out to be useful to # track all categories added via the type manager. def add_category(self, klass, category_class): """ Adds a category to a class. """ klass.add_trait_category(category_class) return # Hooks. # # Currently, there is no technical reason why we have these convenience # methods to add and remove hooks. However, it may well turn out to be # useful to track all hooks added via the type manager. def add_pre(self, klass, method_name, callable): """ Adds a pre-hook to method 'method_name' on class 'klass. """ add_pre(klass, method_name, callable) return def add_post(self, klass, method_name, callable): """ Adds a post-hook to method 'method_name' on class 'klass. """ add_post(klass, method_name, callable) return def remove_pre(self, klass, method_name, callable): """ Removes a pre-hook to method 'method_name' on class 'klass. """ remove_pre(klass, method_name, callable) return def remove_post(self, klass, method_name, callable): """ Removes a post-hook to method 'method_name' on class 'klass. """ remove_post(klass, method_name, callable) return ########################################################################### # Private interface. ########################################################################### #### Initializers ######################################################### def __adapter_manager_default(self): """ Initializes the '_adapter_manager' trait. """ return AdapterManager(type_system=self.type_system) #### Methods ############################################################## def _is_factory_for(self, obj, target_class, *args, **kw): """ Returns True iff the object is a factory for the target class. """ is_factory_for = self.type_system.is_a(obj, Factory) \ and obj.can_create(target_class, *args, **kw) return is_factory_for #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/util.py0000644000076500000240000000323713422276145022520 0ustar mdickinsonstaff00000000000000""" Useful functions to do with types! fixme: I don't really like collections of functions, but I'm not sure where these should go? Class methods on 'TypeManager'? """ # Standard library imports. from inspect import getclasstree def sort_by_class_tree(objs): """ Sorts a list of objects by their class tree. Each item in the list should inherit from a common base class. The list is returned ordered from the most specific to the least specific. """ # Since all objects must inherit from a common base class the list # returned by 'getclasstree' will be of length two:- # # The first element is a tuple of the form:- # # (CommonBaseClass, (HasTraits,)) # # The second element is a possibly nested list containing all of the # classes derived from 'CommonBaseClass'. hierarchy = getclasstree([type(obj) for obj in objs]) # Do an in-order traversal of the tree and return just the classes. # # This returns them ordered from least specific to most specific. classes = get_classes(hierarchy) # Note the reverse comparison (i.e., compare y with x). This is # because we want to return the classes ordered from the MOST # specfic to the least specific. objs.sort(key=lambda x: classes.index(type(x)), reverse=True) return def get_classes(hierarchy): """ Walks a class hierarchy and returns all of the classes. """ classes = [] for item in hierarchy: if type(item) is tuple: classes.append(item[0]) else: classes.extend(get_classes(item)) return classes #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/adapter.py0000644000076500000240000000101011640354733023146 0ustar mdickinsonstaff00000000000000""" A default adapter class. """ # Enthought library imports. from traits.api import Any, HasTraits class Adapter(HasTraits): """ A default adapter class. This comes in handy when using the default 'AdapterFactory' as it expects the adpter class to have an 'adaptee' trait. """ #### 'Adapter' interface ################################################## # The object that we are adapting. adaptee = Any #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/tests/0000755000076500000240000000000013547652535022337 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/type_manager/tests/type_manager_test_case.py0000644000076500000240000002654511640354733027421 0ustar mdickinsonstaff00000000000000""" Tests the type manager. """ # Standard library imports. import unittest # Enthought library imports. from traits.api import HasTraits, Instance, Str from apptools.type_manager import AdapterFactory, Factory, TypeManager # Test classes. class Foo(HasTraits): name = Str def foogle(self): return 'Foo.foogle.%s' % self.name class SubOfFoo(Foo): def foogle(self): return 'Sub.foogle.%s' % self.name class EmptySubOfFoo(Foo): pass class Bar(HasTraits): name = Str def blargle(self): return 'Bar.blargle.%s' % self.name # Test adapters and factories. class FooToBarAdapter(HasTraits): adaptee = Instance(Foo) def blargle(self): return self.adaptee.foogle() class FooToBarAdapterFactory(AdapterFactory): #### 'AdapterFactory' interface ########################################### # The type of object that the factory can adapt. adaptee_class = Foo # The adapter class (the class that adapts the adaptee to the target # class). adapter_class = FooToBarAdapter # The target class (the class that the factory can adapt objects to). target_class = Bar class SubOfFooToBarAdapter(HasTraits): adaptee = Instance(SubOfFoo) def blargle(self): return self.adaptee.foogle() class SubOfFooToBarAdapterFactory(AdapterFactory): #### 'AdapterFactory' interface ########################################### # The type of object that the factory can adapt. adaptee_class = SubOfFoo # The adapter class (the class that adapts the adaptee to the target # class). adapter_class = SubOfFooToBarAdapter # The target class (the class that the factory can adapt objects to). target_class = Bar class BarFactory(Factory): def can_create(self, target_class, *args, **kw): return target_class is Bar def create(self, target_class, *args, **kw): return Bar(*args, **kw) class TypeManagerTestCase(unittest.TestCase): """ Tests the type manager. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ # An empty type manager. self.type_manager = TypeManager() return def tearDown(self): """ Called immediately after each test method has been called. """ return ########################################################################### # Tests. ########################################################################### def test_no_adapter_required(self): """ no adapter required """ # Create a Bar. b = Bar() # Try to adapt it to a Bar. bar = self.type_manager.object_as(b, Bar) # The type manager should simply return the same object. self.assert_(bar is b) return def test_no_adapter(self): """ no adapter available """ # Create a Foo. foo = Foo(name='fred') # Try to adapt it to a bar. bar = self.type_manager.object_as(foo, Bar) # There should be no way to adapt a Foo to a Bar. self.assertEqual(bar, None) return def test_instance_adapter(self): """ instance adapter """ # Create a Foo. foo = Foo(name='fred') # Register an adapter Foo->Bar on the INSTANCE (this one should take # precedence). self.type_manager.register_instance_adapters( FooToBarAdapterFactory(), foo ) # Register an adapter Foo->Bar on the TYPE (this will fail if it gets # picked up since it won't actually adapt 'Foo' objects!). self.type_manager.register_instance_adapters( SubOfFooToBarAdapterFactory(), Foo ) # Adapt it to a Bar. bar = self.type_manager.object_as(foo, Bar) self.assertNotEqual(bar, None) self.assertEqual(bar.blargle(), 'Foo.foogle.fred') return def test_unregister_instance_adapter(self): """ unregister instance adapter """ # Create a Foo. foo = Foo(name='fred') # The factory. factory = FooToBarAdapterFactory() # Register an adapter Foo->Bar on the INSTANCE (this one should take # precedence). self.type_manager.register_instance_adapters(factory, foo) # Adapt it to a Bar. bar = self.type_manager.object_as(foo, Bar) self.assertNotEqual(bar, None) self.assertEqual(bar.blargle(), 'Foo.foogle.fred') # Remove the adapter. self.type_manager.unregister_instance_adapters(factory, foo) # Now we shouldn't be able to adapt the object. # # Try to adapt it to a bar. bar = self.type_manager.object_as(foo, Bar) # There should be no way to adapt a Foo to a Bar. self.assertEqual(bar, None) return def test_adapter_on_class(self): """ an adapter registered on an object's actual class. """ # Register an adapter Foo->Bar. self.type_manager.register_type_adapters(FooToBarAdapterFactory(), Foo) # Create a Foo. foo = Foo(name='fred') # Adapt it to a Bar. bar = self.type_manager.object_as(foo, Bar) self.assertNotEqual(bar, None) self.assertEqual(bar.blargle(), 'Foo.foogle.fred') return def test_adapter_on_base_class(self): """ an adapter registered on one of an object's base classes. """ # Register an adapter Foo->Bar. self.type_manager.register_type_adapters(FooToBarAdapterFactory(), Foo) # Create an instance of a class derived from Foo. sub = SubOfFoo(name='fred') # Adapt it to a Bar. bar = self.type_manager.object_as(sub, Bar) self.assertNotEqual(bar, None) self.assertEqual(bar.blargle(), 'Sub.foogle.fred') return def test_ignore_adapter_on_class(self): """ ignore an adapter on an object's actual class. """ # Register an adapter SubOfFoo->Bar on the Foo class. self.type_manager.register_type_adapters( SubOfFooToBarAdapterFactory(), Foo ) # Create a Foo. foo = Foo(name='fred') # Adapt it to a Bar. bar = self.type_manager.object_as(foo, Bar) # There should be no way to adapt a Foo to a Bar. self.assertEqual(bar, None) return def test_ignore_adapter_on_derived_class(self): """ ignore an adapter registered on a derived class. """ # Register an adapter Foo->Bar on the SubOfFoo class. self.type_manager.register_type_adapters( FooToBarAdapterFactory(), SubOfFoo ) # Create a Foo. foo = Foo(name='fred') # Adapt it to a Bar. bar = self.type_manager.object_as(foo, Bar) # There should be no way to adapt a Foo to a Bar. self.assertEqual(bar, None) return def test_unregister_adapter(self): """ unregister an adapter. """ factory = FooToBarAdapterFactory() # Register an adapter Foo->Bar. self.type_manager.register_type_adapters(factory, Foo) # Create a Foo. foo = Foo(name='fred') # Adapt it to a Bar. bar = self.type_manager.object_as(foo, Bar) self.assertNotEqual(bar, None) self.assertEqual(bar.blargle(), 'Foo.foogle.fred') # Unregister the adapter. self.type_manager.unregister_type_adapters(factory) # Try to adapt it to a Bar. bar = self.type_manager.object_as(foo, Bar) # There should be no way to adapt a Foo to a Bar. self.assertEqual(bar, None) return def test_factory(self): """ simple factory """ # Create a Bar factory. factory = BarFactory() # Try to create a Bar using the factory. bar = self.type_manager.object_as(factory, Bar, name='joe') self.assertNotEqual(bar, None) self.assertEqual(bar.blargle(), 'Bar.blargle.joe') return def test_pre_hook(self): """ pre hook """ l = [] def hook(*args, **kw): l.append('Hello') # Create a Foo. foo = Foo(name='fred') # Hook a method. self.type_manager.add_pre(Foo, 'foogle', hook) # Call the method that we have hooked. foo.foogle() # Make sure that the hook was called. self.assertEqual(len(l), 1) # Remove the hook. self.type_manager.remove_pre(Foo, 'foogle', hook) # Call the method that we have hooked. foo.foogle() # Make sure that the hook was NOT called. self.assertEqual(len(l), 1) return def test_post_hook(self): """ post hook """ def hook(result, *args, **kw): return 'Hello' # Create a Foo. foo = Foo(name='fred') # Hook a method. self.type_manager.add_post(Foo, 'foogle', hook) # Call the method that we have hooked. self.assertEqual(foo.foogle(), 'Hello') # Remove the hook. self.type_manager.remove_post(Foo, 'foogle', hook) # Call the method that we have hooked. foo.foogle() # Make sure that the hook was NOT called. self.assertEqual(foo.foogle(), 'Foo.foogle.fred') return def test_pre_hook_on_inherited_method(self): """ test pre hook on an inherited method """ l = [] def hook(*args, **kw): l.append('Hello') # Create an instance of a subclass of Foo that does NOT override # 'foogle'. esof = EmptySubOfFoo(name='fred') # Prove that it does not override 'foogle'! method = EmptySubOfFoo.__dict__.get('foogle') self.assertEqual(method, None) # Hook a method. self.type_manager.add_pre(EmptySubOfFoo, 'foogle', hook) # Make sure that the method was added to the class dictionary. method = EmptySubOfFoo.__dict__.get('foogle') self.assertNotEqual(method, None) # Call the method that we have hooked. esof.foogle() # Make sure that the hook was called. self.assertEqual(len(l), 1) # Remove the hook. self.type_manager.remove_pre(EmptySubOfFoo, 'foogle', hook) # Call the method that we have hooked. esof.foogle() # Make sure that the hook was NOT called. self.assertEqual(len(l), 1) # Make sure that we didn't put the original method back onto # 'EmptySubOfFoo'(since it didn't override it in the first place). method = EmptySubOfFoo.__dict__.get('foogle') self.assertEqual(method, None) return def test_type_manager_hierarchy(self): """ type manager hierarchy """ # Register an adapter Foo->Bar. self.type_manager.register_type_adapters(FooToBarAdapterFactory(), Foo) # Create an empy type manager with the main type manager as its # parent. type_manager = TypeManager(parent=self.type_manager) # Create a Foo. foo = Foo(name='fred') # Adapt it to a Bar. bar = type_manager.object_as(foo, Bar) self.assertNotEqual(bar, None) self.assertEqual(bar.blargle(), 'Foo.foogle.fred') return #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/hook.py0000644000076500000240000001014413547637361022507 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Implementation of hooked methods. """ def add_pre(klass, method_name, callable): """ Adds a pre-hook to method 'method_name' on class 'klass'. """ _add_hook(klass, method_name, callable, pre=True) return def add_post(klass, method_name, callable): """ Adds a post-hook to method 'method_name' on class 'klass'. """ _add_hook(klass, method_name, callable, pre=False) return def remove_pre(klass, method_name, callable): """ Removes a pre-hook from method 'method_name' on class 'klass'. """ _remove_hook(klass, method_name, callable, pre=True) return def remove_post(klass, method_name, callable): """ Removes a post-hook from method 'method_name' on class 'klass'. """ _remove_hook(klass, method_name, callable, pre=False) return ############################################################################### # Private functions. ############################################################################### def _add_hook(klass, method_name, callable, pre): """ Adds a pre/post hook to method 'method_name' on class 'klass'. """ # Get the method on the klass. method = getattr(klass, method_name) # Have we already hooked it? if hasattr(method, '__pre__'): hooked_method = method # Obviously not! else: def hooked_method(self, *args, **kw): for pre in hooked_method.__pre__: pre(self, *args, **kw) result = method(self, *args, **kw) for post in hooked_method.__post__: result = post(self, result, *args, **kw) return result # Python < 2.4 does not allow this. try: hooked_method.__name__ = method_name except: pass hooked_method.__pre__ = [] hooked_method.__post__ = [] # Is the original method actually defined on the class, or is it # inherited? hooked_method.__inherited__ = method_name not in klass.__dict__ # Save the original method... # # fixme: Twisted uses 'method.im_func' instead of 'method' here, but # both seem to work just as well! setattr(klass, '__hooked__' + method_name, method) # ... and put in the hooked one! setattr(klass, method_name, hooked_method) if pre: hooked_method.__pre__.append(callable) else: hooked_method.__post__.append(callable) return def _remove_hook(klass, method_name, callable, pre): """ Removes a pre/post hook from method 'method_name' on class 'klass'. """ # Get the method on the klass. method = klass.__dict__[method_name] # Is it actually hooked? if hasattr(method, '__pre__'): # Remove the hook. if pre: method.__pre__.remove(callable) else: method.__post__.remove(callable) # If there are no more hooks left then cleanup. if len(method.__pre__) == 0 and len(method.__post__) == 0: # If the method is inherited then just delete the hooked version. if method.__inherited__: delattr(klass, method_name) # Otherwise, reinstate the original method. else: original = getattr(klass, '__hooked__' + method_name) setattr(klass, method_name, original) # Remove the saved original method. delattr(klass, '__hooked__' + method_name) return #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/__init__.py0000644000076500000240000000067113547637361023312 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. #------------------------------------------------------------------------------ """ Manages type extensions, including factories to generate adapters, and hooks for methods and functions. Part of the AppTools project of the Enthought Tool Suite. """ from apptools.type_manager.api import * apptools-4.5.0/apptools/type_manager/factory.py0000644000076500000240000000230413547637361023215 0ustar mdickinsonstaff00000000000000""" A generic object factory. """ # Enthought library imports. from traits.api import Any # Local imports. from .abstract_factory import AbstractFactory class Factory(AbstractFactory): """ A generic object factory. This implementation of the abstract factory interface provides for the common scenario where the factory produces objects of exactly one type. """ #### 'Factory' interface ################################################## # The type of object that we create. # # fixme: This trait definition should be 'Class' but currently this only # allows old-style classes! target_class = Any ########################################################################### # 'AbstractFactory' interface. ########################################################################### def can_create(self, target_class, *args, **kw): """ Returns True if the factory can create objects of a class. """ return target_class is self.target_class def create(self, *args, **kw): """ Creates an object! """ return self.target_class(*args, **kw) #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/python_type_system.py0000644000076500000240000000216613547637361025542 0ustar mdickinsonstaff00000000000000""" A type system with standard(ish) Python semantics. """ # Standard library imports. import inspect # Local imports. from .abstract_type_system import AbstractTypeSystem class PythonObject: """ The root type in the type system. fixme: Python is currently a bit broken as it has dual type hierarchies, one for old-style and one for new-style classes. This class is used to create a fake root to unify them. """ __class__ = type class PythonTypeSystem(AbstractTypeSystem): """ A type system with standard(ish) Python semantics. """ ########################################################################### # 'AbstractTypeSystem' interface. ########################################################################### def is_a(self, obj, type): """ Is an object and instance of the specified type? """ return isinstance(obj, type) or type is PythonObject def get_mro(self, type): """ Returns the MRO of a type. """ return list(inspect.getmro(type)) + [PythonObject] #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/api.py0000644000076500000240000000076513547637361022330 0ustar mdickinsonstaff00000000000000from .abstract_adapter_factory import AbstractAdapterFactory from .abstract_factory import AbstractFactory from .abstract_type_system import AbstractTypeSystem from .adaptable import Adaptable from .adapter import Adapter from .adapter_factory import AdapterFactory from .adapter_manager import AdapterManager from .factory import Factory from .hook import add_pre, add_post, remove_pre, remove_post from .python_type_system import PythonObject, PythonTypeSystem from .type_manager import TypeManager apptools-4.5.0/apptools/type_manager/adapter_manager.py0000644000076500000240000001770613547637361024674 0ustar mdickinsonstaff00000000000000""" A manager for adapter factories. """ # Standard library imports import warnings # Third-party imports import six # Enthought library imports. from traits.api import Dict, HasTraits, Instance, Property # Local imports. from .abstract_type_system import AbstractTypeSystem from .python_type_system import PythonTypeSystem class AdapterManager(HasTraits): """ A manager for adapter factories. """ #### 'AdapterManager' interface ########################################### # All registered type-scope factories by the type of object that they # adapt. # # The dictionary is keyed by the *name* of the class rather than the class # itself to allow for adapter factory proxies to register themselves # without having to load and create the factories themselves (i.e., to # allow us to lazily load adapter factories contributed by plugins). This # is a slight compromise as it is obviously geared towards use in Envisage, # but it doesn't affect the API other than allowing a class OR a string to # be passed to 'register_adapters'. # # { String adaptee_class_name : List(AdapterFactory) factories } type_factories = Property(Dict) # All registered instance-scope factories by the object that they adapt. # # { id(obj) : List(AdapterFactory) factories } instance_factories = Property(Dict) # The type system used by the manager (it determines 'is_a' relationships # and type MROs etc). By default we use standard Python semantics. type_system = Instance(AbstractTypeSystem, PythonTypeSystem()) #### Private interface #################################################### # All registered type-scope factories by the type of object that they # adapt. _type_factories = Dict # All registered instance-scope factories by the object that they adapt. _instance_factories = Dict ########################################################################### # 'AdapterManager' interface. ########################################################################### #### Properties ########################################################### def _get_type_factories(self): """ Returns all registered type-scope factories. """ return self._type_factories.copy() def _get_instance_factories(self): """ Returns all registered instance-scope factories. """ return self._instance_factories.copy() #### Methods ############################################################## def adapt(self, adaptee, target_class, *args, **kw): """ Returns an adapter that adapts an object to the target class. 'adaptee' is the object that we want to adapt. 'target_class' is the class that the adaptee should be adapted to. Returns None if no such adapter exists. """ # If the object is already an instance of the target class then we # simply return it. if self.type_system.is_a(adaptee, target_class): adapter = adaptee # Otherwise, look at each class in the adaptee's MRO to see if there # is an adapter factory registered that can adapt the object to the # target class. else: # Look for instance-scope adapters first. adapter = self._adapt_instance(adaptee, target_class, *args, **kw) # If no instance-scope adapter was found then try type-scope # adapters. if adapter is None: for adaptee_class in self.type_system.get_mro(type(adaptee)): adapter = self._adapt_type( adaptee, adaptee_class, target_class, *args, **kw ) if adapter is not None: break return adapter def register_instance_adapters(self, factory, obj): """ Registers an instance-scope adapter factory. A factory can be in exactly one manager (as it uses the manager's type system). """ factories = self._instance_factories.setdefault(id(obj), []) factories.append(factory) # A factory can be in exactly one manager. factory.adapter_manager = self return def unregister_instance_adapters(self, factory, obj): """ Unregisters an instance scope adapter factory. A factory can be in exactly one manager (as it uses the manager's type system). """ factories = self._instance_factories.setdefault(id(obj), []) if factory in factories: factories.remove(factory) # A factory can be in exactly one manager. factory.adapter_manager = None return def register_type_adapters(self, factory, adaptee_class): """ Registers a type-scope adapter factory. 'adaptee_class' can be either a class object or the name of a class. A factory can be in exactly one manager (as it uses the manager's type system). """ if isinstance(adaptee_class, six.string_types): adaptee_class_name = adaptee_class else: adaptee_class_name = self._get_class_name(adaptee_class) factories = self._type_factories.setdefault(adaptee_class_name, []) factories.append(factory) # A factory can be in exactly one manager. factory.adapter_manager = self return def unregister_type_adapters(self, factory): """ Unregisters a type-scope adapter factory. """ for adaptee_class_name, factories in self._type_factories.items(): if factory in factories: factories.remove(factory) # The factory is no longer deemed to be part of this manager. factory.adapter_manager = None return #### DEPRECATED ########################################################### def register_adapters(self, factory, adaptee_class): """ Registers an adapter factory. 'adaptee_class' can be either a class object or the name of a class. A factory can be in exactly one manager (as it uses the manager's type system). """ warnings.warn( 'Use "register_type_adapters" instead.', DeprecationWarning ) self.register_type_adapters(factory, adaptee_class) return def unregister_adapters(self, factory): """ Unregisters an adapter factory. """ warnings.warn( 'use "unregister_type_adapters" instead.', DeprecationWarning ) self.unregister_type_adapters(factory) return ########################################################################### # Private interface. ########################################################################### def _adapt_instance(self, adaptee, target_class, *args, **kw): """ Returns an adapter that adaptes an object to the target class. Returns None if no such adapter exists. """ for factory in self._instance_factories.get(id(adaptee), []): adapter = factory.adapt(adaptee, target_class, *args, **kw) if adapter is not None: break else: adapter = None return adapter def _adapt_type(self, adaptee, adaptee_class, target_class, *args, **kw): """ Returns an adapter that adapts an object to the target class. Returns None if no such adapter exists. """ adaptee_class_name = self._get_class_name(adaptee_class) for factory in self._type_factories.get(adaptee_class_name, []): adapter = factory.adapt(adaptee, target_class, *args, **kw) if adapter is not None: break else: adapter = None return adapter def _get_class_name(self, klass): """ Returns the full class name for a class. """ return "%s.%s" % (klass.__module__, klass.__name__) #### EOF ###################################################################### apptools-4.5.0/apptools/type_manager/abstract_adapter_factory.py0000644000076500000240000000550113547637361026602 0ustar mdickinsonstaff00000000000000""" Abstract base class for all adapter factories. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Delegate, HasTraits, Instance # Local imports. from .adapter_manager import AdapterManager logger = logging.getLogger(__name__) class AbstractAdapterFactory(HasTraits): """ Abstract base class for all adapter factories. Adapter factories define behavioural extensions for classes. """ #### 'AbstractAdapterFactory' interface ################################### # The adapter manager that the factory is registered with (this will be # None iff the factory is not registered with a manager). adapter_manager = Instance(AdapterManager) # The type system used by the factory (it determines 'is_a' relationships # and type MROs etc). By default we use standard Python semantics. type_system = Delegate('adapter_manager') ########################################################################### # 'AbstractAdapterFactory' interface. ########################################################################### def adapt(self, adaptee, target_class, *args, **kw): """ Returns an adapter that adapts an object to the target class. Returns None if the factory cannot produce such an adapter. """ if self._can_adapt(adaptee, target_class, *args, **kw): adapter = self._adapt(adaptee, target_class, *args, **kw) if adapter is None: logger.warn(self._get_warning_message(adaptee, target_class)) else: adapter = None return adapter ########################################################################### # Protected 'AbstractAdapterFactory' interface. ########################################################################### def _can_adapt(self, adaptee, target_class, *args, **kw): """ Returns True if the factory can produce an appropriate adapter. """ raise NotImplementedError def _adapt(self, adaptee, target_class, *args, **kw): """ Returns an adapter that adapts an object to the target class. """ raise NotImplementedError ########################################################################### # Private interface. ########################################################################### def _get_warning_message(self, adaptee, target_class): """ Returns a warning message. The warning message is used when a factory fails to adapt something that it said it could! """ message = '%s failed to adapt %s to %s' % ( self.__class__.__name__, str(adaptee), target_class.__name__ ) return message #### EOF ###################################################################### apptools-4.5.0/apptools/permissions/0000755000076500000240000000000013547652535021075 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/permissions/i_user.py0000644000076500000240000000340011640354733022721 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, Dict, Interface, Unicode class IUser(Interface): """The interface implemented by a user (or principal).""" # The user's name, ie. how they identified themselves to the permissions # policy. It is only valid if the authenticated trait is True. name = Unicode # This is set if the user has been authenticated, ie. the name trait is # valid. authenticated = Bool(False) # An optional description of the user (eg. their full name). The exact # meaning is defined by the user manager. description = Unicode # This allows application defined, user specific data to be persisted in # the user database. An application (or plugin) should save the data as a # single value in the dictionary keyed on the application's (or plugin's) # unique id. The data will be saved in the user database whenever it is # changed, and will be read from the user database whenever the user is # authenticated. blob = Dict def __str__(self): """Return a user friendly representation of the user.""" apptools-4.5.0/apptools/permissions/permission.py0000644000076500000240000000723113547637361023642 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, HasTraits, Property, Str, Unicode # Locals imports. from .package_globals import get_permissions_manager class Permission(HasTraits): """A permission is the link between an application action and the current user - if the user has a permission attached to the action then the user is allowed to perform that action.""" #### 'Permission' interface ############################################### # The id of the permission. By convention a dotted format is used for the # id with the id of the application being the first part. id = Str # A user friendly description of the permission. description = Unicode # Set if the current user has this permission. This is typically used with # the enabled_when and visible_when traits of a TraitsUI Item object when # the permission instance has been placed in the TraitsUI context. granted = Property # Set if the permission should be granted automatically when bootstrapping. # This is normally only ever set for permissions related to user management # and permissions. The user manager determines exactly what is meant by # "bootstrapping" but it is usually when it determines that no user or # permissions information has been defined. bootstrap = Bool(False) # Set if the permission has been defined by application code rather than as # a result of loading the policy database. application_defined = Bool(True) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """Initialise the object.""" super(Permission, self).__init__(**traits) # Register the permission. get_permissions_manager().policy_manager.register_permission(self) def __str__(self): """Return a user friendly representation.""" s = self.description if not s: s = self.id return s ########################################################################### # Trait handlers. ########################################################################### def _get_granted(self): """Check the user has this permission.""" return get_permissions_manager().check_permissions(self) class ManagePolicyPermission(Permission): """The standard permission for managing permissions policies.""" #### 'Permission' interface ############################################### id = Str('ets.permissions.manage_policy') description = Unicode(u"Manage permissions policy") bootstrap = Bool(True) class ManageUsersPermission(Permission): """The standard permission for managing permissions users.""" #### 'Permission' interface ############################################### id = Str('ets.permissions.manage_users') description = Unicode(u"Manage users") bootstrap = Bool(True) apptools-4.5.0/apptools/permissions/i_policy_manager.py0000644000076500000240000000341411640354733024741 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Instance, Interface, List class IPolicyManager(Interface): """The interface implemented by a policy manager. A policy manager defines how permissions are assigned to users and stored. A default policy manager is provided, but it may be replaced using the permissions manager.""" # The list of PyFace policy management actions implemented by this policy. management_actions = List(Instance(Action)) # The list of permissions assigned to the current user. user_permissions = List(Instance('apptools.permissions.api.Permission')) def bootstrapping(self): """Return True if the policy manager is bootstrapping. Typically this is when no permissions have been assigned.""" def load_user(self, user): """Load the policy for the given user. user is an object that implements IUser. If it is None then unload the policy for the current user.""" def register_permission(self, permission): """Register the given permission defined by the application.""" apptools-4.5.0/apptools/permissions/secure_proxy.py0000644000076500000240000001513213547637361024200 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.etsconfig.api import ETSConfig from traitsui.api import Handler # Local imports. from .adapter_base import AdapterBase from .package_globals import get_permissions_manager # Register the bundled adapters. from .adapters import pyface_action if ETSConfig.toolkit == 'wx': from .adapters import wx_window elif ETSConfig.toolkit == 'qt4': from .adapters import qt4_widget class SecureProxy(object): """The SecureProxy class is a wrapper for an object whose enabled and visible states can be managed. It attaches one or more permissions to the object and enables and shows the object only if those permissions allow it. In all other respects it behaves exactly like the object. It is based on Tomer Filiba's Object Proxy cookbook recipe.""" __slots__ = ('_ets', '__weakref__') def __init__(self, proxied, permissions, show=True): """Initialise the instance. proxied is the object whose enabled and visible states are managed according to the permissions of the current user. permissions is a list of permissions to attach to the object. show is set if the proxied object should be visible when it is disabled.""" adapter = object.__getattribute__(self, '_ets') # Correct the current values. if not show: adapter.update_visible(adapter.get_visible()) adapter.update_enabled(adapter.get_enabled()) # Proxying (special cases). def __getattribute__(self, name): return getattr(object.__getattribute__(self, '_ets').proxied, name) def __delattr__(self, name): delattr(object.__getattribute__(self, '_ets').proxied, name) def __setattr__(self, name, value): object.__getattribute__(self, '_ets').setattr(name, value) def __nonzero__(self): return bool(object.__getattribute__(self, '_ets').proxied) def __str__(self): return str(object.__getattribute__(self, '_ets').proxied) def __repr__(self): return repr(object.__getattribute__(self, '_ets').proxied) # Factories. _special_names = [ '__abs__', '__add__', '__and__', '__call__', '__cmp__', '__coerce__', '__contains__', '__delitem__', '__delslice__', '__div__', '__divmod__', '__eq__', '__float__', '__floordiv__', '__ge__', '__getitem__', '__getslice__', '__gt__', '__hash__', '__hex__', '__iadd__', '__iand__', '__idiv__', '__idivmod__', '__ifloordiv__', '__ilshift__', '__imod__', '__imul__', '__int__', '__invert__', '__ior__', '__ipow__', '__irshift__', '__isub__', '__iter__', '__itruediv__', '__ixor__', '__le__', '__len__', '__long__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rfloorfiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setitem__', '__setslice__', '__sub__', '__truediv__', '__xor__', 'next', ] @classmethod def _ets_class_proxy(cls, theclass): """Creates a proxy for the given class.""" def make_method(name): def method(self, *args, **kw): return getattr(object.__getattribute__(self, '_ets').proxied, name)(*args, **kw) return method namespace = {} for name in cls._special_names: if hasattr(theclass, name) and not hasattr(cls, name): namespace[name] = make_method(name) return type("%s(%s)" % (cls.__name__, theclass.__name__), (cls,), namespace) def __new__(cls, proxied, permissions, show=True): """Apply a set of permissions to an object. This may be done by creating a proxy or by modifying the object in situ depending on its type. """ # Create the adapter. adapter = AdapterBase.factory(proxied, permissions, show) # Try and adapt the object itself. adapted = adapter.adapt() if adapted is None: # Create a wrapper for the object. The cache is unique per # deriving class to ensure there are no clashes with any # sub-classes with the same name. try: cache = cls.__dict__['_ets_cache'] except KeyError: cls._ets_cache = cache = {} pclass = proxied.__class__ try: theclass = cache[pclass] except KeyError: cache[pclass] = theclass = cls._ets_class_proxy(pclass) adapted = object.__new__(theclass) # Save the adapter in the adapted object. object.__setattr__(adapted, '_ets', adapter) else: # Correct the current values. if not show: adapter.update_visible(adapter.get_visible()) adapter.update_enabled(adapter.get_enabled()) # Save the adapter in the adapted object. adapted._ets = adapter return adapted class SecureHandler(Handler): """The SecureHandler class is a sub-class of the TraitsUI Handler class that ensures that the enabled and visible state of the items of a TraitsUI view are updated when the user's authorisation state changes. """ def __init__(self, **traits): """Initialise the object.""" super(SecureHandler, self).__init__(**traits) get_permissions_manager().user_manager.on_trait_event(self._refresh, 'user_authenticated') def init_info(self, info): """Reimplemented to save the UIInfo object.""" self._info = info def _refresh(self): """Invoked whenever the current user's authorisation state changes.""" # FIXME: This is (currently) an internal method. self._info.ui._evaluate_when() apptools-4.5.0/apptools/permissions/permissions_manager.py0000644000076500000240000000761113547637361025521 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, HasTraits, Instance # Local imports. from .i_policy_manager import IPolicyManager from .i_user_manager import IUserManager class PermissionsManager(HasTraits): """A singleton class that provides access to the current policy and user managers.""" #### 'PermissionsManager' interface ####################################### # Set if bootstrap permissions should be automatically enabled in a # bootstrap situation (ie. when no policy or user data has been defined). # Bootstrap permissions are normally attached to actions used to define # policy and user data. Normally this is True, unless policy and user data # is to be managed by an external application. allow_bootstrap_permissions = Bool(True) # The current policy manager. policy_manager = Instance(IPolicyManager) # The current user manager. user_manager = Instance(IUserManager) #### Private interface #################################################### # Set if we are bootstrapping. _bootstrap = Bool ########################################################################### # 'PermissionsManager' interface. ########################################################################### def check_permissions(self, *permissions): """Check that the current user has one or more of the given permissions and return True if they have. permissions is a list of Permission instances.""" # Get the current user and their assigned permissions. user = self.user_manager.user user_perms = self.policy_manager.user_permissions for perm in permissions: # If this is a bootstrap permission then see if we are in a # bootstrap situation. if perm.bootstrap and self._bootstrap: return True if user.authenticated and perm in user_perms: return True return False ########################################################################### # Trait handlers. ########################################################################### def _policy_manager_default(self): """Provide a default policy manager.""" # Defer to an external manager if there is one. try: from apptools.permissions.external.policy_manager import PolicyManager except ImportError: from apptools.permissions.default.policy_manager import PolicyManager return PolicyManager() def _user_manager_default(self): """Provide a default user manager.""" # Defer to an external manager if there is one. try: from apptools.permissions.external.user_manager import UserManager except ImportError: from apptools.permissions.default.user_manager import UserManager return UserManager() def __bootstrap_default(self): """Determine whether or not we are bootstrapping. We only want to do it once as checking may involve one or more remote database queries.""" return (self.allow_bootstrap_permissions and (self.policy_manager.bootstrapping() or self.user_manager.bootstrapping())) apptools-4.5.0/apptools/permissions/__init__.py0000644000076500000240000000032311640354733023173 0ustar mdickinsonstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. """ Supports limiting access to parts of an application to authorised users. Part of the AppTools project of the Enthought Tool Suite. """ apptools-4.5.0/apptools/permissions/default/0000755000076500000240000000000013547652535022521 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/permissions/default/user_storage.py0000644000076500000240000001355313547637361025604 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import HasTraits, Instance, provides # Local imports. from .i_user_storage import IUserStorage, UserStorageError from .persistent import Persistent, PersistentError @provides(IUserStorage) class UserStorage(HasTraits): """This implements a user database that pickles its data in a local file. """ #### 'IUserStorage' interface ############################################# capabilities = ['user_password', 'user_add', 'user_modify', 'user_delete'] #### Private interface #################################################### # The persisted database. The database itself is a dictionary, keyed by # the user name, and with a value that is a tuple of the description, blob # and clear text password. _db = Instance(Persistent) ########################################################################### # 'IUserStorage' interface. ########################################################################### def add_user(self, name, description, password): """Add a new user.""" self._db.lock() try: users = self._db.read() if name in users: raise UserStorageError("The user \"%s\" already exists." % name) users[name] = (description, {}, password) self._db.write(users) finally: self._db.unlock() def authenticate_user(self, name, password): """Return the tuple of the user name, description, and blob if the user was successfully authenticated.""" users = self._readonly_copy() try: description, blob, pword = users[name] except KeyError: raise UserStorageError("The name or password is invalid.") if password != pword: raise UserStorageError("The name or password is invalid.") return name, description, blob def delete_user(self, name): """Delete a user.""" self._db.lock() try: users = self._db.read() try: del users[name] except KeyError: raise UserStorageError("The user \"%s\" does not exist." % name) self._db.write(users) finally: self._db.unlock() def is_empty(self): """See if the database is empty.""" return (len(self._readonly_copy()) == 0) def matching_users(self, name): """Return the full name and description of all the users that match the given name.""" # Return any user that starts with the name. users = [(full_name, description) for full_name, (description, _, _) in self._readonly_copy().items() if full_name.startswith(name)] return sorted(users) def modify_user(self, name, description, password): """Update the description and password for the given user.""" self._db.lock() try: users = self._db.read() try: _, blob, _ = users[name] except KeyError: raise UserStorageError("The user \"%s\" does not exist." % name) users[name] = (description, blob, password) self._db.write(users) finally: self._db.unlock() def update_blob(self, name, blob): """Update the blob for the given user.""" self._db.lock() try: users = self._db.read() try: description, _, password = users[name] except KeyError: raise UserStorageError("The user \"%s\" does not exist." % name) users[name] = (description, blob, password) self._db.write(users) finally: self._db.unlock() def unauthenticate_user(self, user): """Unauthenticate the given user.""" # There is nothing to do. return True def update_password(self, name, password): """Update the password for the given user.""" self._db.lock() try: users = self._db.read() try: description, blob, _ = users[name] except KeyError: raise UserStorageError("The user \"%s\" does not exist." % name) users[name] = (description, blob, password) self._db.write(users) finally: self._db.unlock() ########################################################################### # Trait handlers. ########################################################################### def __db_default(self): """Return the default persisted database.""" return Persistent(dict, 'ets_perms_userdb', "the user database") ########################################################################### # Private interface. ########################################################################### def _readonly_copy(self): """Return the user database (which should not be modified).""" try: self._db.lock() try: data = self._db.read() finally: self._db.unlock() except PersistentError as e: raise UserStorageError(str(e)) return data apptools-4.5.0/apptools/permissions/default/i_policy_storage.py0000644000076500000240000000561111640354733026420 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Interface class PolicyStorageError(Exception): """This is the exception raised by an IPolicyStorage object when an error occurs accessing the database. Its string representation is displayed as an error message to the user.""" class IPolicyStorage(Interface): """This defines the interface expected by a PolicyManager to handle the low level storage of the policy data.""" ########################################################################### # 'IPolicyStorage' interface. ########################################################################### def add_role(self, name, description, perm_ids): """Add a new role.""" def all_roles(self): """Return a list of all roles where each element is a tuple of the name and description.""" def delete_role(self, name): """Delete the role with the given name (which will not be empty).""" def get_assignment(self, user_name): """Return a tuple of the user name and list of role names of the assignment for the given user name. The tuple will contain an empty string and list if the user isn't known.""" def get_policy(self, user_name): """Return a tuple of the user name and list of permission names for the user with the given name. The tuple will contain an empty string and list if the user isn't known.""" def is_empty(self): """Return True if the user database is empty. It will only ever be called once.""" def matching_roles(self, name): """Return a list of tuples of the full name, description and list of permission names, sorted by the full name, of all roles that match the given name. How the name is interpreted (eg. as a regular expression) is determined by the storage.""" def modify_role(self, name, description, perm_ids): """Update the description and permissions for the role with the given name (which will not be empty).""" def set_assignment(self, user_name, role_names): """Save the assignment of the given role names to the given user. Note that there may or may not be an existing assignment for the user.""" apptools-4.5.0/apptools/permissions/default/policy_storage.py0000644000076500000240000001462313547637361026124 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import HasTraits, Instance, provides # Local imports. from .i_policy_storage import IPolicyStorage, PolicyStorageError from .persistent import Persistent, PersistentError @provides(IPolicyStorage) class PolicyStorage(HasTraits): """This implements a policy database that pickles its data in a local file. """ #### Private interface #################################################### # The persisted database. The database itself is a tuple of the role and # assignment dictionaries. The role dictionary is keyed by the role name, # and has a value that is a tuple of the description and the list of # permission names. The assigment dictionary is keyed by the user name and # has a value that is the list of role names. _db = Instance(Persistent) ########################################################################### # 'IPolicyStorage' interface. ########################################################################### def add_role(self, name, description, perm_ids): """Add a new role.""" self._db.lock() try: roles, assigns = self._db.read() if name in roles: raise PolicyStorageError("The role \"%s\" already exists." % name) roles[name] = (description, perm_ids) self._db.write((roles, assigns)) finally: self._db.unlock() def all_roles(self): """Return a list of all roles.""" roles, _ = self._readonly_copy() return [(name, description) for name, (description, _) in roles.items()] def delete_role(self, name): """Delete a role.""" self._db.lock() try: roles, assigns = self._db.read() if name not in roles: raise PolicyStorageError("The role \"%s\" does not exist." % name) del roles[name] # Remove the role from any users who have it. for user, role_names in assigns.items(): try: role_names.remove(name) except ValueError: continue assigns[user] = role_names self._db.write((roles, assigns)) finally: self._db.unlock() def get_assignment(self, user_name): """Return the details of the assignment for the given user name.""" _, assigns = self._readonly_copy() try: role_names = assigns[user_name] except KeyError: return '', [] return user_name, role_names def get_policy(self, user_name): """Return the details of the policy for the given user name.""" roles, assigns = self._readonly_copy() try: role_names = assigns[user_name] except KeyError: return '', [] perm_ids = [] for r in role_names: _, perms = roles[r] perm_ids.extend(perms) return user_name, perm_ids def is_empty(self): """See if the database is empty.""" roles, assigns = self._readonly_copy() # Both have to be non-empty for the whole thing to be non-empty. return (len(roles) == 0 or len(assigns) == 0) def matching_roles(self, name): """Return the full name, description and permissions of all the roles that match the given name.""" roles, _ = self._readonly_copy() # Return any role that starts with the name. roles = [(full_name, description, perm_ids) for full_name, (description, perm_ids) in roles.items() if full_name.startswith(name)] return sorted(roles) def modify_role(self, name, description, perm_ids): """Update the description and permissions for the given role.""" self._db.lock() try: roles, assigns = self._db.read() if name not in roles: raise PolicyStorageError("The role \"%s\" does not exist." % name) roles[name] = (description, perm_ids) self._db.write((roles, assigns)) finally: self._db.unlock() def set_assignment(self, user_name, role_names): """Save the roles assigned to a user.""" self._db.lock() try: roles, assigns = self._db.read() if len(role_names) == 0: # Delete the user, but don't worry if there is no current # assignment. try: del assigns[user_name] except KeyError: pass else: assigns[user_name] = role_names self._db.write((roles, assigns)) finally: self._db.unlock() ########################################################################### # Trait handlers. ########################################################################### def __db_default(self): """Return the default persisted database.""" return Persistent(self._db_factory, 'ets_perms_policydb', "the policy database") ########################################################################### # Private interface. ########################################################################### def _readonly_copy(self): """Return the policy database (which should not be modified).""" try: self._db.lock() try: data = self._db.read() finally: self._db.unlock() except PersistentError as e: raise PolicyStorageError(str(e)) return data @staticmethod def _db_factory(): """Return an empty policy database.""" return ({}, {}) apptools-4.5.0/apptools/permissions/default/select_role.py0000644000076500000240000000461211640354733025365 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import HasTraits, Instance, List, Unicode from traitsui.api import Item, TableEditor, View from traitsui.menu import OKCancelButtons from traitsui.table_column import ObjectColumn class _Role(HasTraits): """This represents the role model.""" #### '_RoleModel' interface ############################################### # The role name. name = Unicode # The role description. description = Unicode # The permissions ids. permissions = List class _RolesView(HasTraits): """This represents the view used to select a role.""" #### '_UsersView' interface ############################################### # The list of roles to select from. model = List(_Role) # The selected user. selection = Instance(_Role) # The editor used by the view. table_editor = TableEditor(columns=[ObjectColumn(name='name'), ObjectColumn(name='description')], selected='selection', sort_model=True, configurable=False) # The default view. traits_view = View(Item('model', show_label=False, editor=table_editor), title="Select a Role", style='readonly', kind='modal', buttons=OKCancelButtons) def select_role(roles): """Return a single role from the given list of roles.""" # Construct the model. model = [_Role(name=name, description=description, permissions=permissions) for name, description, permissions in roles] # Construct the view. view = _RolesView(model=model) if view.configure_traits() and view.selection is not None: role = view.selection.name, view.selection.description, view.selection.permissions else: role = '', '', [] return role apptools-4.5.0/apptools/permissions/default/user_manager.py0000644000076500000240000001400713547637361025545 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Bool, Event, HasTraits, provides, \ Instance, List, Unicode # Local imports. from apptools.permissions.i_user import IUser from apptools.permissions.i_user_manager import IUserManager from apptools.permissions.package_globals import get_permissions_manager from apptools.permissions.permission import ManageUsersPermission from .i_user_database import IUserDatabase @provides(IUserManager) class UserManager(HasTraits): """The default user manager implementation.""" #### 'IUserManager' interface ############################################# management_actions = List(Instance(Action)) user = Instance(IUser) user_actions = List(Instance(Action)) user_authenticated = Event(IUser) #### 'UserManager' interface ############################################## # The user database. user_db = Instance(IUserDatabase) ########################################################################### # 'IUserManager' interface. ########################################################################### def bootstrapping(self): """Return True if we are bootstrapping, ie. no users have been defined. """ return self.user_db.bootstrapping() def authenticate_user(self): """Authenticate the user.""" if self.user_db.authenticate_user(self.user): self.user.authenticated = True # Tell the policy manager before everybody else. get_permissions_manager().policy_manager.load_policy(self.user) self.user_authenticated = self.user def unauthenticate_user(self): """Unauthenticate the user.""" if self.user.authenticated and self.user_db.unauthenticate_user(self.user): self.user.authenticated = False # Tell the policy manager before everybody else. get_permissions_manager().policy_manager.load_policy(None) self.user_authenticated = None def matching_user(self, name): """Select a user.""" return self.user_db.matching_user(name) ########################################################################### # Trait handlers. ########################################################################### def _management_actions_default(self): """Return the list of management actions.""" from apptools.permissions.secure_proxy import SecureProxy user_db = self.user_db actions = [] perm = ManageUsersPermission() if user_db.can_add_user: act = Action(name="&Add a User...", on_perform=user_db.add_user) actions.append(SecureProxy(act, permissions=[perm], show=False)) if user_db.can_modify_user: act = Action(name="&Modify a User...", on_perform=user_db.modify_user) actions.append(SecureProxy(act, permissions=[perm], show=False)) if user_db.can_delete_user: act = Action(name="&Delete a User...", on_perform=user_db.delete_user) actions.append(SecureProxy(act, permissions=[perm], show=False)) return actions def _user_actions_default(self): """Return the list of user actions.""" actions = [] if self.user_db.can_change_password: actions.append(_ChangePasswordAction()) return actions def _user_default(self): """Return the default current user.""" return self.user_db.user_factory() def _user_db_default(self): """Return the default user database.""" # Defer to an external user database if there is one. try: from apptools.permissions.external.user_database import UserDatabase except ImportError: from apptools.permissions.default.user_database import UserDatabase return UserDatabase() class _ChangePasswordAction(Action): """An action that allows the current user to change their password. It isn't exported through actions/api.py because it is specific to this user manager implementation.""" #### 'Action' interface ################################################### enabled = Bool(False) name = Unicode("&Change Password...") ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """Initialise the object.""" super(_ChangePasswordAction, self).__init__(**traits) get_permissions_manager().user_manager.on_trait_event(self._refresh_enabled, 'user_authenticated') ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """Perform the action.""" um = get_permissions_manager().user_manager um.user_db.change_password(um.user) ########################################################################### # Private interface. ########################################################################### def _refresh_enabled(self, user): """Invoked whenever the current user's authorisation state changes.""" self.enabled = user is not None apptools-4.5.0/apptools/permissions/default/i_user_database.py0000644000076500000240000000554213547637361026213 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, Instance, Interface # Local imports. from .i_user_storage import IUserStorage class IUserDatabase(Interface): """The interface to be implemented by a user database for the default user manager.""" # Set if the implementation supports changing a user's password. can_change_password = Bool # Set if the implementation supports adding users. can_add_user = Bool # Set if the implementation supports modifying users. can_modify_user = Bool # Set if the implementation supports deleting users. can_delete_user = Bool # The user data storage. user_storage = Instance(IUserStorage) def bootstrapping(self): """Return True if the user database is bootstrapping. Typically this is when no users have been defined.""" def authenticate_user(self, user): """Authenticate the given user and return True if successful. user implements IUser.""" def unauthenticate_user(self, user): """Unauthenticate the given user and return True if successful. user implements IUser.""" def change_password(self, user): """Change a user's password in the database. This only needs to be reimplemented if 'can_change_password' is True.""" def add_user(self): """Add a user account to the database. This only needs to be reimplemented if 'can_add_user' is True.""" def modify_user(self): """Modify a user account in the database. This only needs to be reimplemented if 'can_modify_user' is True.""" def delete_user(self): """Delete a user account from the database. This only needs to be reimplemented if 'can_delete_user' is True.""" def matching_user(self, name): """Return an object that implements IUser for the user selected based on the given name. If there was no user selected then return None. How the name is interpreted (eg. as a regular expression) is determined by the user database. Note that the blob attribute of the user will not be set.""" def user_factory(self): """Return a new object that implements the IUser interface.""" apptools-4.5.0/apptools/permissions/default/select_user.py0000644000076500000240000000442111640354733025400 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import HasTraits, Instance, List, Unicode from traitsui.api import Item, TableEditor, View from traitsui.menu import OKCancelButtons from traitsui.table_column import ObjectColumn class _User(HasTraits): """This represents the user model.""" #### '_User' interface #################################################### # The user name. name = Unicode # The user description. description = Unicode class _UsersView(HasTraits): """This represents the view used to select a user.""" #### '_UsersView' interface ############################################### # The list of users to select from. model = List(_User) # The selected user. selection = Instance(_User) # The editor used by the view. table_editor = TableEditor(columns=[ObjectColumn(name='name'), ObjectColumn(name='description')], selected='selection', sort_model=True, configurable=False) # The default view. traits_view = View(Item('model', show_label=False, editor=table_editor), title="Select a User", style='readonly', kind='modal', buttons=OKCancelButtons) def select_user(users): """Return a single user from the given list of users.""" # Construct the model. model = [_User(name=name, description=description) for name, description in users] # Construct the view. view = _UsersView(model=model) if view.configure_traits() and view.selection is not None: user = view.selection.name, view.selection.description else: user = '', '' return user apptools-4.5.0/apptools/permissions/default/policy_manager.py0000644000076500000240000001257313547637361026074 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.api import error from pyface.action.api import Action from traits.api import Dict, HasTraits, provides, Instance, List # Local imports. from apptools.permissions.i_policy_manager import IPolicyManager from apptools.permissions.permission import ManagePolicyPermission, Permission from apptools.permissions.secure_proxy import SecureProxy from .i_policy_storage import IPolicyStorage, PolicyStorageError from .role_assignment import role_assignment from .role_definition import role_definition @provides(IPolicyManager) class PolicyManager(HasTraits): """The default policy manager implementation. This policy enforces the use of roles. Permissions are associated with roles rather than directly with users. Users are then associated with one or more roles.""" #### 'IPolicyManager' interface ########################################### management_actions = List(Instance(Action)) user_permissions = List(Instance(Permission)) #### 'PolicyManager' interface ############################################ # The dictionary of registered permissions keyed on the permission name. permissions = Dict # The policy data storage. policy_storage = Instance(IPolicyStorage) ########################################################################### # 'IPolicyManager' interface. ########################################################################### def bootstrapping(self): """Return True if we are bootstrapping, ie. no roles have been defined or assigned.""" try: bootstrap = self.policy_storage.is_empty() except PolicyStorageError: # Suppress the error and assume it isn't empty. bootstrap = False return bootstrap def load_policy(self, user): """Load the policy for the given user.""" self.user_permissions = [] # See if the policy is to be unloaded. if user is None: return # Get the user's policy. try: user_name, perm_ids = self.policy_storage.get_policy(user.name) except PolicyStorageError as e: error(None, str(e)) return for id in perm_ids: try: permission = self.permissions[id] except KeyError: # This shouldn't happen if referential integrity is maintained. continue self.user_permissions.append(permission) def register_permission(self, permission): """Register the given permission.""" if permission.id in self.permissions: other = self.permissions[permission.id] if other.application_defined: if permission.application_defined: raise KeyError('permission "%s" has already been defined' % permission.id) # Use the description from the policy manager, if there is # one, in preference to the application supplied one. if permission.description: other.description = permission.description elif permission.application_defined: # Again, prefer the policy manager description. if other.description: permission.description = other.description self.permissions[permission.id] = permission else: # This should never happen if the policy manager is working # properly. raise KeyError('permission "%s" has already been defined by the same policy manager' % permission.id) else: self.permissions[permission.id] = permission ########################################################################### # Trait handlers. ########################################################################### def _management_actions_default(self): """Return the management actions to manage the policy.""" actions = [] perm = ManagePolicyPermission() act = Action(name='&Role Definitions...', on_perform=role_definition) actions.append(SecureProxy(act, permissions=[perm], show=False)) act = Action(name='&Role Assignments...', on_perform=role_assignment) actions.append(SecureProxy(act, permissions=[perm], show=False)) return actions def _policy_storage_default(self): """Return the default storage for the policy data.""" # Defer to an external storage manager if there is one. try: from apptools.permissions.external.policy_storage import PolicyStorage except ImportError: from apptools.permissions.default.policy_storage import PolicyStorage return PolicyStorage() apptools-4.5.0/apptools/permissions/default/__init__.py0000644000076500000240000000010411640354733024614 0ustar mdickinsonstaff00000000000000# Copyright (c) 2008-2011 by Enthought, Inc. # All rights reserved. apptools-4.5.0/apptools/permissions/default/role_assignment.py0000644000076500000240000001311513547637361026265 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.api import error from traits.api import Dict, Instance from traitsui.api import Group, Handler, Item, SetEditor, View from traitsui.menu import Action, CancelButton # Local imports. from apptools.permissions.package_globals import get_permissions_manager from .i_policy_storage import PolicyStorageError from .policy_data import Assignment, Role class _AssignmentView(View): """The view for handling role assignments.""" #### 'View' interface ##################################################### kind = 'modal' title = "Assign roles" ########################################################################### # 'object' interface. ########################################################################### def __init__(self, all_roles, **traits): """Initialise the object.""" buttons = [Action(name="Search"), Action(name="Save"), CancelButton] roles_editor = SetEditor(values=list(all_roles.values()), left_column_title="Available Roles", right_column_title="Assigned Roles") roles_group = Group(Item(name='roles', editor=roles_editor), label='Roles', show_border=True, show_labels=False) super(_AssignmentView, self).__init__(Item(name='user_name'), Item(name='description', style='readonly'), roles_group, buttons=buttons, **traits) class _AssignmentHandler(Handler): """The view handler for role assignments.""" #### Private interface #################################################### all_roles = Dict ########################################################################### # Trait handlers. ########################################################################### def _search_clicked(self, info): """Invoked by the "Search" button.""" pm = get_permissions_manager() assignment = self._assignment(info) user = pm.user_manager.matching_user(assignment.user_name) if user is None: return try: user_name, role_names = pm.policy_manager.policy_storage.get_assignment(user.name) except PolicyStorageError as e: self._ps_error(e) return # Update the viewed object. assignment.user_name = user.name assignment.description = user.description assignment.roles = self._roles_to_list(role_names) def _save_clicked(self, info): """Invoked by the "Save" button.""" assignment = self._validate(info) if assignment is None: return # Update the data in the database. try: get_permissions_manager().policy_manager.policy_storage.set_assignment(assignment.user_name, [r.name for r in assignment.roles]) info.ui.dispose() except PolicyStorageError as e: self._ps_error(e) ########################################################################### # Private interface. ########################################################################### def _validate(self, info): """Validate the assignment and return it if there were no problems.""" assignment = self._assignment(info) assignment.user_name = assignment.user_name.strip() if not assignment.user_name: self._error("A user name must be given.") return None return assignment def _roles_to_list(self, role_names): """Return a list of Role instances created from the given list of role names.""" rl = [] for name in role_names: try: r = self.all_roles[name] except KeyError: # This shouldn't happen if the policy database is maintaining # referential integrity. continue rl.append(r) return rl @staticmethod def _assignment(info): """Return the assignment instance being handled.""" return info.ui.context['object'] @staticmethod def _error(msg): """Display an error message to the user.""" error(None, msg) @staticmethod def _ps_error(e): """Display a message to the user after a PolicyStorageError exception has been raised.""" error(None, str(e)) def role_assignment(): """Implement the role assignment for the current policy manager.""" # Create a dictionary of roles keyed by the role name. all_roles = {} try: roles = get_permissions_manager().policy_manager.policy_storage.all_roles() except PolicyStorageError as e: error(None, str(e)) return for name, description in roles: all_roles[name] = Role(name=name, description=description) assignment = Assignment() view = _AssignmentView(all_roles) handler = _AssignmentHandler(all_roles=all_roles) assignment.edit_traits(view=view, handler=handler) apptools-4.5.0/apptools/permissions/default/api.py0000644000076500000240000000167513547637361023655 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ from .i_policy_storage import IPolicyStorage, PolicyStorageError from .i_user_database import IUserDatabase from .i_user_storage import IUserStorage, UserStorageError from .policy_manager import PolicyManager from .user_database import UserDatabase from .user_manager import UserManager apptools-4.5.0/apptools/permissions/default/i_user_storage.py0000644000076500000240000000633611640354733026104 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Interface, List, Str class UserStorageError(Exception): """This is the exception raised by an IUserStorage object when an error occurs accessing the database. Its string representation is displayed as an error message to the user.""" class IUserStorage(Interface): """This defines the interface expected by a UserManager instance to handle the low level storage of the user data.""" #### 'IUserStorage' interface ############################################# # A list of strings describing the storage capabilities. 'user_password' # means a user's password can be changed. 'user_add' means a user can be # added. 'user_modify' means a user can be modified. 'user_delete' means # a user can be deleted. capabilities = List(Str) ########################################################################### # 'IUserStorage' interface. ########################################################################### def add_user(self, name, description, password): """Add a new user with the given name, description and password.""" def authenticate_user(self, name, password): """Return a tuple of the name, description, and blob of the user with the given name if they are successfully authenticated with the given password. A tuple of empty strings will be returned if the authentication failed.""" def delete_user(self, name): """Delete the user with the given name (which will not be empty).""" def is_empty(self): """Return True if the user database is empty. It will only ever be called once.""" def matching_users(self, name): """Return a list of tuples of the full name and description of all users, sorted by the full name, that match the given name. How the name is interpreted (eg. as a regular expression) is determined by the storage.""" def modify_user(self, name, description, password): """Update the description and password for the user with the given name (which will not be empty).""" def unauthenticate_user(self, user): """Unauthenticate the given user (which is an object implementing IUser) and return True if it was successful.""" def update_blob(self, name, blob): """Update the blob for the user with the given name (which will not be empty).""" def update_password(self, name, password): """Update the password for the user with the given name (which will not be empty).""" apptools-4.5.0/apptools/permissions/default/policy_data.py0000644000076500000240000000274211640354733025357 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import HasTraits, Instance, List, Unicode # Local imports. from apptools.permissions.permission import Permission class Role(HasTraits): """This represents a role.""" # The role name. name = Unicode # The role description. description = Unicode # The permissions that define the role. permissions = List(Instance(Permission)) def __str__(self): """Return a user friendly representation.""" s = self.description if not s: s = self.name return s class Assignment(HasTraits): """This represents the assignment of roles to a user.""" # The user name. user_name = Unicode # The user description. description = Unicode # The list of assigned roles. roles = List(Instance(Role)) apptools-4.5.0/apptools/permissions/default/role_definition.py0000644000076500000240000001470113547637361026247 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.api import confirm, error, YES from traits.api import Instance from traitsui.api import Group, Handler, Item, SetEditor, View from traitsui.menu import Action, CancelButton # Local imports. from apptools.permissions.package_globals import get_permissions_manager from apptools.permissions.permission import Permission from .i_policy_storage import PolicyStorageError from .policy_data import Role from .select_role import select_role class _RoleView(View): """The view for handling roles.""" #### 'View' interface ##################################################### kind = 'modal' title = "Define roles" ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """Initialise the object.""" buttons = [Action(name="Search"), Action(name="Add"), Action(name="Modify"), Action(name="Delete"), CancelButton] all_perms = list(get_permissions_manager().policy_manager.permissions.values()) perms_editor = SetEditor(values=all_perms, left_column_title="Available Permissions", right_column_title="Assigned Permissions") perms_group = Group(Item(name='permissions', editor=perms_editor), label='Permissions', show_border=True, show_labels=False) super(_RoleView, self).__init__(Item(name='name'), Item(name='description'), perms_group, buttons=buttons, **traits) class _RoleHandler(Handler): """The view handler for roles.""" ########################################################################### # Trait handlers. ########################################################################### def _search_clicked(self, info): """Invoked by the "Search" button.""" role = self._role(info) # Get all roles that satisfy the criteria. try: roles = get_permissions_manager().policy_manager.policy_storage.matching_roles(role.name) except PolicyStorageError as e: self._ps_error(e) return if len(roles) == 0: self._error("There is no role that matches \"%s\"." % role.name) return name, description, perm_ids = select_role(roles) if name: # Update the viewed object. role.name = name role.description = description role.permissions = self._perms_to_list(perm_ids) def _add_clicked(self, info): """Invoked by the "Add" button.""" role = self._validate(info) if role is None: return # Add the data to the database. try: get_permissions_manager().policy_manager.policy_storage.add_role( role.name, role.description, [p.id for p in role.permissions]) info.ui.dispose() except PolicyStorageError as e: self._ps_error(e) def _modify_clicked(self, info): """Invoked by the "Modify" button.""" role = self._validate(info) if role is None: return # Update the data in the database. try: get_permissions_manager().policy_manager.policy_storage.modify_role( role.name, role.description, [p.id for p in role.permissions]) info.ui.dispose() except PolicyStorageError as e: self._ps_error(e) def _delete_clicked(self, info): """Invoked by the "Delete" button.""" role = self._validate(info) if role is None: return if confirm(None, "Are you sure you want to delete the role \"%s\"?" % role.name) == YES: # Delete the data from the database. try: get_permissions_manager().policy_manager.policy_storage.delete_role(role.name) info.ui.dispose() except PolicyStorageError as e: self._ps_error(e) ########################################################################### # Private interface. ########################################################################### def _validate(self, info): """Validate the role and return it if there were no problems.""" role = self._role(info) role.name = role.name.strip() if not role.name: self._error("A role name must be given.") return None return role def _perms_to_list(self, perm_ids): """Return a list of Permission instances created from the given list of permission ids.""" pl = [] for id in perm_ids: try: p = get_permissions_manager().policy_manager.permissions[id] except KeyError: # FIXME: permissions should be populated from the policy # database - or is it needed at all? Should it just be read # when managing roles? p = Permission(id=id, application_defined=False) pl.append(p) return pl @staticmethod def _role(info): """Return the role instance being handled.""" return info.ui.context['object'] @staticmethod def _error(msg): """Display an error message to the user.""" error(None, msg) @staticmethod def _ps_error(e): """Display a message to the user after a PolicyStorageError exception has been raised.""" error(None, str(e)) def role_definition(): """Implement the role definition for the current policy manager.""" role = Role() view = _RoleView() handler = _RoleHandler() role.edit_traits(view=view, handler=handler) apptools-4.5.0/apptools/permissions/default/persistent.py0000644000076500000240000000716613547637361025305 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import errno import os # Third-party imports. import six.moves.cPickle as pickle # Enthought library imports. from traits.etsconfig.api import ETSConfig class Persistent(object): """This persists a Traits class to a file. It is used by the default permissions policy and user manager to implement basic (ie. insecure) shared data storage.""" def __init__(self, factory, file_name, desc): """Initialise the object. factory is a callable that will create a new instance if there is no existing data. file_name is the name of the file in the ETSConfig.application_home directory (or the directory specified by the ETS_PERMS_DATA_DIR environment variable if set) to persist the data to. desc is a description of the data used in exceptions.""" self._factory = factory self._desc = desc # Get the name of the file to use. data_dir = os.environ.get('ETS_PERMS_DATA_DIR', ETSConfig.application_home) self._fname = os.path.join(data_dir, file_name) self._lock = self._fname + '.lock' try: os.makedirs(data_dir) except OSError: pass def lock(self): """Obtain a lock on the persisted data.""" try: os.mkdir(self._lock) except OSError as e: if e.errno == errno.EEXIST: msg = "The lock on %s is held by another application or user." % self._desc else: msg = "Unable to acquire lock on %s: %s." % (self._desc, e) raise PersistentError(msg) def unlock(self): """Release the lock on the persisted data.""" try: os.rmdir(self._lock) except OSError as e: raise PersistentError("Unable to release lock on %s: %s." % (self._desc, e)) def read(self): """Read and return the persisted data.""" try: f = open(self._fname, 'r') try: try: data = pickle.load(f) except: raise PersistentError("Unable to read %s." % self._desc) finally: f.close() except IOError as e: if e.errno == errno.ENOENT: data = self._factory() else: raise PersistentError("Unable to open %s: %s." % (self._desc, e)) return data def write(self, data): """Write the persisted data.""" try: f = open(self._fname, 'w') try: try: pickle.dump(data, f) except: raise PersistentError("Unable to write %s." % self._desc) finally: f.close() except IOError as e: raise PersistentError("Unable to create %s: %s." % (self._desc, e)) class PersistentError(Exception): """The class used for all persistence related exceptions.""" apptools-4.5.0/apptools/permissions/default/user_database.py0000644000076500000240000003665513547637361025714 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import os # Enthought library imports. from pyface.api import confirm, error, YES from traits.api import Bool, Dict, HasTraits, provides, Instance, \ List, Password, Property, Unicode from traitsui.api import Handler, Item, View from traitsui.menu import Action, OKCancelButtons # Local imports. from apptools.permissions.i_user import IUser from .i_user_database import IUserDatabase from .i_user_storage import IUserStorage, UserStorageError from .select_user import select_user @provides(IUser) class _LoginUser(HasTraits): """This represents the login data and view.""" #### '_LoginUser' interface ############################################### # The user name. name = Unicode # The user password. password = Password # The default view. traits_view = View(Item(name='name'), Item(name='password'), title="Login", kind='modal', buttons=OKCancelButtons) class _ChangePassword(HasTraits): """This represents the change password data and view.""" #### '_ChangePassword' interface ########################################## # The user name. name = Unicode # The new user password. new_password = Password # The confirmed new user password. confirm_new_password = Password # The default view. traits_view = View(Item(name='name', style='readonly'), Item(name='new_password'), Item(name='confirm_new_password'), title="Change password", kind='modal', buttons=OKCancelButtons) class _ViewUserAccount(HasTraits): """This represents a single account when in a view.""" #### '_ViewUserAccount' interface ######################################### # The name the user uses to identify themselves. name = Unicode # A description of the user (typically their full name). description = Unicode # The password password = Password # The password confirmation. confirm_password = Password # The user database. user_db = Instance(IUserDatabase) class _UserAccountHandler(Handler): """The base traits handler for user account views.""" ########################################################################### # 'Handler' interface. ########################################################################### def close(self, info, is_ok): """Reimplemented to validate the data.""" # If the user cancelled then close the dialog. if not is_ok: return True # Validate the form, only closing the dialog if the form is valid. return self.validate(self._user_account(info)) ########################################################################### # '_UserAccountHandler' interface. ########################################################################### def validate(self, vuac): """Validate the given object and return True if there were no problems. """ if not vuac.name.strip(): self.error("A user name must be given.") return False return True @staticmethod def error(msg): """Display an error message to the user.""" error(None, msg) ########################################################################### # Trait handlers. ########################################################################### def _search_clicked(self, info): """Invoked by the "Search" button.""" # Get the user name. vuac = self._user_account(info) name, description = vuac.user_db._select_user(vuac.name) if not name: return # Update the viewed object. vuac.name = name vuac.description = description vuac.password = vuac.confirm_password = '' ########################################################################### # Private interface. ########################################################################### @staticmethod def _user_account(info): """Return the user account instance being handled.""" return info.ui.context['object'] class _AddUserAccountHandler(_UserAccountHandler): """The traits handler for the add user account view.""" ########################################################################### # '_UserAccountHandler' interface. ########################################################################### def validate(self, vuac): """Validate the given object and return True if there were no problems. """ if not super(_AddUserAccountHandler, self).validate(vuac): return False if not _validate_password(vuac.password, vuac.confirm_password): return False return True class _UserAccountView(View): """The base view for handling user accounts.""" #### 'View' interface ##################################################### kind = 'modal' ########################################################################### # Trait handlers. ########################################################################### def _buttons_default(self): """Return the view's buttons.""" # Create an action that will search the database. buttons = [Action(name="Search")] buttons.extend(OKCancelButtons) return buttons class _AddUserAccountView(_UserAccountView): """A view to handle adding a user account.""" #### 'View' interface ##################################################### title = "Add a user" ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """Initialise the object.""" super(_AddUserAccountView, self).__init__(Item(name='name'), Item(name='description'), Item(name='password'), Item(name='confirm_password'), **traits) class _ModifyUserAccountView(_AddUserAccountView): """A view to handle modifying a user account.""" #### 'View' interface ##################################################### title = "Modify a user" class _DeleteUserAccountView(_UserAccountView): """A view to handle deleting a user account.""" #### 'View' interface ##################################################### title = "Delete a user" ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """Initialise the object.""" super(_DeleteUserAccountView, self).__init__(Item(name='name'), Item(name='description', style='readonly'), **traits) class User(HasTraits): """The user implementation. We don't store any extra information other than that defined by IUser.""" #### 'IUser' interface #################################################### name = Unicode authenticated = Bool(False) description = Unicode blob = Dict @provides(IUserDatabase) class UserDatabase(HasTraits): """This implements a user database that supports IUser for the default user manager (ie. using password authorisation) except that it leaves the actual access of the data to an implementation of IUserStorage.""" #### 'IUserDatabase' interface ############################################ can_change_password = Property can_add_user = Property can_modify_user = Property can_delete_user = Property user_storage = Instance(IUserStorage) #### Private interface #################################################### # Set if updating the user blob internally. _updating_blob_internally = Bool(False) ########################################################################### # 'IUserDatabase' interface. ########################################################################### def bootstrapping(self): """See if we are bootstrapping.""" try: bootstrap = self.user_storage.is_empty() except UserStorageError: # Suppress the error and assume it isn't empty. bootstrap = False return bootstrap def authenticate_user(self, user): """Authenticate a user.""" # Get the login details. name = user.name lu = _LoginUser(name=name) if not lu.edit_traits().result: return False # Get the user account and compare passwords. try: name, description, blob = self.user_storage.authenticate_user( lu.name.strip(), lu.password) except UserStorageError as e: self._us_error(e) return False # Update the user details. user.name = name user.description = description # Suppress the trait notification. self._updating_blob_internally = True user.blob = blob self._updating_blob_internally = False return True def unauthenticate_user(self, user): """Unauthenticate a user.""" return self.user_storage.unauthenticate_user(user) def change_password(self, user): """Change a user's password.""" # Get the new password. name = user.name np = _ChangePassword(name=name) if not np.edit_traits().result: return # Validate the password. if not _validate_password(np.new_password, np.confirm_new_password): return # Update the password in the database. try: self.user_storage.update_password(name, np.new_password) except UserStorageError as e: self._us_error(e) def add_user(self): """Add a user.""" # Get the data from the user. vuac = _ViewUserAccount(user_db=self) view = _AddUserAccountView() handler = _AddUserAccountHandler() if vuac.edit_traits(view=view, handler=handler).result: # Add the data to the database. try: self.user_storage.add_user(vuac.name.strip(), vuac.description, vuac.password) except UserStorageError as e: self._us_error(e) def modify_user(self): """Modify a user.""" # Get the data from the user. vuac = _ViewUserAccount(user_db=self) view = _ModifyUserAccountView() handler = _UserAccountHandler() if vuac.edit_traits(view=view, handler=handler).result: # Update the data in the database. try: self.user_storage.modify_user(vuac.name.strip(), vuac.description, vuac.password) except UserStorageError as e: self._us_error(e) def delete_user(self): """Delete a user.""" # Get the data from the user. vuac = _ViewUserAccount(user_db=self) view = _DeleteUserAccountView() handler = _UserAccountHandler() if vuac.edit_traits(view=view, handler=handler).result: # Make absolutely sure. name = vuac.name.strip() if confirm(None, "Are you sure you want to delete the user \"%s\"?" % name) == YES: # Delete the data from the database. try: self.user_storage.delete_user(name) except UserStorageError as e: self._us_error(e) def matching_user(self, name): """Select a user.""" name, description = self._select_user(name) if not name: return None return User(name=name, description=description) def user_factory(self): """Create a new user object.""" user = User(name=os.environ.get('USER', '')) # Monitor when the blob changes. user.on_trait_change(self._blob_changed, name='blob') return user ########################################################################### # Trait handlers. ########################################################################### def _get_can_change_password(self): """See if a user can change their password.""" return 'user_password' in self.user_storage.capabilities def _get_can_add_user(self): """See if a user can be added.""" return 'user_add' in self.user_storage.capabilities def _get_can_modify_user(self): """See if a user can be modified.""" return 'user_modify' in self.user_storage.capabilities def _get_can_delete_user(self): """See if a user can be deleted.""" return 'user_delete' in self.user_storage.capabilities def _user_storage_default(self): """Return the default storage for the user data.""" # Defer to an external storage manager if there is one. try: from apptools.permissions.external.user_storage import UserStorage except ImportError: from apptools.permissions.default.user_storage import UserStorage return UserStorage() def _blob_changed(self, user, tname, old, new): """Invoked when the user's blob data changes.""" if not self._updating_blob_internally: try: self.user_storage.update_blob(user.name, user.blob) except UserStorageError as e: self._us_error(e) ########################################################################### # Private interface. ########################################################################### def _select_user(self, name): """Select a user returning the data as a tuple of name and description. """ # Get all users that satisfy the criteria. try: users = self.user_storage.matching_users(name) except UserStorageError as e: self._us_error(e) return '', '' if len(users) == 0: error(None, "There is no user that matches \"%s\"." % name) return '', '' return select_user(users) @staticmethod def _us_error(e): """Display a message to the user after a UserStorageError exception has been raised. If the message is empty then we assume the user has already been informed.""" msg = str(e) if msg: error(None, msg) def _validate_password(password, confirmation): """Validate a password and return True if it is valid.""" MIN_PASSWORD_LEN = 6 if password != confirmation: error(None, "The passwords do not match.") return False if not password: error(None, "A password must be given.") return False if len(password) < MIN_PASSWORD_LEN: error(None, "The password must be at least %d characters long." % MIN_PASSWORD_LEN) return False return True apptools-4.5.0/apptools/permissions/api.py0000644000076500000240000000207513547637361022224 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ from .adapter_base import AdapterBase from .i_policy_manager import IPolicyManager from .i_user import IUser from .i_user_manager import IUserManager from .package_globals import get_permissions_manager, set_permissions_manager from .permission import ManagePolicyPermission, ManageUsersPermission, Permission from .permissions_manager import PermissionsManager from .secure_proxy import SecureHandler, SecureProxy apptools-4.5.0/apptools/permissions/adapters/0000755000076500000240000000000013547652535022700 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/permissions/adapters/qt4_widget.py0000644000076500000240000000405211640354733025315 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major library imports. from pyface.qt import QtGui # Enthought library imports. from apptools.permissions.adapter_base import AdapterBase class QWidgetAdapter(AdapterBase): """This is the adapter for PyQt QWidget instances.""" def adapt(self): """Reimplemented to adapt the proxied object.""" # Replace the real methods. self.proxied.setEnabled = self.update_enabled self.proxied.setVisible = self.update_visible self.proxied.hide = self._hide self.proxied.show = self._show return self.proxied def get_enabled(self): """Get the enabled state of the proxied object.""" return self.proxied.isEnabled() def set_enabled(self, value): """Set the enabled state of the proxied object.""" QtGui.QWidget.setEnabled(self.proxied, value) def get_visible(self): """Get the visible state of the proxied object.""" return self.proxied.isVisible() def set_visible(self, value): """Set the visible state of the proxied object.""" QtGui.QWidget.setVisible(self.proxied, value) def _hide(self): """The replacement QWidget.hide() implementation.""" self.update_visible(False) def _show(self): """The replacement QWidget.show() implementation.""" self.update_visible(True) AdapterBase.register_adapter(QWidgetAdapter, QtGui.QWidget) apptools-4.5.0/apptools/permissions/adapters/wx_window.py0000644000076500000240000000327211640354733025272 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major library imports. import wx # Enthought library imports. from apptools.permissions.adapter_base import AdapterBase class wxWindowAdapter(AdapterBase): """This is the adapter for wx Window instances.""" def adapt(self): """Reimplemented to adapt the proxied object.""" # Replace the real methods. self.proxied.Enable = self.update_enabled self.proxied.Show = self.update_visible return self.proxied def get_enabled(self): """Get the enabled state of the proxied object.""" return self.proxied.IsEnabled() def set_enabled(self, value): """Set the enabled state of the proxied object.""" wx.Window.Enable(self.proxied, value) def get_visible(self): """Get the visible state of the proxied object.""" return self.proxied.IsShown() def set_visible(self, value): """Set the visible state of the proxied object.""" wx.Window.Show(self.proxied, value) AdapterBase.register_adapter(wxWindowAdapter, wx.Window) apptools-4.5.0/apptools/permissions/adapters/__init__.py0000644000076500000240000000010411640354733024773 0ustar mdickinsonstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. apptools-4.5.0/apptools/permissions/adapters/pyface_action.py0000644000076500000240000000344511640354733026053 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from apptools.permissions.adapter_base import AdapterBase from pyface.action.api import Action class ActionAdapter(AdapterBase): """This is the adapter for PyFace actions.""" def get_enabled(self): """Get the enabled state of the proxied object.""" return self.proxied.enabled def set_enabled(self, value): """Set the enabled state of the proxied object.""" self.proxied.enabled = value def get_visible(self): """Get the visible state of the proxied object.""" return self.proxied.visible def set_visible(self, value): """Set the visible state of the proxied object.""" self.proxied.visible = value def setattr(self, name, value): """Reimplemented to intercept the setting of the enabled and visible attributes of the proxied action. """ if name == 'enabled': self.update_enabled(value) elif name == 'visible': self.update_visible(value) else: super(ActionAdapter, self).setattr(name, value) AdapterBase.register_adapter(ActionAdapter, Action) apptools-4.5.0/apptools/permissions/action/0000755000076500000240000000000013547652535022352 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/permissions/action/logout_action.py0000644000076500000240000000420511640354733025562 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Bool, Unicode # Local imports. from apptools.permissions.package_globals import get_permissions_manager class LogoutAction(Action): """An action that unauthenticates the current user.""" #### 'Action' interface ################################################### enabled = Bool(False) name = Unicode("Log&out") ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """Initialise the object.""" super(LogoutAction, self).__init__(**traits) get_permissions_manager().user_manager.on_trait_event(self._refresh_enabled, 'user_authenticated') ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """Perform the action.""" get_permissions_manager().user_manager.unauthenticate_user() ########################################################################### # Private interface. ########################################################################### def _refresh_enabled(self, user): """Invoked whenever the current user's authorisation state changes.""" self.enabled = user is not None apptools-4.5.0/apptools/permissions/action/user_menu_manager.py0000644000076500000240000000400413547637361026416 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Group, MenuManager from traits.api import Unicode # Local imports. from apptools.permissions.package_globals import get_permissions_manager from .login_action import LoginAction from .logout_action import LogoutAction class UserMenuManager(MenuManager): """A menu that contains all the actions related to users and permissions. """ #### 'MenuManager' interface ############################################## id = 'User' name = Unicode("&User") ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """Initialise the object.""" pm = get_permissions_manager() # Put them in a group so we can optionally append (because the PyFace # API doesn't do what you expect with append()). group = Group() group.append(LoginAction()) for act in pm.user_manager.user_actions: group.append(act) group.append(LogoutAction()) for act in pm.user_manager.management_actions: group.append(act) for act in pm.policy_manager.management_actions: group.append(act) super(UserMenuManager, self).__init__(group, **traits) apptools-4.5.0/apptools/permissions/action/__init__.py0000644000076500000240000000010411640354733024445 0ustar mdickinsonstaff00000000000000# Copyright (c) 2008-2011 by Enthought, Inc. # All rights reserved. apptools-4.5.0/apptools/permissions/action/api.py0000644000076500000240000000143313547637361023476 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ from .login_action import LoginAction from .logout_action import LogoutAction from .user_menu_manager import UserMenuManager apptools-4.5.0/apptools/permissions/action/login_action.py0000644000076500000240000000254311640354733025364 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Unicode # Local imports. from apptools.permissions.package_globals import get_permissions_manager class LoginAction(Action): """An action that authenticates the current user.""" #### 'Action' interface ################################################### name = Unicode("Log&in...") ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """Perform the action.""" get_permissions_manager().user_manager.authenticate_user() apptools-4.5.0/apptools/permissions/package_globals.py0000644000076500000240000000243713547637361024553 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # The permissions manager. _permissions_manager = None def get_permissions_manager(): """Return the IPermissionsManager implementation, creating a PermissionsManager instance if no other implementation has been set.""" global _permissions_manager if _permissions_manager is None: from .permissions_manager import PermissionsManager _permissions_manager = PermissionsManager() return _permissions_manager def set_permissions_manager(permissions_manager): """Set the IPermissionsManager implementation to use.""" global _permissions_manager _permissions_manager = permissions_manager apptools-4.5.0/apptools/permissions/i_user_manager.py0000644000076500000240000000436413547637361024436 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Bool, Event, Instance, Interface, List # Local imports. from .i_user import IUser class IUserManager(Interface): """The interface implemented by a user manager to manage users.""" # The list of PyFace management actions (ie. actions related to all users) # implemented by this user manager. management_actions = List(Instance(Action)) # The current user. user = Instance(IUser) # The list of PyFace user actions (ie. actions related to the current user) # implemented by this user manager. user_actions = List(Instance(Action)) # This is fired whenever the currently authenticated user changes. It will # be None if the current user isn't authenticated. user_authenticated = Event(IUser) def bootstrapping(self): """Return True if the user manager is bootstrapping. Typically this is when no users have been defined.""" def authenticate_user(self): """Authenticate (ie. login) the user. If successfully authenticated all secured objects are re-enabled according to the user's permissions. """ def unauthenticate_user(self): """Unauthenticate (ie. logout) the user. All secured objects are disabled.""" def select_user(self, name): """Return an object that implements IUser for the user selected based on the given name. If there was no user selected then return None. How the name is interpreted (eg. as a regular expression) is determined by the user manager.""" apptools-4.5.0/apptools/permissions/adapter_base.py0000644000076500000240000001155513547637361024070 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, Bool, HasTraits, Instance, List # Local imports. from .package_globals import get_permissions_manager from .permission import Permission class AdapterBase(HasTraits): """This is the base class for object specific adapters.""" # The object being proxied. proxied = Any # The list of permissions applied to the proxied object. permissions = List(Instance(Permission)) # Set if the proxied object should be shown when it is enabled. show = Bool # The desired enabled state of the proxied object, ie. the state set by the # application before any permissions were applied. _desired_enabled = Bool # The desired visible state of the proxied object, ie. the state set by the # application before any permissions were applied. _desired_visible = Bool # The registered adapter types. _adapter_types = {} @classmethod def register_adapter(cls, adapter, *types): """Register an adapter type for one or more object types.""" for t in types: cls._adapter_types[t] = adapter @classmethod def factory(cls, proxied, permissions, show): """Return an adapter for the proxied object. permissions is a list of permissions to attach to the object. show is set if the proxied object should be visible when it is disabled. """ # Find a suitable adapter type. for object_type, adapter_type in cls._adapter_types.items(): if isinstance(proxied, object_type): break else: raise TypeError("no SecureProxy adapter registered for %s" % proxied) adapter = adapter_type(proxied=proxied, permissions=permissions, show=show) # Refresh the state of the object when the authentication state of the # current user changes. get_permissions_manager().user_manager.on_trait_event(adapter._refresh, 'user_authenticated') return adapter def adapt(self): """Try and adapt the proxied object and return the adapted object if successful. If None is returned a proxy wrapper will be created. """ return None def setattr(self, name, value): """The default implementation to set an attribute of the proxied object. """ setattr(self.proxied, name, value) def get_enabled(self): """Get the enabled state of the proxied object.""" raise NotImplementedError def set_enabled(self, value): """Set the enabled state of the proxied object.""" raise NotImplementedError def update_enabled(self, value): """Update the proxied object after a possible new value of the enabled attribute. """ if get_permissions_manager().check_permissions(*self.permissions): self.set_enabled(value) if not self.show: self.set_visible(self._desired_visible) else: self.set_enabled(False) if not self.show: self.set_visible(False) # Save the desired value so that it can be checked if the user becomes # authenticated. self._desired_enabled = value def get_visible(self): """Get the visible state of the proxied object.""" raise NotImplementedError def set_visible(self, value): """Set the visible state of the proxied object.""" raise NotImplementedError def update_visible(self, value): """Update the proxied object after a possible new value of the visible attribute. """ if self.show: self.set_visible(value) elif get_permissions_manager().check_permissions(*self.permissions): self.set_visible(value) else: self.set_visible(False) # Save the desired value so that it can be checked if the user becomes # authenticated. self._desired_visible = value def _refresh(self, user): """Invoked whenever the current user's authorisation state changes.""" if not self.show: self.update_visible(self._desired_visible) self.update_enabled(self._desired_enabled) apptools-4.5.0/apptools/lru_cache/0000755000076500000240000000000013547652535020447 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/lru_cache/lru_cache.py0000644000076500000240000000617513547637361022757 0ustar mdickinsonstaff00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # ----------------------------------------------------------------------------- from threading import RLock try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict from traits.api import Callable, Event, HasStrictTraits, Instance, Int class LRUCache(HasStrictTraits): """ A least-recently used cache. Items older than `size()` accesses are dropped from the cache. """ size = Int # Called with the key and value that was dropped from the cache cache_drop_callback = Callable # This event contains the set of cached cell keys whenever it changes updated = Event() _lock = Instance(RLock, args=()) _cache = Instance(OrderedDict) def __init__(self, size, **traits): self.size = size self._initialize_cache() super(LRUCache, self).__init__(**traits) def _initialize_cache(self): with self._lock: if self._cache is None: self._cache = OrderedDict() else: self._cache.clear() def _renew(self, key): with self._lock: r = self._cache.pop(key) self._cache[key] = r return r # ------------------------------------------------------------------------- # LRUCache interface # ------------------------------------------------------------------------- def __contains__(self, key): with self._lock: return key in self._cache def __len__(self): with self._lock: return len(self._cache) def __getitem__(self, key): with self._lock: return self._renew(key) def __setitem__(self, key, result): try: dropped = None with self._lock: self._cache[key] = result self._renew(key) if self.size < len(self._cache): dropped = self._cache.popitem(last=False) if dropped and self.cache_drop_callback is not None: self.cache_drop_callback(*dropped) finally: self.updated = list(self.keys()) def get(self, key, default=None): try: return self[key] except KeyError: return default def items(self): with self._lock: return list(self._cache.items()) def keys(self): with self._lock: return list(self._cache.keys()) def values(self): with self._lock: return list(self._cache.values()) def clear(self): with self._lock: self._initialize_cache() self.updated = [] apptools-4.5.0/apptools/lru_cache/tests/0000755000076500000240000000000013547652535021611 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/lru_cache/tests/__init__.py0000644000076500000240000000000012473127616023702 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/lru_cache/tests/test_lru_cache.py0000644000076500000240000000577513547637361025165 0ustar mdickinsonstaff00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import division from nose.tools import assert_equal from ..lru_cache import LRUCache def test_cache_callback(): dropped = [] callback = lambda *args: dropped.append(args) c = LRUCache(2, cache_drop_callback=callback) for i in range(5): c[i] = str(i) expected = [(0, '0'), (1, '1'), (2, '2')] assert_equal(expected, dropped) def test_cache_len(): c = LRUCache(2) assert_equal(0, len(c)) assert_equal(2, c.size) c[0] = None assert_equal(1, len(c)) assert_equal(2, c.size) c[1] = None assert_equal(2, len(c)) assert_equal(2, c.size) c[2] = None assert_equal(2, len(c)) assert_equal(2, c.size) def test_cache_resize(): c = LRUCache(1) c[0] = 0 c[1] = 1 assert_equal(1, len(c)) assert_equal(1, c.size) assert 0 not in c assert 1 in c c.size = 3 assert_equal(1, len(c)) assert_equal(3, c.size) assert 1 in c c[2] = 2 assert_equal(2, len(c)) assert_equal(3, c.size) assert 1 in c assert 2 in c c[3] = 3 assert_equal(3, len(c)) assert_equal(3, c.size) assert 1 in c assert 2 in c assert 3 in c c[4] = 4 assert_equal(3, len(c)) assert_equal(3, c.size) assert 1 not in c assert 2 in c assert 3 in c assert 4 in c def test_cache_items(): c = LRUCache(2) assert_equal([], c.items()) c[0] = str(0) c[1] = str(1) c[2] = str(2) expected = [(1, '1'), (2, '2')] assert_equal(expected, sorted(c.items())) def test_cache_idempotency(): c = LRUCache(2) c[1] = 1 assert_equal(1, len(c)) c[1] = 1 assert_equal(1, len(c)) c[2] = 2 assert_equal(2, len(c)) c[1] = 1 assert_equal(2, len(c)) c[2] = 2 assert_equal(2, len(c)) c[3] = 3 assert_equal(2, len(c)) c[3] = 3 assert_equal(2, len(c)) def test_cache_keys_values(): c = LRUCache(2) assert_equal([], c.items()) c[0] = str(0) c[1] = str(1) c[2] = str(2) expected = [1, 2] assert_equal(expected, sorted(c.keys())) assert_equal([str(val) for val in expected], sorted(c.values())) def test_cache_clear(): c = LRUCache(2) for i in range(c.size): c[i] = i assert_equal(c.size, len(c)) c.clear() assert_equal(0, len(c)) def test_cache_get(): c = LRUCache(2) for i in range(c.size): c[i] = i assert_equal(0, c.get(0)) assert_equal(None, c.get(c.size)) def test_updated_event(): c = LRUCache(2) events = [] # Traits can't handle builtins as handlers c.on_trait_change(lambda x: events.append(x), 'updated') c[0] = 0 assert_equal(sorted(events), [[0]]) c[1] = 1 assert_equal(sorted(events), [[0], [0, 1]]) c[2] = 2 assert_equal(sorted(events), [[0], [0, 1], [1, 2]]) # Getting items doesn't fire anything c[1] assert_equal(sorted(events), [[0], [0, 1], [1, 2]]) c.get(3) assert_equal(sorted(events), [[0], [0, 1], [1, 2]]) apptools-4.5.0/apptools/lru_cache/__init__.py0000644000076500000240000000007113547637361022556 0ustar mdickinsonstaff00000000000000# -*- coding: utf-8 -*- from .lru_cache import LRUCache apptools-4.5.0/apptools/naming/0000755000076500000240000000000013547652535017773 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/naming/referenceable.py0000644000076500000240000000232513547637361023131 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Base class for classes that can produce a reference to themselves. """ # Enthought library imports. from traits.api import HasPrivateTraits, Instance # Local imports. from .reference import Reference class Referenceable(HasPrivateTraits): """ Base class for classes that can produce a reference to themselves. """ #### 'Referenceable' interface ############################################ # The object's reference suitable for binding in a naming context. reference = Instance(Reference) #### EOF ###################################################################### apptools-4.5.0/apptools/naming/ui/0000755000076500000240000000000013547652535020410 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/naming/ui/naming_tree.py0000644000076500000240000000517713547637361023264 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A tree view of a naming system. """ # Enthought library imports. from apptools.naming.api import OperationNotSupportedError from pyface.tree.api import NodeTree from traits.api import Instance # Local imports. from .naming_tree_model import NamingTreeModel class NamingTree(NodeTree): """ A tree view of a naming system. """ #### 'Tree' interface ##################################################### # The model that provides the data for the tree. model = Instance(NamingTreeModel) ########################################################################### # 'Tree' interface. ########################################################################### #### Trait initializers ################################################### def _model_default(self): """ Initializes the model trait. """ return NamingTreeModel() ########################################################################### # 'NamingTree' interface. ########################################################################### def ensure_visible(self, node): """ Make sure that the specified node is visible. """ try: components = node.namespace_name.split('/') # Make sure that the tree is expanded down to the context that # contains the node. binding = self.root for atom in components[:-1]: binding = binding.obj.lookup_binding(atom) self.expand(binding) # The context is expanded so we know that the node will be in the # node to Id map. wxid = self._node_to_id_map.get(self.model.get_key(node), None) self.control.EnsureVisible(wxid) # We need 'namespace_name' to make this work. If we don't have it # then we simply cannot do this! except OperationNotSupportedError: binding = None return binding ##### EOF ##################################################################### apptools-4.5.0/apptools/naming/ui/object_node_type.py0000644000076500000240000000453211640354733024271 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The node type for NON-contexts in a naming system. """ # Enthought library imports. from apptools.naming.api import Context from pyface.tree.api import NodeType class ObjectNodeType(NodeType): """ The node type for NON-contexts in a naming system. """ ########################################################################### # 'NodeType' interface. ########################################################################### # 'node' in this case is a 'Binding' instance. def is_type_for(self, node): """ Returns True if this node type recognized a node. """ return True def allows_children(self, node): """ Does the node allow children (ie. a folder vs a file). """ return False def get_drag_value(self, node): """ Get the value that is dragged for a node. """ return node.obj def is_editable(self, node): """ Returns True if the node is editable, otherwise False. If the node is editable, its text can be set via the UI. """ return True def get_text(self, node): """ Returns the label text for a node. """ return node.name def can_set_text(self, node, text): """ Returns True if the node's label can be set. """ # The parent context will NOT be None here (an object is ALWAYS # contained in a context). parent = node.context return len(text.strip()) > 0 and text not in parent.list_names('') def set_text(self, node, text): """ Sets the label text for a node. """ node.context.rename(node.name, text) node.name = text return ##### EOF ##################################################################### apptools-4.5.0/apptools/naming/ui/naming_tree_model.py0000644000076500000240000000377413547637361024445 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The model for a tree view of a naming system. """ # Enthought library imports. from apptools.naming.api import Binding from pyface.tree.api import NodeTreeModel from traits.api import Instance # Local imports. from .context_node_type import ContextNodeType from .naming_node_manager import NamingNodeManager from .object_node_type import ObjectNodeType class NamingTreeModel(NodeTreeModel): """ The model for a tree view of a naming system. """ #### 'TreeModel' interface ################################################ # The root of the model. root = Instance(Binding) #### 'NodeTreeModel' interface ############################################ # The node manager looks after all node types. node_manager = Instance(NamingNodeManager) ########################################################################### # 'NodeTreeModel' interface. ########################################################################### #### Trait initializers ################################################### def _node_manager_default(self): """ Initializes the node manaber trait. """ node_manager = NamingNodeManager() node_manager.add_node_type(ContextNodeType()) node_manager.add_node_type(ObjectNodeType()) return node_manager ##### EOF ##################################################################### apptools-4.5.0/apptools/naming/ui/images/0000755000076500000240000000000013547652535021655 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/naming/ui/images/document.png0000644000076500000240000000051511640354733024171 0ustar mdickinsonstaff00000000000000PNG  IHDRabKGD pHYs  tIME8`&BIDAT8˭=n0H[!!l"A!QsL$^Xj,{|3m1 Z;騬8xt/5ǒ4MW(w=ιURW/P&AH৞"h@ ? qs1 31#~I0r ܊@2x/!W؀C@>ӿ>  q2N1H; h(#+Xُ[{`W]h3j?5ge`bv;5\ۇ p/wyW8D H~aՎ )@C?$a/ o~%H3_ 4~<?°7b? ? `Qǁ@ x3î? V z+~lalH!PHw :t @۫ D@L cĠ&D@?y(8^ `DC @L3(0IENDB`apptools-4.5.0/apptools/naming/ui/images/closed_folder.png0000644000076500000240000000102511640354733025154 0ustar mdickinsonstaff00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxbd```, ־Rc@15址 YH'^{ "(5h_d@~aÚU @@Pkn#. 2 ~+}  o{S,f+@A x?00D3 #aj'@[v + 5>Bbh H(j;`ÀBFpXq R@ 6( ]qD q? P%K-` H @l@,D@|,chvaPb 4r@2s^IENDB`apptools-4.5.0/apptools/naming/ui/__init__.py0000644000076500000240000000120011640354733022501 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ apptools-4.5.0/apptools/naming/ui/explorer.py0000644000076500000240000000625713547637361022634 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A naming system explorer. """ # Enthought library imports. from apptools.naming.api import Binding, PyContext from pyface.api import PythonShell, SplitApplicationWindow from traits.api import Float, Instance, Str # Local imports. from .naming_tree import NamingTree # Factory function for exploring a Python namespace. def explore(obj): """ View a Python object as a naming context. """ root = Binding(name='root', obj=PyContext(namespace=obj)) explorer = Explorer(root=root, size=(1200, 400)) explorer.open() return class Explorer(SplitApplicationWindow): """ The main application window. """ #### 'Window' interface ################################################### title = Str('Naming System Explorer') #### 'SplitApplicationWindow' interface ################################### # The direction in which the panel is split. direction = Str('vertical') # The ratio of the size of the left/top pane to the right/bottom pane. ratio = Float(0.3) # The root binding (usually a binding to a context!). root = Instance(Binding) ########################################################################### # Protected 'SplitApplicationWindow' interface. ########################################################################### def _create_lhs(self, parent): """ Creates the left hand side or top depending on the style. """ return self._create_tree(parent, self.root) def _create_rhs(self, parent): """ Creates the panel containing the selected preference page. """ return self._create_python_shell(parent) ########################################################################### # Private interface. ########################################################################### def _create_tree(self, parent, root): """ Creates the tree. """ self._tree = tree = NamingTree(parent, root=root) return tree.control def _create_python_shell(self, parent): """ Creates the Python shell. """ self._python_shell = python_shell = PythonShell(parent) # Bind useful names. python_shell.bind('widget', self._tree) python_shell.bind('w', self._tree) python_shell.bind('window', self) python_shell.bind('explore', explore) # Execute useful commands to bind useful names ;^) python_shell.execute_command('from apptools.naming.api import *') return python_shell.control ##### EOF ##################################################################### apptools-4.5.0/apptools/naming/ui/api.py0000644000076500000240000000166613547637361021544 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from .context_monitor import ContextMonitor from .context_node_type import ContextNodeType from .explorer import Explorer, explore from .naming_node_manager import NamingNodeManager from .naming_tree import NamingTree from .naming_tree_model import NamingTreeModel from .object_node_type import ObjectNodeType apptools-4.5.0/apptools/naming/ui/context_node_type.py0000644000076500000240000000607313547637361024522 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The node type for contexts in a naming system. """ from __future__ import print_function # Enthought library imports. from apptools.naming.api import Context from pyface.tree.api import NodeType # Local imports. from .context_monitor import ContextMonitor class ContextNodeType(NodeType): """ The node type for contexts in a naming system. """ ########################################################################### # 'NodeType' interface. ########################################################################### # 'node' in this case is a 'Binding' instance whose 'obj' trait is a # 'Context' instance. def is_type_for(self, node): """ Returns True if this node type recognizes a node. """ return isinstance(node.obj, Context) def allows_children(self, node): """ Does the node allow children (ie. a folder vs a file). """ return True def has_children(self, node): """ Returns True if a node has children, otherwise False. """ return len(node.obj.list_names('')) > 0 def get_children(self, node): """ Returns the children of a node. """ return node.obj.list_bindings('') def get_drag_value(self, node): """ Get the value that is dragged for a node. """ return node.obj def is_editable(self, node): """ Returns True if the node is editable, otherwise False. """ # You can't rename the root context! return node.context is not None def get_text(self, node): """ Returns the label text for a node. """ return node.name def can_set_text(self, node, text): """ Returns True if the node's label can be set. """ # The parent context will NOT be None here (see 'is_editable'). parent = node.context return len(text.strip()) > 0 and text not in parent.list_names('') def set_text(self, node, text): """ Sets the label text for a node. """ print('Setting text on', node.name, node.obj) print('Context details', node.obj.name, node.obj.path) # Do the rename in the naming system. node.context.rename(node.name, text) # Update the binding. node.name = text return def get_monitor(self, node): """ Returns a monitor that detects changes to a node. """ return ContextMonitor(node=node) ##### EOF ##################################################################### apptools-4.5.0/apptools/naming/ui/naming_node_manager.py0000644000076500000240000000363511640354733024730 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The node manager for a naming tree. """ # Enthought library imports. from pyface.tree.api import NodeManager class NamingNodeManager(NodeManager): """ The node manager for a naming tree. """ ########################################################################### # 'NodeManager' interface. ########################################################################### def get_key(self, node): """ Generates a unique key for a node. """ # fixme: This scheme does NOT allow the same object to be in the tree # in more than one place. return self._get_hash_value(node.obj) ########################################################################### # Private interface. ########################################################################### def _get_hash_value(self, obj): """ Returns a hash value for an object. """ # We do it like this 'cos, for example, using id() on a string # doesn't give us what we want, but things like lists aren't # hashable, so we can't always use hash()). try: hash_value = hash(obj) except: hash_value = id(obj) return hash_value ##### EOF ##################################################################### apptools-4.5.0/apptools/naming/ui/context_monitor.py0000644000076500000240000000705611640354733024214 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A monitor that detects changes to a naming context. """ # Enthought library imports. from pyface.tree.api import NodeMonitor class ContextMonitor(NodeMonitor): """ A monitor that detects changes to a naming context. """ ########################################################################### # 'NodeMonitor' interface. ########################################################################### def start(self): """ Start listening to changes to the object. """ context = self.node.obj context.on_trait_change(self._on_object_added, 'object_added') context.on_trait_change(self._on_object_changed, 'object_changed') context.on_trait_change(self._on_object_removed, 'object_removed') context.on_trait_change(self._on_object_renamed, 'object_renamed') context.on_trait_change(self._on_context_changed, 'context_changed') return def stop(self): """ Stop listening to changes to the object. """ context = self.node.obj context.on_trait_change( self._on_object_added, 'object_added', remove=True ) context.on_trait_change( self._on_object_changed, 'object_changed', remove=True ) context.on_trait_change( self._on_object_removed, 'object_removed', remove=True ) context.on_trait_change( self._on_object_renamed, 'object_renamed', remove=True ) context.on_trait_change( self._on_context_changed, 'context_changed', remove=True ) return ########################################################################### # Private interface. ########################################################################### #### Trait event handlers ################################################# def _on_object_added(self, event): """ Called when an object has been added to the context. """ self.fire_nodes_inserted([event.new_binding]) return def _on_object_changed(self, event): """ Called when an object in the context has been changed. """ # fixme: Can we get enough information to fire a 'nodes_replaced' # event? Something a little more granular than this! self.fire_structure_changed() return def _on_object_removed(self, event): """ Called when an object has been removed from the context. """ self.fire_nodes_removed([event.old_binding]) return def _on_object_renamed(self, event): """ Called when an object has been renamed in the context. """ self.fire_nodes_replaced([event.old_binding], [event.new_binding]) return def _on_context_changed(self, event): """ Called when a context has changed dramatically. """ self.fire_structure_changed() return ##### EOF ##################################################################### apptools-4.5.0/apptools/naming/dir_context.py0000644000076500000240000001363513547637361022677 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all directory contexts. """ # Enthought library imports. from traits.api import Dict # Local imports. from .context import Context from .exception import NameNotFoundError, NotContextError class DirContext(Context): """ The base class for all directory contexts. """ # The attributes of every object in the context. The attributes for the # context itself have the empty string as the key. # # {str name : dict attributes} _attributes = Dict ########################################################################### # 'DirContext' interface. ########################################################################### def get_attributes(self, name): """ Returns the attributes associated with a named object. """ # If the name is empty then we return the attributes of this context. if len(name) == 0: attributes = self._get_attributes(name) else: # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual get. attributes = self._get_attributes(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) attributes = next_context.get_attributes( '/'.join(components[1:])) return attributes def set_attributes(self, name, attributes): """ Sets the attributes associated with a named object. """ # If the name is empty then we set the attributes of this context. if len(name) == 0: attributes = self._set_attributes(name, attributes) else: # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual set. self._set_attributes(atom, attributes) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.set_attributes( '/'.join(components[1:]), attributes ) return # fixme: Non-JNDI def find_bindings(self, visitor): """ Find bindings with attributes matching criteria in visitor. Visitor is a function that is passed the bindings for each level of the heirarchy and the attribute dictionary for those bindings. The visitor examines the bindings and dictionary and returns the bindings it is interested in. """ bindings = visitor(self.list_bindings(), self._attributes) # recursively check other sub contexts. for binding in self.list_bindings(): obj = binding.obj if isinstance(obj, DirContext): bindings.extend(obj.find_bindings(visitor)) return bindings ########################################################################### # Protected 'DirContext' interface. ########################################################################### def _get_attributes(self, name): """ Returns the attributes of an object in this context. """ attributes = self._attributes.setdefault(name, {}) return attributes.copy() def _set_attributes(self, name, attributes): """ Sets the attributes of an object in this context. """ self._attributes[name] = attributes return ########################################################################### # Protected 'Context' interface. ########################################################################### def _unbind(self, name): """ Unbinds a name from this context. """ super(DirContext, self)._unbind(name) if name in self._attributes: del self._attributes[name] return def _rename(self, old_name, new_name): """ Renames an object in this context. """ super(DirContext, self)._rename(old_name, new_name) if old_name in self._attributes: self._attributes[new_name] = self._attributes[old_name] del self._attributes[old_name] return def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ super(DirContext, self)._destroy_subcontext(name) if name in self._attributes: del self._attributes[name] return #### EOF ###################################################################### apptools-4.5.0/apptools/naming/address.py0000644000076500000240000000215011640354733021757 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The address of a commuications endpoint. """ # Enthought library imports. from traits.api import Any, HasTraits, Str class Address(HasTraits): """ The address of a communications end-point. It contains a type that describes the communication mechanism, and the actual address content. """ # The type of the address. type = Str # The actual content. content = Any #### EOF ###################################################################### apptools-4.5.0/apptools/naming/exception.py0000644000076500000240000000342611640354733022337 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Naming exceptions. """ class NamingError(Exception): """ Base class for all naming exceptions. """ class InvalidNameError(NamingError): """ Invalid name. This exception is thrown when the name passed to a naming operation does not conform to the syntax of the naming system (or is empty etc). """ class NameAlreadyBoundError(NamingError): """ Name already bound. This exception is thrown when an attempt is made to bind a name that is already bound in the current context. """ class NameNotFoundError(NamingError): """ Name not found. This exception is thrown when a component of a name cannot be resolved because it is not bound in the current context. """ class NotContextError(NamingError): """ Not a context. This exception is thrown when a naming operation has reached a point where a context is required to continue the operation, but the resolved object is not a context. """ class OperationNotSupportedError(NamingError): """ The context does support the requested operation. """ #### EOF ###################################################################### apptools-4.5.0/apptools/naming/unique_name.py0000644000076500000240000000156311640354733022647 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. # #----------------------------------------------------------------------------- """ A re-usable method for calculating a unique name given a list of existing names. """ def make_unique_name(base, existing=[], format="%s_%s"): """ Return a name, unique within a context, based on the specified name. base: the desired base name of the generated unique name. existing: a sequence of the existing names to avoid returning. format: a formatting specification for how the name is made unique. """ count = 2 name = base while name in existing: name = format % (base, count) count += 1 return name #### EOF #################################################################### apptools-4.5.0/apptools/naming/referenceable_state_factory.py0000644000076500000240000000272413547637361026063 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ State factory for referenceable objects. """ # Local imports. from .referenceable import Referenceable from .state_factory import StateFactory class ReferenceableStateFactory(StateFactory): """ State factory for referenceable objects. """ ########################################################################### # 'StateFactory' interface. ########################################################################### def get_state_to_bind(self, obj, name, context): """ Returns the state of an object for binding. """ state = None # If the object knows how to create a reference to it then let it # do so. if isinstance(obj, Referenceable): state = obj.reference return state ### EOF ####################################################################### apptools-4.5.0/apptools/naming/pyfs_initial_context_factory.py0000644000076500000240000000410013547637361026325 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The initial context factory for Python file system contexts. """ # Local imports. from .context import Context from .initial_context_factory import InitialContextFactory from .object_serializer import ObjectSerializer from .pyfs_context import PyFSContext from .pyfs_context_factory import PyFSContextFactory from .pyfs_object_factory import PyFSObjectFactory from .pyfs_state_factory import PyFSStateFactory class PyFSInitialContextFactory(InitialContextFactory): """ The initial context factory for Python file system contexts. """ ########################################################################### # 'InitialContextFactory' interface. ########################################################################### def get_initial_context(self, environment): """ Creates an initial context for beginning name resolution. """ # Object factories. object_factories = [PyFSObjectFactory(), PyFSContextFactory()] environment[Context.OBJECT_FACTORIES] = object_factories # State factories. state_factories = [PyFSStateFactory()] environment[Context.STATE_FACTORIES] = state_factories # Object serializers. object_serializers = [ObjectSerializer()] environment[PyFSContext.OBJECT_SERIALIZERS] = object_serializers return PyFSContext(path=r'', environment=environment) #### EOF ###################################################################### apptools-4.5.0/apptools/naming/object_serializer.py0000644000076500000240000000561313547637361024051 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all object serializers. """ # Standard library imports. import logging from traceback import print_exc from os.path import splitext #import cPickle #import pickle # Enthought library imports. import apptools.sweet_pickle as sweet_pickle from traits.api import HasTraits, Str # Setup a logger for this module. logger = logging.getLogger(__name__) class ObjectSerializer(HasTraits): """ The base class for all object serializers. """ #### 'ObjectSerializer' interface ######################################### # The file extension recognized by this serializer. ext = Str('.pickle') ########################################################################### # 'ObjectSerializer' interface. ########################################################################### def can_load(self, path): """ Returns True if the serializer can load a file. """ rest, ext = splitext(path) return ext == self.ext def load(self, path): """ Loads an object from a file. """ # Unpickle the object. f = open(path, 'rb') try: try: obj = sweet_pickle.load(f) # obj = cPickle.load(f) # obj = pickle.load(f) except Exception as ex: print_exc() logger.exception( "Failed to load pickle file: %s, %s" % (path, ex)) raise finally: f.close() return obj def can_save(self, obj): """ Returns True if the serializer can save an object. """ return True def save(self, path, obj): """ Saves an object to a file. """ if not path.endswith(self.ext): actual_path = path + self.ext else: actual_path = path # Pickle the object. f = open(actual_path, 'wb') try: sweet_pickle.dump(obj, f, 1) # cPickle.dump(obj, f, 1) # pickle.dump(obj, f, 1) except Exception as ex: logger.exception( "Failed to pickle into file: %s, %s, object:%s" % (path, ex, obj)) print_exc() f.close() return actual_path ### EOF ####################################################################### apptools-4.5.0/apptools/naming/trait_defs/0000755000076500000240000000000013547652535022117 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/naming/trait_defs/__init__.py0000644000076500000240000000100713547637361024226 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # # Define traits useful with Naming. # # Written by: David C. Morrill # # Date: 08/16/2005 # # (c) Copyright 2005 by Enthought, Inc. # #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # Imports: #------------------------------------------------------------------------------ from apptools.naming.trait_defs.api import * apptools-4.5.0/apptools/naming/trait_defs/api.py0000644000076500000240000000052713547637361023246 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # # Define traits useful with Naming. # # Written by: David C. Morrill # # Date: 08/16/2005 # # (c) Copyright 2005 by Enthought, Inc. # #------------------------------------------------------------------------------ from .naming_traits import NamingInstance apptools-4.5.0/apptools/naming/trait_defs/naming_traits.py0000644000076500000240000001400013547637361025323 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------- # # Defines the NamingLog and NamingIndex traits # # Written by: David C. Morrill # # Date: 08/15/2005 # # (c) Copyright 2005 by Enthought, Inc. # #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- import sys import six from traits.api \ import Trait, TraitHandler, TraitFactory from traits.trait_base \ import class_of, get_module_name from traitsui.api \ import DropEditor from apptools.naming.api \ import Binding #------------------------------------------------------------------------------- # 'NamingInstance' trait factory: #------------------------------------------------------------------------------- def NamingInstance ( klass = None, value = '', allow_none = False, **metadata ): metadata.setdefault( 'copy', 'deep' ) return Trait( value, NamingTraitHandler( klass, or_none = allow_none, module = get_module_name() ), **metadata ) NamingInstance = TraitFactory( NamingInstance ) #------------------------------------------------------------------------------- # 'NamingTraitHandler' class: #------------------------------------------------------------------------------- class NamingTraitHandler ( TraitHandler ): #--------------------------------------------------------------------------- # Initializes the object: #--------------------------------------------------------------------------- def __init__ ( self, aClass, or_none, module ): """ Initializes the object. """ self.or_none = (or_none != False) self.module = module self.aClass = aClass if (aClass is not None) \ and (not isinstance( aClass, ( six.string_types, type ) )): self.aClass = aClass.__class__ def validate ( self, object, name, value ): if isinstance( value, six.string_types ): if value == '': if self.or_none: return '' else: self.validate_failed( object, name, value ) try: value = self._get_binding_for( value ) except: self.validate_failed( object, name, value ) if isinstance(self.aClass, six.string_types): self.resolve_class( object, name, value ) if (isinstance( value, Binding ) and ((self.aClass is None) or isinstance( value.obj, self.aClass ))): return value.namespace_name self.validate_failed( object, name, value ) def info ( self ): aClass = self.aClass if aClass is None: result = 'path' else: if type( aClass ) is not str: aClass = aClass.__name__ result = 'path to an instance of ' + class_of( aClass ) if self.or_none is None: return result + ' or an empty string' return result def validate_failed ( self, object, name, value ): if not isinstance( value, type ): msg = 'class %s' % value.__class__.__name__ else: msg = '%s (i.e. %s)' % ( str( type( value ) )[1:-1], repr( value ) ) self.error( object, name, msg ) def get_editor ( self, trait ): if self.editor is None: from traitsui.api import DropEditor self.editor = DropEditor( klass = self.aClass, binding = True, readonly = False ) return self.editor def post_setattr ( self, object, name, value ): other = None if value != '': other = self._get_binding_for( value ).obj object.__dict__[ name + '_' ] = other def _get_binding_for ( self, value ): result = None # FIXME: The following code makes this whole component have a dependency # on envisage, and worse, assumes the use of a particular project # plugin! This is horrible and should be refactored out, possibly to # a custom sub-class of whoever needs this behavior. try: from envisage import get_application workspace = get_application().service_registry.get_service( 'envisage.project.IWorkspace' ) result = workspace.lookup_binding( value ) except ImportError: pass return result def resolve_class ( self, object, name, value ): aClass = self.find_class() if aClass is None: self.validate_failed( object, name, value ) self.aClass = aClass # fixme: The following is quite ugly, because it wants to try and fix # the trait referencing this handler to use the 'fast path' now that the # actual class has been resolved. The problem is finding the trait, # especially in the case of List(Instance('foo')), where the # object.base_trait(...) value is the List trait, not the Instance # trait, so we need to check for this and pull out the List # 'item_trait'. Obviously this does not extend well to other traits # containing nested trait references (Dict?)... trait = object.base_trait( name ) handler = trait.handler if (handler is not self) and hasattr( handler, 'item_trait' ): trait = handler.item_trait trait.validate( self.fast_validate ) def find_class ( self ): module = self.module aClass = self.aClass col = aClass.rfind( '.' ) if col >= 0: module = aClass[ : col ] aClass = aClass[ col + 1: ] theClass = getattr( sys.modules.get( module ), aClass, None ) if (theClass is None) and (col >= 0): try: theClass = getattr( __import__( module ), aClass, None ) except: pass return theClass apptools-4.5.0/apptools/naming/tests/0000755000076500000240000000000013547652535021135 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/naming/tests/dir_context_test_case.py0000644000076500000240000001021113422276145026045 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Tests the default directory context. """ # Enthought library imports. from apptools.naming.api import * # Local imports. from context_test_case import ContextTestCase class DirContextTestCase(ContextTestCase): """ Tests the default directory context. """ ########################################################################### # 'ContextTestCase' interface. ########################################################################### def create_context(self): """ Creates the context that we are testing. """ return DirContext() ########################################################################### # Tests. ########################################################################### def test_get_attributes(self): """ get attributes """ # Convenience. context = self.context sub = self.context.lookup('sub') self.assert_(isinstance(sub, DirContext)) #### Generic name resolution tests #### # Non-existent name. self.assertRaises(NameNotFoundError, context.get_attributes, 'x') # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.get_attributes, 'x/a') # Attempt to resolve via an existing name that is not a context. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) self.assertRaises(NotContextError,context.get_attributes,'sub/a/x') #### Operation specific tests #### # Attributes of the root context. attributes = self.context.get_attributes('') self.assertEqual(len(attributes), 0) # Attributes of a sub-context. attributes = context.get_attributes('sub') self.assertEqual(len(attributes), 0) return def test_set_get_attributes(self): """ get and set attributes """ defaults = {'colour' : 'blue'} # Convenience. context = self.context sub = self.context.lookup('sub') self.assert_(isinstance(sub, DirContext)) #### Generic name resolution tests #### # Non-existent name. self.assertRaises( NameNotFoundError, context.set_attributes, 'x', defaults ) # Attempt to resolve via a non-existent context. self.assertRaises( NameNotFoundError, context.set_attributes, 'x/a', defaults ) # Attempt to resolve via an existing name that is not a context. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) self.assertRaises( NotContextError, context.set_attributes, 'sub/a/xx', defaults ) #### Operation specific tests #### # Attributes of the root context. attributes = self.context.get_attributes('') self.assertEqual(len(attributes), 0) # Set the attributes. context.set_attributes('', defaults) attributes = context.get_attributes('') self.assertEqual(len(attributes), 1) self.assertEqual(attributes['colour'], 'blue') # Attributes of a sub-context. attributes = context.get_attributes('sub') self.assertEqual(len(attributes), 0) # Set the attributes. context.set_attributes('sub', defaults) attributes = context.get_attributes('sub') self.assertEqual(len(attributes), 1) self.assertEqual(attributes['colour'], 'blue') return #### EOF ###################################################################### apptools-4.5.0/apptools/naming/tests/pyfs_context_test_case.py0000644000076500000240000002744313422276145026267 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Tests naming operations on PyFS contexts. """ # Standard library imports. import os, shutil, unittest # Enthought library imports. from apptools.io import File from apptools.naming.api import * class PyFSContextTestCase(unittest.TestCase): """ Tests naming operations on PyFS contexts. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ try: if os.path.exists('data'): shutil.rmtree('data') if os.path.exists('other'): shutil.rmtree('other') except: pass os.mkdir('data') os.mkdir('other') self.context = PyFSContext(path='data') self.context.create_subcontext('sub') self.context.bind('x', 123) self.context.bind('y', 321) return def tearDown(self): """ Called immediately after each test method has been called. """ self.context = None shutil.rmtree('data') shutil.rmtree('other') return ########################################################################### # Tests. ########################################################################### def test_initialization(self): """ initialization of an existing context """ context = PyFSContext(path='data') self.assertEqual(len(context.list_bindings('')), 3) return def test_initialization_with_empty_environment(self): """ initialization with empty environmentt """ context = PyFSContext(path='other', environment={}) self.assertEqual(len(context.list_names('')), 0) return def test_bind(self): """ pyfs context bind """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.bind, '', 1) # Bind a local file object. f = File(os.path.join(sub.path, 'foo.py')) #f.create_file('print "foo!"\n') context.bind('sub/foo.py', f) self.assertEqual(len(sub.list_bindings('')), 1) # Bind a reference to a non-local file. f = File('/tmp') context.bind('sub/tmp', f) self.assertEqual(len(sub.list_bindings('')), 2) self.assertEqual(context.lookup('sub/tmp').path, f.path) # Bind a reference to a non-local context. f = PyFSContext(path='other') context.bind('sub/other', f) self.assertEqual(len(sub.list_bindings('')), 3) self.assert_(f.path in context.lookup('sub/other').path) # Bind a Python object. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 4) # Try to bind it again. self.assertRaises(NameAlreadyBoundError, context.bind, 'sub/a', 1) return def test_rebind(self): """ pyfs context rebind """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.rebind, '', 1) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Rebind it. context.rebind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) return def test_unbind(self): """ pyfs context unbind """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.unbind, '') # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Unbind it. context.unbind('sub/a') self.assertEqual(len(sub.list_bindings('')), 0) # Try to unbind a non-existent name. self.assertRaises(NameNotFoundError, context.unbind, 'sub/b') return def test_rename(self): """ multi-context rename """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.rename, '', 'x') self.assertRaises(InvalidNameError, context.rename, 'x', '') # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Rename it. context.rename('sub/a', 'sub/b') self.assertEqual(len(sub.list_bindings('')), 1) # Lookup using the new name. self.assertEqual(context.lookup('sub/b'), 1) # Lookup using the old name. self.assertRaises(NameNotFoundError, context.lookup, 'sub/a') def test_lookup(self): """ pyfs context lookup """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Bind a file object. f = File(os.path.join(sub.path, 'foo.py')) #f.create_file('print "foo!"\n') context.bind('sub/foo.py', f) self.assertEqual(len(sub.list_bindings('')), 1) # Look it up. self.assertEqual(context.lookup('sub/foo.py').path, f.path) # Bind a Python object. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 2) # Look it up. self.assertEqual(context.lookup('sub/a'), 1) # Looking up the Empty name returns the context itself. self.assertEqual(context.lookup(''), context) # Non-existent name. self.assertRaises(NameNotFoundError, context.lookup, 'sub/b') return def test_create_subcontext(self): """ pyfs context create sub-context """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.create_subcontext, '') # Create a sub-context. a = context.create_subcontext('sub/a') self.assertEqual(len(sub.list_bindings('')), 1) self.assert_(os.path.isdir(os.path.join(sub.path, 'a'))) # Try to bind it again. self.assertRaises( NameAlreadyBoundError, context.create_subcontext, 'sub/a' ) return def test_destroy_subcontext(self): """ single context destroy sub-context """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.destroy_subcontext, '') # Create a sub-context. a = context.create_subcontext('sub/a') self.assertEqual(len(sub.list_bindings('')), 1) # Destroy it. context.destroy_subcontext('sub/a') self.assertEqual(len(sub.list_bindings('')), 0) self.assert_(not os.path.isdir(os.path.join(sub.path, 'a'))) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Try to destroy it. self.assertRaises( NotContextError, context.destroy_subcontext, 'sub/a' ) return # Try to destroy a non-existent name. self.assertRaises( NameNotFoundError, context.destroy_subcontext, 'sub/b' ) return def test_get_attributes(self): """ get attributes """ # Convenience. context = self.context sub = self.context.lookup('sub') self.assert_(isinstance(sub, DirContext)) #### Generic name resolution tests #### # Non-existent name. self.assertRaises(NameNotFoundError, context.get_attributes, 'xx') # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.get_attributes,'xx/a') # Attempt to resolve via an existing name that is not a context. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) self.assertRaises(NotContextError,context.get_attributes,'sub/a/x') #### Operation specific tests #### # Attributes of the root context. attributes = context.get_attributes('') self.assertEqual(len(attributes), 0) # Attributes of a sub-context. attributes = context.get_attributes('sub') self.assertEqual(len(attributes), 0) return def test_set_get_attributes(self): """ get and set attributes """ defaults = {'colour' : 'blue'} # Convenience. context = self.context sub = self.context.lookup('sub') self.assert_(isinstance(sub, DirContext)) #### Generic name resolution tests #### # Non-existent name. self.assertRaises( NameNotFoundError, context.set_attributes, 'xx', defaults ) # Attempt to resolve via a non-existent context. self.assertRaises( NameNotFoundError, context.set_attributes, 'xx/a', defaults ) # Attempt to resolve via an existing name that is not a context. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) self.assertRaises( NotContextError, context.set_attributes, 'sub/a/xx', defaults ) #### Operation specific tests #### # Attributes of the root context. attributes = self.context.get_attributes('') self.assertEqual(len(attributes), 0) # Set the attributes. context.set_attributes('', defaults) attributes = context.get_attributes('') self.assertEqual(len(attributes), 1) self.assertEqual(attributes['colour'], 'blue') # Attributes of a sub-context. attributes = context.get_attributes('sub') self.assertEqual(len(attributes), 0) # Set the attributes. context.set_attributes('sub', defaults) attributes = context.get_attributes('sub') self.assertEqual(len(attributes), 1) self.assertEqual(attributes['colour'], 'blue') return def test_namespace_name(self): """ get the name of a context within its namespace. """ # Convenience. context = self.context sub = self.context.lookup('sub') self.assert_(isinstance(sub, DirContext)) self.assertEqual(context.namespace_name, 'data') self.assertEqual(sub.namespace_name, 'data/sub') return #### EOF ###################################################################### apptools-4.5.0/apptools/naming/tests/py_context_test_case.py0000644000076500000240000000244611640354733025732 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Tests the Python namespace context. """ # Enthought library imports. from apptools.naming.api import PyContext # Local imports. from context_test_case import ContextTestCase class PyContextTestCase(ContextTestCase): """ Tests the Python namespace context. """ ########################################################################### # 'ContextTestCase' interface. ########################################################################### def create_context(self): """ Creates the context that we are testing. """ return PyContext(namespace={}) #### EOF ###################################################################### apptools-4.5.0/apptools/naming/tests/context_test_case.py0000644000076500000240000004106513422276145025222 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Tests operations that span contexts. """ # Standard library imports. import unittest # Enthought library imports. from apptools.naming.api import * class ContextTestCase(unittest.TestCase): """ Tests naming operations that span contexts. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.context = self.create_context() self.context.create_subcontext('sub') return def tearDown(self): """ Called immediately after each test method has been called. """ self.context = None return ########################################################################### # 'ContextTestCase' interface. ########################################################################### def create_context(self): """ Creates the context that we are testing. """ return Context() ########################################################################### # Tests. ########################################################################### def test_bind(self): """ bind """ # Convenience. context = self.context sub = self.context.lookup('sub') self.assert_(isinstance(sub, Context)) # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.bind, '', 1) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.bind, 'xx/a', 1) self.assertEqual(len(sub.list_bindings('')), 0) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.bind, 'sub/a/xx', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Try to bind it again. self.assertRaises(NameAlreadyBoundError, context.bind, 'sub/a', 1) return def test_bind_with_make_contexts(self): """ bind with make contexts """ # Convenience. context = self.context sub = self.context.lookup('sub') self.assert_(isinstance(sub, Context)) # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.bind, '', 1, True) # Attempt to resolve via a non-existent context - which should result # in the context being created automatically. context.bind('xx/a', 1, True) self.assertEqual(len(context.list_bindings('xx')), 1) self.assertEqual(1, context.lookup('xx/a')) # Bind an even more 'nested' name. context.bind('xx/foo/bar/baz', 42, True) self.assertEqual(len(context.list_bindings('xx/foo/bar')), 1) self.assertEqual(42, context.lookup('xx/foo/bar/baz')) return def test_rebind(self): """ context rebind """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.rebind, '', 1) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.rebind, 'xx/a', 1) self.assertEqual(len(sub.list_bindings('')), 0) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Rebind it. context.rebind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.rebind, 'sub/a/xx', 1) self.assertEqual(len(sub.list_bindings('')), 1) return def test_rebind_with_make_contexts(self): """ rebind with make contexts """ # Convenience. context = self.context sub = self.context.lookup('sub') self.assert_(isinstance(sub, Context)) # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.rebind, '', 1, True) # Attempt to resolve via a non-existent context - which should result # in the context being created automatically. context.rebind('xx/a', 1, True) self.assertEqual(len(context.list_bindings('xx')), 1) self.assertEqual(1, context.lookup('xx/a')) # Rebind an even more 'nested' name. context.rebind('xx/foo/bar/baz', 42, True) self.assertEqual(len(context.list_bindings('xx/foo/bar')), 1) self.assertEqual(42, context.lookup('xx/foo/bar/baz')) # And do it again... (this is REbind after all). context.rebind('xx/foo/bar/baz', 42, True) self.assertEqual(len(context.list_bindings('xx/foo/bar')), 1) self.assertEqual(42, context.lookup('xx/foo/bar/baz')) return def test_unbind(self): """ context unbind """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.unbind, '') # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.unbind, 'xx/a') self.assertEqual(len(sub.list_bindings('')), 0) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.unbind, 'sub/a/xx') self.assertEqual(len(sub.list_bindings('')), 1) # Unbind it. context.unbind('sub/a') self.assertEqual(len(sub.list_bindings('')), 0) # Try to unbind a non-existent name. self.assertRaises(NameNotFoundError, context.unbind, 'sub/b') return def test_rename_object(self): """ rename an object """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.rename, '', 'x') self.assertRaises(InvalidNameError, context.rename, 'x', '') # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.rename, 'x/a', 'x/b') self.assertEqual(len(sub.list_bindings('')), 0) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.bind, 'sub/a/xx', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Rename it. context.rename('sub/a', 'sub/b') self.assertEqual(len(sub.list_bindings('')), 1) # Lookup using the new name. self.assertEqual(context.lookup('sub/b'), 1) # Lookup using the old name. self.assertRaises(NameNotFoundError, context.lookup, 'sub/a') return def test_rename_context(self): """ rename a context """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.rename, '', 'x') self.assertRaises(InvalidNameError, context.rename, 'x', '') # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.rename, 'x/a', 'x/b') self.assertEqual(len(sub.list_bindings('')), 0) # Create a context. context.create_subcontext('sub/a') self.assertEqual(len(sub.list_bindings('')), 1) # Rename it. context.rename('sub/a', 'sub/b') self.assertEqual(len(sub.list_bindings('')), 1) # Lookup using the new name. context.lookup('sub/b') # Lookup using the old name. self.assertRaises(NameNotFoundError, context.lookup, 'sub/a') return def test_lookup(self): """ lookup """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.lookup, 'xx/a') self.assertEqual(len(sub.list_bindings('')), 0) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.lookup, 'sub/a/xx') self.assertEqual(len(sub.list_bindings('')), 1) # Look it up. self.assertEqual(context.lookup('sub/a'), 1) # Looking up the Empty name returns the context itself. self.assertEqual(context.lookup(''), context) # Non-existent name. self.assertRaises(NameNotFoundError, context.lookup, 'sub/b') return def test_create_subcontext(self): """ create sub-context """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.create_subcontext, '') # Attempt to resolve via a non-existent context. self.assertRaises( NameNotFoundError, context.create_subcontext, 'xx/a' ) self.assertEqual(len(sub.list_bindings('')), 0) # Create a sub-context. context.create_subcontext('sub/a') self.assertEqual(len(sub.list_bindings('')), 1) # Try to bind it again. self.assertRaises( NameAlreadyBoundError, context.create_subcontext, 'sub/a' ) # Bind a name. context.bind('sub/b', 1) self.assertEqual(len(sub.list_bindings('')), 2) # Attempt to resolve via an existing name that is not a context. self.assertRaises( NotContextError, context.create_subcontext, 'sub/b/xx' ) self.assertEqual(len(sub.list_bindings('')), 2) return def test_destroy_subcontext(self): """ single context destroy sub-context """ # Convenience. context = self.context sub = self.context.lookup('sub') # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings('')), 0) # Empty name. self.assertRaises(InvalidNameError, context.destroy_subcontext, '') # Attempt to resolve via a non-existent context. self.assertRaises( NameNotFoundError, context.destroy_subcontext, 'xx/a' ) self.assertEqual(len(sub.list_bindings('')), 0) # Create a sub-context. context.create_subcontext('sub/a') self.assertEqual(len(sub.list_bindings('')), 1) # Destroy it. context.destroy_subcontext('sub/a') self.assertEqual(len(sub.list_bindings('')), 0) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Try to destroy it. self.assertRaises( NotContextError, context.destroy_subcontext, 'sub/a' ) # Try to destroy a non-existent name. self.assertRaises( NameNotFoundError, context.destroy_subcontext, 'sub/b' ) # Attempt to resolve via an existing name that is not a context. self.assertRaises( NotContextError, context.destroy_subcontext, 'sub/a/xx' ) self.assertEqual(len(sub.list_bindings('')), 1) return def test_list_bindings(self): """ list bindings """ # Convenience. context = self.context sub = self.context.lookup('sub') # List the bindings in the root. bindings = context.list_bindings('') self.assertEqual(len(bindings), 1) # List the names in the sub-context. bindings = context.list_bindings('sub') self.assertEqual(len(bindings), 0) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.list_bindings, 'xx/a') self.assertEqual(len(sub.list_bindings('')), 0) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises( NotContextError, context.list_bindings, 'sub/a/xx' ) self.assertEqual(len(sub.list_bindings('')), 1) return def test_list_names(self): """ list names """ # Convenience. context = self.context sub = self.context.lookup('sub') # List the names in the root. names = context.list_names('') self.assertEqual(len(names), 1) # List the names in the sub-context. names = context.list_names('sub') self.assertEqual(len(names), 0) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.list_names, 'xx/a') self.assertEqual(len(sub.list_bindings('')), 0) # Bind a name. context.bind('sub/a', 1) self.assertEqual(len(sub.list_bindings('')), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises( NotContextError, context.list_names, 'sub/a/xx' ) self.assertEqual(len(sub.list_bindings('')), 1) return def test_default_factories(self): """ default object and state factories. """ object_factory = ObjectFactory() self.assertRaises( NotImplementedError, object_factory.get_object_instance, 0, 0, 0 ) state_factory = StateFactory() self.assertRaises( NotImplementedError, state_factory.get_state_to_bind, 0, 0, 0 ) return def test_search(self): """ test retrieving the names of bound objects """ # Convenience. context = self.context sub = self.context.lookup('sub') sub_sibling = context.create_subcontext('sub sibling') sub_sub = sub.create_subcontext('sub sub') context.bind('one',1) names = context.search( 1 ) self.assertEqual( len(names), 1) self.assertEqual( names[0], 'one' ) names = sub.search(1) self.assertEqual( len(names), 0) context.bind('sub/two',2) names = context.search( 2 ) self.assertEqual( len(names), 1) self.assertEqual( names[0], 'sub/two' ) names = sub.search( 2 ) self.assertEqual( len(names), 1) self.assertEqual( names[0], 'two' ) context.bind('sub/sub sub/one',1) names = context.search( 1 ) self.assertEqual( len(names), 2) self.assertEqual( sorted(names), sorted(['one', 'sub/sub sub/one']) ) names = sub.search(None) self.assertEqual( len(names), 0) names = context.search( sub_sub ) self.assertEqual( len(names), 1) self.assertEqual( names[0], 'sub/sub sub' ) return #### EOF ###################################################################### apptools-4.5.0/apptools/naming/__init__.py0000644000076500000240000000147113547637361022107 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Manages naming contexts. Supports non-string data types and scoped preferences. Part of the AppTools project of the Enthought Tool Suite. """ from apptools.naming.api import * apptools-4.5.0/apptools/naming/py_object_factory.py0000644000076500000240000000342613547637361024057 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Object factory for Python namespace contexts. """ # Local imports. from .object_factory import ObjectFactory from .reference import Reference class PyObjectFactory(ObjectFactory): """ Object factory for Python namespace contexts. """ ########################################################################### # 'ObjectFactory' interface. ########################################################################### def get_object_instance(self, state, name, context): """ Creates an object using the specified state information. """ obj = None if isinstance(state, Reference): if len(state.addresses) > 0: if state.addresses[0].type == 'py_context': namespace = state.addresses[0].content obj = context._context_factory(name, namespace) elif hasattr(state, '__dict__'): from apptools.naming.py_context import PyContext if not isinstance(state, PyContext): obj = context._context_factory(name, state) return obj ### EOF ####################################################################### apptools-4.5.0/apptools/naming/pyfs_context_factory.py0000644000076500000240000000310113547637361024614 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Object factory for Python File System contexts. """ # Local imports. from .object_factory import ObjectFactory from .reference import Reference class PyFSContextFactory(ObjectFactory): """ Object factory for Python File System contexts. """ ########################################################################### # 'ObjectFactory' interface. ########################################################################### def get_object_instance(self, state, name, context): """ Creates an object using the specified state information. """ obj = None if isinstance(state, Reference): if len(state.addresses) > 0: if state.addresses[0].type == 'pyfs_context': path = state.addresses[0].content obj = context._context_factory(name, path) return obj ### EOF ####################################################################### apptools-4.5.0/apptools/naming/adapter/0000755000076500000240000000000013547652535021413 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/naming/adapter/dict_context_adapter_factory.py0000644000076500000240000000211413547637361027701 0ustar mdickinsonstaff00000000000000""" Context adapter factory for Python dictionaries. """ # Enthought library imports. from apptools.naming.api import ContextAdapterFactory # Local imports. from .dict_context_adapter import DictContextAdapter class DictContextAdapterFactory(ContextAdapterFactory): """ Context adapter factory for Python dictionaries. """ #### 'ContextAdapterFactory' interface #################################### # The type of object that we adapt. adaptee_class = dict ########################################################################### # Protected 'AbstractAdapterFactory' interface. ########################################################################### def _adapt(self, adaptee, target_class, environment, context): """ Returns an adapter that adapts an object to the target class. """ adapter = DictContextAdapter( adaptee = adaptee, environment = environment, context = context ) return adapter #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/list_context_adapter.py0000644000076500000240000001172511640354733026201 0ustar mdickinsonstaff00000000000000""" Context adapter for Python lists. """ # Enthought library imports. from apptools.naming.api import Binding, ContextAdapter, naming_manager from traits.api import List, Property class ListContextAdapter(ContextAdapter): """ Context adapter for Python lists. """ #### 'ContextAdapter' interface ########################################### # The object that we are adapting. # # fixme: We would like to specialize the 'adaptee' trait here, but if we # make it of type 'List' then, on assignment, traits will create a *copy* # of the actual list which I think you'll agree is not very adapter-like! ## adaptee = List #### 'ListContextAdapter' interface ####################################### # The list that we are adapting. collection = Property(List) ########################################################################### # Protected 'Context' interface. ########################################################################### def _is_bound(self, name): """ Is a name bound in this context? """ return name in self._list_names() def _lookup(self, name): """ Looks up a name in this context. """ binding = self._get_binding_with_name(name) return naming_manager.get_object_instance(binding.obj, name, self) def _bind(self, name, obj): """ Binds a name to an object in this context. """ state = naming_manager.get_state_to_bind(obj, name, self) self.collection.append(state) return def _rebind(self, name, obj): """ Rebinds a name to an object in this context. """ index = 0 for binding in self.list_bindings(''): if binding.name == name: self.collection[index] = obj break index = index + 1 # The name is not already bound. else: self._bind(name, obj) return def _unbind(self, name): """ Unbinds a name from this context. """ index = 0 for binding in self.list_bindings(''): if binding.name == name: del self.collection[index] break index = index + 1 else: raise SystemError('no binding with name %s' % name) return def _rename(self, old_name, new_name): """ Renames an object in this context. """ binding = self._get_binding_with_name(old_name) self._set_name(binding.obj, new_name) return def _create_subcontext(self, name): """ Creates a sub-context of this context. """ raise OperationNotSupportedError() def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ raise OperationNotSupportedError() def _list_bindings(self): """ Lists the bindings in this context. """ bindings = [] for obj in self.collection: # Bindings have to have a string name. name = self._get_name(obj) # Create the binding. bindings.append(Binding(name=name, obj=obj, context=self)) return bindings def _list_names(self): """ Lists the names bound in this context. """ return [self._get_name(obj) for obj in self.collection] ########################################################################### # Protected 'ListContext' interface. ########################################################################### def _get_collection(self): """ Returns the collection that we are adapting. """ return self.adaptee ########################################################################### # Private interface. ########################################################################### # fixme: Allow an item name trait to be specified instead of guessing at # 'name' or 'id'! def _get_name(self, obj): """ Returns the name of an object. """ if hasattr(obj, 'name'): name = str(obj.name) elif hasattr(obj, 'id'): name = str(obj.id) else: name = str(obj) return name def _set_name(self, obj, name): """ Sets the name of an object. """ if hasattr(obj, 'name'): obj.name = name elif hasattr(obj, 'id'): obj.id = name return def _get_binding_with_name(self, name): """ Returns the binding with the specified name. """ for binding in self.list_bindings(''): if binding.name == name: break # The reason that this is a system error and not just a naming error # is that this method is only called from inside the protected # 'Context' interface when we have already determined that the name # is bound else: raise SystemError('no binding with name %s' % name) return binding #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/dict_context_adapter.py0000644000076500000240000000564411640354733026154 0ustar mdickinsonstaff00000000000000""" Context adapter for Python dictionaries. """ # Enthought library imports. from apptools.naming.api import Binding, ContextAdapter, naming_manager class DictContextAdapter(ContextAdapter): """ Context adapter for Python dictionaries. """ #### 'ContextAdapter' interface ########################################### # The object that we are adapting. # # # fixme: We would like to specialize the 'adaptee' trait here, but if we # make it of type 'Dict' then, on assignment, traits will create a *copy* # of the actual dict which I think you'll agree is not very adapter-like! ## adaptee = Dict ########################################################################### # Protected 'Context' interface. ########################################################################### def _is_bound(self, name): """ Is a name bound in this context? """ return name in self.adaptee def _lookup(self, name): """ Looks up a name in this context. """ obj = self.adaptee[name] return naming_manager.get_object_instance(obj, name, self) def _lookup_binding(self, name): """ Looks up the binding for a name in this context. """ return Binding(name=name, obj=self._lookup(name), context=self) def _bind(self, name, obj): """ Binds a name to an object in this context. """ state = naming_manager.get_state_to_bind(obj, name, self) self.adaptee[name] = state return def _rebind(self, name, obj): """ Rebinds a name to an object in this context. """ self._bind(name, obj) return def _unbind(self, name): """ Unbinds a name from this context. """ del self.adaptee[name] return def _rename(self, old_name, new_name): """ Renames an object in this context. """ # Bind the new name. self._bind(new_name, self._lookup(old_name)) # Unbind the old one. self._unbind(old_name) return def _create_subcontext(self, name): """ Creates a sub-context of this context. """ # Create a dictionary of the same type as the one we are adapting. sub = type(self.adaptee)() self.adaptee[name] = sub return DictContextAdapter(adaptee=sub) def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ del self.adaptee[name] return def _list_bindings(self): """ Lists the bindings in this context. """ bindings = [] for key in self.adaptee: bindings.append( Binding(name=str(key), obj=self._lookup(key), context=self) ) return bindings def _list_names(self): """ Lists the names bound in this context. """ return [str(key) for key in self.adaptee] #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/instance_context_adapter_factory.py0000644000076500000240000000352013547637361030564 0ustar mdickinsonstaff00000000000000""" Context adapter factory for Python instances. """ # Enthought library imports. from apptools.naming.api import ContextAdapterFactory from traits.api import List, Str from apptools.type_manager import PythonObject # Local imports. from .instance_context_adapter import InstanceContextAdapter class InstanceContextAdapterFactory(ContextAdapterFactory): """ Context adapter factoryfor Python instances. """ #### 'ContextAdapterFactory' interface #################################### # The type of object that we adapt. adaptee_class = PythonObject #### 'InstanceContextAdapterFactory' interface ############################ # By default every public attribute of an instance is exposed. Use the # following traits to either include or exclude attributes as appropriate. # # Regular expressions that describe the names of attributes to include. include = List(Str) # Regular expressions that describe the names of attributes to exclude. By # default we exclude 'protected' and 'private' attributes and any # attributes that are artifacts of the traits mechanism. exclude = List(Str, ['_', 'trait_']) ########################################################################### # Protected 'AbstractAdapterFactory' interface. ########################################################################### def _adapt(self, adaptee, target_class, environment, context): """ Returns an adapter that adapts an object to the target class. """ adapter = InstanceContextAdapter( adaptee = adaptee, environment = environment, context = context, include = self.include, exclude = self.exclude ) return adapter #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/instance_context_adapter.py0000644000076500000240000001247713547637361027050 0ustar mdickinsonstaff00000000000000""" Context adapter for Python instances. """ # Standard library imports. import re # Enthought library imports. from apptools.naming.api import Binding, ContextAdapter from apptools.naming.api import OperationNotSupportedError, naming_manager from traits.api import HasTraits, List, Property, Str class InstanceContextAdapter(ContextAdapter): """ Context adapter for Python instances. """ #### 'Context' interface ################################################## # The name of the context within its own namespace. namespace_name = Property(Str) #### 'InstanceContextAdapter' interface ################################### # By default every public attribute of an instance is exposed. Use the # following traits to either include or exclude attributes as appropriate. # # Regular expressions that describe the names of attributes to include. include = List(Str) # Regular expressions that describe the names of attributes to exclude. By # default we exclude 'protected' and 'private' attributes and any # attributes that are artifacts of the traits mechanism. exclude = List(Str, ['_', 'trait_']) ########################################################################### # 'Context' interface. ########################################################################### #### Properties ########################################################### def _get_namespace_name(self): """ Returns the name of the context within its own namespace. """ base = self.context.namespace_name if len(base) > 0: base += '/' names = self.context.search(self.adaptee) return base + names[0] ########################################################################### # Protected 'Context' interface. ########################################################################### def _is_bound(self, name): """ Is a name bound in this context? """ return name in self._list_names() def _lookup(self, name): """ Looks up a name in this context. """ obj = getattr(self.adaptee, name) return naming_manager.get_object_instance(obj, name, self) def _lookup_binding(self, name): """ Looks up the binding for a name in this context. """ return Binding(name=name, obj=self._lookup(name), context=self) def _bind(self, name, obj): """ Binds a name to an object in this context. """ state = naming_manager.get_state_to_bind(obj, name, self) setattr(self.adaptee, name, state) return def _rebind(self, name, obj): """ Rebinds a name to an object in this context. """ self._bind(name, obj) return def _unbind(self, name): """ Unbinds a name from this context. """ delattr(self.adaptee, name) return def _rename(self, old_name, new_name): """ Renames an object in this context. """ # Bind the new name. setattr(self.adaptee, new_name, self._lookup(old_name)) # Unbind the old one. delattr(self.adaptee, old_name) return def _create_subcontext(self, name): """ Creates a sub-context of this context. """ raise OperationNotSupportedError() def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ raise OperationNotSupportedError() def _list_bindings(self): """ Lists the bindings in this context. """ bindings = [] for name in self._list_names(): try: obj = self._lookup(name) bindings.append(Binding(name=name, obj=obj, context=self)) # We get attribute errors when we try to look up Event traits (they # are write-only). except AttributeError: pass return bindings def _list_names(self): """ Lists the names bound in this context. """ return self._get_public_attribute_names(self.adaptee) ########################################################################### # Private interface. ########################################################################### def _get_public_attribute_names(self, obj): """ Returns the names of an object's public attributes. """ if isinstance(obj, HasTraits): names = obj.trait_names() elif hasattr(obj, '__dict__'): names = list(self.adaptee.__dict__.keys()) else: names = [] return [name for name in names if self._is_exposed(name)] def _is_exposed(self, name): """ Returns True iff a name should be exposed. """ if len(self.include) > 0: is_exposed = self._matches(self.include, name) elif len(self.exclude) > 0: is_exposed = not self._matches(self.exclude, name) else: is_exposed = True return is_exposed def _matches(self, expressions, name): """ Returns True iff a name matches any of a list of expressions. """ for expression in expressions: if re.match(expression, name) is not None: matches = True break else: matches = False return matches #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/__init__.py0000644000076500000240000000005213547637361023521 0ustar mdickinsonstaff00000000000000from apptools.naming.adapter.api import * apptools-4.5.0/apptools/naming/adapter/tuple_context_adapter_factory.py0000644000076500000240000000210313547637361030105 0ustar mdickinsonstaff00000000000000""" Context adapter factory for Python tuple. """ # Enthought library imports. from apptools.naming.api import ContextAdapterFactory # Local imports. from .tuple_context_adapter import TupleContextAdapter class TupleContextAdapterFactory(ContextAdapterFactory): """ Context adapter factoryfor Python tuples. """ #### 'ContextAdapterFactory' interface #################################### # The type of object that we adapt. adaptee_class = tuple ########################################################################### # Protected 'AbstractAdapterFactory' interface. ########################################################################### def _adapt(self, adaptee, target_class, environment, context): """ Returns an adapter that adapts an object to the target class. """ adapter = TupleContextAdapter( adaptee = adaptee, environment = environment, context = context ) return adapter #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/trait_list_context_adapter_factory.py0000644000076500000240000000253713547637361031145 0ustar mdickinsonstaff00000000000000""" Context adapter factory for trait lists. """ # Enthought library imports. from apptools.naming.api import ContextAdapterFactory from traits.api import Str, TraitList # Local imports. from .trait_list_context_adapter import TraitListContextAdapter class TraitListContextAdapterFactory(ContextAdapterFactory): """ Context adapter factoryfor Python trait lists. """ #### 'ContextAdapterFactory' interface #################################### # The type of object that we adapt. adaptee_class = TraitList #### 'TraitListContextAdapterFactory' interface ########################### # The name of the trait (on the adaptee) that provides the trait list. trait_name = Str ########################################################################### # Protected 'AbstractAdapterFactory' interface. ########################################################################### def _adapt(self, adaptee, target_class, environment, context): """ Returns an adapter that adapts an object to the target class. """ adapter = TraitListContextAdapter( adaptee = adaptee, environment = environment, context = context, trait_name = self.trait_name ) return adapter #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/list_context_adapter_factory.py0000644000076500000240000000207613547637361027740 0ustar mdickinsonstaff00000000000000""" Context adapter factory for Python lists. """ # Enthought library imports. from apptools.naming.api import ContextAdapterFactory # Local imports. from .list_context_adapter import ListContextAdapter class ListContextAdapterFactory(ContextAdapterFactory): """ Context adapter factory for Python lists. """ #### 'ContextAdapterFactory' interface #################################### # The type of object that we adapt. adaptee_class = list ########################################################################### # Protected 'AbstractAdapterFactory' interface. ########################################################################### def _adapt(self, adaptee, target_class, environment, context): """ Returns an adapter that adapts an object to the target class. """ adapter = ListContextAdapter( adaptee = adaptee, environment = environment, context = context ) return adapter #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/api.py0000644000076500000240000000131013547637361022531 0ustar mdickinsonstaff00000000000000from .dict_context_adapter import DictContextAdapter from .dict_context_adapter_factory import DictContextAdapterFactory from .instance_context_adapter import InstanceContextAdapter from .instance_context_adapter_factory import InstanceContextAdapterFactory from .list_context_adapter import ListContextAdapter from .list_context_adapter_factory import ListContextAdapterFactory from .trait_list_context_adapter import TraitListContextAdapter from .trait_list_context_adapter_factory import TraitListContextAdapterFactory from .tuple_context_adapter import TupleContextAdapter from .tuple_context_adapter_factory import TupleContextAdapterFactory from .trait_dict_context_adapter import TraitDictContextAdapter apptools-4.5.0/apptools/naming/adapter/trait_dict_context_adapter.py0000644000076500000240000000231413547637361027357 0ustar mdickinsonstaff00000000000000""" Context adapter for trait dictionaries. """ # Enthought library imports. from traits.api import Dict, Property, Str # Local imports. from .dict_context_adapter import DictContextAdapter class TraitDictContextAdapter(DictContextAdapter): """ Context adapter for trait dictionaries. """ #### 'Context' interface ################################################## # The name of the context within its own namespace. namespace_name = Property(Str) #### 'ContextAdapter' interface ########################################### # The object that we are adapting. adaptee = Dict #### 'TraitDictContextAdapter' interface ################################## # The name of the object's trait that provides the dictionary. trait_name = Str ########################################################################### # 'Context' interface. ########################################################################### def _get_namespace_name(self): """ Returns the name of the context within its own namespace. """ return self.context.namespace_name + '/' + self.trait_name #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/trait_list_context_adapter.py0000644000076500000240000000416013547637361027410 0ustar mdickinsonstaff00000000000000""" Context adapter for trait lists. """ # Enthought library imports. from traits.api import Any, List, Property, Str # Local imports. from .list_context_adapter import ListContextAdapter class TraitListContextAdapter(ListContextAdapter): """ Context adapter for trait lists. """ #### 'Context' interface ################################################## # The name of the context within its own namespace. namespace_name = Property(Str) #### 'ContextAdapter' interface ########################################### # The object that we are adapting. adaptee = Any #### 'ListContextAdapter' interface ####################################### # The list that we are adapting. collection = Property(List) #### 'TraitListContextAdapter' interface ################################## # The name of the object's trait that provides the list. trait_name = Str ########################################################################### # 'Context' interface. ########################################################################### def _get_namespace_name(self): """ Returns the name of the context within its own namespace. """ return self.context.namespace_name + '/' + self.trait_name ########################################################################### # Protected 'ListContext' interface. ########################################################################### #### 'Properties' ######################################################### def _get_collection(self): """ Returns the collection that we are adapting. """ components = self.trait_name.split('.') if len(components) == 1: collection = getattr(self.adaptee, self.trait_name) else: # Find the object that contains the trait. obj = self.adaptee for component in components[:-1]: obj = getattr(obj, component) collection = getattr(obj, components[-1]) return collection #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/trait_dict_context_adapter_factory.py0000644000076500000240000000253713547637361031115 0ustar mdickinsonstaff00000000000000""" Context adapter factory for trait dicts. """ # Enthought library imports. from apptools.naming.api import ContextAdapterFactory from traits.api import Str, TraitDict # Local imports. from .trait_dict_context_adapter import TraitDictContextAdapter class TraitDictContextAdapterFactory(ContextAdapterFactory): """ Context adapter factoryfor Python trait dicts. """ #### 'ContextAdapterFactory' interface #################################### # The type of object that we adapt. adaptee_class = TraitDict #### 'TraitDictContextAdapterFactory' interface ########################### # The name of the trait (on the adaptee) that provides the trait dict. trait_name = Str ########################################################################### # Protected 'AbstractAdapterFactory' interface. ########################################################################### def _adapt(self, adaptee, target_class, environment, context): """ Returns an adapter that adapts an object to the target class. """ adapter = TraitDictContextAdapter( adaptee = adaptee, environment = environment, context = context, trait_name = self.trait_name ) return adapter #### EOF ###################################################################### apptools-4.5.0/apptools/naming/adapter/tuple_context_adapter.py0000644000076500000240000000075413547637361026370 0ustar mdickinsonstaff00000000000000""" Context adapter for Python tuples. """ # Enthought library imports. from traits.api import Tuple # Local imports. from .list_context_adapter import ListContextAdapter class TupleContextAdapter(ListContextAdapter): """ Context adapter for Python tuples. """ #### 'ContextAdapter' interface ########################################### # The object that we are adapting. adaptee = Tuple #### EOF ###################################################################### apptools-4.5.0/apptools/naming/pyfs_context.py0000644000076500000240000004501613547637361023100 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A Python File System context. """ # Standard library imports. import glob import logging import os from os.path import join, splitext # Third-party library imports. import six.moves.cPickle as pickle # Enthought library imports. from apptools.io.api import File from traits.api import Any, Dict, Instance, Property, Str # Local imports. from .address import Address from .binding import Binding from .context import Context from .dir_context import DirContext from .exception import NameNotFoundError, NotContextError from .naming_event import NamingEvent from .naming_manager import naming_manager from .object_serializer import ObjectSerializer from .pyfs_context_factory import PyFSContextFactory from .pyfs_object_factory import PyFSObjectFactory from .pyfs_state_factory import PyFSStateFactory from .reference import Reference from .referenceable import Referenceable # Setup a logger for this module. logger = logging.getLogger(__name__) # The name of the 'special' file in which we store object attributes. ATTRIBUTES_FILE = '__attributes__' # Constants for environment property keys. FILTERS = "apptools.naming.pyfs.filters" OBJECT_SERIALIZERS = "apptools.naming.pyfs.object.serializers" # The default environment. ENVIRONMENT = { #### 'Context' properties ################################################# # Object factories. Context.OBJECT_FACTORIES : [PyFSObjectFactory(), PyFSContextFactory()], # State factories. Context.STATE_FACTORIES : [PyFSStateFactory()], #### 'PyFSContext' properties ############################################# # Object serializers. OBJECT_SERIALIZERS : [ObjectSerializer()], # List of filename patterns to ignore. These patterns are passed to # 'glob.glob', so things like '*.pyc' will do what you expect. # # fixme: We should have a generalized filter mechanism here, and '.svn' # should be moved elsewhere! FILTERS : [ATTRIBUTES_FILE, '.svn'] } class PyFSContext(DirContext, Referenceable): """ A Python File System context. This context represents a directory on a local file system. """ # The name of the 'special' file in which we store object attributes. ATTRIBUTES_FILE = ATTRIBUTES_FILE # Environment property keys. FILTERS = FILTERS OBJECT_SERIALIZERS = OBJECT_SERIALIZERS #### 'Context' interface ################################################## # The naming environment in effect for this context. environment = Dict(ENVIRONMENT) # The name of the context within its own namespace. namespace_name = Property(Str) #### 'PyFSContext' interface ############################################## # The name of the context (the last component of the path). name = Str # The path name of the directory on the local file system. path = Str #### 'Referenceable' interface ############################################ # The object's reference suitable for binding in a naming context. reference = Property(Instance(Reference)) #### Private interface #################################################### # A mapping from bound name to the name of the corresponding file or # directory on the file system. _name_to_filename_map = Dict#(Str, Str) # The attributes of every object in the context. The attributes for the # context itself have the empty string as the key. # # {str name : dict attributes} # # fixme: Don't use 'Dict' here as it causes problems when pickling because # trait dicts have a reference back to the parent object (hence we end up # pickling all kinds of things that we don't need or want to!). _attributes = Any ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Creates a new context. """ # Base class constructor. super(PyFSContext, self).__init__(**traits) # We cache each object as it is looked up so that all accesses to a # serialized Python object return a reference to exactly the same one. self._cache = {} return ########################################################################### # 'PyFSContext' interface. ########################################################################### #### Properties ########################################################### def _get_namespace_name(self): """ Returns the name of the context within its own namespace. """ # fixme: clean this up with an initial context API! if 'root' in self.environment: root = self.environment['root'] namespace_name = self.path[len(root) + 1:] else: namespace_name = self.path # fixme: This is a bit dodgy 'cos we actually return a name that can # be looked up, and not the file system name... namespace_name = '/'.join(namespace_name.split(os.path.sep)) return namespace_name #### methods ############################################################## def refresh(self): """ Refresh the context to reflect changes in the file system. """ # fixme: This needs more work 'cos if we refresh a context then we # will load new copies of serialized Python objects! # This causes the initializer to run again the next time the trait is # accessed. self.reset_traits(['_name_to_filename_map']) # Clear out the cache. self._cache = {} # fixme: This is a bit hacky since the context in the binding may # not be None! self.context_changed = NamingEvent( new_binding=Binding(name=self.name, obj=self, context=None) ) return ########################################################################### # 'Referenceable' interface. ########################################################################### #### Properties ########################################################### def _get_reference(self): """ Returns a reference to this object suitable for binding. """ abspath = os.path.abspath(self.path) reference = Reference( class_name = self.__class__.__name__, addresses = [Address(type='pyfs_context', content=abspath)] ) return reference ########################################################################### # Protected 'Context' interface. ########################################################################### def _is_bound(self, name): """ Is a name bound in this context? """ return name in self._name_to_filename_map def _lookup(self, name): """ Looks up a name in this context. """ if name in self._cache: obj = self._cache[name] else: # Get the full path to the file. path = join(self.path, self._name_to_filename_map[name]) # If the file contains a serialized Python object then load it. for serializer in self._get_object_serializers(): if serializer.can_load(path): try: state = serializer.load(path) # If the load fails then we create a generic file resource # (the idea being that it might be useful to have access to # the file to see what went wrong). except: state = File(path) logger.exception('Error loading resource at %s' % path) break # Otherwise, it must just be a file or folder. else: # Directories are contexts. if os.path.isdir(path): state = self._context_factory(name, path) # Files are just files! elif os.path.isfile(path): state = File(path) else: raise ValueError('unrecognized file for %s' % name) # Get the actual object from the naming manager. obj = naming_manager.get_object_instance(state, name, self) # Update the cache. self._cache[name] = obj return obj def _bind(self, name, obj): """ Binds a name to an object in this context. """ # Get the actual state to bind from the naming manager. state = naming_manager.get_state_to_bind(obj, name, self) # If the object is actually an abstract file then we don't have to # do anything. if isinstance(state, File): if not state.exists: state.create_file() filename = name # Otherwise we are binding an arbitrary Python object, so find a # serializer for it. else: for serializer in self._get_object_serializers(): if serializer.can_save(obj): path = serializer.save(join(self.path, name), obj) filename = os.path.basename(path) break else: raise ValueError('cannot serialize object %s' % name) # Update the name to filename map. self._name_to_filename_map[name] = filename # Update the cache. self._cache[name] = obj return state def _rebind(self, name, obj): """ Rebinds a name to an object in this context. """ # We unbind first to make sure that the old file gets removed (this # is handy if the object that we are rebinding has a different # serializer than the current one). #self._unbind(name) self._bind(name, obj) return def _unbind(self, name): """ Unbinds a name from this context. """ # Get the full path to the file. path = join(self.path, self._name_to_filename_map[name]) # Remove it! f = File(path) f.delete() # Update the name to filename map. del self._name_to_filename_map[name] # Update the cache. if name in self._cache: del self._cache[name] # Remove any attributes. if name in self._attributes: del self._attributes[name] self._save_attributes() return def _rename(self, old_name, new_name): """ Renames an object in this context. """ # Get the old filename. old_filename = self._name_to_filename_map[old_name] old_file = File(join(self.path, old_filename)) # Lookup the object bound to the old name. This has the side effect # of adding the object to the cache under the name 'old_name'. obj = self._lookup(old_name) # We are renaming a LOCAL context (ie. a folder)... if old_file.is_folder: # Create the new filename. new_filename = new_name new_file = File(join(self.path, new_filename)) # Move the folder. old_file.move(new_file) # Update the 'Context' object. obj.path = new_file.path # Update the cache. self._cache[new_name] = obj del self._cache[old_name] # Refreshing the context makes sure that all of its contents # reflect the new name (i.e., sub-folders and files have the # correct path). # # fixme: This currently results in new copies of serialized # Python objects! We need to be a bit more judicious in the # refresh. obj.refresh() # We are renaming a file... elif isinstance(obj, File): # Create the new filename. new_filename = new_name new_file = File(join(self.path, new_filename)) # Move the file. old_file.move(new_file) # Update the 'File' object. obj.path = new_file.path # Update the cache. self._cache[new_name] = obj del self._cache[old_name] # We are renaming a serialized Python object... else: # Create the new filename. new_filename = new_name + old_file.ext new_file = File(join(self.path, new_filename)) old_file.delete() # Update the cache. if old_name in self._cache: self._cache[new_name] = self._cache[old_name] del self._cache[old_name] # Force the creation of the new file. # # fixme: I'm not sure that this is really the place for this. We # do it because often the 'name' of the object is actually an # attribute of the object itself, and hence we want the serialized # state to reflect the new name... Hmmm... self._rebind(new_name, obj) # Update the name to filename map. del self._name_to_filename_map[old_name] self._name_to_filename_map[new_name] = new_filename # Move any attributes over to the new name. if old_name in self._attributes: self._attributes[new_name] = self._attributes[old_name] del self._attributes[old_name] self._save_attributes() return def _create_subcontext(self, name): """ Creates a sub-context of this context. """ path = join(self.path, name) # Create a directory. os.mkdir(path) # Create a sub-context that represents the directory. sub = self._context_factory(name, path) # Update the name to filename map. self._name_to_filename_map[name] = name # Update the cache. self._cache[name] = sub return sub def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ return self._unbind(name) def _list_names(self): """ Lists the names bound in this context. """ return list(self._name_to_filename_map.keys()) # fixme: YFI this is not part of the protected 'Context' interface so # what is it doing here? def get_unique_name(self, name): ext = splitext(name)[1] # specially handle '.py' files if ext != '.py': return super(PyFSContext, self).get_unique_name(name) body = splitext(name)[0] names = self.list_names() i = 2 unique = name while unique in names: unique = body + '_' + str(i) + '.py' i += 1 return unique ########################################################################### # Protected 'DirContext' interface. ########################################################################### def _get_attributes(self, name): """ Returns the attributes of an object in this context. """ attributes = self._attributes.setdefault(name, {}) return attributes.copy() def _set_attributes(self, name, attributes): """ Sets the attributes of an object in this context. """ self._attributes[name] = attributes self._save_attributes() return ########################################################################### # Private interface. ########################################################################### def _get_filters(self): """ Returns the filters for this context. """ return self.environment.get(self.FILTERS, []) def _get_object_serializers(self): """ Returns the object serializers for this context. """ return self.environment.get(self.OBJECT_SERIALIZERS, []) def _context_factory(self, name, path): """ Create a sub-context. """ return self.__class__(path=path, environment=self.environment) def _save_attributes(self): """ Saves all attributes to the attributes file. """ path = join(self.path, self.ATTRIBUTES_FILE) f = open(path, 'wb') pickle.dump(self._attributes, f, 1) f.close() return #### Trait initializers ################################################### def __name_to_filename_map_default(self): """ Initializes the '_name_to_filename' trait. """ # fixme: We should have a generalized filter mechanism (instead of # just 'glob' patterns we should have filter objects that can be a bit # more flexible in how they do the filtering). patterns = [join(self.path, filter) for filter in self._get_filters()] name_to_filename_map = {} for filename in os.listdir(self.path): path = join(self.path, filename) for pattern in patterns: if path in glob.glob(pattern): break else: for serializer in self._get_object_serializers(): if serializer.can_load(filename): # fixme: We should probably get the name from the # serializer instead of assuming that we can just # drop the file exension. name, ext = os.path.splitext(filename) break else: name = filename name_to_filename_map[name] = filename return name_to_filename_map def __attributes_default(self): """ Initializes the '_attributes' trait. """ attributes_file = File(join(self.path, self.ATTRIBUTES_FILE)) if attributes_file.is_file: f = open(attributes_file.path, 'rb') attributes = pickle.load(f) f.close() else: attributes = {} return attributes #### Trait event handlers ################################################# def _path_changed(self): """ Called when the context's path has changed. """ basename = os.path.basename(self.path) self.name, ext = os.path.splitext(basename) return #### EOF ###################################################################### apptools-4.5.0/apptools/naming/initial_context_factory.py0000644000076500000240000000253013547637361025271 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all initial context factories. """ # Enthought library imports. from traits.api import HasTraits # Local imports. from .context import Context class InitialContextFactory(HasTraits): """ The base class for all initial context factories. """ ########################################################################### # 'InitialContextFactory' interface. ########################################################################### def get_initial_context(self, environment): """ Creates an initial context for beginning name resolution. """ return Context(environment=environment) #### EOF ###################################################################### apptools-4.5.0/apptools/naming/context_adapter.py0000644000076500000240000000226513547637361023536 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all context adapters. """ # Enthought library imports. from traits.api import Any, Dict, Instance, Property, Str # Local imports. from .context import Context class ContextAdapter(Context): """ The base class for all context adapters. """ #### 'ContextAdapter' interface ########################################### # The object that we are adapting. adaptee = Any # The context that the object is in. context = Instance(Context) #### EOF ###################################################################### apptools-4.5.0/apptools/naming/naming_manager.py0000644000076500000240000000576313547637361023323 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The naming manager. """ # Enthought library imports. from traits.api import HasTraits class NamingManager(HasTraits): """ The naming manager. """ ########################################################################### # 'NamingManager' interface. ########################################################################### def get_state_to_bind(self, obj, name, context): """ Returns the state of an object for binding. The naming manager asks the context for its list of STATE factories and then calls them one by one until it gets a non-None result indicating that the factory recognised the object and created state information for it. If none of the factories recognize the object (or if the context has no factories) then the object itself is returned. """ # Local imports. from .context import Context # We get the state factories from the context's environment. state_factories = context.environment[Context.STATE_FACTORIES] for state_factory in state_factories: state = state_factory.get_state_to_bind(obj, name, context) if state is not None: break else: state = obj return state def get_object_instance(self, info, name, context): """ Creates an object using the specified state information. The naming manager asks the context for its list of OBJECT factories and calls them one by one until it gets a non-None result, indicating that the factory recognised the information and created an object. If none of the factories recognize the state information (or if the context has no factories) then the state information itself is returned. """ # Local imports. from .context import Context # We get the object factories from the context's environment. object_factories = context.environment[Context.OBJECT_FACTORIES] for object_factory in object_factories: obj = object_factory.get_object_instance(info, name, context) if obj is not None: break else: obj = info return obj # Singleton instance. naming_manager = NamingManager() ### EOF ####################################################################### apptools-4.5.0/apptools/naming/api.py0000644000076500000240000000354213547637361021122 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from .exception import NamingError, InvalidNameError, NameAlreadyBoundError from .exception import NameNotFoundError, NotContextError from .exception import OperationNotSupportedError from .address import Address from .binding import Binding from .context import Context from .context_adapter import ContextAdapter from .context_adapter_factory import ContextAdapterFactory from .dynamic_context import DynamicContext from .dir_context import DirContext from .initial_context import InitialContext from .initial_context_factory import InitialContextFactory from .naming_event import NamingEvent from .naming_manager import naming_manager from .object_factory import ObjectFactory from .object_serializer import ObjectSerializer from .py_context import PyContext from .py_object_factory import PyObjectFactory from .pyfs_context import PyFSContext from .pyfs_context_factory import PyFSContextFactory from .pyfs_initial_context_factory import PyFSInitialContextFactory from .pyfs_object_factory import PyFSObjectFactory from .pyfs_state_factory import PyFSStateFactory from .reference import Reference from .referenceable import Referenceable from .referenceable_state_factory import ReferenceableStateFactory from .state_factory import StateFactory apptools-4.5.0/apptools/naming/state_factory.py0000644000076500000240000000302111640354733023177 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all state factories. """ # Enthought library imports. from traits.api import HasPrivateTraits class StateFactory(HasPrivateTraits): """ The base class for all state factories. A state factory accepts an object and returns some data representing the object that is suitable for storing in a particular context. """ ########################################################################### # 'StateFactory' interface. ########################################################################### def get_state_to_bind(self, obj, name, context): """ Returns the state of an object for binding. Returns None if the factory cannot create the state (ie. it does not recognise the object passed to it). """ raise NotImplementedError ### EOF ####################################################################### apptools-4.5.0/apptools/naming/context.py0000644000076500000240000006032713547637361022041 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all naming contexts. """ # Enthought library imports. from traits.api import Any, Dict, Event, HasTraits, Instance from traits.api import Property, Str from apptools.type_manager.api import TypeManager # Local imports. from .binding import Binding from .exception import InvalidNameError, NameAlreadyBoundError from .exception import NameNotFoundError, NotContextError from .exception import OperationNotSupportedError from .naming_event import NamingEvent from .naming_manager import naming_manager from .object_factory import ObjectFactory from .state_factory import StateFactory from .unique_name import make_unique_name # Constants for environment property keys. INITIAL_CONTEXT_FACTORY = "apptools.naming.factory.initial" OBJECT_FACTORIES = "apptools.naming.factory.object" STATE_FACTORIES = "apptools.naming.factory.state" # Non-JNDI. TYPE_MANAGER = "apptools.naming.factory.type.manager" # The default environment. ENVIRONMENT = { # 'Context' properties. OBJECT_FACTORIES : [], STATE_FACTORIES : [], # Non-JNDI. TYPE_MANAGER : None, } class Context(HasTraits): """ The base class for all naming contexts. """ # Keys for environment properties. INITIAL_CONTEXT_FACTORY = INITIAL_CONTEXT_FACTORY OBJECT_FACTORIES = OBJECT_FACTORIES STATE_FACTORIES = STATE_FACTORIES # Non-JNDI. TYPE_MANAGER = TYPE_MANAGER #### 'Context' interface ################################################## # The naming environment in effect for this context. environment = Dict(ENVIRONMENT) # The name of the context within its own namespace. namespace_name = Property(Str) # The type manager in the context's environment (used to create context # adapters etc.). # # fixme: This is an experimental 'convenience' trait, since it is common # to get hold of the context's type manager to see if some object has a # context adapter. type_manager = Property(Instance(TypeManager)) #### Events #### # Fired when an object has been added to the context (either via 'bind' or # 'create_subcontext'). object_added = Event(NamingEvent) # Fired when an object has been changed (via 'rebind'). object_changed = Event(NamingEvent) # Fired when an object has been removed from the context (either via # 'unbind' or 'destroy_subcontext'). object_removed = Event(NamingEvent) # Fired when an object in the context has been renamed (via 'rename'). object_renamed = Event(NamingEvent) # Fired when the contents of the context have changed dramatically. context_changed = Event(NamingEvent) #### Protected 'Context' interface ####################################### # The bindings in the context. _bindings = Dict(Str, Any) ########################################################################### # 'Context' interface. ########################################################################### #### Properties ########################################################### def _get_namespace_name(self): """ Return the name of the context within its own namespace. That is the full-path, through the namespace this context participates in, to get to this context. For example, if the root context of the namespace was called 'Foo', and there was a subcontext of that called 'Bar', and we were within that and called 'Baz', then this should return 'Foo/Bar/Baz'. """ # FIXME: We'd like to raise an exception and force implementors to # decide what to do. However, it appears to be pretty common that # most Context implementations do not override this method -- possibly # because the comments aren't clear on what this is supposed to be? # # Anyway, if we raise an exception then it is impossible to use any # evaluations when building a Traits UI for a Context. That is, the # Traits UI can't include items that have a 'visible_when' or # 'enabled_when' evaluation. This is because the Traits evaluation # code calls the 'get()' method on the Context which attempts to # retrieve the current namespace_name value. #raise OperationNotSupportedError() return '' def _get_type_manager(self): """ Returns the type manager in the context's environment. This will return None if no type manager was used to create the initial context. """ return self.environment.get(self.TYPE_MANAGER) #### Methods ############################################################## def bind(self, name, obj, make_contexts=False): """ Binds a name to an object. If 'make_contexts' is True then any missing intermediate contexts are created automatically. """ if len(name) == 0: raise InvalidNameError('empty name') # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] # Is the name already bound? if self._is_bound(atom): raise NameAlreadyBoundError(name) # Do the actual bind. self._bind(atom, obj) # Trait event notification. self.object_added = NamingEvent( new_binding=Binding(name=name, obj=obj, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): if make_contexts: self._create_subcontext(components[0]) else: raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.bind('/'.join(components[1:]), obj, make_contexts) return def rebind(self, name, obj, make_contexts=False): """ Binds an object to a name that may already be bound. If 'make_contexts' is True then any missing intermediate contexts are created automatically. The object may be a different object but may also be the same object that is already bound to the specified name. The name may or may not be already used. Think of this as a safer version of 'bind' since this one will never raise an exception regarding a name being used. """ if len(name) == 0: raise InvalidNameError('empty name') # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: # Do the actual rebind. self._rebind(components[0], obj) # Trait event notification. self.object_changed = NamingEvent( new_binding=Binding(name=name, obj=obj, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): if make_contexts: self._create_subcontext(components[0]) else: raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.rebind('/'.join(components[1:]), obj, make_contexts) return def unbind(self, name): """ Unbinds a name. """ if len(name) == 0: raise InvalidNameError('empty name') # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Lookup the object that we are unbinding to use in the event # notification. obj = self._lookup(atom) # Do the actual unbind. self._unbind(atom) # Trait event notification. self.object_removed = NamingEvent( old_binding=Binding(name=name, obj=obj, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.unbind('/'.join(components[1:])) return def rename(self, old_name, new_name): """ Binds a new name to an object. """ if len(old_name) == 0 or len(new_name) == 0: raise InvalidNameError('empty name') # Parse the names. old_components = self._parse_name(old_name) new_components = self._parse_name(new_name) # If there is axactly one component in BOTH names then the operation # takes place ENTIRELY in this context. if len(old_components) == 1 and len(new_components) == 1: # Is the old name actually bound? if not self._is_bound(old_name): raise NameNotFoundError(old_name) # Is the new name already bound? if self._is_bound(new_name): raise NameAlreadyBoundError(new_name) # Do the actual rename. self._rename(old_name, new_name) # Lookup the object that we are renaming to use in the event # notification. obj = self._lookup(new_name) # Trait event notification. self.object_renamed = NamingEvent( old_binding=Binding(name=old_name, obj=obj, context=self), new_binding=Binding(name=new_name, obj=obj, context=self) ) else: # fixme: This really needs to be transactional in case the bind # succeeds but the unbind fails. To be safe should we just not # support cross-context renaming for now?!?! # # Lookup the object. obj = self.lookup(old_name) # Bind the new name. self.bind(new_name, obj) # Unbind the old one. self.unbind(old_name) return def lookup(self, name): """ Resolves a name relative to this context. """ # If the name is empty we return the context itself. if len(name) == 0: # fixme: The JNDI spec. says that this should return a COPY of # the context. return self # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual lookup. obj = self._lookup(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) obj = next_context.lookup('/'.join(components[1:])) return obj # fixme: Non-JNDI def lookup_binding(self, name): """ Looks up the binding for a name relative to this context. """ if len(name) == 0: raise InvalidNameError('empty name') # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual lookup. binding = self._lookup_binding(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) binding = next_context.lookup_binding('/'.join(components[1:])) return binding # fixme: Non-JNDI def lookup_context(self, name): """ Resolves a name relative to this context. The name MUST resolve to a context. This method is useful to return context adapters. """ # If the name is empty we return the context itself. if len(name) == 0: # fixme: The JNDI spec. says that this should return a COPY of # the context. return self # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual lookup. obj = self._get_next_context(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) obj = next_context.lookup('/'.join(components[1:])) return obj def create_subcontext(self, name): """ Creates a sub-context. """ if len(name) == 0: raise InvalidNameError('empty name') # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] # Is the name already bound? if self._is_bound(atom): raise NameAlreadyBoundError(name) # Do the actual creation of the sub-context. sub = self._create_subcontext(atom) # Trait event notification. self.object_added = NamingEvent( new_binding=Binding(name=name, obj=sub, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) sub = next_context.create_subcontext('/'.join(components[1:])) return sub def destroy_subcontext(self, name): """ Destroys a sub-context. """ if len(name) == 0: raise InvalidNameError('empty name') # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) obj = self._lookup(atom) if not self._is_context(atom): raise NotContextError(name) # Do the actual destruction of the sub-context. self._destroy_subcontext(atom) # Trait event notification. self.object_removed = NamingEvent( old_binding=Binding(name=name, obj=obj, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.destroy_subcontext('/'.join(components[1:])) return # fixme: Non-JNDI def get_unique_name(self, prefix): """ Returns a name that is unique within the context. The name returned will start with the specified prefix. """ return make_unique_name(prefix, existing=self.list_names(''), format='%s (%d)') def list_names(self, name=''): """ Lists the names bound in a context. """ # If the name is empty then the operation takes place in this context. if len(name) == 0: names = self._list_names() # Otherwise, attempt to continue resolution into the next context. else: # Parse the name. components = self._parse_name(name) if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) names = next_context.list_names('/'.join(components[1:])) return names def list_bindings(self, name=''): """ Lists the bindings in a context. """ # If the name is empty then the operation takes place in this context. if len(name) == 0: bindings = self._list_bindings() # Otherwise, attempt to continue resolution into the next context. else: # Parse the name. components = self._parse_name(name) if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) bindings = next_context.list_bindings('/'.join(components[1:])) return bindings # fixme: Non-JNDI def is_context(self, name): """ Returns True if the name is bound to a context. """ # If the name is empty then it refers to this context. if len(name) == 0: is_context = True else: # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual check. is_context = self._is_context(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) is_context = next_context.is_context('/'.join(components[1:])) return is_context # fixme: Non-JNDI def search(self, obj): """ Returns a list of namespace names that are bound to obj. """ # don't look for None if obj is None: return [] # Obj is bound to these names relative to this context names = [] # path contain the name components down to the current context path = [] self._search( obj, names, path, {} ) return names ########################################################################### # Protected 'Context' interface. ########################################################################### def _parse_name(self, name): """ Parse a name into a list of components. e.g. 'foo/bar/baz' -> ['foo', 'bar', 'baz'] """ return name.split('/') def _is_bound(self, name): """ Is a name bound in this context? """ return name in self._bindings def _lookup(self, name): """ Looks up a name in this context. """ obj = self._bindings[name] return naming_manager.get_object_instance(obj, name, self) def _lookup_binding(self, name): """ Looks up the binding for a name in this context. """ return Binding(name=name, obj=self._lookup(name), context=self) def _bind(self, name, obj): """ Binds a name to an object in this context. """ state = naming_manager.get_state_to_bind(obj, name, self) self._bindings[name] = state return def _rebind(self, name, obj): """ Rebinds a name to an object in this context. """ self._bind(name, obj) return def _unbind(self, name): """ Unbinds a name from this context. """ del self._bindings[name] return def _rename(self, old_name, new_name): """ Renames an object in this context. """ # Bind the new name. self._bindings[new_name] = self._bindings[old_name] # Unbind the old one. del self._bindings[old_name] return def _create_subcontext(self, name): """ Creates a sub-context of this context. """ sub = self.__class__(environment=self.environment) self._bindings[name] = sub return sub def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ del self._bindings[name] return def _list_bindings(self): """ Lists the bindings in this context. """ bindings = [] for name in self._list_names(): bindings.append( Binding(name=name, obj=self._lookup(name), context=self) ) return bindings def _list_names(self): """ Lists the names bound in this context. """ return list(self._bindings.keys()) def _is_context(self, name): """ Returns True if a name is bound to a context. """ return self._get_next_context(name) is not None def _get_next_context(self, name): """ Returns the next context. """ obj = self._lookup(name) # If the object is a context then everything is just dandy. if isinstance(obj, Context): next_context = obj # Otherwise, instead of just giving up, see if the context has a type # manager that knows how to adapt the object to make it quack like a # context. else: next_context = self._get_context_adapter(obj) # If no adapter was found then we cannot continue name resolution. if next_context is None: raise NotContextError(name) return next_context def _search( self, obj, names, path, searched): """ Append to names any name bound to obj. Join path and name with '/' to for a complete name from the top context. """ # Check the bindings recursively. for binding in self.list_bindings(): if binding.obj is obj: path.append( binding.name ) names.append( '/'.join(path) ) path.pop() if isinstance( binding.obj, Context ) \ and not binding.obj in searched: path.append( binding.name ) searched[binding.obj] = True binding.obj._search( obj, names, path, searched ) path.pop() return ########################################################################### # Private interface. ########################################################################### def _get_context_adapter(self, obj): """ Returns a context adapter for an object. Returns None if no such adapter is available. """ if self.type_manager is not None: adapter = self.type_manager.object_as( obj, Context, environment=self.environment, context=self ) else: adapter = None return adapter #### EOF ###################################################################### apptools-4.5.0/apptools/naming/py_context.py0000644000076500000240000001454213547637361022547 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A naming context for a Python namespace. """ # Enthought library imports. from traits.api import Any, Dict, Instance, Property # Local imports. from .address import Address from .binding import Binding from .context import Context from .naming_manager import naming_manager from .py_object_factory import PyObjectFactory from .reference import Reference from .referenceable import Referenceable from .referenceable_state_factory import ReferenceableStateFactory # The default environment. ENVIRONMENT = { # 'Context' properties. Context.OBJECT_FACTORIES : [PyObjectFactory()], Context.STATE_FACTORIES : [ReferenceableStateFactory()], } class PyContext(Context, Referenceable): """ A naming context for a Python namespace. """ #### 'Context' interface ################################################## # The naming environment in effect for this context. environment = Dict(ENVIRONMENT) #### 'PyContext' interface ################################################ # The Python namespace that we represent. namespace = Any # If the namespace is actual a Python object that has a '__dict__' # attribute, then this will be that object (the namespace will be the # object's '__dict__'. obj = Any #### 'Referenceable' interface ############################################ # The object's reference suitable for binding in a naming context. reference = Property(Instance(Reference)) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Creates a new context. """ # Base class constructor. super(PyContext, self).__init__(**traits) if type(self.namespace) is not dict: if hasattr(self.namespace, '__dict__'): self.obj = self.namespace self.namespace = self.namespace.__dict__ else: raise ValueError('Need a dictionary or a __dict__ attribute') return ########################################################################### # 'Referenceable' interface. ########################################################################### #### Properties ########################################################### def _get_reference(self): """ Returns a reference to this object suitable for binding. """ reference = Reference( class_name = self.__class__.__name__, addresses = [Address(type='py_context', content=self.namespace)] ) return reference ########################################################################### # Protected 'Context' interface. ########################################################################### def _is_bound(self, name): """ Is a name bound in this context? """ return name in self.namespace def _lookup(self, name): """ Looks up a name in this context. """ obj = self.namespace[name] return naming_manager.get_object_instance(obj, name, self) def _bind(self, name, obj): """ Binds a name to an object in this context. """ state = naming_manager.get_state_to_bind(obj, name,self) self.namespace[name] = state # Trait event notification. # An "added" event is fired by the bind method of the base calss (which calls # this one), so we don't need to do the changed here (which would be the wrong # thing anyway) -- LGV # # self.trait_property_changed('context_changed', None, None) return def _rebind(self, name, obj): """ Rebinds a name to a object in this context. """ self._bind(name, obj) return def _unbind(self, name): """ Unbinds a name from this context. """ del self.namespace[name] # Trait event notification. self.trait_property_changed('context_changed', None, None) return def _rename(self, old_name, new_name): """ Renames an object in this context. """ state = self.namespace[old_name] # Bind the new name. self.namespace[new_name] = state # Unbind the old one. del self.namespace[old_name] # Trait event notification. self.context_changed = True return def _create_subcontext(self, name): """ Creates a sub-context of this context. """ sub = self._context_factory(name, {}) self.namespace[name] = sub # Trait event notification. self.trait_property_changed('context_changed', None, None) return sub def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ del self.namespace[name] # Trait event notification. self.trait_property_changed('context_changed', None, None) return def _list_bindings(self): """ Lists the bindings in this context. """ bindings = [] for name, value in self.namespace.items(): bindings.append(Binding(name=name, obj=self._lookup(name), context=self)) return bindings def _list_names(self): """ Lists the names bound in this context. """ return list(self.namespace.keys()) ########################################################################### # Private interface. ########################################################################### def _context_factory(self, name, namespace): """ Create a sub-context. """ return self.__class__(namespace=namespace) #### EOF ###################################################################### apptools-4.5.0/apptools/naming/pyfs_object_factory.py0000644000076500000240000000304313547637361024403 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Object factory for Python File System contexts. """ # Enthought library imports. from apptools.io.api import File # Local imports. from .object_factory import ObjectFactory from .reference import Reference class PyFSObjectFactory(ObjectFactory): """ Object factory for Python File System contexts. """ ########################################################################### # 'ObjectFactory' interface. ########################################################################### def get_object_instance(self, state, name, context): """ Creates an object using the specified state information. """ obj = None if isinstance(state, Reference): if state.class_name == 'File' and len(state.addresses) > 0: obj = File(state.addresses[0].content) return obj ### EOF ####################################################################### apptools-4.5.0/apptools/naming/pyfs_state_factory.py0000644000076500000240000000343413547637361024261 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ State factory for Python File System contexts. """ # Enthought library imports. from apptools.io.api import File # Local imports. from .address import Address from .reference import Reference from .state_factory import StateFactory class PyFSStateFactory(StateFactory): """ State factory for Python File System contexts. """ ########################################################################### # 'StateFactory' interface. ########################################################################### def get_state_to_bind(self, obj, name, context): """ Returns the state of an object for binding. """ state = None if isinstance(obj, File): # If the file is not actually in the directory represented by the # context then we create and bind a reference to it. if obj.parent.path != context.path: state = Reference( class_name = obj.__class__.__name__, addresses = [Address(type='file', content=obj.path)] ) return state ### EOF ####################################################################### apptools-4.5.0/apptools/naming/reference.py0000644000076500000240000000357213547637361022312 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A reference to an object that lives outside of the naming system. """ # Enthought library imports. from traits.api import Any, HasPrivateTraits, List, Str # Local imports. from .address import Address class Reference(HasPrivateTraits): """ A reference to an object that lives outside of the naming system. References provide a way to store the address(s) of objects that live outside of the naming system. A reference consists of a list of addresses that represent a communications endpoint for the object being referenced. A reference also contains information to assist in the creation of an instance of the object to which it refers. It contains the name of the class that will be created and the class name and location of a factory that will be used to do the actual instance creation. """ #### 'Reference' interface ################################################ # The list of addresses that can be used to 'contact' the object. addresses = List(Address) # The class name of the object that this reference refers to. class_name = Str # The class name of the object factory. factory_class_name = Str #### EOF ###################################################################### apptools-4.5.0/apptools/naming/initial_context.py0000644000076500000240000000421613547637361023545 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The starting point for performing naming operations. """ # Local imports. from .context import Context def InitialContext(environment): """ Creates an initial context for beginning name resolution. """ # Get the class name of the factory that will produce the initial context. klass_name = environment.get(Context.INITIAL_CONTEXT_FACTORY) if klass_name is None: raise ValueError("No initial context factory specified") # Import the factory class. klass = _import_symbol(klass_name) # Create the factory. factory = klass() # Ask the factory for a context implementation instance. return factory.get_initial_context(environment) # fixme: This is the same code as in the Envisage import manager but we don't # want naming to be dependent on Envisage, so we need some other package # for useful 'Python' tools etc. def _import_symbol(symbol_path): """ Imports the symbol defined by 'symbol_path'. 'symbol_path' is a string in the form 'foo.bar.baz' which is turned into an import statement 'from foo.bar import baz' (ie. the last component of the name is the symbol name, the rest is the package/ module path to load it from). """ components = symbol_path.split('.') module_name = '.'.join(components[:-1]) symbol_name = components[-1] module = __import__(module_name, globals(), locals(), [symbol_name]) symbol = getattr(module, symbol_name) return symbol #### EOF ###################################################################### apptools-4.5.0/apptools/naming/context_adapter_factory.py0000644000076500000240000000325013547637361025260 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all context adapter factories. """ # Enthought library imports. from apptools.type_manager.api import AdapterFactory # Local imports. from .context import Context class ContextAdapterFactory(AdapterFactory): """ The base class for all context adapter factories. """ #### 'AdapterFactory' interface ########################################### # The target class (the class that the factory can adapt objects to). target_class = Context ########################################################################### # Protected 'AbstractAdapterFactory' interface. ########################################################################### def _adapt(self, adaptee, target_class, environment, context): """ Returns an adapter that adapts an object to the target class. """ adapter = self.adapter_class( adaptee=adaptee, environment=environment, context=context ) return adapter #### EOF ###################################################################### apptools-4.5.0/apptools/naming/object_factory.py0000644000076500000240000000303111640354733023326 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all object factories. """ # Enthought library imports. from traits.api import HasTraits class ObjectFactory(HasTraits): """ The base class for all object factories. An object factory accepts some information about how to create an object (such as a reference) and returns an instance of that object. """ ########################################################################### # 'ObjectFactory' interface. ########################################################################### def get_object_instance(self, state, name, context): """ Creates an object using the specified state information. Returns None if the factory cannot create the object (ie. it does not recognise the state passed to it). """ raise NotImplementedError ### EOF ####################################################################### apptools-4.5.0/apptools/naming/naming_event.py0000644000076500000240000000215313547637361023020 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The event fired by the tree model when it changes. """ # Enthought library imports. from traits.api import HasTraits, Instance # Local imports. from .binding import Binding # Classes for event traits. class NamingEvent(HasTraits): """ Information about tree model changes. """ # The old binding. old_binding = Instance(Binding) # The new binding. new_binding = Instance(Binding) #### EOF ###################################################################### apptools-4.5.0/apptools/naming/binding.py0000644000076500000240000000666311640354733021761 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The representation of a name-to-object binding in a context. """ # Enthought libary imports. from traits.api import Any, HasTraits, Property, Str class Binding(HasTraits): """ The representation of a name-to-object binding in a context. """ #### 'Binding' interface ################################################## # The class name of the object bound to the name in the binding. class_name = Property(Str) # The name. name = Str # The object bound to the name in the binding. obj = Any #### Experimental 'Binding' interface ##################################### # fixme: These is not part of the JNDI spec, but they do seem startlingly # useful! # # fixme: And, errr, startlingly broken! If the context that the binding # is in is required then just look up the name minus the last component! # # The context that the binding came from. context = Any # The name of the bound object within the namespace. namespace_name = Property(Str) #### 'Private' interface ################################################## # Shadow trait for the 'class_name' property. _class_name = Str ########################################################################### # 'object' interface. ########################################################################### def __str__(self): """ Returns an informal string representation of the object. """ return super(Binding, self).__str__() + '(name=%s, obj=%s)' % ( self.name, self.obj) ########################################################################### # 'Binding' interface. ########################################################################### #### Properties ########################################################### # class_name def _get_class_name(self): """ Returns the class name of the object. """ if len(self._class_name) == 0: if self.obj is None: class_name = None else: klass = self.obj.__class__ class_name = '%s.%s' % (klass.__module__, klass.__name__) return class_name def _set_class_name(self, class_name): """ Sets the class name of the object. """ self._class_name = class_name return # namespace_name def _get_namespace_name(self): """ Returns the name of the context within its own namespace. """ if self.context is not None: base = self.context.namespace_name else: base = '' if len(base) > 0: namespace_name = base + '/' + self.name else: namespace_name = self.name return namespace_name #### EOF ###################################################################### apptools-4.5.0/apptools/naming/dynamic_context.py0000644000076500000240000001474013547637361023543 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # #----------------------------------------------------------------------------- """ Provider of a framework that dynamically determines the contents of a context at the time of interaction with the contents rather than at the time a class is written. This capability is particularly useful when the object acting as a context is part of a plug-in application -- such as Envisage. In general, this capability allows the context to be: - Extendable by contributions from somewhere other than the original code writer - Dynamic in that the elements it is composed of can change each time someone interacts with the contents of the context. It should be noted that this capability is explicitly different from contexts that look at another container to determine their contents, such as a file system context! Users of this framework contribute items to a dynamic context by adding traits to the dynamic context instance. (This addition can happen statically through the use of a Traits Category.) The trait value is the context item's value and the trait definition's metadata determines how the item is treated within the context. The support metadata is: context_name: A non-empty string Represents the name of the item within this context. This must be present for the trait to show up as a context item though the value may change over time as the item gets bound to different names. context_order: A float value Indicates the position for the item within this context. All dynamically contributed context items are sorted by ascending order of this value using the standard list sort function. is_context: A boolean value True if the item is itself a context. """ # Standardlibrary imports import logging # Local imports from .binding import Binding from .context import Context from .exception import OperationNotSupportedError # Setup a logger for this module. logger = logging.getLogger(__name__) class DynamicContext(Context): """ A framework that dynamically determines the contents of a context at the time of interaction with the contents rather than at the time a context class is written. It should be noted that this capability is explicitly different from contexts that look at another container to determine their contents, such as a file system context! """ ########################################################################## # 'Context' interface. ########################################################################## ### protected interface ################################################## def _is_bound(self, name): """ Is a name bound in this context? """ item = self._get_contributed_context_item(name) result = item != (None, None) return result def _is_context(self, name): """ Returns True if a name is bound to a context. """ item = self._get_contributed_context_item(name) if item != (None, None): obj, trait = item result = True == trait.is_context else: result = False return result def _list_bindings(self): """ Lists the bindings in this context. """ result = [ Binding(name=n, obj=o, context=self) for n, o, t in \ self._get_contributed_context_items() ] return result def _list_names(self): """ Lists the names bound in this context. """ result = [ n for n, o, t in self._get_contributed_context_items() ] return result def _lookup(self, name): """ Looks up a name in this context. """ item = self._get_contributed_context_item(name) if item != (None, None): obj, trait = item result = obj else: result = None return result def _rename(self, old_name, new_name): """ Renames an object in this context. """ item = self._get_contributed_context_item(old_name) if item != (None, None): obj, trait = item trait.context_name = new_name else: raise ValueError('Name "%s" not in context', old_name) def _unbind(self, name): """ Unbinds a name from this context. """ # It is an error to try to unbind any contributed context items item = self._get_contributed_context_item(name) if item != (None, None): raise OperationNotSupportedError('Unable to unbind ' + \ 'built-in with name [%s]' % name) ########################################################################## # 'DynamicContext' interface. ########################################################################## ### protected interface ################################################## def _get_contributed_context_item(self, name): """ If the specified name matches a contributed context item then returns a tuple of the item's current value and trait definition (in that order.) Otherwise, returns a tuple of (None, None). """ result = (None, None) for n, o, t in self._get_contributed_context_items(): if n == name: result = (o, t) return result def _get_contributed_context_items(self): """ Returns an ordered list of items to be treated as part of our context. Each item in the list is a tuple of its name, object, and trait definition (in that order.) """ # Our traits that get treated as context items are those that declare # themselves via metadata on the trait definition. filter = { 'context_name': lambda v: v is not None and len(v) > 0 } traits = self.traits(**filter) # Sort the list of context items according to the name of the item. traits = [ (t.context_order, n, t) for n, t in traits.items() ] traits.sort() # Convert these trait definitions into a list of name and object tuples. result = [(t.context_name, getattr(self, n), t) for order, n, t \ in traits] return result ### EOF ###################################################################### apptools-4.5.0/apptools/persistence/0000755000076500000240000000000013547652535021046 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/persistence/tests/0000755000076500000240000000000013547652535022210 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/persistence/tests/test_version_registry.py0000644000076500000240000000731113422276145027227 0ustar mdickinsonstaff00000000000000"""Tests for the version registry. """ # Author: Prabhu Ramachandran # Copyright (c) 2005, Enthought, Inc. # License: BSD Style. # Standard library imports. import sys from imp import reload import unittest # Enthought library imports. from traits.api import HasTraits from apptools.persistence import version_registry, state_pickler class Classic: __version__ = 0 class New(object): __version__ = 0 class TraitClass(HasTraits): __version__ = 0 class Test(New): __version__ = 1 def __init__(self): self.a = Classic() class Handler: def __init__(self): self.calls = [] def upgrade(self, state, version): self.calls.append(('upgrade', state, version)) def upgrade1(self, state, version): self.calls.append(('upgrade1', state, version)) class TestVersionRegistry(unittest.TestCase): def test_get_version(self): """Test the get_version function.""" if sys.version_info[0] > 2: extra = [(('object', 'builtins'), -1)] else: extra = [] c = Classic() v = version_registry.get_version(c) res = extra + [(('Classic', __name__), 0)] self.assertEqual(v, res) state = state_pickler.get_state(c) self.assertEqual(state.__metadata__['version'], res) n = New() v = version_registry.get_version(n) res = extra + [(('New', __name__), 0)] self.assertEqual(v, res) state = state_pickler.get_state(n) self.assertEqual(state.__metadata__['version'], res) t = TraitClass() v = version_registry.get_version(t) res = extra + [(('CHasTraits', 'traits.ctraits'), -1), (('HasTraits', 'traits.has_traits'), -1), (('TraitClass', __name__), 0)] self.assertEqual(v, res) state = state_pickler.get_state(t) self.assertEqual(state.__metadata__['version'], res) def test_reload(self): """Test if the registry is reload safe.""" # A dummy handler. def h(x, y): pass registry = version_registry.registry registry.register('A', __name__, h) self.assertEqual(registry.handlers.get(('A', __name__)), h) reload(version_registry) registry = version_registry.registry self.assertEqual(registry.handlers.get(('A', __name__)), h) del registry.handlers[('A', __name__)] self.assertFalse(('A', __name__) in registry.handlers) def test_update(self): """Test if update method calls the handlers in order.""" registry = version_registry.registry # First an elementary test. c = Classic() state = state_pickler.get_state(c) h = Handler() registry.register('Classic', __name__, h.upgrade) c1 = state_pickler.create_instance(state) state_pickler.set_state(c1, state) self.assertEqual(h.calls, [('upgrade', state, 0)]) # Remove the handler. registry.unregister('Classic', __name__) # Now check to see if this works for inheritance trees. t = Test() state = state_pickler.get_state(t) h = Handler() registry.register('Classic', __name__, h.upgrade) registry.register('New', __name__, h.upgrade) registry.register('Test', __name__, h.upgrade1) t1 = state_pickler.create_instance(state) state_pickler.set_state(t1, state) # This should call New handler, then the Test and then # Classic. self.assertEqual(h.calls, [('upgrade', state, 0), ('upgrade1', state, 1), ('upgrade', state.a, 0)]) if __name__ == "__main__": unittest.main() apptools-4.5.0/apptools/persistence/tests/test_spickle.py0000644000076500000240000000314013547652336025250 0ustar mdickinsonstaff00000000000000"""Test for the spickle module. """ # Author: Prabhu Ramachandran # Copyright (c) 2007, Enthought, Inc. # License: BSD Style. import unittest import numpy from pickle import dumps try: from apptools.persistence import spickle except ImportError: import nose raise nose.SkipTest('spickle is not supported with Python3') from traits.api import HasTraits, Float, Int class A: def __init__(self): self.a = 100 self.array = numpy.linspace(0, 1, 5) class B(HasTraits): i = Int(10) f = Float(1.0) class Foo(object): def __init__(self, a=1): self.a = A() self.a.b = 200 self.ref = self.a self.b = B() self.b.trait_set(i=20, f=2.0) class TestStatePickler(unittest.TestCase): def _test_object(self, x): assert x.a.a == 100 assert numpy.all(x.a.array == numpy.linspace(0, 1, 5)) assert x.a.b == 200 assert x.a == x.ref assert x.b.i == 20 assert x.b.f == 2.0 def test_dump_state(self): "Test if we are able to dump the state to a string." f = Foo() str = dumps(f) st = spickle.get_state(f) str1 = spickle.dumps_state(st) self.assertEqual(str, str1) st = spickle.loads_state(str) self.assertEqual(str, spickle.dumps_state(st)) def test_state2object(self): "Test if we can convert a state to an object." f = Foo() str = dumps(f) st = spickle.get_state(f) g = spickle.state2object(st) self._test_object(g) if __name__ == "__main__": unittest.main() apptools-4.5.0/apptools/persistence/tests/__init__.py0000644000076500000240000000000013547637361024307 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/persistence/tests/test_file_path.py0000644000076500000240000000760713547637370025566 0ustar mdickinsonstaff00000000000000"""Tests for the file_path module. """ # Author: Prabhu Ramachandran # Copyright (c) 2005, Enthought, Inc. # License: BSD Style. # Standard library imports. import unittest import os import sys from os.path import abspath, dirname, basename, join from io import BytesIO # 3rd party imports. import pkg_resources # Enthought library imports. from apptools.persistence import state_pickler from apptools.persistence import file_path class Test: def __init__(self): self.f = file_path.FilePath() class TestFilePath(unittest.TestCase): def setUp(self): # If the cwd is somewhere under /tmp, that confuses the tests below. # Use the directory containing this file, instead. test_cwd = pkg_resources.resource_filename("apptools.persistence", "") self.old_cwd = os.getcwd() os.chdir(test_cwd) def tearDown(self): os.chdir(self.old_cwd) def test_relative(self): """Test if relative paths are set correctly. """ fname = 't.vtk' f = file_path.FilePath(fname) cwd = os.getcwd() # Trivial case of both in same dir. f.set_relative(abspath(join(cwd, 't.mv2'))) self.assertEqual(f.rel_pth, fname) # Move one directory deeper. f.set_relative(abspath(join(cwd, 'tests', 't.mv2'))) self.assertEqual(f.rel_pth, join(os.pardir, fname)) # Move one directory shallower. f.set_relative(abspath(join(dirname(cwd), 't.mv2'))) diff = basename(cwd) self.assertEqual(f.rel_pth, join(diff, fname)) # Test where the path is relative to the root. f.set(abspath(join('data', fname))) f.set_relative('/tmp/test.mv2') if sys.platform.startswith('win'): expect = os.pardir + abspath(join('data', fname))[2:] else: expect = os.pardir + abspath(join('data', fname)) self.assertEqual(f.rel_pth, expect) def test_absolute(self): """Test if absolute paths are set corectly. """ fname = 't.vtk' f = file_path.FilePath(fname) cwd = os.getcwd() # Easy case of both in same dir. f.set_absolute(join(cwd, 'foo', 'test', 't.mv2')) self.assertEqual(f.abs_pth, join(cwd, 'foo', 'test', fname)) # One level lower. fname = join(os.pardir, 't.vtk') f.set(fname) f.set_absolute(join(cwd, 'foo', 'test', 't.mv2')) self.assertEqual(f.abs_pth, abspath(join(cwd, 'foo', 'test', fname))) # One level higher. fname = join('test', 't.vtk') f.set(fname) f.set_absolute(join(cwd, 'foo', 't.mv2')) self.assertEqual(f.abs_pth, abspath(join(cwd, 'foo', fname))) def test_pickle(self): """Test if pickler works correctly with FilePaths. """ t = Test() t.f.set('t.vtk') cwd = os.getcwd() curdir = basename(cwd) # Create a dummy file in the parent dir. s = BytesIO() # Spoof its location. s.name = abspath(join(cwd, os.pardir, 't.mv2')) # Dump into it state_pickler.dump(t, s) # Rewind the stream s.seek(0) # "Move" the file elsewhere s.name = join(cwd, 'foo', 'test', 't.mv2') state = state_pickler.load_state(s) self.assertEqual(state.f.abs_pth, join(cwd, 'foo', 'test', curdir, 't.vtk')) # Create a dummy file in a subdir. s = BytesIO() # Spoof its location. s.name = abspath(join(cwd, 'data', 't.mv2')) # Dump into it. state_pickler.dump(t, s) # Rewind the stream s.seek(0) # "Move" the file elsewhere s.name = join(cwd, 'foo', 'test', 't.mv2') state = state_pickler.load_state(s) self.assertEqual(state.f.abs_pth, join(cwd, 'foo', 't.vtk')) if __name__ == "__main__": unittest.main() apptools-4.5.0/apptools/persistence/tests/test_state_pickler.py0000644000076500000240000003644313547637370026464 0ustar mdickinsonstaff00000000000000"""Unit tests for the state pickler and unpickler. """ # Author: Prabhu Ramachandran # Copyright (c) 2005-2015, Enthought, Inc. # License: BSD Style. import base64 import pickle import unittest import math import os import tempfile import numpy from traits.api import Bool, Int, Long, Array, Float, Complex, Any, \ Str, Unicode, Instance, Tuple, List, Dict, HasTraits try: from tvtk.api import tvtk except ImportError: TVTK_AVAILABLE = False else: TVTK_AVAILABLE = True from apptools.persistence import state_pickler # A simple class to test instances. class A(object): def __init__(self): self.a = 'a' # NOTE: I think that TVTK specific testing should be moved to the # TVTK package. # A classic class for testing the pickler. class TestClassic: def __init__(self): self.b = False self.i = 7 self.l = 1234567890123456789 self.f = math.pi self.c = complex(1.01234, 2.3) self.n = None self.s = 'String' self.u = u'Unicode' self.inst = A() self.tuple = (1, 2, 'a', A()) self.list = [1, 1.1, 'a', 1j, self.inst] self.pure_list = list(range(5)) self.dict = {'a': 1, 'b': 2, 'ref': self.inst} self.numeric = numpy.ones((2, 2, 2), 'f') self.ref = self.numeric if TVTK_AVAILABLE: self._tvtk = tvtk.Property() # A class with traits for testing the pickler. class TestTraits(HasTraits): b = Bool(False) i = Int(7) l = Long(12345678901234567890) f = Float(math.pi) c = Complex(complex(1.01234, 2.3)) n = Any s = Str('String') u = Unicode(u'Unicode') inst = Instance(A) tuple = Tuple list = List pure_list = List(list(range(5))) dict = Dict numeric = Array(value=numpy.ones((2, 2, 2), 'f')) ref = Array if TVTK_AVAILABLE: _tvtk = Instance(tvtk.Property, ()) def __init__(self): self.inst = A() self.tuple = (1, 2, 'a', A()) self.list = [1, 1.1, 'a', 1j, self.inst] self.dict = {'a': 1, 'b': 2, 'ref': self.inst} self.ref = self.numeric class TestDictPickler(unittest.TestCase): def set_object(self, obj): """Changes the objects properties to test things.""" obj.b = True obj.i = 8 obj.s = 'string' obj.u = u'unicode' obj.inst.a = 'b' obj.list[0] = 2 obj.tuple[-1].a = 't' obj.dict['a'] = 10 if TVTK_AVAILABLE: obj._tvtk.trait_set( point_size=3, specular_color=(1, 0, 0), representation='w' ) def _check_instance_and_references(self, obj, data): """Asserts that there is one instance and two references in the state. We need this as there isn't a guarantee as to which will be the reference and which will be the instance. """ inst = data['inst'] list_end = data['list']['data'][-1] dict_ref = data['dict']['data']['ref'] all_inst = [inst, list_end, dict_ref] types = [x['type'] for x in all_inst] self.assertEqual(types.count('instance'), 1) self.assertEqual(types.count('reference'), 2) inst_state = all_inst[types.index('instance')] self.assertEqual(inst_state['data']['data']['a'], 'b') def verify(self, obj, state): data = state['data'] self.assertEqual(state['class_name'], obj.__class__.__name__) data = data['data'] self.assertEqual(data['b'], obj.b) self.assertEqual(data['i'], obj.i) self.assertEqual(data['l'], obj.l) self.assertEqual(data['f'], obj.f) self.assertEqual(data['c'], obj.c) self.assertEqual(data['n'], obj.n) self.assertEqual(data['s'], obj.s) self.assertEqual(data['u'], obj.u) tup = data['tuple']['data'] self.assertEqual(tup[:-1], obj.tuple[:-1]) self.assertEqual(tup[-1]['data']['data']['a'], 't') lst = data['list']['data'] self.assertEqual(lst[:-1], obj.list[:-1]) pure_lst = data['pure_list']['data'] self.assertEqual(pure_lst, obj.pure_list) dct = data['dict']['data'] self.assertEqual(dct['a'], obj.dict['a']) self.assertEqual(dct['b'], obj.dict['b']) self._check_instance_and_references(obj, data) num_attr = 'numeric' if data['numeric']['type'] == 'numeric' else 'ref' decodestring = getattr(base64, 'decodebytes', base64.decodestring) junk = state_pickler.gunzip_string( decodestring(data[num_attr]['data']) ) num = pickle.loads(junk) self.assertEqual(numpy.alltrue(numpy.ravel(num == obj.numeric)), 1) self.assertTrue(data['ref']['type'] in ['reference', 'numeric']) if data['ref']['type'] == 'numeric': self.assertEqual(data['numeric']['type'], 'reference') else: self.assertEqual(data['numeric']['type'], 'numeric') self.assertEqual(data['ref']['id'], data['numeric']['id']) def verify_unpickled(self, obj, state): self.assertEqual(state.__metadata__['class_name'], obj.__class__.__name__) self.assertEqual(state.b, obj.b) self.assertEqual(state.i, obj.i) self.assertEqual(state.l, obj.l) self.assertEqual(state.f, obj.f) self.assertEqual(state.c, obj.c) self.assertEqual(state.n, obj.n) self.assertEqual(state.s, obj.s) self.assertEqual(state.u, obj.u) self.assertEqual(state.inst.__metadata__['type'], 'instance') tup = state.tuple self.assertEqual(state.tuple.has_instance, True) self.assertEqual(tup[:-1], obj.tuple[:-1]) self.assertEqual(tup[-1].a, 't') lst = state.list self.assertEqual(state.list.has_instance, True) self.assertEqual(lst[:-1], obj.list[:-1]) # Make sure the reference is the same self.assertEqual(id(state.inst), id(lst[-1])) self.assertEqual(lst[-1].a, 'b') pure_lst = state.pure_list self.assertEqual(pure_lst, obj.pure_list) self.assertEqual(state.pure_list.has_instance, False) dct = state.dict self.assertEqual(dct.has_instance, True) self.assertEqual(dct['a'], obj.dict['a']) self.assertEqual(dct['b'], obj.dict['b']) self.assertEqual(dct['ref'].__metadata__['type'], 'instance') num = state.numeric self.assertEqual(numpy.alltrue(numpy.ravel(num == obj.numeric)), 1) self.assertEqual(id(state.ref), id(num)) if TVTK_AVAILABLE: _tvtk = state._tvtk self.assertEqual(_tvtk.representation, obj._tvtk.representation) self.assertEqual(_tvtk.specular_color, obj._tvtk.specular_color) self.assertEqual(_tvtk.point_size, obj._tvtk.point_size) def verify_state(self, state1, state): self.assertEqual(state.__metadata__, state1.__metadata__) self.assertEqual(state.b, state1.b) self.assertEqual(state.i, state1.i) self.assertEqual(state.l, state1.l) self.assertEqual(state.f, state1.f) self.assertEqual(state.c, state1.c) self.assertEqual(state.n, state1.n) self.assertEqual(state.s, state1.s) self.assertEqual(state.u, state1.u) # The ID's need not be identical so we equate them here so the # tests pass. Note that the ID's only need be consistent not # identical! if TVTK_AVAILABLE: instances = ('inst', '_tvtk') else: instances = ('inst',) for attr in instances: getattr(state1, attr).__metadata__['id'] = \ getattr(state, attr).__metadata__['id'] if TVTK_AVAILABLE: self.assertEqual(state1._tvtk, state._tvtk) state1.tuple[-1].__metadata__['id'] = \ state.tuple[-1].__metadata__['id'] self.assertEqual(state.inst.__metadata__, state1.inst.__metadata__) self.assertEqual(state.tuple, state1.tuple) self.assertEqual(state.list, state1.list) self.assertEqual(state.pure_list, state1.pure_list) self.assertEqual(state.dict, state1.dict) self.assertEqual((state1.numeric == state.numeric).all(), True) self.assertEqual(id(state.ref), id(state.numeric)) self.assertEqual(id(state1.ref), id(state1.numeric)) def test_has_instance(self): """Test to check has_instance correctness.""" a = A() r = state_pickler.get_state(a) self.assertEqual(r.__metadata__['has_instance'], True) l = [1, a] r = state_pickler.get_state(l) self.assertEqual(r.has_instance, True) self.assertEqual(r[1].__metadata__['has_instance'], True) d = {'a': l, 'b': 1} r = state_pickler.get_state(d) self.assertEqual(r.has_instance, True) self.assertEqual(r['a'].has_instance, True) self.assertEqual(r['a'][1].__metadata__['has_instance'], True) class B: def __init__(self): self.a = [1, A()] b = B() r = state_pickler.get_state(b) self.assertEqual(r.__metadata__['has_instance'], True) self.assertEqual(r.a.has_instance, True) self.assertEqual(r.a[1].__metadata__['has_instance'], True) def test_pickle_classic(self): """Test if classic classes can be pickled.""" t = TestClassic() self.set_object(t) # Generate the dict that is actually pickled. state = state_pickler.StatePickler().dump_state(t) # First check if all the attributes are handled. keys = sorted(state['data']['data'].keys()) expect = [x for x in t.__dict__.keys() if '__' not in x] expect.sort() self.assertEqual(keys, expect) # Check each attribute. self.verify(t, state) def test_unpickle_classic(self): """Test if classic classes can be unpickled.""" t = TestClassic() self.set_object(t) # Get the pickled state. res = state_pickler.get_state(t) # Check each attribute. self.verify_unpickled(t, res) def test_state_setter_classic(self): """Test if classic classes' state can be set.""" t = TestClassic() self.set_object(t) # Get the pickled state. res = state_pickler.get_state(t) # Now create a new instance and set its state. t1 = state_pickler.create_instance(res) state_pickler.set_state(t1, res) # Check each attribute. self.verify_unpickled(t1, res) def test_state_setter(self): """Test some of the features of the set_state method.""" t = TestClassic() self.set_object(t) # Get the saved state. res = state_pickler.get_state(t) # Now create a new instance and test the setter. t1 = state_pickler.create_instance(res) keys = ['c', 'b', 'f', 'i', 'tuple', 'list', 'l', 'numeric', 'n', 's', 'u', 'pure_list', 'inst', 'ref', 'dict'] ignore = list(keys) ignore.remove('b') first = ['b'] last = [] state_pickler.set_state(t1, res, ignore=ignore, first=first, last=last) # Only 'b' should have been set. self.assertEqual(t1.b, True) # Rest are unchanged. self.assertEqual(t1.i, 7) self.assertEqual(t1.s, 'String') self.assertEqual(t1.u, u'Unicode') self.assertEqual(t1.inst.a, 'a') self.assertEqual(t1.list[0], 1) self.assertEqual(t1.tuple[-1].a, 'a') self.assertEqual(t1.dict['a'], 1) # Check if last works. last = ignore ignore = [] first = [] state_pickler.set_state(t1, res, ignore=ignore, first=first, last=last) # Check everything. self.verify_unpickled(t1, res) def test_pickle_traits(self): """Test if traited classes can be pickled.""" t = TestTraits() self.set_object(t) # Generate the dict that is actually pickled. state = state_pickler.StatePickler().dump_state(t) # First check if all the attributes are handled. keys = sorted(state['data']['data'].keys()) expect = [x for x in t.__dict__.keys() if '__' not in x] expect.sort() self.assertEqual(keys, expect) # Check each attribute. self.verify(t, state) def test_unpickle_traits(self): """Test if traited classes can be unpickled.""" t = TestTraits() self.set_object(t) # Get the pickled state. res = state_pickler.get_state(t) # Check each attribute. self.verify_unpickled(t, res) def test_state_setter_traits(self): """Test if traited classes' state can be set.""" t = TestTraits() self.set_object(t) # Get the saved state. res = state_pickler.get_state(t) # Now create a new instance and set its state. t1 = state_pickler.create_instance(res) state_pickler.set_state(t1, res) # Check each attribute. self.verify_unpickled(t1, res) def test_reference_cycle(self): """Test if reference cycles are handled when setting the state.""" class A: pass class B: pass a = A() b = B() a.a = b b.b = a state = state_pickler.get_state(a) z = A() z.a = B() z.a.b = z state_pickler.set_state(z, state) def test_get_state_on_tuple_with_numeric_references(self): num = numpy.zeros(10, float) data = (num, num) # If this just completes without error, we are good. state = state_pickler.get_state(data) # The two should be the same object. self.assertTrue(state[0] is state[1]) numpy.testing.assert_allclose(state[0], num) def test_state_is_saveable(self): """Test if the state can be saved like the object itself.""" t = TestClassic() self.set_object(t) state = state_pickler.get_state(t) # Now get the state of the state itself. state1 = state_pickler.get_state(state) self.verify_state(state1, state) # Same thing for the traited class. t = TestTraits() self.set_object(t) state = state_pickler.get_state(t) # Now get the state of the state itself. state1 = state_pickler.get_state(state) self.verify_state(state1, state) def test_get_pure_state(self): """Test if get_pure_state is called first.""" class B: def __init__(self): self.a = 'dict' def __get_pure_state__(self): return {'a': 'get_pure_state'} def __getstate__(self): return {'a': 'getstate'} b = B() s = state_pickler.get_state(b) self.assertEqual(s.a, 'get_pure_state') del B.__get_pure_state__ s = state_pickler.get_state(b) self.assertEqual(s.a, 'getstate') del B.__getstate__ s = state_pickler.get_state(b) self.assertEqual(s.a, 'dict') def test_dump_to_file_str(self): """Test if dump can take a str as file""" obj = A() filepath = os.path.join(tempfile.gettempdir(), "tmp.file") try: state_pickler.dump(obj, filepath) finally: os.remove(filepath) if __name__ == "__main__": unittest.main() apptools-4.5.0/apptools/persistence/__init__.py0000644000076500000240000000060011640354733023142 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2004 by Enthought, Inc. # All rights reserved. #------------------------------------------------------------------------------ """ Supports flexible pickling and unpickling of the state of a Python object to a dictionary. Part of the AppTools project of the Enthought Tool Suite. """ apptools-4.5.0/apptools/persistence/spickle.py0000644000076500000240000002317213422276145023046 0ustar mdickinsonstaff00000000000000"""A special unpickler that gives you a state object and a special pickler that lets you re-pickle that state. The nice thing about creating state objects is that it does not import any modules and does not create any instances. Instead of instances it creates `State` instances which have the same attributes as the real object. With this you can load a pickle (without even having the modules on the machine), modify it and re-pickle it back. NOTE: This module is not likely to work for very complex pickles but it should work for most common cases. """ # Author: Prabhu Ramachandran # Copyright (c) 2006-2015, Prabhu Ramachandran # License: BSD Style. import sys if sys.version_info[0] > 2: raise ImportError("This module does not work with Python 3.x") import warnings import pickle import struct from pickle import Pickler, Unpickler, dumps, BUILD, NEWOBJ, REDUCE, \ MARK, OBJ, INST, BUILD, PicklingError, GLOBAL, \ EXT1, EXT2, EXT4, _extension_registry, _keep_alive from io import BytesIO ###################################################################### # `State` class ###################################################################### class State(dict): """Used to encapsulate the state of an instance in a very convenient form. The '__METADATA__' attribute/key is a dictionary that has class specific details like the class name, module name etc. """ def __init__(self, **kw): dict.__init__(self, **kw) self.__dict__ = self ###################################################################### # `StatePickler` class ###################################################################### class StatePickler(Pickler): """Pickles a `State` object back as a regular pickle that may be unpickled. """ def __init__(self, file, protocol, bin=None): Pickler.__init__(self, file, protocol) self.bin = bin def save(self, obj): # Check the memo x = self.memo.get(id(obj)) if x: self.write(self.get(x[0])) return if isinstance(obj, State): md = obj.__METADATA__ typ = md['type'] if typ == 'instance': self._state_instance(obj) elif typ in ['newobj', 'reduce']: self._state_reduce(obj) elif typ in ['class']: self._save_global(obj) else: Pickler.save(self, obj) def _state_instance(self, obj): md = obj.__METADATA__ cls = md.get('class') cls_md = cls.__METADATA__ memo = self.memo write = self.write save = self.save args = md.get('initargs') if len(args) > 0: _keep_alive(args, memo) write(MARK) if self.bin: save(cls) for arg in args: save(arg) write(OBJ) else: for arg in args: save(arg) write(INST + cls_md.get('module') + '\n' + cls_md.get('name') + '\n') self.memoize(obj) stuff = dict(obj.__dict__) stuff.pop('__METADATA__') if '__setstate_data__' in stuff: data = stuff.pop('__setstate_data__') _keep_alive(data, memo) save(data) else: save(stuff) write(BUILD) def _state_reduce(self, obj): # FIXME: this code is not as complete as pickle's reduce # handling code and is likely to not work in all cases. md = obj.__METADATA__ func = md.get('class') func_md = func.__METADATA__ args = md.get('initargs') state = dict(obj.__dict__) state.pop('__METADATA__') # This API is called by some subclasses # Assert that args is a tuple or None if not isinstance(args, tuple): if args is None: # A hack for Jim Fulton's ExtensionClass, now deprecated. # See load_reduce() warnings.warn("__basicnew__ special case is deprecated", DeprecationWarning) else: raise PicklingError( "args from reduce() should be a tuple") # Assert that func is callable #if not callable(func): # raise PicklingError("func from reduce should be callable") save = self.save write = self.write # Protocol 2 special case: if func's name is __newobj__, use NEWOBJ if self.proto >= 2 and func_md.get("name", "") == "__newobj__": # FIXME: this is unlikely to work. cls = args[0] if not hasattr(cls, "__new__"): raise PicklingError( "args[0] from __newobj__ args has no __new__") if obj is not None and cls is not obj.__class__: raise PicklingError( "args[0] from __newobj__ args has the wrong class") args = args[1:] save(cls) save(args) write(NEWOBJ) else: save(func) save(args) write(REDUCE) if obj is not None: self.memoize(obj) if state is not None: if '__setstate_data__' in state: data = state.pop('__setstate_data__') save(data) else: save(state) write(BUILD) def _save_global(self, obj, name=None, pack=struct.pack): write = self.write memo = self.memo md = obj.__METADATA__ if name is None: name = md.get('name') module = md.get('module') if self.proto >= 2: code = _extension_registry.get((module, name)) if code: assert code > 0 if code <= 0xff: write(EXT1 + chr(code)) elif code <= 0xffff: write("%c%c%c" % (EXT2, code&0xff, code>>8)) else: write(EXT4 + pack(">> class A: ... def __init__(self): ... self.a = 'a' ... >>> a = A() >>> a.a = 100 >>> import state_pickler >>> s = state_pickler.dumps(a) # Dump the state of `a`. >>> state = state_pickler.loads_state(s) # Get the state back. >>> b = state_pickler.create_instance(state) # Create the object. >>> state_pickler.set_state(b, state) # Set the object's state. >>> assert b.a == 100 Features -------- - The output is a plain old dictionary so is easy to parse, edit etc. - Handles references to avoid duplication. - Gzips Numeric arrays when dumping them. - Support for versioning. Caveats ------- - Does not pickle a whole bunch of stuff including code objects and functions. - The output is a pure dictionary and does not contain instances. So using this *as it is* in `__setstate__` will not work. Instead define a `__set_pure_state__` and use the `StateSetter` class or the `set_state` function provided by this module. Notes ----- Browsing the code from XMarshaL_ and pickle.py proved useful for ideas. None of the code is taken from there though. .. _XMarshaL: http://www.dezentral.de/soft/XMarshaL """ # Author: Prabhu Ramachandran # Copyright (c) 2005-2015, Enthought, Inc. # License: BSD Style. # Standard library imports. import base64 import sys import types import pickle import gzip from io import BytesIO, StringIO import numpy # Local imports. from . import version_registry from .file_path import FilePath PY_VER = sys.version_info[0] NumpyArrayType = type(numpy.array([])) def gzip_string(data): """Given a string (`data`) this gzips the string and returns it. """ s = BytesIO() writer = gzip.GzipFile(mode='wb', fileobj=s) writer.write(data) writer.close() s.seek(0) return s.read() def gunzip_string(data): """Given a gzipped string (`data`) this unzips the string and returns it. """ if PY_VER== 2 or (bytes is not str and type(data) is bytes): s = BytesIO(data) else: s = StringIO(data) writer = gzip.GzipFile(mode='rb', fileobj=s) data = writer.read() writer.close() return data class StatePicklerError(Exception): pass class StateUnpicklerError(Exception): pass class StateSetterError(Exception): pass ###################################################################### # `State` class ###################################################################### class State(dict): """Used to encapsulate the state of an instance in a very convenient form. The '__metadata__' attribute/key is a dictionary that has class specific details like the class name, module name etc. """ def __init__(self, **kw): dict.__init__(self, **kw) self.__dict__ = self ###################################################################### # `StateDict` class ###################################################################### class StateDict(dict): """Used to encapsulate a dictionary stored in a `State` instance. The has_instance attribute specifies if the dict has an instance embedded in it. """ def __init__(self, **kw): dict.__init__(self, **kw) self.has_instance = False ###################################################################### # `StateList` class ###################################################################### class StateList(list): """Used to encapsulate a list stored in a `State` instance. The has_instance attribute specifies if the list has an instance embedded in it. """ def __init__(self, seq=None): if seq: list.__init__(self, seq) else: list.__init__(self) self.has_instance = False ###################################################################### # `StateTuple` class ###################################################################### class StateTuple(tuple): """Used to encapsulate a tuple stored in a `State` instance. The has_instance attribute specifies if the tuple has an instance embedded in it. """ def __new__(cls, seq=None): if seq: obj = super(StateTuple, cls).__new__(cls, tuple(seq)) else: obj = super(StateTuple, cls).__new__(cls) obj.has_instance = False return obj ###################################################################### # `StatePickler` class ###################################################################### class StatePickler: """Pickles the state of an object into a dictionary. The dictionary is itself either saved as a pickled file (`dump`) or pickled string (`dumps`). Alternatively, the `dump_state` method will return the dictionary that is pickled. The format of the state dict is quite strightfoward. Basic types (bool, int, long, float, complex, None, string and unicode) are represented as they are. Everything else is stored as a dictionary containing metadata information on the object's type etc. and also the actual object in the 'data' key. For example:: >>> p = StatePickler() >>> p.dump_state(1) 1 >>> l = [1,2.0, None, [1,2,3]] >>> p.dump_state(l) {'data': [1, 2.0, None, {'data': [1, 2, 3], 'type': 'list', 'id': 1}], 'id': 0, 'type': 'list'} Classes are also represented similarly. The state in this case is obtained from the `__getstate__` method or from the `__dict__`. Here is an example:: >>> class A: ... __version__ = 1 # State version ... def __init__(self): ... self.attribute = 1 ... >>> a = A() >>> p = StatePickler() >>> p.dump_state(a) {'class_name': 'A', 'data': {'data': {'attribute': 1}, 'type': 'dict', 'id': 2}, 'id': 0, 'initargs': {'data': (), 'type': 'tuple', 'id': 1}, 'module': '__main__', 'type': 'instance', 'version': [(('A', '__main__'), 1)]} When pickling data, references are taken care of. Numeric arrays can be pickled and are stored as a gzipped base64 encoded string. """ def __init__(self): self._clear() type_map = {bool: self._do_basic_type, complex: self._do_basic_type, float: self._do_basic_type, int: self._do_basic_type, type(None): self._do_basic_type, str: self._do_basic_type, bytes: self._do_basic_type, tuple: self._do_tuple, list: self._do_list, dict: self._do_dict, NumpyArrayType: self._do_numeric, State: self._do_state, } if PY_VER == 2: type_map[long] = self._do_basic_type type_map[unicode] = self._do_basic_type self.type_map = type_map def dump(self, value, file): """Pickles the state of the object (`value`) into the passed file. """ try: # Store the file name we are writing to so we can munge # file paths suitably. self.file_name = file.name except AttributeError: pass pickle.dump(self._do(value), file) def dumps(self, value): """Pickles the state of the object (`value`) and returns a string. """ return pickle.dumps(self._do(value)) def dump_state(self, value): """Returns a dictionary or a basic type representing the complete state of the object (`value`). This value is pickled by the `dump` and `dumps` methods. """ return self._do(value) ###################################################################### # Non-public methods ###################################################################### def _clear(self): # Stores the file name of the file being used to dump the # state. This is used to change any embedded paths relative # to the saved file. self.file_name = '' # Caches id's to handle references. self.obj_cache = {} # Misc cache to cache things that are not persistent. For # example, object.__getstate__()/__getinitargs__() usually # returns a copy of a dict/tuple that could possibly be reused # on another object's __getstate__. Caching these prevents # some wierd problems with the `id` of the object. self._misc_cache = [] def _flush_traits(self, obj): """Checks if the object has traits and ensures that the traits are set in the `__dict__` so we can pickle it. """ # Not needed with Traits3. return def _do(self, obj): obj_type = type(obj) key = self._get_id(obj) if key in self.obj_cache: return self._do_reference(obj) elif obj_type in self.type_map: return self.type_map[obj_type](obj) elif isinstance(obj, tuple): # Takes care of StateTuples. return self._do_tuple(obj) elif isinstance(obj, list): # Takes care of TraitListObjects. return self._do_list(obj) elif isinstance(obj, dict): # Takes care of TraitDictObjects. return self._do_dict(obj) elif hasattr(obj, '__dict__'): return self._do_instance(obj) def _get_id(self, value): try: key = hash(value) except TypeError: key = id(value) return key def _register(self, value): key = self._get_id(value) cache = self.obj_cache idx = len(cache) cache[key] = idx return idx def _do_basic_type(self, value): return value def _do_reference(self, value): key = self._get_id(value) idx = self.obj_cache[key] return dict(type='reference', id=idx, data=None) def _do_instance(self, value): # Flush out the traits. self._flush_traits(value) # Setup the relative paths of FilePaths before dumping. if self.file_name and isinstance(value, FilePath): value.set_relative(self.file_name) # Get the initargs. args = () if hasattr(value, '__getinitargs__') and value.__getinitargs__: args = value.__getinitargs__() # Get the object state. if hasattr(value, '__get_pure_state__'): state = value.__get_pure_state__() elif hasattr(value, '__getstate__'): state = value.__getstate__() else: state = value.__dict__ state.pop('__traits_version__', None) # Cache the args and state since they are likely to be gc'd. self._misc_cache.extend([args, state]) # Register and process. idx = self._register(value) args_data = self._do(args) data = self._do(state) # Get the version of the object. version = version_registry.get_version(value) module = value.__class__.__module__ class_name = value.__class__.__name__ return dict(type='instance', module=module, class_name=class_name, version=version, id=idx, initargs=args_data, data=data) def _do_state(self, value): metadata = value.__metadata__ args = metadata.get('initargs') state = dict(value) state.pop('__metadata__') self._misc_cache.extend([args, state]) idx = self._register(value) args_data = self._do(args) data = self._do(state) return dict(type='instance', module=metadata['module'], class_name=metadata['class_name'], version=metadata['version'], id=idx, initargs=args_data, data=data) def _do_tuple(self, value): idx = self._register(value) data = tuple([self._do(x) for x in value]) return dict(type='tuple', id=idx, data=data) def _do_list(self, value): idx = self._register(value) data = [self._do(x) for x in value] return dict(type='list', id=idx, data=data) def _do_dict(self, value): idx = self._register(value) vals = [self._do(x) for x in value.values()] data = dict(zip(value.keys(), vals)) return dict(type='dict', id=idx, data=data) def _do_numeric(self, value): idx = self._register(value) if PY_VER > 2: data = base64.encodebytes(gzip_string(numpy.ndarray.dumps(value))) else: data = base64.encodestring(gzip_string(numpy.ndarray.dumps(value))) return dict(type='numeric', id=idx, data=data) ###################################################################### # `StateUnpickler` class ###################################################################### class StateUnpickler: """Unpickles the state of an object saved using StatePickler. Please note that unlike the standard Unpickler, no instances of any user class are created. The data for the state is obtained from the file or string, reference objects are setup to refer to the same state value and this state is returned in the form usually in the form of a dictionary. For example:: >>> class A: ... def __init__(self): ... self.attribute = 1 ... >>> a = A() >>> p = StatePickler() >>> s = p.dumps(a) >>> up = StateUnpickler() >>> state = up.loads_state(s) >>> state.__class__.__name__ 'State' >>> state.attribute 1 >>> state.__metadata__ {'class_name': 'A', 'has_instance': True, 'id': 0, 'initargs': (), 'module': '__main__', 'type': 'instance', 'version': [(('A', '__main__'), -1)]} Note that the state is actually a `State` instance and is navigable just like the original object. The details of the instance are stored in the `__metadata__` attribute. This is highly convenient since it is possible for someone to view and modify the state very easily. """ def __init__(self): self._clear() self.type_map = {'reference': self._do_reference, 'instance': self._do_instance, 'tuple': self._do_tuple, 'list': self._do_list, 'dict': self._do_dict, 'numeric': self._do_numeric, } def load_state(self, file): """Returns the state of an object loaded from the pickled data in the given file. """ try: self.file_name = file.name except AttributeError: pass data = pickle.load(file) result = self._process(data) return result def loads_state(self, string): """Returns the state of an object loaded from the pickled data in the given string. """ data = pickle.loads(string) result = self._process(data) return result ###################################################################### # Non-public methods ###################################################################### def _clear(self): # The file from which we are being loaded. self.file_name = '' # Cache of the objects. self._obj_cache = {} # Paths to the instances. self._instances = [] # Caches the references. self._refs = {} # Numeric arrays. self._numeric = {} def _set_has_instance(self, obj, value): if isinstance(obj, State): obj.__metadata__['has_instance'] = value elif isinstance(obj, (StateDict, StateList, StateTuple)): obj.has_instance = value def _process(self, data): result = self._do(data) # Setup all the Numeric arrays. Do this first since # references use this. for key, (path, val) in self._numeric.items(): if isinstance(result, StateTuple): result = list(result) exec('result%s = val'%path) result = StateTuple(result) else: exec('result%s = val'%path) # Setup the references so they really are references. for key, paths in self._refs.items(): for path in paths: x = self._obj_cache[key] if isinstance(result, StateTuple): result = list(result) exec('result%s = x'%path) result = StateTuple(result) else: exec('result%s = x'%path) # if the reference is to an instance append its path. if isinstance(x, State): self._instances.append(path) # Now setup the 'has_instance' attribute. If 'has_instance' # is True then the object contains an instance somewhere # inside it. for path in self._instances: pth = path while pth: ns = {'result': result} exec('val = result%s'%pth, ns, ns) self._set_has_instance(ns['val'], True) end = pth.rfind('[') pth = pth[:end] # Now make sure that the first element also has_instance. self._set_has_instance(result, True) return result def _do(self, data, path=''): if type(data) is dict: return self.type_map[data['type']](data, path) else: return data def _do_reference(self, value, path): id = value['id'] if id in self._refs: self._refs[id].append(path) else: self._refs[id] = [path] return State(__metadata__=value) def _handle_file_path(self, value): if (value['class_name'] == 'FilePath') and \ ('file_path' in value['module']) and \ self.file_name: data = value['data']['data'] fp = FilePath(data['rel_pth']) fp.set_absolute(self.file_name) data['abs_pth'] = fp.abs_pth def _do_instance(self, value, path): self._instances.append(path) initargs = self._do(value['initargs'], path + '.__metadata__["initargs"]') # Handle FilePaths. self._handle_file_path(value) d = self._do(value['data'], path) md = dict(type='instance', module=value['module'], class_name=value['class_name'], version=value['version'], id=value['id'], initargs=initargs, has_instance=True) result = State(**d) result.__metadata__ = md self._obj_cache[value['id']] = result return result def _do_tuple(self, value, path): res = [] for i, x in enumerate(value['data']): res.append(self._do(x, path + '[%d]'%i)) result = StateTuple(res) self._obj_cache[value['id']] = result return result def _do_list(self, value, path): result = StateList() for i, x in enumerate(value['data']): result.append(self._do(x, path + '[%d]'%i)) self._obj_cache[value['id']] = result return result def _do_dict(self, value, path): result = StateDict() for key, val in value['data'].items(): result[key] = self._do(val, path + '["%s"]'%key) self._obj_cache[value['id']] = result return result def _do_numeric(self, value, path): if PY_VER > 2: data = value['data'] if isinstance(data, str): data = value['data'].encode('utf-8') junk = gunzip_string(base64.decodebytes(data)) result = pickle.loads(junk, encoding='bytes') else: junk = gunzip_string(value['data'].decode('base64')) result = pickle.loads(junk) self._numeric[value['id']] = (path, result) self._obj_cache[value['id']] = result return result ###################################################################### # `StateSetter` class ###################################################################### class StateSetter: """This is a convenience class that helps a user set the attributes of an object given its saved state. For instances it checks to see if a `__set_pure_state__` method exists and calls that when it sets the state. """ def __init__(self): # Stores the ids of instances already done. self._instance_ids = [] self.type_map = {State: self._do_instance, StateTuple: self._do_tuple, StateList: self._do_list, StateDict: self._do_dict, } def set(self, obj, state, ignore=None, first=None, last=None): """Sets the state of the object. This is to be used as a means to simplify loading the state of an object from its `__setstate__` method using the dictionary describing its state. Note that before the state is set, the registered handlers for the particular class are called in order to upgrade the version of the state to the latest version. Parameters ---------- - obj : `object` The object whose state is to be set. If this is `None` (default) then the object is created. - state : `dict` The dictionary representing the state of the object. - ignore : `list(str)` The list of attributes specified in this list are ignored and the state of these attributes are not set (this excludes the ones specified in `first` and `last`). If one specifies a '*' then all attributes are ignored except the ones specified in `first` and `last`. - first : `list(str)` The list of attributes specified in this list are set first (in order), before any other attributes are set. - last : `list(str)` The list of attributes specified in this list are set last (in order), after all other attributes are set. """ if (not isinstance(state, State)) and \ state.__metadata__['type'] != 'instance': raise StateSetterError( 'Can only set the attributes of an instance.' ) # Upgrade the state to the latest using the registry. self._update_and_check_state(obj, state) self._register(obj) # This wierdness is needed since the state's own `keys` might # be set to something else. state_keys = list(dict.keys(state)) state_keys.remove('__metadata__') if first is None: first = [] if last is None: last = [] # Remove all the ignored keys. if ignore: if '*' in ignore: state_keys = first + last else: for name in ignore: try: state_keys.remove(name) except KeyError: pass # Do the `first` attributes. for key in first: state_keys.remove(key) self._do(obj, key, state[key]) # Remove the `last` attributes. for key in last: state_keys.remove(key) # Set the remaining attributes. for key in state_keys: self._do(obj, key, state[key]) # Do the last ones in order. for key in last: self._do(obj, key, state[key]) ###################################################################### # Non-public methods. ###################################################################### def _register(self, obj): idx = id(obj) if idx not in self._instance_ids: self._instance_ids.append(idx) def _is_registered(self, obj): return (id(obj) in self._instance_ids) def _has_instance(self, value): """Given something (`value`) that is part of the state this returns if the value has an instance embedded in it or not. """ if isinstance(value, State): return True elif isinstance(value, (StateDict, StateList, StateTuple)): return value.has_instance return False def _get_pure(self, value): """Returns the Python representation of the object (usually a list, tuple or dict) that has no instances embedded within it. """ result = value if self._has_instance(value): raise StateSetterError( 'Value has an instance: %s'%value ) if isinstance(value, (StateList, StateTuple)): result = [self._get_pure(x) for x in value] if isinstance(value, StateTuple): result = tuple(result) elif isinstance(value, StateDict): result = {} for k, v in value.items(): result[k] = self._get_pure(v) return result def _update_and_check_state(self, obj, state): """Updates the state from the registry and then checks if the object and state have same class. """ # Upgrade this state object to the latest using the registry. # This is done before testing because updating may change the # class name/module. version_registry.registry.update(state) # Make sure object and state have the same class and module names. metadata = state.__metadata__ cls = obj.__class__ if (metadata['class_name'] != cls.__name__): raise StateSetterError( 'Instance (%s) and state (%s) do not have the same class'\ ' name!'%(cls.__name__, metadata['class_name']) ) if (metadata['module'] != cls.__module__): raise StateSetterError( 'Instance (%s) and state (%s) do not have the same module'\ ' name!'%(cls.__module__, metadata['module']) ) def _do(self, obj, key, value): try: attr = getattr(obj, key) except AttributeError: raise StateSetterError( 'Object %s does not have an attribute called: %s'%(obj, key) ) if isinstance(value, (State, StateDict, StateList, StateTuple)): # Special handlers are needed. if not self._has_instance(value): result = self._get_pure(value) setattr(obj, key, result) elif isinstance(value, StateTuple): setattr(obj, key, self._do_tuple(getattr(obj, key), value)) else: self._do_object(getattr(obj, key), value) else: setattr(obj, key, value) def _do_object(self, obj, state): self.type_map[state.__class__](obj, state) def _do_instance(self, obj, state): if self._is_registered(obj): return else: self._register(obj) metadata = state.__metadata__ if hasattr(obj, '__set_pure_state__'): self._update_and_check_state(obj, state) obj.__set_pure_state__(state) elif 'tvtk_classes' in metadata['module']: self._update_and_check_state(obj, state) tmp = self._get_pure(StateDict(**state)) del tmp['__metadata__'] obj.__setstate__(tmp) else: # No need to update or check since `set` does it for us. self.set(obj, state) def _do_tuple(self, obj, state): if not self._has_instance(state): return self._get_pure(state) else: result = list(obj) self._do_list(result, state) return tuple(result) def _do_list(self, obj, state): if len(obj) == len(state): for i in range(len(obj)): if not self._has_instance(state[i]): obj[i] = self._get_pure(state[i]) elif isinstance(state[i], tuple): obj[i] = self._do_tuple(state[i]) else: self._do_object(obj[i], state[i]) else: raise StateSetterError( 'Cannot set state of list of incorrect size.' ) def _do_dict(self, obj, state): for key, value in state.items(): if not self._has_instance(value): obj[key] = self._get_pure(value) elif isinstance(value, tuple): obj[key] = self._do_tuple(value) else: self._do_object(obj[key], value) ###################################################################### # Internal Utility functions. ###################################################################### def _get_file_read(f): if hasattr(f, 'read'): return f else: return open(f, 'rb') def _get_file_write(f): if hasattr(f, 'write'): return f else: return open(f, 'wb') ###################################################################### # Utility functions. ###################################################################### def dump(value, file): """Pickles the state of the object (`value`) into the passed file (or file name). """ f = _get_file_write(file) try: StatePickler().dump(value, f) finally: f.flush() if f is not file: f.close() def dumps(value): """Pickles the state of the object (`value`) and returns a string. """ return StatePickler().dumps(value) def load_state(file): """Returns the state of an object loaded from the pickled data in the given file (or file name). """ f = _get_file_read(file) try: state = StateUnpickler().load_state(f) finally: if f is not file: f.close() return state def loads_state(string): """Returns the state of an object loaded from the pickled data in the given string. """ return StateUnpickler().loads_state(string) def get_state(obj): """Returns the state of the object (usually as a dictionary). The returned state may be used directy to set the state of the object via `set_state`. """ s = dumps(obj) return loads_state(s) def set_state(obj, state, ignore=None, first=None, last=None): StateSetter().set(obj, state, ignore, first, last) set_state.__doc__ = StateSetter.set.__doc__ def update_state(state): """Given the state of an object, this updates the state to the latest version using the handlers given in the version registry. The state is modified in-place. """ version_registry.registry.update(state) def create_instance(state): """Create an instance from the state if possible. """ if (not isinstance(state, State)) and \ ('class_name' not in state.__metadata__): raise StateSetterError('No class information in state') metadata = state.__metadata__ class_name = metadata.get('class_name') mod_name = metadata.get('module') if 'tvtk_classes' in mod_name: # FIXME: This sort of special-case is probably indicative of something # that needs more thought, plus it makes it tought to decide whether # this component depends on tvtk! from tvtk.api import tvtk return getattr(tvtk, class_name)() initargs = metadata['initargs'] if initargs.has_instance: raise StateUnpicklerError('Cannot unpickle non-trivial initargs') __import__(mod_name, globals(), locals(), class_name) mod = sys.modules[mod_name] cls = getattr(mod, class_name) return cls(*initargs) apptools-4.5.0/apptools/persistence/version_registry.py0000644000076500000240000000663611640354733025037 0ustar mdickinsonstaff00000000000000"""A version registry that manages handlers for different state versions. """ # Author: Prabhu Ramachandran # Copyright (c) 2005, Enthought, Inc. # License: BSD Style. # Standard library imports. import sys import inspect import logging logger = logging.getLogger(__name__) ###################################################################### # Utility functions. ###################################################################### def get_version(obj): """Walks the class hierarchy and obtains the versions of the various classes and returns a list of tuples of the form ((class_name, module), version) in reverse order of the MRO. """ res = [] for cls in inspect.getmro(obj.__class__): class_name, module = cls.__name__, cls.__module__ if module in ['__builtin__']: # No point in versioning builtins. continue try: version = cls.__version__ except AttributeError: version = -1 res.append( ( (class_name, module), version) ) res.reverse() return res ###################################################################### # `HandlerRegistry` class. ###################################################################### class HandlerRegistry: """A simple version conversion handler registry. Classes register handlers in order to convert the state version to the latest version. When an object's state is about to be set, the `update` method of the registy is called. This in turn calls any handlers registered for the class/module and this handler is then called with the state and the version of the state. The state is modified in-place by the handlers. """ def __init__(self): # The version conversion handlers. # Key: (class_name, module), value: handler self.handlers = {} def register(self, class_name, module, handler): """Register `handler` that handles versioning for class having class name (`class_name`) and module name (`module`). The handler function will be passed the state and its version to fix. """ key = (class_name, module) if key in self.handlers: msg = 'Overwriting version handler for (%s, %s)'%(key[0], key[1]) logger.warn(msg) self.handlers[(class_name, module)] = handler def unregister(self, class_name, module): """Unregisters any handlers for a class and module. """ self.handlers.pop((class_name, module)) def update(self, state): """Updates the given state using the handlers. Note that the state is modified in-place. """ if (not self.handlers) or (not hasattr(state, '__metadata__')): return versions = state.__metadata__['version'] for ver in versions: key = ver[0] try: self.handlers[key](state, ver[1]) except KeyError: pass def _create_registry(): """Creates a reload safe, singleton registry. """ registry = None for key in sys.modules.keys(): if 'version_registry' in key: mod = sys.modules[key] if hasattr(mod, 'registry'): registry = mod.registry break if not registry: registry = HandlerRegistry() return registry # The singleton registry. registry = _create_registry() apptools-4.5.0/apptools/persistence/versioned_unpickler.py0000644000076500000240000001760013547637361025476 0ustar mdickinsonstaff00000000000000# Standard library imports from pickle import * import sys, new import logging from types import GeneratorType # Enthought library imports from apptools.persistence.updater import __replacement_setstate__ logger = logging.getLogger(__name__) ############################################################################## # class 'NewUnpickler' ############################################################################## class NewUnpickler(Unpickler): """ An unpickler that implements a two-stage pickling process to make it possible to unpickle complicated Python object hierarchies where the unserialized state of an object depends on the state of other objects in the same pickle. """ def load(self, max_pass=-1): """Read a pickled object representation from the open file. Return the reconstituted object hierarchy specified in the file. """ # List of objects to be unpickled. self.objects = [] # We overload the load_build method. dispatch = self.dispatch dispatch[BUILD[0]] = NewUnpickler.load_build # call the super class' method. ret = Unpickler.load(self) self.initialize(max_pass) self.objects = [] # Reset the Unpickler's dispatch table. dispatch[BUILD[0]] = Unpickler.load_build return ret def initialize(self, max_pass): # List of (object, generator) tuples that initialize objects. generators = [] # Execute object's initialize to setup the generators. for obj in self.objects: if hasattr(obj, '__initialize__') and \ callable(obj.__initialize__): ret = obj.__initialize__() if isinstance(ret, GeneratorType): generators.append((obj, ret)) elif ret is not None: raise UnpicklingError('Unexpected return value from ' '__initialize__. %s returned %s' % (obj, ret)) # Ensure a maximum number of passes if max_pass < 0: max_pass = len(generators) # Now run the generators. count = 0 while len(generators) > 0: count += 1 if count > max_pass: not_done = [x[0] for x in generators] msg = """Reached maximum pass count %s. You may have a deadlock! The following objects are uninitialized: %s""" % (max_pass, not_done) raise UnpicklingError(msg) for o, g in generators[:]: try: next(g) except StopIteration: generators.remove((o, g)) # Make this a class method since dispatch is a class variable. # Otherwise, supposing the initial sweet_pickle.load call (which would # have overloaded the load_build method) makes a pickle.load call at some # point, we would have the dispatch still pointing to # NewPickler.load_build whereas the object being passed in will be an # Unpickler instance, causing a TypeError. def load_build(cls, obj): # Just save the instance in the list of objects. if isinstance(obj, NewUnpickler): obj.objects.append(obj.stack[-2]) Unpickler.load_build(obj) load_build = classmethod(load_build) class VersionedUnpickler(NewUnpickler): """ This class reads in a pickled file created at revision version 'n' and then applies the transforms specified in the updater class to generate a new set of objects which are at revision version 'n+1'. I decided to keep the loading of the updater out of this generic class because we will want updaters to be generated for each plugin's type of project. This ensures that the VersionedUnpickler can remain ignorant about the actual version numbers - all it needs to do is upgrade one release. """ def __init__(self, file, updater=None): Unpickler.__init__(self, file) self.updater = updater return def find_class(self, module, name): """ Overridden method from Unpickler. NB __setstate__ is not called until later. """ if self.updater: # check to see if this class needs to be mapped to a new class # or module name original_module, original_name = module, name #logger.debug('omodule:%s oname:%s' % (original_module, original_name)) module, name = self.updater.get_latest(module, name) #logger.debug('module:%s name:%s' % (module, name)) # load the class... '''__import__(module) mod = sys.modules[module] klass = getattr(mod, name)''' klass = self.import_name(module, name) # add the updater.... TODO - why the old name? self.add_updater(original_module, original_name, klass) else: # there is no updater so we will be reading in an up to date # version of the file... try: klass = Unpickler.find_class(self, module, name) except: logger.error("Looking for [%s] [%s]" % (module, name)) logger.exception('Problem using default unpickle functionality') # restore the original __setstate__ if necessary fn = getattr(klass, '__setstate_original__', False) if fn: m = new.instancemethod(fn, None, klass) setattr(klass, '__setstate__', m) return klass def add_updater(self, module, name, klass): """ If there is an updater defined for this class we will add it to the class as the __setstate__ method. """ fn = self.updater.setstates.get((module, name), False) if fn: # move the existing __setstate__ out of the way self.backup_setstate(module, klass) # add the updater into the class m = new.instancemethod(fn, None, klass) setattr(klass, '__updater__', m) # hook up our __setstate__ which updates self.__dict__ m = new.instancemethod(__replacement_setstate__, None, klass) setattr(klass, '__setstate__', m) else: pass #print 'No updater fn to worry about' return def backup_setstate(self, module, klass): """ If the class has a user defined __setstate__ we back it up. """ if getattr(klass, '__setstate__', False): if getattr(klass, '__setstate_original__', False): # don't overwrite the original __setstate__ name = '__setstate__%s' % self.updater.__class__ else: # backup the original __setstate__ which we will restore # and run later when we have finished updating the class name = '__setstate_original__' #logger.debug('renaming __setstate__ to %s' % name) method = getattr(klass, '__setstate__') m = new.instancemethod(method, None, klass) setattr(klass, name, m) else: # the class has no __setstate__ method so do nothing pass return def import_name(self, module, name): """ If the class is needed for the latest version of the application then it should presumably exist. If the class no longer exists then we should perhaps return a proxy of the class. If the persisted file is at v1 say and the application is at v3 then objects that are required for v1 and v2 do not have to exist they only need to be placeholders for the state during an upgrade. """ #print "importing %s %s" % (name, module) module = __import__(module, globals(), locals(), [name]) return vars(module)[name] ### EOF ################################################################# apptools-4.5.0/apptools/persistence/file_path.py0000644000076500000240000000517111640354733023346 0ustar mdickinsonstaff00000000000000"""Simple class to support file path objects that work well in the context of persistent storage with the state_pickler. """ # Author: Prabhu Ramachandran # Copyright (c) 2005, Enthought, Inc. # License: BSD Style. # Standard library imports. import os from os.path import abspath, normpath, dirname, commonprefix, join class FilePath(object): """This class stores two paths to the file. A relative path and an absolute one. The absolute path is used by the end user. When this object is pickled the state_pickler sets the relative path relative to the file that is being generated. When unpickled, the stored relative path is used to set the absolute path correctly based on the path of the saved file. """ def __init__(self, value=''): self.set(value) def __str__(self): return self.abs_pth def __repr__(self): return self.abs_pth.__repr__() def get(self): """Get the path. """ return self.abs_pth def set(self, value): """Sets the value of the path. """ self.rel_pth = value if value: self.abs_pth = normpath(abspath(value)) else: self.abs_pth = '' def set_relative(self, base_f_name): """Sets the path relative to `base_f_name`. Note that `base_f_name` and self.rel_pth should be valid file names correct on the current os. The set name is a file name that has a POSIX path. """ # Get normalized paths. _src = abspath(base_f_name) _dst = self.abs_pth # Now strip out any common prefix between the two paths. for part in _src.split(os.sep): if _dst.startswith(part+os.sep): length = len(part) + 1 _src = _src[length:] _dst = _dst[length:] else: break # For each directory in the source, we need to add a reference to # the parent directory to the destination. ret = (_src.count(os.sep) * ('..' + os.sep)) + _dst # Make it posix style. if os.sep is not '/': ret.replace(os.sep, '/') # Store it. self.rel_pth = ret def set_absolute(self, base_f_name): """Sets the absolute file name for the current relative file name with respect to the given `base_f_name`. """ base_f_name = normpath(abspath(base_f_name)) rel_file_name = normpath(self.rel_pth) file_name = join(dirname(base_f_name), rel_file_name) file_name = os.path.normpath(file_name) self.abs_pth = file_name apptools-4.5.0/apptools/persistence/project_loader.py0000644000076500000240000000741413547637361024422 0ustar mdickinsonstaff00000000000000 # Standard library imports import sys import pickle import logging # Enthought library imports from apptools.persistence.versioned_unpickler import VersionedUnpickler logger = logging.getLogger(__name__) def load_project(pickle_filename, updater_path, application_version, protocol, max_pass=-1): """ Reads a project from a pickle file and if necessary will update it to the latest version of the application. """ latest_file = pickle_filename # Read the pickled project's metadata. f = open(latest_file, 'rb') metadata = VersionedUnpickler(f).load(max_pass) f.close() project_version = metadata.get('version', False) if not project_version: raise ValueError("Could not read version number from the project file") logger.debug('Project version: %d, Application version: %d' % (project_version, application_version)) # here you can temporarily force an upgrade each time for testing .... # project_version = 0 latest_file = upgrade_project(pickle_filename, updater_path, project_version, application_version, protocol, max_pass) # Finally we can import the project ... logger.info('loading %s' % latest_file) i_f = open(latest_file, 'rb') version = VersionedUnpickler(i_f).load(max_pass) project = VersionedUnpickler(i_f).load(max_pass) i_f.close() return project def upgrade_project(pickle_filename, updater_path, project_version, application_version, protocol, max_pass=-1): """ Repeatedly read and write the project to disk updating it one version at a time. Example the p5.project is at version 0 The application is at version 3 p5.project --- Update1 ---> p5.project.v1 p5.project.v1 --- Update2 ---> p5.project.v2 p5.project.v2 --- Update3 ---> p5.project.v3 p5.project.v3 ---> loaded into app The user then has the option to save the updated project as p5.project """ first_time = True latest_file = pickle_filename # update the project until it's version matches the application's while project_version < application_version: next_version = project_version + 1 if first_time: i_f = open(pickle_filename, 'rb') data = i_f.read() open('%s.bak' % pickle_filename, 'wb').write(data) i_f.seek(0) # rewind the file to the start else: name = '%s.v%d' % (pickle_filename, project_version) i_f = open(name, 'rb') latest_file = name logger.info('converting %s' % latest_file) # find this version's updater ... updater_name = '%s.update%d' % (updater_path, next_version) __import__(updater_name) mod = sys.modules[updater_name] klass = getattr(mod, 'Update%d' % next_version) updater = klass() # load and update this version of the project version = VersionedUnpickler(i_f).load(max_pass) project = VersionedUnpickler(i_f, updater).load(max_pass) i_f.close() # set the project version to be the same as the updater we just # ran on the unpickled files ... project.metadata['version'] = next_version # Persist the updated project ... name = '%s.v%d' % (pickle_filename, next_version) latest_file = name o_f = open(name, 'wb') pickle.dump(project.metadata, o_f, protocol=protocol) pickle.dump(project, o_f, protocol=protocol) o_f.close() # Bump up the version number of the pickled project... project_version += 1 first_time = False return latest_file ### EOF ################################################################# apptools-4.5.0/apptools/sweet_pickle/0000755000076500000240000000000013547652535021200 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/sweet_pickle/placeholder.py0000644000076500000240000000163311640354733024026 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # # Author: Vibha Srinivasan # #----------------------------------------------------------------------------- """ An empty class that serves as a placeholder to map a class to when that class has been deleted as a result of a refactoring. """ # Enthought library imports from traits.api import HasTraits ############################################################################## # class 'PlaceHolder' ############################################################################## class PlaceHolder(HasTraits): """ An empty class that serves as a placeholder to map a class to when that class has been deleted as a result of a refactoring. """ ### EOF ###################################################################### apptools-4.5.0/apptools/sweet_pickle/tests/0000755000076500000240000000000013547652535022342 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/sweet_pickle/tests/two_stage_unpickler_test_case.py0000644000076500000240000001025513547637361031021 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2008 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # Prabhu Ramachandran #----------------------------------------------------------------------------- # Test cases. from __future__ import print_function import random import pickle import apptools.sweet_pickle as sweet_pickle ######################################## # Usecase1: generic case class A(object): def __init__(self, b=None): self.x = 0 self.set_b(b) def set_b(self, b): self.b_ref = b if b and hasattr(b, 'y'): self.x = b.y def __setstate__(self, state): self.__dict__.update(state) self.set_b(self.b_ref) def __initialize__(self): while self.b_ref is None and self.b_ref.y != 0: yield True self.set_b(self.b_ref) class B(object): def __init__(self, a=None): self.y = 0 self.set_a(a) def __getstate__(self): state = self.__dict__.copy() del state['y'] return state def __setstate__(self, state): self.__dict__.update(state) self.set_a(self.a_ref) def set_a(self, a): self.a_ref = a if a and hasattr(a, 'x'): self.y = a.x def __initialize__(self): while self.a_ref is None and self.a_ref.x != 0: yield True self.set_a(self.a_ref) def test_generic(): print('\nRunning generic test...') a = A() b = B() a.x = random.randint(1, 100) b.set_a(a) a.set_b(b) value = a.x # This will fail, even though we have a __setstate__ method. s = pickle.dumps(a) new_a = pickle.loads(s) try: print('\ta.x: %s' % new_a.x) print('\ta.b_ref.y: %s' % new_a.b_ref.y) except Exception as msg: print('\t%s' % 'Expected Error'.center(75,'*')) print('\t%s' % msg) print('\t%s' % ('*'*75)) # This will work! s = pickle.dumps(a) new_a = sweet_pickle.loads(s) assert new_a.x == new_a.b_ref.y == value print('Generic test succesfull.\n\n') ######################################## # Usecase2: Toy Application import re class StringFinder(object): def __init__(self, source, pattern): self.pattern = pattern self.source = source self.data = [] def __getstate__(self): s = self.__dict__.copy() del s['data'] return s def __initialize__(self): while not self.source.initialized: yield True self.find() def find(self): pattern = self.pattern string = self.source.data self.data = [(x.start(), x.end()) for x in re.finditer(pattern, string)] class XMLFileReader(object): def __init__(self, file_name): self.data = '' self.initialized = False self.file_name = file_name self.read() def __getstate__(self): s = self.__dict__.copy() del s['data'] del s['initialized'] return s def __setstate__(self, state): self.__dict__.update(state) self.read() def read(self): # Make up random data from the filename data = [10*x for x in self.file_name] random.shuffle(data) self.data = ' '.join(data) self.initialized = True class Application(object): def __init__(self): self.reader = XMLFileReader('some_test_file.xml') self.finder = StringFinder(self.reader, 'e') def get(self): print('\t%s' % self.finder.data) print('\t%s' % self.reader.data) def test_toy_app(): print('\nRunning toy app test...') a = Application() a.finder.find() a.get() s = pickle.dumps(a) b = pickle.loads(s) # Won't work. try: b.get() except Exception as msg: print('\t%s' % 'Expected Error'.center(75,'*')) print('\t%s' % msg) print('\t%s' % ('*'*75)) # Works fine. c = sweet_pickle.loads(s) c.get() print('Toy app test succesfull.\n\n') if __name__ == '__main__': test_generic() test_toy_app() print('ALL TESTS SUCCESFULL\n') apptools-4.5.0/apptools/sweet_pickle/tests/class_mapping_test_case.py0000644000076500000240000000760413422276145027564 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # #----------------------------------------------------------------------------- """ Tests the class mapping functionality of the enthought.pickle framework. """ # Standard library imports. import unittest # Enthought library imports import apptools.sweet_pickle as sweet_pickle from apptools.sweet_pickle.global_registry import _clear_global_registry ############################################################################## # Classes to use within the tests ############################################################################## # Need complete package name so that mapping matches correctly. # The problem here is the Python loader that will load the same module with # multiple names in sys.modules due to relative naming. Nice. from apptools.sweet_pickle.tests.class_mapping_classes import Foo, Bar, Baz ############################################################################## # class 'ClassMappingTestCase' ############################################################################## class ClassMappingTestCase(unittest.TestCase): """ Tests the class mapping functionality of the apptools.sweet_pickle framework. """ ########################################################################## # 'TestCase' interface ########################################################################## ### public interface ##################################################### def setUp(self): """ Creates the test fixture. Overridden here to ensure each test starts with an empty global registry. """ # Clear the global registry _clear_global_registry() # Cache a reference to the new global registry self.registry = sweet_pickle.get_global_registry() ########################################################################## # 'ClassMappingTestCase' interface ########################################################################## ### public interface ##################################################### def test_infinite_loop_detection(self): """ Validates that the class mapping framework detects infinite loops of class mappings. """ # Add mappings to the registry self.registry.add_mapping_to_class(Foo.__module__, Foo.__name__, Bar) self.registry.add_mapping_to_class(Bar.__module__, Bar.__name__, Baz) self.registry.add_mapping_to_class(Baz.__module__, Baz.__name__, Foo) # Validate that an exception is raised when trying to unpickle an # instance anywhere within the circular definition. def fn(o): sweet_pickle.loads(sweet_pickle.dumps(o)) self.assertRaises(sweet_pickle.UnpicklingError, fn, Foo()) self.assertRaises(sweet_pickle.UnpicklingError, fn, Bar()) self.assertRaises(sweet_pickle.UnpicklingError, fn, Baz()) def test_unpickled_class_mapping(self): # Add the mappings to the registry self.registry.add_mapping_to_class(Foo.__module__, Foo.__name__, Bar) self.registry.add_mapping_to_class(Bar.__module__, Bar.__name__, Baz) # Validate that unpickling the first class gives us an instance of # the third class. start = Foo() end = sweet_pickle.loads(sweet_pickle.dumps(start)) self.assertEqual(True, isinstance(end, Baz)) # Validate that unpickling the second class gives us an instance of # the third class. start = Bar() end = sweet_pickle.loads(sweet_pickle.dumps(start)) self.assertEqual(True, isinstance(end, Baz)) if __name__ == "__main__": unittest.main() ### EOF ###################################################################### apptools-4.5.0/apptools/sweet_pickle/tests/global_registry_test_case.py0000644000076500000240000000555111640354733030133 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # #----------------------------------------------------------------------------- """ Tests the global registry functionality of the sweet_pickle framework. """ # Standard library imports. import unittest # Enthought library imports import apptools.sweet_pickle as sweet_pickle from apptools.sweet_pickle.global_registry import _clear_global_registry ############################################################################## # class 'GlobalRegistryTestCase' ############################################################################## class GlobalRegistryTestCase(unittest.TestCase): """ Tests the global registry functionality of the sweet_pickle framework. """ ########################################################################## # 'TestCase' interface ########################################################################## ### public interface ##################################################### def setUp(self): """ Creates the test fixture. Overridden here to ensure each test starts with an empty global registry. """ # Clear the global registry _clear_global_registry() # Cache a reference to the new global registry self.registry = sweet_pickle.get_global_registry() ########################################################################## # 'GlobalRegistryTestCase' interface ########################################################################## ### public interface ##################################################### def test_clearing(self): """ Validates that clearing the registry gives us a new registry. """ _clear_global_registry() self.assertNotEqual(self.registry, sweet_pickle.get_global_registry()) def test_registry_starts_empty(self): """ Validates that the registry is starting empty for each test. """ self.assertEqual(0, len(self.registry.class_map)) self.assertEqual(0, len(self.registry.state_functions)) self.assertEqual(0, len(self.registry.version_attribute_map)) self.assertEqual(0, len(self.registry._state_function_classes)) def test_returns_singleton(self): """ Validates that the getter returns the same global registry """ # Just try it a few times. self.assertEqual(self.registry, sweet_pickle.get_global_registry()) self.assertEqual(self.registry, sweet_pickle.get_global_registry()) self.assertEqual(self.registry, sweet_pickle.get_global_registry()) if __name__ == "__main__": unittest.main() ### EOF ###################################################################### apptools-4.5.0/apptools/sweet_pickle/tests/state_function_test_case.py0000644000076500000240000001456111640354733027771 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # #----------------------------------------------------------------------------- """ Tests the state function functionality of the apptools.sweet_pickle framework. """ # Standard library imports. import unittest import logging # Enthought library imports import apptools.sweet_pickle as sweet_pickle from apptools.sweet_pickle.global_registry import _clear_global_registry from traits.api import Bool, Float, HasTraits, Int, Str logger = logging.getLogger(__name__) ############################################################################## # Classes to use within the tests ############################################################################## # Need complete package name so that mapping matches correctly. # The problem here is the Python loader that will load the same module with # multiple names in sys.modules due to relative naming. Nice. from apptools.sweet_pickle.tests.state_function_classes import Foo, Bar, Baz ############################################################################## # State functions to use within the tests ############################################################################## def bar_state_function(state): for old, new in [('b1', 'b2'), ('f1', 'f2'), ('i1', 'i2'), ('s1', 's2')]: state[new] = state[old] del state[old] state['_enthought_pickle_version'] = 2 return state ############################################################################## # class 'StateFunctionTestCase' ############################################################################## class StateFunctionTestCase(unittest.TestCase): """ Tests the state function functionality of the apptools.sweet_pickle framework. """ ########################################################################## # 'TestCase' interface ########################################################################## ### public interface ##################################################### def setUp(self): """ Creates the test fixture. Overridden here to ensure each test starts with an empty global registry. """ # Clear the global registry _clear_global_registry() # Cache a reference to the new global registry self.registry = sweet_pickle.get_global_registry() # Add the class mappings to the registry self.registry.add_mapping_to_class(Foo.__module__, Foo.__name__, Bar) self.registry.add_mapping_to_class(Bar.__module__, Bar.__name__, Baz) ########################################################################## # 'StateFunctionTestCase' interface ########################################################################## ### public interface ##################################################### def test_normal_setstate(self): """ Validates that only existing setstate methods are called when there are no registered state functions in the class chain. """ # Validate that unpickling the first class gives us an instance of # the third class with the appropriate attribute values. It will have # the default Foo values (because there is no state function to move # them) and also the default Baz values (since they inherit the # trait defaults because nothing overwrote the values.) start = Foo() end = sweet_pickle.loads(sweet_pickle.dumps(start)) self.assertEqual(True, isinstance(end, Baz)) self._assertAttributes(end, 1, (False, 1, 1, 'foo')) self._assertAttributes(end, 2, None) self._assertAttributes(end, 3, (False, 3, 3, 'baz')) # Validate that unpickling the second class gives us an instance of # the third class with the appropriate attribute values. It will have # only the Baz attributes with the Bar values (since the __setstate__ # on Baz converted the Bar attributes to Baz attributes.) start = Bar() end = sweet_pickle.loads(sweet_pickle.dumps(start)) self.assertEqual(True, isinstance(end, Baz)) self._assertAttributes(end, 2, None) self._assertAttributes(end, 3, (True, 2, 2, 'bar')) def test_unpickled_chain_functionality(self): """ Validates that the registered state functions are used when unpickling. """ # Add the state function to the registry self.registry.add_state_function_for_class(Bar, 2, bar_state_function) # Validate that unpickling the first class gives us an instance of # the third class with the appropriate attribute values. start = Foo() end = sweet_pickle.loads(sweet_pickle.dumps(start)) self.assertEqual(True, isinstance(end, Baz)) self._assertAttributes(end, 1, None) self._assertAttributes(end, 2, None) self._assertAttributes(end, 3, (False, 1, 1, 'foo')) # Validate that unpickling the second class gives us an instance of # the third class. start = Bar() end = sweet_pickle.loads(sweet_pickle.dumps(start)) self.assertEqual(True, isinstance(end, Baz)) self._assertAttributes(end, 2, None) self._assertAttributes(end, 3, (True, 2, 2, 'bar')) ### protected interface ################################################## def _assertAttributes(self, obj, suffix, values): """ Ensures that the specified object's attributes with the specified suffix have the expected values. If values is None, then the attributes shouldn't exist. """ attributeNames = ['b', 'f', 'i', 's'] for i in range(len(attributeNames)): name = attributeNames[i] + str(suffix) if values is None: self.assertEqual(False, hasattr(obj, name), 'Obj [%s] has attribute [%s]' % (obj, name)) else: self.assertEqual(values[i], getattr(obj, name), 'Obj [%s] attribute [%s] has [%s] instead of [%s]' % \ (obj, name, values[i], getattr(obj, name))) if __name__ == "__main__": unittest.main() ### EOF ###################################################################### apptools-4.5.0/apptools/sweet_pickle/tests/updater_test_case.py0000644000076500000240000003166313547637361026423 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # #----------------------------------------------------------------------------- """ Tests the updater functionality of the sweet_pickle framework. """ # Standard library imports. import unittest import logging # Enthought library imports import apptools.sweet_pickle as sweet_pickle from apptools.sweet_pickle.global_registry import _clear_global_registry logger = logging.getLogger(__name__) ############################################################################## # class 'UpdaterTestCase' ############################################################################## class UpdaterTestCase(unittest.TestCase): """ Tests the updater functionality of the sweet_pickle framework. """ ########################################################################## # 'TestCase' interface ########################################################################## ### public interface ##################################################### def setUp(self): """ Creates the test fixture. Overridden here to ensure each test starts with an empty global registry. """ # Clear the global registry _clear_global_registry() # Cache a reference to the new global registry self.registry = sweet_pickle.get_global_registry() ########################################################################## # 'UpdaterTestCase' interface ########################################################################## ### public interface ##################################################### def test_add_mapping(self): """ Validates the behavior of the add_mapping function. """ # Add a single mapping and validate that all it did was add a class # mapping and that the mapping has the expected values. key = ('foo', 'Foo') value = ('bar', 'Bar') self.registry.add_mapping(key[0], key[1], value[0], value[1]) self._validate_exactly_one_class_map(key, value) # Overwrite with a new mapping and validate the state is what we # expect. value = ('baz', 'Baz') self.registry.add_mapping(key[0], key[1], value[0], value[1]) self._validate_exactly_one_class_map(key, value) def test_add_mapping_to_class(self): """ Validates the behavior of the add_mapping_to_class function. """ # Add a single mapping and validate that all it did was add a class # mapping and that the mapping has the expected values. key = ('foo', 'Foo') class Bar: pass value = (Bar.__module__, 'Bar') self.registry.add_mapping_to_class(key[0], key[1], Bar) self._validate_exactly_one_class_map(key, value) # Overwrite with a new mapping and validate the state is what we # expect. class Baz: pass value = (Baz.__module__, 'Baz') self.registry.add_mapping_to_class(key[0], key[1], Baz) self._validate_exactly_one_class_map(key, value) def test_add_mappings(self): """ Validates the behavior of the add_mappings function. """ # Add a single mapping and validate that all it did was add a class # mapping and that the mapping has the expected values key = ('foo', 'Foo') value = ('bar', key[1]) names = [key[1]] self.registry.add_mappings(key[0], value[0], names) self._validate_exactly_one_class_map(key, value) # Add multiple mappings and validate that the registry has the expected # values. key = ('foo', 'Foo') value = ('bar', 'Bar') names = ['Foo', 'Bar', 'Baz', 'Enthought'] self.registry.add_mappings(key[0], value[0], names) self.assertEqual(len(names), len(self.registry.class_map)) self.assertEqual(0, len(self.registry.state_functions)) self.assertEqual(0, len(self.registry.version_attribute_map)) self.assertEqual(0, len(self.registry._state_function_classes)) items = [] for n in names: items.append( ((key[0], n), (value[0], n)) ) self._validate_class_map_contents(items) def test_add_state_function(self): """ Validates the behavior of the add_state_function function. """ # Add a single function and validate that all it did was add a state # mapping and that the mapping has the expected values. key = ('foo', 'Foo', 1) def fn(): pass self.registry.add_state_function(key[0], key[1], key[2], fn) self._validate_exactly_one_state_function(key, [fn]) # Add an additional function for the same state and validate the state # is what we expect. def fn2(): pass self.registry.add_state_function(key[0], key[1], key[2], fn2) self._validate_exactly_one_state_function(key, [fn, fn2]) # Add a state function for another version of the same class and # validate that all the values are as expected. key2 = ('foo', 'Foo', 2) self.registry.add_state_function(key2[0], key2[1], key2[2], fn2) self.assertEqual(0, len(self.registry.class_map)) self.assertEqual(2, len(self.registry.state_functions)) self.assertEqual(0, len(self.registry.version_attribute_map)) self.assertEqual(1, len(self.registry._state_function_classes)) self._validate_state_function_contents( [(key, [fn, fn2]), (key2, [fn2])], {(key[0], key[1]): 3} ) def test_add_state_function_for_class(self): """ Validates the behavior of the add_state_function_for_class function. """ # Add a single function and validate that all it did was add a state # mapping and that the mapping has the expected values. class Bar: pass key = (Bar.__module__, 'Bar', 1) def fn(): pass self.registry.add_state_function_for_class(Bar, key[2], fn) self._validate_exactly_one_state_function(key, [fn]) # Add an additional function for the same state and validate the state # is what we expect. def fn2(): pass self.registry.add_state_function_for_class(Bar, key[2], fn2) self._validate_exactly_one_state_function(key, [fn, fn2]) # Add a state function for another class and validate that all the # values are as expected. class Baz: pass key2 = (Baz.__module__, 'Baz', 2) self.registry.add_state_function_for_class(Baz, key2[2], fn2) self.assertEqual(0, len(self.registry.class_map)) self.assertEqual(2, len(self.registry.state_functions)) self.assertEqual(0, len(self.registry.version_attribute_map)) self.assertEqual(2, len(self.registry._state_function_classes)) self._validate_state_function_contents( [(key, [fn, fn2]), (key2, [fn2])], {(key[0], key[1]): 2, (key2[0], key2[1]): 1} ) def test_merge_updater(self): """ Validates the behavior of the merge_updater function. """ # Merge in one update and validate the state of the registry is as # expected. def fn1(): pass updater = sweet_pickle.Updater( class_map = { ('foo', 'Foo'): ('foo.bar', 'Foo'), }, state_functions = { ('foo', 'Foo', 1): [fn1], }, version_attribute_map = { ('foo', 'Foo'): 'version', }, ) self.registry.merge_updater(updater) self.assertEqual(1, len(self.registry.class_map)) self.assertEqual(1, len(self.registry.state_functions)) self.assertEqual(1, len(self.registry.version_attribute_map)) self.assertEqual(1, len(self.registry._state_function_classes)) self._validate_class_map_contents(list(updater.class_map.items())) counts = {('foo', 'Foo'): 1} self._validate_state_function_contents(list(updater.state_functions.items()), counts) # Merge in a second updater and validate the state of the registry is # as expected. def fn2(): pass updater2 = sweet_pickle.Updater( class_map = { ('foo.bar', 'Foo'): ('bar', 'Bar'), ('bar', 'Bar'): ('foo.bar.baz', 'Baz'), }, state_functions = { ('foo', 'Foo', 1): [fn2], ('foo', 'Foo', 2): [fn2], }, version_attribute_map = { ('foo.bar', 'Foo'): '_version', }, ) self.registry.merge_updater(updater2) self.assertEqual(3, len(self.registry.class_map)) self.assertEqual(2, len(self.registry.state_functions)) self.assertEqual(2, len(self.registry.version_attribute_map)) self.assertEqual(1, len(self.registry._state_function_classes)) self._validate_class_map_contents( list(updater.class_map.items()) + list(updater2.class_map.items()) ) counts = {('foo', 'Foo'): 3} self._validate_state_function_contents( [ (('foo', 'Foo', 1), [fn1, fn2]), (('foo', 'Foo', 2), [fn2]) ], counts) def test_registry_starts_empty(self): """ Validates that the registry is starting empty for each test. """ self.assertEqual(0, len(self.registry.class_map)) self.assertEqual(0, len(self.registry.state_functions)) self.assertEqual(0, len(self.registry.version_attribute_map)) self.assertEqual(0, len(self.registry._state_function_classes)) ### protected interface ################################################## def _validate_class_map_contents(self, items): """ Validates that the registry's class_map contains the specified items. """ for key, value in items: self.assertEqual(True, key in self.registry.class_map, 'Key ' + str(key) + ' not in class_map') self.assertEqual(value, self.registry.class_map[key], str(value) + ' != ' + str(self.registry.class_map[key]) + \ ' for key ' + str(key)) self.assertEqual(True, self.registry.has_class_mapping(key[0], key[1]), 'Registry reports no class mapping for key ' + str(key)) def _validate_exactly_one_class_map(self, key, value): """ Validates that the registry has exactly one class_map entry with the specified key and value. """ self.assertEqual(1, len(self.registry.class_map)) self.assertEqual(0, len(self.registry.state_functions)) self.assertEqual(0, len(self.registry.version_attribute_map)) self.assertEqual(0, len(self.registry._state_function_classes)) self._validate_class_map_contents([(key, value)]) def _validate_exactly_one_state_function(self, key, value): """ Validates that the registry has exactly one state_function entry with the specified key and value. """ self.assertEqual(0, len(self.registry.class_map)) self.assertEqual(1, len(self.registry.state_functions)) self.assertEqual(0, len(self.registry.version_attribute_map)) self.assertEqual(1, len(self.registry._state_function_classes)) self.assertEqual(key, list(self.registry.state_functions.keys())[0]) self.assertEqual(value, self.registry.state_functions[key]) classes_key = (key[0], key[1]) self.assertEqual(classes_key, list(self.registry._state_function_classes.keys())[0]) self.assertEqual(len(value), self.registry._state_function_classes[classes_key]) def _validate_state_function_contents(self, items, counts): """ Validates that the registry's state functions contains the specified items and the class count matches the specified count. """ for key, value in items: self.assertEqual(True, key in self.registry.state_functions, 'Key ' + str(key) + ' not in state functions') self.assertEqual(value, self.registry.state_functions[key], str(value) + ' != ' + \ str(self.registry.state_functions[key]) + ' for key ' + \ str(key)) self.assertEqual(True, self.registry.has_state_function( key[0], key[1]), 'Registry reports no state function for key ' + str(key)) classes_key = (key[0], key[1]) count = counts[classes_key] self.assertEqual(count, self.registry._state_function_classes[classes_key]) if __name__ == "__main__": unittest.main() ### EOF ###################################################################### apptools-4.5.0/apptools/sweet_pickle/tests/__init__.py0000644000076500000240000000000111640354733024431 0ustar mdickinsonstaff00000000000000 apptools-4.5.0/apptools/sweet_pickle/tests/state_function_classes.py0000644000076500000240000000325111640354733027446 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # #----------------------------------------------------------------------------- # Standard library imports import logging # Enthought library imports import apptools.sweet_pickle as sweet_pickle from apptools.sweet_pickle.global_registry import _clear_global_registry from traits.api import Bool, Float, HasTraits, Int, Str logger = logging.getLogger(__name__) ############################################################################## # Classes to use within the tests ############################################################################## class Foo(HasTraits): _enthought_pickle_version = Int(1) b1 = Bool(False) f1 = Float(1) i1 = Int(1) s1 = Str('foo') class Bar(HasTraits): _enthought_pickle_version = Int(2) b2 = Bool(True) f2 = Float(2) i2 = Int(2) s2 = Str('bar') class Baz(HasTraits): _enthought_pickle_version = Int(3) b3 = Bool(False) f3 = Float(3) i3 = Int(3) s3 = Str('baz') def __setstate__(self, state): logger.debug('Running Baz\'s original __setstate__') if state['_enthought_pickle_version'] < 3: info = [('b2', 'b3'), ('f2', 'f3'), ('i2', 'i3'), ('s2', 's3')] for old, new in info: if old in state: state[new] = state[old] del state[old] state['_enthought_pickle_version'] = 3 self.__dict__.update(state) ### EOF ###################################################################### apptools-4.5.0/apptools/sweet_pickle/tests/class_mapping_classes.py0000644000076500000240000000103011640354733027232 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # #----------------------------------------------------------------------------- ############################################################################## # Classes to use within the tests ############################################################################## class Foo: pass class Bar: pass class Baz: pass apptools-4.5.0/apptools/sweet_pickle/__init__.py0000644000076500000240000001663313547637361023322 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # #----------------------------------------------------------------------------- """ A pickle framework that supports unpickling of refactored versions of old classes. Part of the AppTools project of the Enthought Tool Suite. Beyond refactoring support, there are additional useful capabilities of this package: - HasTraits objects are fully pickled when sweet_pickle.dump() or sweet_pickle.dumps() are called. This means every trait's value is pickled explicitly. This allows improved self-consistency as values within unpickled objects correspond to the default trait values of the instance that was pickled rather than the default values of the current class definition. - Multiple updaters can be registered to update state for any version of a class. This is extremely useful when an instance being pickled and unpickled is the result of an addition of one or more Trait Categories to a class. In that case, the Category contributor can also register a state function to update the Category's traits through versioning. We have duplicated the API of the Python 'pickle' module within this package. Thus, users can simply import sweet_pickle in places where they previously used pickle or cPickle. For example:: import cPickle ---> import apptools.sweet_pickle as pickle s = pickle.dumps(obj) s = pickle.dumps(obj) pickle.loads(s) pickle.loads(s) Pickles generated by this package *can be* unpickled by standard pickle or cPickle, though you lose the benefit of the refactoring support during unpickling. As a result of the above, this package is a drop-in replacement for the Python 'pickle' module or 'cPickle' module and should be safe to use even if you never version or refactor your own classes. In fact, we STRONGLY RECOMMEND that you use this framework for all of your pickle needs because you never know when one of the classes you encounter during unpickling has been versioned or otherwise refactored by someone else. See module 'pickle' for more basic information about pickling. The most common way to benefit from the versioning capabilities of this framework is to register class mappings and state modification functions with the global updater registry (more detail is below.) However, you may also choose to explicitly instantiate an Unpickler and provide it with your own explicit definition of class mappings and state modification functions. We do not provide any help on the latter at this time. You can register class mappings and state modification functions with the global updater registry using the methods provided on the Updater instance that is the global registry. You can get this instance by calling the 'get_global_registry' function exposed through this package's namespace. This framework has been designed so that you can register your class mappings and state modification functions during the import of the module or package that contained the original class. However, you may also register your mappings and functions at any point such that the they are known to the framework prior to, or become known during the attempt to, unpickle the class they modify. The framework will call a __setstate__ method on the final target class of any unpickled instance. It will not call __setstate__ methods on any beginning or intermediate classes within a chain of class mappings. A class mapping is used to redirect the unpickling of one class to return an instantiation of another class. The classes can be in different modules, and the modules in different packages. Mappings can be chained. For example, given the mappings:: foo.bar.Bar --> foo.baz.Baz foo.baz.Baz --> foo.Foo An attempt to unpickle a foo.bar.Bar would actually generate a foo.Foo instance. A state modification function is called during the execution of the __setstate__ method during unpickling of an object of the type and version for which it was registered for. The function must accept a single argument which is a state dictionary and must then return the modified state dictionary. Additionally, the function should change the version variable within the state to represent the version the new state represents. (The framework will assume an increment of one if this is not done.) The framework ensures that state modification functions are chained appropriately to convert through multiple versions and/or class mappings. Note that refactorings that cause classes to be completed removed from the source code can be supported, without breaking unpickling of object hierarchies that include an instace of that class, by adding a mapping to the Placeholder class in the placeholder module. """ ############################################################################## # Implement the Python pickle package API ############################################################################## # Expose our custom pickler as the standard Unpickler from .versioned_unpickler import VersionedUnpickler as Unpickler # Use our custom unpickler to load from files def load(file, max_pass=-1): return Unpickler(file).load(max_pass) # Use our custom unpickler to load from strings def loads(str, max_pass=-1): from io import BytesIO file = BytesIO(str) return Unpickler(file).load(max_pass) # We don't customize the Python pickler, though we do use the cPickle module # for improved performance. from six.moves.cPickle import Pickler # Implement the dump and dumps methods so that all traits in a HasTraits object # get included in the pickle. def dump(obj, file, protocol=2): _flush_traits(obj) from six.moves.cPickle import dump as d return d(obj, file, protocol) def dumps(obj, protocol=2): _flush_traits(obj) from six.moves.cPickle import dumps as ds return ds(obj, protocol) # We don't customize exceptions so just map to the Python pickle package from pickle import PickleError, PicklingError, UnpicklingError ############################################################################## # Allow retrieval of the global registry ############################################################################## from .global_registry import get_global_registry ############################################################################## # Expose our Updater class so users can explicitly create their own. ############################################################################## from .updater import Updater ############################################################################## # Method to ensure that all traits on a has traits object are included in a # pickle. ############################################################################## def _flush_traits(obj): if hasattr(obj, 'trait_names'): for name, value in obj.traits().items(): if value.type == 'trait': try: getattr(obj, name) except AttributeError: pass ### EOF ###################################################################### apptools-4.5.0/apptools/sweet_pickle/updater.py0000644000076500000240000003052011640354733023205 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005, 2006 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # Author: Duncan Child # #----------------------------------------------------------------------------- """ A record of refactorings to be performed during unpickling of objects. """ # Standard library imports import logging # Enthought library imports from traits.api import Dict, HasPrivateTraits, Int, List, Tuple, Str logger = logging.getLogger(__name__) ############################################################################## # class 'Updater' ############################################################################## class Updater(HasPrivateTraits): """ A record of refactorings to be performed during unpickling of objects. """ ########################################################################## # Traits ########################################################################## ### public 'Updater' interface ########################################### # Mappings from a pickled class to a class it should be unpickled as. # # The keys are a tuple of the source class's module and class names in # that order. The values are the target class's module and class names # in that order. class_map = Dict(Tuple(Str, Str), Tuple(Str, Str)) # State functions that should be called to convert state from one version # of a class to another. # # The keys are a tuple of the class's module name, class name, and version # in that order. The values are a list of functions to be called during # unpickling to do the state conversion. Note that the version in the # key represents the version that the function converts *TO*. state_functions = Dict(Tuple(Str, Str, Int), List) # Our record of the attribute that records the version number for a # specific class. If no record is found for a given class, then the # default value is used instead -- see '_default_version_attribute'. # # The key is a tuple of the class's module name and class name in that # order. The value is the name of the version attribute. version_attribute_map = Dict(Tuple(Str, Str), Str) ### protected 'Updater' interface ######################################## # The default name of the attribute that declares the version of a class # or class instance. _default_version_attribute = '_enthought_pickle_version' # A record of which classes we have state functions for. # # The keys are a tuple of the class's module name and class name in that # order. The values are reference counts. _state_function_classes = Dict(Tuple(Str, Str), Int) ########################################################################## # 'Updater' interface ########################################################################## ### public interface ##################################################### def add_mapping(self, source_module, source_name, target_module, target_name): """ Adds a mapping from the class with the source name in the source module to the class with the target name in the target module. """ self.class_map[(source_module, source_name)] = (target_module, target_name) def add_mapping_to_class(self, source_module, source_name, target_class): """ Convenience method to add a mapping, from the class with the source name in the source module to the target class. """ self.add_mapping(source_module, source_name, target_class.__module__, target_class.__name__) def add_mappings(self, source_module, target_module, class_names): """ Adds mappings, from the specified source module to the specified target module, for each of the class names in the specified list. """ for name in class_names: self.add_mapping(source_module, name, target_module, name) def add_state_function(self, module, name, target_version, function): """ Adds the specified function as a state function to be called to convert an instance of the class with the specified name within the specified module *TO* the specified version. Note that the framework handles calling of state functions to make the smallest version jumps possible. """ key = (module, name, target_version) list = self.state_functions.setdefault(key, []) list = list[:] # Copy necessary because traits only recognizes list # changes by list instance - not its contents. list.append(function) self.state_functions[key] = list def add_state_function_for_class(self, klass, target_version, function): """ Convenience method to add the specified function as a state function to be called to convert an instance of the specified class *TO* the specified version. """ self.add_state_function(klass.__module__, klass.__name__, target_version, function) def declare_version_attribute(self, module, name, attribute_name): """ Adds the specified attribute name as the version attribute for the class within the specified module with the specified name. """ self.version_attribute_map[(module, name)] = attribute_name def declare_version_attribute_for_class(self, klass, attribute_name): """ Covenience method to add the specified attribute name as the version attribute for the specified class. """ self.declare_version_attribute(klass.__module__, klass.__name__, attribute_name) def get_version_attribute(self, module, name): """ Returns the name of the version attribute for the class of the specified name within the specified module. """ return self.version_attribute_map.get( (module, name), self._default_version_attribute) def has_class_mapping(self, module, name): """ Returns True if this updater contains a class mapping for the class identified by the specified module and class name. """ return (module, name) in self.class_map def has_state_function(self, module, name): """ Returns True if this updater contains any state functions for the class identified by the specified module and class name. """ return (module, name) in self._state_function_classes def merge_updater(self, updater): """ Merges the mappings and state functions from the specified updater into this updater. """ self.class_map.update(updater.class_map) self.version_attribute_map.update(updater.version_attribute_map) # The state functions dictionary requires special processing because # each value is a list and we don't just want to replace the existing # list with only the new content. for key, value in updater.state_functions.items(): if isinstance(value, list) and len(value) > 0: funcs = self.state_functions.setdefault(key, []) funcs = funcs[:] # Copy necessary because traits only recognizes # funcs changes by funcs instance - not its # contents. funcs.extend(value) self.state_functions[key] = funcs ### trait handlers ####################################################### def _class_map_changed(self, old, new): logger.debug('Detected class_map change from [%s] to [%s] in [%s]', old, new, self) def _class_map_items_changed(self, event): for o in event.removed: logger.debug('Detected [%s] removed from class_map in [%s]', o, self) for k, v in event.changed.items(): logger.debug('Detected [%s] changed from [%s] to [%s] in ' + \ 'class_map in [%s]', k, v, self.class_map[k], self) for k, v in event.added.items(): logger.debug('Detected mapping from [%s] to [%s] added to ' + \ 'class_map in [%s]', k, v, self) def _state_functions_changed(self, old, new): logger.debug('Detected state_functions changed from [%s] to [%s] ' + \ 'in [%s]', old, new, self) # Update our record of which classes we have state functions for. # All of our old state functions are gone so we simply need to rescan # the new functions. self._state_function_classes.clear() for key, value in new.items(): module, name, version = key klass_key = (module, name) count = self._state_function_classes.setdefault(klass_key, 0) self._state_function_classes[klass_key] = count + len(value) def _state_functions_items_changed(self, event): # Decrement our reference counts for the classes we no longer # have state functions for. If the reference count reaches zero, # remove the record completely. for k, v in event.removed.items(): logger.debug('Detected [%s] removed from state_functions in [%s]', k, self) # Determine the new reference count of state functions for the # class who the removed item was for. module, name, version = k key = (module, name) count = self._state_function_classes[key] - len(v) # Store the new reference count. Delete the entry if it is zero. if count < 0: logger.warn('Unexpectedly reached negative reference count ' + 'value of [%s] for [%s]', count, key) del self._state_function_classes[key] elif count == 0: del self._state_function_classes[key] else: self._state_function_classes[key] = count # Update our reference counts for changes to the list of functions # for a specific class and version. The 'changed' dictionary's values # are the old values. for k, v in event.changed.items(): value = self.state_functions[k] logger.debug('Detected [%s] changed in state_functions from ' + \ '[%s] to [%s] in [%s]', k, v, value, self) # Determine the new reference count as a result of the change. module, name, version = k key = (module, name) count = self._state_function_classes[key] - len(v) + len(value) # Store the new reference count. Delete the entry if it is zero. if count < 0: logger.warn('Unexpectedly reached negative reference count ' + 'value of [%s] for [%s]', count, key) del self._state_function_classes[key] elif count == 0: del self._state_function_classes[key] else: self._state_function_classes[key] = count # Update our reference counts for newly registered state functions. for k, v in event.added.items(): logger.debug('Detected mapping of [%s] to [%s] added to ' + \ 'state_functions in [%s]', k, v, self) # Determine the new reference count as a result of the change. module, name, version = k key = (module, name) count = self._state_function_classes.setdefault(key, 0) + len(v) # Store the new reference count self._state_function_classes[key] = count def _version_attribute_map_changed(self, old, new): logger.debug('Detected version_attribute_map change from [%s] ' + \ 'to [%s] in [%s]', old, new, self) def _version_attribute_map_items_changed(self, event): for o in event.removed: logger.debug('Detected [%s] removed from version_attribute_map ' + \ 'in [%s]', o, self) for o in event.changed: logger.debug('Detected [%s] changed in version_attribute_map ' + \ 'in [%s]', o, self) for o in event.added: logger.debug('Detected [%s] added to version_attribute_map in ' + \ '[%s]', o, self) ### EOF ###################################################################### apptools-4.5.0/apptools/sweet_pickle/global_registry.py0000644000076500000240000000752213547637361024750 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2006 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # #----------------------------------------------------------------------------- """ Manages a singleton updater that acts as a global registry. Our goal is to enable the apptools.sweet_pickle framework to understand how pickled data should be treated during unpickling so that the resulting object hierarchy reflects the current versions of the object's classes -AND- that this should work no matter who is doing the unpickling. This requires a global registry since there is no way to know in advance what objects are contained within a given pickle file, and thus no way to gather the required information to know how to treat the pickle data during unpickling. For example, a pickle of an Envisage project may contain many custom classes, instantiated at the behest of various plugins, which have gone through various versionings and refactorings. But it is the project plugin that needs to unpickle these objects to 'load' a project, not the other plugins that added those custom class instances into the project. This registry is used by the apptools.sweet_pickle framework's unpickler only by default. That is, only if no updater was explicitly provided. It is important that users interact with the registry through the provided methods. If they do not, then the reference they receive will be the one that was in place at the time of the import which may or MAY NOT be the current repository due to the way this framework manages the repository. """ try: import six.moves._thread as _thread except ImportError: import six.moves._dummy_thread as _thread ############################################################################## # function 'get_global_registry' ############################################################################## def get_global_registry(): """ Returns the global registry in a manner that allows for lazy instantiation. """ global _global_registry, _global_registry_lock # Do we need to create the registry? if _global_registry is None: # We can only do this safely in a threaded situation by using a lock. # Note that the previous check for None doesn't guarantee we are # the only one trying to create an instance, so, we'll check for none # again once we acquire the lock and then only create the singleton # if there still isn't an instance. _global_registry_lock.acquire() try: if _global_registry is None: from .updater import Updater _global_registry = Updater() finally: _global_registry_lock.release() return _global_registry ############################################################################## # private function '_clear_global_registry' ############################################################################## def _clear_global_registry(): """ Clears out the current global registry. This exists purely to allow testing of the global registry and the apptools.sweet_pickle framework. THIS METHOD SHOULD NEVER BE CALLED DURING NORMAL OPERATIONS! """ global _global_registry _global_registry = None ############################################################################## # private, but global, variables ############################################################################## # The global singleton updater _global_registry = None # The lock used to make access to the global singleton thread safe _global_registry_lock = _thread.allocate_lock() #### EOF ##################################################################### apptools-4.5.0/apptools/sweet_pickle/versioned_unpickler.py0000644000076500000240000005116513547637361025634 0ustar mdickinsonstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005-2008 by Enthought, Inc. # All rights reserved. # # Author: Dave Peterson # Author: Duncan Child # #----------------------------------------------------------------------------- # The code for two-stage unpickling support has been taken from a PEP draft # prepared by Dave Peterson and Prabhu Ramachandran. """ An unpickler that is tolerant of class refactorings, and implements a two-stage pickling process to make it possible to unpickle complicated Python object hierarchies where the unserialized state of an object depends on the state of other objects in the same pickle. """ # Standard library imports. import sys import logging from os import path from types import GeneratorType if sys.version_info[0] >= 3: from pickle import _Unpickler as Unpickler else: from pickle import Unpickler from pickle import UnpicklingError, BUILD # Enthought library imports from traits.api import HasTraits, Instance # Setup a logger for this module logger = logging.getLogger(__name__) ############################################################################## # constants ############################################################################## # The name we backup the original setstate method to. _BACKUP_NAME = '__enthought_sweet_pickle_original_setstate__' # The name of the setstate method we hook _SETSTATE_NAME = '__setstate__' # The name we store our unpickling data under. _UNPICKLER_DATA = '__enthought_sweet_pickle_unpickler__' ############################################################################## # function '__replacement_setstate__' ############################################################################## def __replacement_setstate__(self, state): """ Called to enable an unpickler to modify the state of this instance. """ # Retrieve the unpickling information and use it to let the unpickler # modify our state. unpickler, module, name = getattr(self, _UNPICKLER_DATA) state = unpickler.modify_state(self, state, module, name) # If we were given a state, apply it to this instance now. if state is not None: # Save our state logger.debug('Final state: %s', state) self.__dict__.update(state) ############################################################################## # function 'load_build_with_meta_data' ############################################################################## def load_build_with_meta_data(self): """ Called prior to the actual load_build() unpickling method which primes the state dictionary with meta-data. """ # Access the state object and check if it is a dictionary (state may also be # a tuple, which is used for other unpickling build operations). Proceed to # the standard load_build() if the state obj is not a dict. state = self.stack[-1] if type(state) == dict: # If a file object is used, reference the file name if hasattr(self._file, 'name'): pickle_file_name = path.abspath(self._file.name) else : pickle_file_name = "" # Add any meta-data needed by __setstate__() methods here... state['_pickle_file_name'] = pickle_file_name # Call the standard load_build() method return self.load_build() ############################################################################## # class 'NewUnpickler' ############################################################################## class NewUnpickler(Unpickler): """ An unpickler that implements a two-stage pickling process to make it possible to unpickle complicated Python object hierarchies where the unserialized state of an object depends on the state of other objects in the same pickle. """ def load(self, max_pass=-1): """Read a pickled object representation from the open file. Return the reconstituted object hierarchy specified in the file. """ # List of objects to be unpickled. self.objects = [] # We overload the load_build method. dispatch = self.dispatch dispatch[BUILD[0]] = NewUnpickler.load_build # call the super class' method. ret = Unpickler.load(self) self.initialize(max_pass) self.objects = [] # Reset the Unpickler's dispatch table. dispatch[BUILD[0]] = Unpickler.load_build return ret def initialize(self, max_pass): # List of (object, generator) tuples that initialize objects. generators = [] # Execute object's initialize to setup the generators. for obj in self.objects: if hasattr(obj, '__initialize__') and \ callable(obj.__initialize__): ret = obj.__initialize__() if isinstance(ret, GeneratorType): generators.append((obj, ret)) elif ret is not None: raise UnpicklingError('Unexpected return value from ' '__initialize__. %s returned %s' % (obj, ret)) # Ensure a maximum number of passes if max_pass < 0: max_pass = len(generators) # Now run the generators. count = 0 while len(generators) > 0: count += 1 if count > max_pass: not_done = [x[0] for x in generators] msg = """Reached maximum pass count %s. You may have a deadlock! The following objects are uninitialized: %s""" % (max_pass, not_done) raise UnpicklingError(msg) for o, g in generators[:]: try: next(g) except StopIteration: generators.remove((o, g)) # Make this a class method since dispatch is a class variable. # Otherwise, supposing the initial sweet_pickle.load call (which would # have overloaded the load_build method) makes a pickle.load call at some # point, we would have the dispatch still pointing to # NewPickler.load_build whereas the object being passed in will be an # Unpickler instance, causing a TypeError. @classmethod def load_build(cls, obj): # Just save the instance in the list of objects. if isinstance(obj, NewUnpickler): obj.objects.append(obj.stack[-2]) Unpickler.load_build(obj) ############################################################################## # class 'VersionedUnpickler' ############################################################################## class VersionedUnpickler(NewUnpickler, HasTraits): """ An unpickler that is tolerant of class refactorings. This class reads in a pickled file and applies the transforms specified in its updater to generate a new hierarchy of objects which are at the current version of the classes they are instances of. Note that the creation of an updater is kept out of this class to ensure that the class can be reused in different situations. However, if no updater is provided during construction, then the global registry updater will be used. """ ########################################################################## # Traits ########################################################################## ### public 'VersionedUnpickler' interface ################################ # The updater used to modify the objects being unpickled. updater = Instance('apptools.sweet_pickle.updater.Updater') ########################################################################## # 'object' interface ########################################################################## ### operator methods ##################################################### def __init__(self, file, **kws): super(VersionedUnpickler, self).__init__(file) self._file = file if self.updater is None: from .global_registry import get_global_registry self.updater = get_global_registry() logger.debug('VersionedUnpickler [%s] using Updater [%s]', self, self.updater) # Update the BUILD instruction to use an overridden load_build method # NOTE: this is being disabled since, on some platforms, the object # is replaced with a regular Unpickler instance, creating a traceback: # AttributeError: Unpickler instance has no attribute '_file' # ...not sure how this happens since only a VersionedUnpickler has # the BUILD instruction replaced with one that uses _file, and it # should have _file defined. #self.dispatch[BUILD[0]] = load_build_with_meta_data ########################################################################## # 'Unpickler' interface ########################################################################## ### public interface ##################################################### def find_class(self, module, name): """ Returns the class definition for the named class within the specified module. Overridden here to: - Allow updaters to redirect to a different class, possibly within a different module. - Ensure that any setstate hooks for the class are called when the instance of this class is unpickled. """ # Remove any extraneous characters that an Unpickler might handle # but a user wouldn't have included in their mapping definitions. module = module.strip() name = name.strip() # Attempt to find the class, this may cause a new mapping for that # very class to be introduced. That's why we ignore the result. try: klass = super(VersionedUnpickler, self).find_class(module, name) except: pass # Determine the target class that the requested class should be # mapped to according to our updater. The target class is the one # at the end of any chain of mappings. original_module, original_name = module, name if self.updater is not None and \ self.updater.has_class_mapping(module, name): module, name = self._get_target_class(module, name) if module != original_module or name != original_name: logger.debug('Unpickling [%s.%s] as [%s.%s]', original_module, original_name, module, name) # Retrieve the target class definition try: klass = super(VersionedUnpickler, self).find_class(module, name) except Exception as e: from apptools.sweet_pickle import UnpicklingError logger.debug('Traceback when finding class [%s.%s]:' \ % (module, name), exc_info=True) raise UnpicklingError('Unable to load class [%s.%s]. ' 'Original exception was, "%s". map:%s' % ( module, name, str(e), self.updater.class_map)) # Make sure we run the updater's state functions if any are declared # for the target class. if self.updater is not None \ and self._has_state_function(original_module, original_name): self._add_unpickler(klass, original_module, original_name) return klass ########################################################################## # 'VersionedUnpickler' interface ########################################################################## ### public interface ##################################################### def modify_state(self, obj, state, module, name): """ Called to update the specified state dictionary, which represents the class of the specified name within the specified module, to complete the unpickling of the specified object. """ # Remove our setstate hook and associated data to ensure that # instances unpickled through some other framework don't call us. # IMPORTANT: Do this first to minimize the time this hook is in place! self._remove_unpickler(obj.__class__) # Determine what class and version we're starting from and going to. # If there is no version information, then assume version 0. (0 is # like an unversioned version.) source_key = self.updater.get_version_attribute(module, name) source_version = state.get(source_key, 0) target_key = self.updater.get_version_attribute( obj.__class__.__module__, obj.__class__.__name__) target_version = getattr(obj, target_key, 0) # Iterate through all the updates to the state by going one version # at a time. Note that we assume there is exactly one path from our # starting class and version to our ending class and version. As a # result, we assume we update a given class to its latest version # before looking for any class mappings. Note that the version in the # updater is the version to convert *TO*. version = source_version next_version = version + 1 while True: # Iterate through all version updates for the current class. key = self.updater.get_version_attribute(module, name) while (module, name, next_version) in self.updater.state_functions: functions = self.updater.state_functions[(module, name, next_version)] for f in functions: logger.debug('Modifying state from [%s.%s (v.%s)] to ' + \ '[%s.%s (v.%s)] using function %s', module, name, version, module, name, next_version, f) state = f(state) # Avoid infinite loops due to versions not changing. new_version = state.get(key, version) if new_version == version: new_version = version + 1 version = new_version next_version = version + 1 # If there is one, move to the next class in the chain. (We # explicitly keep the version number the same.) if self.updater.has_class_mapping(module, name): original_module, original_name = module, name module, name = self.updater.class_map[(module, name)] logger.debug('Modifying state from [%s.%s (v.%s)] to ' + \ '[%s.%s (v.%s)]', original_module, original_name, version, module, name, version) else: break # If one exists, call the final class's setstate method. According to # standard pickling protocol, this method will apply the state to the # instance so our state becomes None so that we don't try to apply our # unfinished state to the object. fn = getattr(obj, _SETSTATE_NAME, None) if fn is not None: fn(state) result = None version = getattr(obj, target_key) else: result = state # Something is wrong if we aren't at our target class and version! if module != obj.__class__.__module__ \ or name != obj.__class__.__name__ \ or version != target_version: from apptools.sweet_pickle import UnpicklingError raise UnpicklingError('Unexpected state! Got ' + \ '[%s.%s (v.%s)] expected [%s.%s (v.%s)]' % (module, name, version, obj.__class__.__module__, obj.__class__.__name__, target_version)) return result ### protected interface ################################################## def _add_unpickler(self, klass, module, name): """ Modifies the specified class so that our 'modify_state' method is called when its next instance is unpickled. """ logger.debug('Adding unpickler hook to [%s]', klass) # Replace the existing setstate method with ours. self._backup_setstate(klass) m = __replacement_setstate__.__get__(None, klass) setattr(klass, _SETSTATE_NAME, m) # Add the information necessary to allow this unpickler to run setattr(klass, _UNPICKLER_DATA, (self, module, name)) def _backup_setstate(self, klass): """ Backs up the specified class's setstate method. """ # We only need to back it up if it actually exists. method = getattr(klass, _SETSTATE_NAME, None) if method is not None: logger.debug('Backing up method [%s] to [%s] on [%s]', _SETSTATE_NAME, _BACKUP_NAME, klass) m = method.__get__(None, klass) setattr(klass, _BACKUP_NAME, m) def _get_target_class(self, module, name): """ Returns the class info that the class, within the specified module and with the specified name, should be instantiated as according to our associated updater. This is done in a manner that allows for chaining of class mappings but is tolerant of the fact that a mapping away from an intermediate class may not be registered until an attempt is made to load that class. """ # Keep a record of the original class asked for. original_module, original_name = module, name # Iterate through any mappings in a manner that allows us to detect any # infinite loops. visited = [] while self.updater.has_class_mapping(module, name): if (module, name) in visited: from apptools.sweet_pickle import UnpicklingError raise UnpicklingError('Detected infinite loop in class ' + \ 'mapping from [%s.%s] to [%s.%s] within Updater [%s]' % \ (original_module, original_name, module, name, self.updater)) visited.append( (module, name) ) # Get the mapping for the current class and try loading the class # to ensure any mappings away from it are registered. module, name = self.updater.class_map[(module, name)] try: super(VersionedUnpickler, self).find_class(module, name) except: logger.exception("_get_target_class can't find: %s" % (module, name)) pass return module, name def _has_state_function(self, module, name): """ Returns True if the updater contains any state functions that could be called by unpickling an instance of the class identified by the specified module and name. Note: If we had a version number we could tell for sure, but we don't have one so we'll have to settle for 'could' be called. """ result = False # Iterate through all the class mappings the requested class would # go through. If any of them have a state function, then we've # determined our answer and can stop searching. # # Note we don't need to check for infinite loops because we're only # ever called after '_get_target_class' which detects the infinite # loops. while not result: result = self.updater.has_state_function(module, name) if not result: if self.updater.has_class_mapping(module, name): module, name = self.updater.class_map[(module, name)] else: break return result def _remove_unpickler(self, klass): """ Restores the specified class to its unmodified state. Meaning we won't get called when its next instance is unpickled. """ logger.debug('Removing unpickler hook from [%s]', klass) # Restore the backed up setstate method self._restore_setstate(klass) # Remove the unpickling data attached to the class. This ensures we # don't pollute the 'real' attributes of the class. delattr(klass, _UNPICKLER_DATA) def _restore_setstate(self, klass): """ Restores the original setstate method back to its rightful place. """ # We only need to restore if the backup actually exists. method = getattr(klass, _BACKUP_NAME, None) if method is not None: logger.debug('Restoring method [%s] to [%s] on [%s]', _BACKUP_NAME, _SETSTATE_NAME, klass) delattr(klass, _BACKUP_NAME) m = method.__get__(None, klass) setattr(klass, _SETSTATE_NAME, m) # Otherwise, we simply remove our setstate. else: delattr(klass, _SETSTATE_NAME) ### EOF ###################################################################### apptools-4.5.0/apptools/selection/0000755000076500000240000000000013547652535020507 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/selection/i_selection_provider.py0000644000076500000240000000235112317502217025252 0ustar mdickinsonstaff00000000000000from traits.api import Event, Interface, Str class ISelectionProvider(Interface): """ Source of selections. """ #: Unique ID identifying the provider. provider_id = Str() #: Event triggered when the selection changes. #: The content of the event is an :class:`~.ISelection` instance. selection = Event def get_selection(self): """ Return the current selection. Returns: selection -- ISelection Object representing the current selection. """ def set_selection(self, items, ignore_missing=False): """ Set the current selection to the given items. If ``ignore_missing`` is ``True``, items that are not available in the selection provider are silently ignored. If it is ``False`` (default), an :class:`~.ValueError` should be raised. Arguments: items -- list List of items to be selected. ignore_missing -- bool If ``False`` (default), the provider raises an exception if any of the items in ``items`` is not available to be selected. Otherwise, missing elements are silently ignored, and the rest is selected. """ apptools-4.5.0/apptools/selection/tests/0000755000076500000240000000000013547652535021651 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/selection/tests/test_list_selection.py0000644000076500000240000000332413547637361026304 0ustar mdickinsonstaff00000000000000import numpy from traits.testing.unittest_tools import unittest from apptools.selection.api import ListSelection class TestListSelection(unittest.TestCase): def test_list_selection(self): all_items = ['a', 'b', 'c', 'd'] selected = ['d', 'b'] list_selection = ListSelection.from_available_items( provider_id='foo', selected=selected, all_items=all_items) self.assertEqual(list_selection.items, selected) self.assertEqual(list_selection.indices, [3, 1]) def test_list_selection_of_sequence_items(self): all_items = [['a', 'b'], ['c', 'd'], ['e', 'f']] selected = [all_items[2], all_items[1]] list_selection = ListSelection.from_available_items( provider_id='foo', selected=selected, all_items=all_items) self.assertEqual(list_selection.items, selected) self.assertEqual(list_selection.indices, [2, 1]) def test_list_selection_of_numpy_array_items(self): data = numpy.arange(10) all_items = [data, data + 10, data + 30] selected = [all_items[0], all_items[2]] list_selection = ListSelection.from_available_items( provider_id='foo', selected=selected, all_items=all_items) self.assertEqual(list_selection.items, selected) self.assertEqual(list_selection.indices, [0, 2]) def test_list_selection_with_invalid_selected_items(self): data = numpy.arange(10) all_items = [data, data + 10, data + 30] selected = [data-10, ] with self.assertRaises(ValueError): ListSelection.from_available_items( provider_id='foo', selected=selected, all_items=all_items) if __name__ == '__main__': unittest.main() apptools-4.5.0/apptools/selection/tests/__init__.py0000644000076500000240000000000012317502217023731 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/selection/tests/test_selection_service.py0000644000076500000240000002277613547637361027005 0ustar mdickinsonstaff00000000000000from traits.api import Any, Event, HasTraits, List, provides, Str from traits.testing.unittest_tools import unittest from apptools.selection.api import ( IDConflictError, ISelection, ISelectionProvider, ListenerNotConnectedError, ListSelection, ProviderNotRegisteredError, SelectionService) @provides(ISelection) class BogusSelection(HasTraits): provider_id = Str # Some content to check that two selections are the same content = Any def is_empty(self): """ Is the selection empty? """ return False @provides(ISelectionProvider) class BogusSelectionProvider(HasTraits): #### 'ISelectionProvider' protocol ######################################## provider_id = Str selection = Event def get_selection(self): return BogusSelection(provider_id=self.provider_id, content='get_selection') def set_selection(self, items, ignore_missing=False): pass #### 'BogusSelectionProvider' protocol #################################### def trigger_selection(self, content): self.selection = BogusSelection(provider_id=self.provider_id, content=content) class BogusListener(HasTraits): selections = List def on_selection_changed(self, selection): self.selections.append(selection) @provides(ISelectionProvider) class SimpleListProvider(HasTraits): #### 'ISelectionProvider' protocol ######################################## provider_id = Str('test.simple_list_provider') selection = Event def get_selection(self): selection = ListSelection.from_available_items( provider_id=self.provider_id, selected=self._selected, all_items=self.items ) return selection def set_selection(self, items, ignore_missing=False): selected = [x for x in items if x in self.items] if not ignore_missing and len(selected) < len(items): raise ValueError() self._selected = selected #### 'SimpleListProvider' protocol ######################################## items = List _selected = List class TestSelectionService(unittest.TestCase): def test_add_selection_provider(self): service = SelectionService() provider = BogusSelectionProvider() service.add_selection_provider(provider) self.assertTrue(service.has_selection_provider(provider.provider_id)) def test_add_selection_id_conflict(self): service = SelectionService() provider_id = 'Foo' provider = BogusSelectionProvider(provider_id=provider_id) another_provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) with self.assertRaises(IDConflictError): service.add_selection_provider(another_provider) def test_remove_selection_provider(self): service = SelectionService() provider = BogusSelectionProvider(provider_id='Bogus') service.add_selection_provider(provider) service.remove_selection_provider(provider) self.assertFalse(service.has_selection_provider(provider.provider_id)) with self.assertRaises(ProviderNotRegisteredError): service.remove_selection_provider(provider) def test_get_selection(self): service = SelectionService() provider_id = 'Bogus' provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) selection = service.get_selection(provider_id) self.assertIsInstance(selection, ISelection) self.assertEqual(selection.provider_id, provider.provider_id) def test_get_selection_id_not_registered(self): service = SelectionService() with self.assertRaises(ProviderNotRegisteredError): service.get_selection('not-registered') def test_connect_listener(self): service = SelectionService() provider_id = 'Bogus' provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) listener = BogusListener() service.connect_selection_listener(provider_id, listener.on_selection_changed) content = [1, 2, 3] provider.trigger_selection(content) selections = listener.selections self.assertEqual(len(selections), 1) self.assertEqual(selections[0].provider_id, provider.provider_id) self.assertEqual(selections[0].content, content) def test_connect_listener_then_add_remove_provider(self): service = SelectionService() provider_id = 'Bogus' # Connect listener before provider is registered. listener = BogusListener() service.connect_selection_listener(provider_id, listener.on_selection_changed) # When the provider is first added, the listener should receive the # initial selection (as returned by provider.get_selection) provider = BogusSelectionProvider(provider_id=provider_id) expected = provider.get_selection() service.add_selection_provider(provider) selections = listener.selections self.assertEqual(len(selections), 1) self.assertEqual(selections[-1].content, expected.content) # When the provider changes the selection, the event arrive as usual. content = [1, 2, 3] provider.trigger_selection(content) self.assertEqual(len(selections), 2) self.assertEqual(selections[-1].content, content) # When we un-register the provider, a change in selection does not # generate a callback. service.remove_selection_provider(provider) provider.trigger_selection(content) self.assertEqual(len(selections), 2) # Finally, we register again and get the current selection. service.add_selection_provider(provider) self.assertEqual(len(selections), 3) self.assertEqual(selections[-1].content, expected.content) def test_disconnect_listener(self): service = SelectionService() provider_id = 'Bogus' provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) listener = BogusListener() service.connect_selection_listener(provider_id, listener.on_selection_changed) service.disconnect_selection_listener(provider_id, listener.on_selection_changed) provider.trigger_selection([1, 2, 3]) self.assertEqual(len(listener.selections), 0) def test_disconnect_unknown_listener(self): service = SelectionService() provider_id = 'Bogus' provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) # First case: there are listeners to a provider, but not the one we # pass to the disconnect method listener_1 = BogusListener() service.connect_selection_listener(provider_id, listener_1.on_selection_changed) listener_2 = BogusListener() with self.assertRaises(ListenerNotConnectedError): service.disconnect_selection_listener( provider_id, listener_2.on_selection_changed) # Second case: there is no listener connected to the ID with self.assertRaises(ListenerNotConnectedError): service.disconnect_selection_listener( 'does-not-exists', listener_2.on_selection_changed) def test_set_selection(self): service = SelectionService() provider = SimpleListProvider(items=list(range(10))) service.add_selection_provider(provider) provider_id = provider.provider_id selection = service.get_selection(provider_id) self.assertTrue(selection.is_empty()) new_selection = [5, 6, 3] service.set_selection(provider_id, new_selection) selection = service.get_selection(provider_id) self.assertFalse(selection.is_empty()) # We can't assume that the order of the items in the selection we set # remains stable. self.assertEqual(selection.items, new_selection) self.assertEqual(selection.indices, selection.items) def test_selection_id_not_registered(self): service = SelectionService() with self.assertRaises(ProviderNotRegisteredError): service.set_selection(provider_id='not-existent', items=[]) def test_ignore_missing(self): # What we are really testing here is that the selection service # passes the keyword argument to the selection provider. It's the # selection provider responsibility to ignore missing elements, or # raise an exception. service = SelectionService() provider = SimpleListProvider(items=list(range(10))) service.add_selection_provider(provider) new_selection = [0, 11, 1] provider_id = provider.provider_id service.set_selection(provider_id, new_selection, ignore_missing=True) selection = service.get_selection(provider_id) self.assertFalse(selection.is_empty()) self.assertEqual(selection.items, [0, 1]) new_selection = [0, 11, 1] with self.assertRaises(ValueError): service.set_selection(provider_id, new_selection, ignore_missing=False) if __name__ == '__main__': unittest.main() apptools-4.5.0/apptools/selection/__init__.py0000644000076500000240000000000012317502217022567 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/selection/api.py0000644000076500000240000000046512317502217021620 0ustar mdickinsonstaff00000000000000from .errors import (IDConflictError, ListenerNotConnectedError, ProviderNotRegisteredError) from .i_selection import ISelection, IListSelection from .i_selection_provider import ISelectionProvider from .list_selection import ListSelection from .selection_service import SelectionService apptools-4.5.0/apptools/selection/errors.py0000644000076500000240000000205312317502217022356 0ustar mdickinsonstaff00000000000000class ProviderNotRegisteredError(Exception): """ Raised when a provider is requested by ID and not found. """ def __init__(self, provider_id): self.provider_id = provider_id def __str__(self): msg = "Selection provider with ID '{id}' not found." return msg.format(id=self.provider_id) class IDConflictError(Exception): """ Raised when a provider is added and its ID is already registered. """ def __init__(self, provider_id): self.provider_id = provider_id def __str__(self): msg = "A selection provider with ID '{id}' is already registered." return msg.format(id=self.provider_id) class ListenerNotConnectedError(Exception): """ Raised when a listener that was never connected is disconnected. """ def __init__(self, provider_id, listener): self.provider_id = provider_id self.listener = listener def __str__(self): msg = "Selection listener {l} is not connected to provider '{id}'." return msg.format(l=self.listener, id=self.provider_id) apptools-4.5.0/apptools/selection/selection_service.py0000644000076500000240000001534212317502217024554 0ustar mdickinsonstaff00000000000000from traits.api import Dict, HasTraits from apptools.selection.errors import ( ProviderNotRegisteredError, IDConflictError, ListenerNotConnectedError ) class SelectionService(HasTraits): """ The selection service connects selection providers and listeners. The selection service is a register of selection providers, i.e., objects that publish their current selection. Selections can be requested actively, by explicitly requesting the current selection in a provider (:meth:`get_selection(id)`), or passively by connecting selection listeners. """ #### 'SelectionService' protocol ########################################## def add_selection_provider(self, provider): """ Add a selection provider. The provider is identified by its ID. If a provider with the same ID has been already registered, an :class:`~.IDConflictError` is raised. Arguments: provider -- ISelectionProvider The selection provider added to the internal registry. """ provider_id = provider.provider_id if self.has_selection_provider(provider_id): raise IDConflictError(provider_id=provider_id) self._providers[provider_id] = provider if provider_id in self._listeners: self._connect_all_listeners(provider_id) def has_selection_provider(self, provider_id): """ Has a provider with the given ID been registered? """ return provider_id in self._providers def remove_selection_provider(self, provider): """ Remove a selection provider. If the provider has not been registered, a :class:`~.ProviderNotRegisteredError` is raised. Arguments: provider -- ISelectionProvider The selection provider added to the internal registry. """ provider_id = provider.provider_id self._raise_if_not_registered(provider_id) if provider_id in self._listeners: self._disconnect_all_listeners(provider_id) del self._providers[provider_id] def get_selection(self, provider_id): """ Return the current selection of the provider with the given ID. If a provider with that ID has not been registered, a :class:`~.ProviderNotRegisteredError` is raised. Arguments: provider_id -- str The selection provider ID. Returns: selection -- ISelection The current selection of the provider. """ self._raise_if_not_registered(provider_id) provider = self._providers[provider_id] return provider.get_selection() def set_selection(self, provider_id, items, ignore_missing=False): """ Set the current selection in a provider to the given items. If a provider with the given ID has not been registered, a :class:`~.ProviderNotRegisteredError` is raised. If ``ignore_missing`` is ``True``, items that are not available in the selection provider are silently ignored. If it is ``False`` (default), a :class:`ValueError` should be raised. Arguments: provider_id -- str The selection provider ID. items -- list List of items to be selected. ignore_missing -- bool If ``False`` (default), the provider raises an exception if any of the items in ``items`` is not available to be selected. Otherwise, missing elements are silently ignored, and the rest is selected. """ self._raise_if_not_registered(provider_id) provider = self._providers[provider_id] return provider.set_selection(items, ignore_missing=ignore_missing) def connect_selection_listener(self, provider_id, func): """ Connect a listener to selection events from a specific provider. The signature if the listener callback is ``func(i_selection)``. The listener is called: 1) When a provider with the given ID is registered, with its initial selection as argument, or 2) whenever the provider fires a selection event. It is perfectly valid to connect a listener before a provider with the given ID is registered. The listener will remain connected even if the provider is repeatedly connected and disconnected. Arguments: provider_id -- str The selection provider ID. func -- callable(i_selection) A callable object that is notified when the selection changes. """ self._listeners.setdefault(provider_id, []) self._listeners[provider_id].append(func) if self.has_selection_provider(provider_id): self._toggle_listener(provider_id, func, remove=False) def disconnect_selection_listener(self, provider_id, func): """ Disconnect a listener from a specific provider. Arguments: provider_id -- str The selection provider ID. func -- callable(provider_id, i_selection) A callable object that is notified when the selection changes. """ if self.has_selection_provider(provider_id): self._toggle_listener(provider_id, func, remove=True) try: self._listeners[provider_id].remove(func) except (ValueError, KeyError): raise ListenerNotConnectedError(provider_id=provider_id, listener=func) #### Private protocol ##################################################### _listeners = Dict() _providers = Dict() def _toggle_listener(self, provider_id, func, remove): provider = self._providers[provider_id] provider.on_trait_change(func, 'selection', remove=remove) def _connect_all_listeners(self, provider_id): """ Connect all listeners connected to a provider. As soon as they are connected, they receive the initial selection. """ provider = self._providers[provider_id] selection = provider.get_selection() for func in self._listeners[provider_id]: self._toggle_listener(provider_id, func, remove=False) # FIXME: make this robust to notifications that raise exceptions. # Can we send the error to the traits exception hook? func(selection) def _disconnect_all_listeners(self, provider_id): for func in self._listeners[provider_id]: self._toggle_listener(provider_id, func, remove=True) def _raise_if_not_registered(self, provider_id): if not self.has_selection_provider(provider_id): raise ProviderNotRegisteredError(provider_id=provider_id) apptools-4.5.0/apptools/selection/i_selection.py0000644000076500000240000000076012317502217023342 0ustar mdickinsonstaff00000000000000from traits.api import Interface, List, Str class ISelection(Interface): """ Collection of selected items. """ #: ID of the selection provider that created this selection object. provider_id = Str def is_empty(self): """ Is the selection empty? """ class IListSelection(ISelection): """ Selection for ordered sequences of items. """ #: Selected objects. items = List #: Indices of the selected objects in the selection provider. indices = List apptools-4.5.0/apptools/selection/list_selection.py0000644000076500000240000000352312473127616024076 0ustar mdickinsonstaff00000000000000from traits.api import HasTraits, List, provides, Str from apptools.selection.i_selection import IListSelection @provides(IListSelection) class ListSelection(HasTraits): """ Selection for ordered sequences of items. This is the default implementation of the :class:`~.IListSelection` interface. """ #### 'ISelection' protocol ################################################ #: ID of the selection provider that created this selection object. provider_id = Str def is_empty(self): """ Is the selection empty? """ return len(self.items) == 0 #### 'IListSelection' protocol ############################################ #: Selected objects. items = List #: Indices of the selected objects in the selection provider. indices = List #### 'ListSelection' class protocol ####################################### @classmethod def from_available_items(cls, provider_id, selected, all_items): """ Create a list selection given a list of all available items. Fills in the required information (in particular, the indices) based on a list of selected items and a list of all available items. .. note:: - The list of available items must not contain any duplicate items. - It is expected that ``selected`` is populated by items in ``all_items``. """ number_of_items = len(all_items) indices = [] for item in selected: for index in range(number_of_items): if all_items[index] is item: indices.append(index) break else: msg = 'Selected item: {!r}, could not be found' raise ValueError(msg.format(item)) return cls(provider_id=provider_id, items=selected, indices=indices) apptools-4.5.0/apptools/help/0000755000076500000240000000000013547652535017452 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/help/__init__.py0000644000076500000240000000052311640354733021552 0ustar mdickinsonstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. #------------------------------------------------------------------------------ """ Support for online help in Traits and Envisage. Part of the AppTools project of the Enthought Tool Suite. """ apptools-4.5.0/apptools/help/help_plugin/0000755000076500000240000000000013547652535021760 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/help/help_plugin/preferences.ini0000644000076500000240000000053611640354733024755 0ustar mdickinsonstaff00000000000000 # Entries for testing purposes #[enthought.help.help_plugin.TraitsDemo] #label = Traits Demo #filename = C:\Documents and Settings\demo\src\Traits_3.0.3\examples\demo\demo.py #[enthought.help.help_plugin.AcmeLab] #label = Envisage AcmeLab #filename = C:\Documents and Settings\demo\src\EnvisagePlugins_3.0.1\examples\workbench\AcmeLab apptools-4.5.0/apptools/help/help_plugin/preferences_pages.py0000644000076500000240000001312411640354733026002 0ustar mdickinsonstaff00000000000000""" Defines classes for preferences pages for the help plugin. :Copyright: 2008, Enthought Inc. :License: BSD :Author: Janet Swisher """ # This software is provided without warranty under the terms of the BSD # license included in AppTools/trunk/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from apptools.preferences.ui.api import PreferencesPage from traits.api import Either, File, Str from traitsui.api import Group, Item, Label, View # This module's package. PKG = '.'.join(__name__.split('.')[:-1]) class HelpPreferencesPage(PreferencesPage): """ Base class for root preference pages for the help plugin. """ #### 'PreferencesPage' interface ########################################## # The page's category (e.g. 'General/Appearance'). The empty string means # that this is a top-level page. category = '' # The page's help identifier (optional). If a help ID *is* provided then # there will be a 'Help' button shown on the preference page. help_id = '' # The page name (this is what is shown in the preferences dialog. name = 'Help' # The path to the preferences node that contains the preferences. preferences_path = PKG traits_view = View() class DocumentsPreferencesPage(HelpPreferencesPage): """ (Blank) page for the "Documents" preferences tree node. """ name = 'Documents' class DemosPreferencesPage(HelpPreferencesPage): """ (Blank) page for the "Demos" preferences tree node. """ name = 'Demos' class ExamplesPreferencesPage(HelpPreferencesPage): """ Page for the "Examples" preferences tree node. """ name = 'Examples' preferences_path = PKG + '.Examples' #### Preferences ########################################################### editor = Str traits_view = View( Item( name='editor', label='Command for external editor'), ) class HelpDocPreferencesPage(PreferencesPage): """ Base class for preferences pages for help documents. """ #### 'PreferencesPage' interface ########################################## # The page's category. category = 'Documents' # The page's help identifier (optional). help_id = '' # The page name (this is what is shown in the preferences dialog. name = Str def _name_default(self): return self.label # The path to the preferences node that contains the preferences. preferences_path = Str #### Preferences ########################################################### # The UI label for the help doc, which appears in menus or dialogs. label = Str # The full path to the document on disk. filename = File # The program to use to view the document. 'browser' means the platform # default web browser. viewer = Either('browser', File) traits_view = View( Group( Item('viewer', show_label=True), Label("Viewer can be 'browser' or a path to a program."), show_border=True, ), Item('filename', show_label=True), Label("Filename can be absolute, or relative to the Python directory."), ) class HelpDemoPreferencesPage(PreferencesPage): """ Base class for preferences pages for help demos. """ #### 'PreferencesPage' interface ########################################## # The page's category (e.g. 'General/Appearance'). The empty string means # that this is a top-level page. category = 'Demos' # The page's help identifier (optional). If a help ID *is* provided then # there will be a 'Help' button shown on the preference page. help_id = '' # The page name (this is what is shown in the preferences dialog. name = Str def _name_default(self): return self.label # The path to the preferences node that contains the preferences. preferences_path = Str #### Preferences ########################################################### # The UI label for the help demo, which appears in menus or dialogs. label = Str # The full path to entry point for the demo. filename = File traits_view = View( Item('filename', show_label=True), Label("Filename can be absolute, or relative to the Python directory."), ) class HelpExamplePreferencesPage(PreferencesPage): """ Base class for preferences pages for help examples. """ #### 'PreferencesPage' interface ########################################## # The page's category (e.g. 'General/Appearance'). The empty string means # that this is a top-level page. category = 'Examples' # The page's help identifier (optional). If a help ID *is* provided then # there will be a 'Help' button shown on the preference page. help_id = '' # The page name (this is what is shown in the preferences dialog. name = Str def _name_default(self): return self.label # The path to the preferences node that contains the preferences. preferences_path = Str #### Preferences ########################################################### # The UI label for the help demo, which appears in menus or dialogs. label = Str # The full path to the main file of the example. filename = File traits_view = View( Item('filename', show_label=True), Label("Filename can be absolute, or relative to the Python directory."), ) #### EOF ###################################################################### apptools-4.5.0/apptools/help/help_plugin/__init__.py0000644000076500000240000000000011640354733024046 0ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/help/help_plugin/examples_preferences.py0000644000076500000240000000215113422276145026517 0ustar mdickinsonstaff00000000000000""" Preferences for all help examples. :Copyright: 2008, Enthought Inc. :License: BSD :Author: Janet Swisher """ # This software is provided without warranty under the terms of the BSD # license included in AppTools/trunk/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from apptools.preferences.api import PreferencesHelper from traits.api import Either, Enum, File, Str, provides # This module's package. PKG = '.'.join(__name__.split('.')[:-1]) class ExamplesPreferences(PreferencesHelper): """ Preferences for all help examples. """ #### Preferences ###################################### # The path to the preferences preferences_path = PKG + '.Examples' # The external program to use to view the document, if editor_choice is # 'external'. It is a command to run, which may be in the program search # path of the current environment, or an absolute path to a program. editor = Str apptools-4.5.0/apptools/help/help_plugin/help_doc.py0000644000076500000240000000275013547637361024113 0ustar mdickinsonstaff00000000000000""" The help doc implementation. :Copyright: 2008, Enthought Inc. :License: BSD :Author: Janet Swisher """ # This software is provided without warranty under the terms of the BSD # license included in AppTools/trunk/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from traits.api import Either, File, Str, provides, Bool from apptools.preferences.api import PreferencesHelper from .i_help_doc import IHelpDoc @provides(IHelpDoc) class HelpDoc(PreferencesHelper): """ The implementation for help docs. A help doc is defined by a UI label, a filename, and a viewer program. """ #### IHelpDoc interface / Preferences ###################################### # NOTE: This class inherits preferences_path from PreferencesHelper. # The UI label for the help doc, which appears in menus or dialogs. label = Str # The path to the document, which can be full, or relative to the Python # installation directory (sys.prefix). filename = File # Is this a url? url = Bool(False) # The program to use to view the document. 'browser' means the platform # default web browser. Otherwise, it is a command to run, which may be # in the program search path of the current environment, or an absolute # path to a program. viewer = Either('browser', Str) apptools-4.5.0/apptools/help/help_plugin/api.py0000644000076500000240000000114713547637361023106 0ustar mdickinsonstaff00000000000000""" Help Plugin API :Copyright: 2008, Enthought Inc. :License: BSD :Author: Janet Swisher """ # This software is provided without warranty under the terms of the BSD # license included in AppTools/trunk/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from .help_code import HelpCode from .help_doc import HelpDoc from .help_plugin import HelpPlugin from .i_help_code import IHelpCode from .i_help_doc import IHelpDoc apptools-4.5.0/apptools/help/help_plugin/action/0000755000076500000240000000000013547652535023235 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/help/help_plugin/action/doc_action.py0000644000076500000240000000765513547637361025726 0ustar mdickinsonstaff00000000000000""" (Pyface) Action for displaying a help doc. :Copyright: 2008, Enthought Inc. :License: BSD :Author: Janet Swisher """ # This software is provided without warranty under the terms of the BSD # license included in AppTools/trunk/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Standard library imports. import logging from os.path import isabs, join, normpath from subprocess import Popen import sys # Enthought library imports. from envisage.api import IExtensionPointUser, IExtensionRegistry from pyface.workbench.action.workbench_action import WorkbenchAction from traits.api import Instance, provides, Property from pyface.api import ImageResource from apptools.help.help_plugin.api import HelpDoc # Local imports from .util import get_sys_prefix_relative_filename # Logging. logger = logging.getLogger(__name__) # This module's parent package. PARENT = '.'.join(__name__.split('.')[:-2]) # Implementation of the ImageResource class to be used for the DocAction class. @provides(IExtensionPointUser) class DocImageResource(ImageResource): """ Implementation of the ImageResource class to be used for the DocAction class. Overrides the '_image_not_found' trait in the base ImageResource class. """ _image_not_found = ImageResource('document') class DocAction(WorkbenchAction): """ (Pyface) Action for displaying a help doc. """ ### Action interface ############################################## # Image associated with this action instance. image = Property ### IExtensionPointUser interface extension_registry = Property(Instance(IExtensionRegistry)) def _get_extension_registry(self): return self.window.application.extension_registry def _get_image(self): """ Returns the image to be used for this DocAction instance. """ # The current implementation searches for an image file matching # 'name' in all of the image paths. If such a file is not to be found, # the '_image_not_found' file for the DocImageResourceClass is used. return DocImageResource(self.name) ### HelpDocAction interface # Help doc associated with this action. my_help_doc = Instance(HelpDoc) def _my_help_doc_default(self): exns = self.extension_registry.get_extensions(PARENT + '.help_docs') for hd in exns: if hd.label == self.name: return hd return None def _get_filename(self, doc): filename = None if doc is not None: if doc.url: filename = doc.filename else: filename = get_sys_prefix_relative_filename(doc.filename) return filename def perform(self, event): """ Perform the action by displaying the document. """ filename = self._get_filename(self.my_help_doc) if filename is not None: if self.my_help_doc.url or self.my_help_doc.viewer == 'browser': import webbrowser try: webbrowser.open(filename) except (OSError, webbrowser.Error) as msg: logger.error('Could not open page in browser for '+ \ 'Document "%s":\n\n' % self.my_help_doc.label + \ str(msg) + '\n\nTry changing Dcoument Preferences.') elif self.my_help_doc.viewer is not None: # Run the viewer, passing it the filename try: Popen([self.my_help_doc.viewer, filename]) except OSError as msg: logger.error('Could not execute program for Document' + \ ' "%s":\n\n ' % self.my_help_doc.label + str(msg) + \ '\n\nTry changing Document Preferences.') apptools-4.5.0/apptools/help/help_plugin/action/images/0000755000076500000240000000000013547652535024502 5ustar mdickinsonstaff00000000000000apptools-4.5.0/apptools/help/help_plugin/action/images/document.png0000644000076500000240000000051511640354733027016 0ustar mdickinsonstaff00000000000000PNG  IHDRabKGD pHYs  tIME8`&BIDAT8˭=n0H[!!l"A!QsL$^Xj,{|3m1 Z;騬8xt/5ǒ4MW(w=ιURW/P&AH৞"Demos submenu. It is defined by a preference node that specifies a UI label and a filename for the demo entry point. Each contribution to this extension point must be an instance of a class that implements IHelpCode. The easiest way to do this is to create an instance of `apptools.help.help_plugin.api.HelpCode`. So, to contribute a help demo: 1. Create a preferences file for your plugin if it doesn't already have one. (Be sure to contribute your preferences file to the `envisage.preferences` extension point.) 2. Define a unique "node" (section heading) in your preferences file for each demo, and specify values for the 'label' and 'filename' settings. (Note that the same preferences section can be used for a help demo and a help example.) 3. For each demo, contribute a HelpDemo to this extension point, and specify its *preferences_path* as the corresponding node name from your preferences file. """ ) help_examples = ExtensionPoint( List(IHelpCode), id=HELP_EXAMPLES, desc=""" A help example is a Python file that is opened for viewing when it is selected from the Help>Examples submenu. It is defined by a preference node that specifies a UI label and a filename for the primary example file. Each contribution to this extension point must be an instance of a class that implements IHelpCode. The easiest way to do this is to create an instance of `apptools.help.help_plugin.api.HelpCode`. So, to contribute a help example: 1. Create a preferences file for your plugin if it doesn't already have one. (Be sure to contribute your preferences file to the `envisage.preferences` extension point.) 2. Define a unique "node" (section heading) in your preferences file for each example, and specify values for the 'label' and 'filename' settings. (Note that the same preferences section can be used for a help demo and a help example.) 3. For each example, contribute a HelpCode to this extension point, and specify its *preferences_path* as the corresponding node name from your preferences file. """ ) help_downloads = ExtensionPoint( List(IHelpDoc), id=HELP_DOWNLOADS, desc=""" A help download is a url that is opened in a browser for viewing when it is selected from the Help>Downloads submenu. It is defined by a preference node that specifies a UI label and a url for the download. Each contribution to this extension point must be an instance of a class that implements IHelpDoc, and has the url trait set to True. The easiest way to do this is to create an instance of `apptools.help.help_plugin.api.HelpDoc`. So, to contribute a help doc: 1. Create a preferences file for your plugin if it doesn't already have one. (Be sure to contribute your preferences file to the `envisage.preferences` extension point.) 2. Define a unique "node" (section heading) in your preferences file for each url, and specify values for the 'label' and 'filename' settings. Also set 'url' to True. 3. For each document, contribute a HelpDoc to this extension point, and specify its *preferences_path* as the corresponding node name from your preferences file. """ ) # FIXME: Ideally, there should be an extension point to allow plugins to # offer editors to display examples (e.g., TextEditorPlugin or # RemoteEditorPlugin). For now, examples open in an external editor # launched with subprocess.Popen. The user can set the editor # command in the Examples preferences page. ###### Contributions to extension points made by this plugin ###### help_action_sets = List(contributes_to=WORKBENCH_ACTION_SETS) def _help_action_sets_default(self): """ Returns a list containing an action set class whose **actions** correspond to the help docs in the help_docs extension point. """ extension_point_mapping = { DOCS_MENU: self.help_docs, EXAMPLES_MENU: self.help_examples, DEMOS_MENU: self.help_demos, DOWNLOADS_MENU: self.help_downloads} # Construct traits for the action set ns = {'id': 'apptools.help.help_plugin.help_action_set', 'name': 'Help Plugin ActionSet', 'groups': [ Group( id=DOCS_GROUP, before='AboutGroup', path=HELP_MENU ) ] } for (menu_name, items) in extension_point_mapping.items(): if len(items) > 0: menu = Menu( name = menu_name, class_name = PKG + '.help_submenu_manager:%sMenuManager' % menu_name ) if menu_name in self.menus: menu.path = 'MenuBar' menu.before = 'Help' else: menu.path = HELP_MENU menu.group = DOCS_GROUP # Append the menu. ns.setdefault('menus', []).append(menu) return [new.classobj('SPLHelpActionSet', (ActionSet,), ns)] preferences = List(contributes_to=PREFERENCES) def _preferences_default(self): return ['pkgfile://%s/preferences.ini' % PKG] preferences_pages = List(contributes_to=PREFERENCES_PAGES) def _preferences_pages_default(self): from apptools.help.help_plugin.preferences_pages import \ DocumentsPreferencesPage, DemosPreferencesPage, \ ExamplesPreferencesPage, HelpDocPreferencesPage, \ HelpDemoPreferencesPage, HelpExamplePreferencesPage pages = [] if len(self.help_docs) > 0: pages.append(DocumentsPreferencesPage) pages.extend( [ new.classobj(doc.preferences_path + 'PreferencesPage', (HelpDocPreferencesPage,), {'preferences_path': doc.preferences_path}, ) for doc in self.help_docs ]) if len(self.help_demos) > 0: pages.append(DemosPreferencesPage) pages.extend( [ new.classobj(demo.preferences_path + 'PreferencesPage', (HelpDemoPreferencesPage,), {'preferences_path': demo.preferences_path}, ) for demo in self.help_demos ]) if len(self.help_examples) > 0: pages.append(ExamplesPreferencesPage) pages.extend( [ new.classobj(example.preferences_path + 'PreferencesPage', (HelpExamplePreferencesPage,), {'preferences_path': example.preferences_path}, ) for example in self.help_examples ]) return pages #my_help_demos = List(contributes_to=HELP_DEMOS) #def _my_help_demos_default(self): # return [HelpCode( preferences_path=PKG + '.TraitsDemo'), # ] #my_help_examples = List(contributes_to=HELP_EXAMPLES) #def _my_help_examples_default(self): # return [HelpCode( preferences_path=PKG + '.AcmeLab'), # ] apptools-4.5.0/apptools/help/help_plugin/help_code.py0000644000076500000240000000247613547637361024265 0ustar mdickinsonstaff00000000000000""" The help code implementation. This may be used to define examples to be displayed or demos to be run. :Copyright: 2008, Enthought Inc. :License: BSD :Author: Janet Swisher """ # This software is provided without warranty under the terms of the BSD # license included in AppTools/trunk/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from traits.api import File, Str, provides from apptools.preferences.api import PreferencesHelper from .i_help_code import IHelpCode @provides(IHelpCode) class HelpCode(PreferencesHelper): """ The implementation for help codes. A help code is defined by a UI label and a filename. """ #### IHelpCode interface / Preferences ##################################### # NOTE: This class inherits preferences_page from PreferencesHelper. # The UI label for the help code, which appears in menus or dialogs. label = Str # The path to the entry point, which can be full, or relative to the Python # installation directory (sys.prefix). filename = File # The code to execute. This is executed when filename is None or an empty # string. code = Str apptools-4.5.0/apptools/help/help_plugin/help_submenu_manager.py0000644000076500000240000002067513547637361026524 0ustar mdickinsonstaff00000000000000""" Managers for submenus of the Help menu. :Copyright: 2008, Enthought Inc. :License: BSD :Author: Janet Swisher """ # This software is provided without warranty under the terms of the BSD # license included in AppTools/trunk/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Standard library imports. import logging # Enthought library imports. from envisage.api import IExtensionPointUser, IExtensionRegistry from pyface.action.api import Group, MenuManager from traits.api import Any, provides, Instance, List, Property # Local imports. from .action.doc_action import DocAction from .action.demo_action import DemoAction from .action.example_action import ExampleAction from .action.load_url_action import LoadURLAction from .examples_preferences import ExamplesPreferences from .i_help_doc import IHelpDoc from .i_help_code import IHelpCode # Logging. logger = logging.getLogger(__name__) # This module's package. PKG = '.'.join(__name__.split('.')[:-1]) @provides(IExtensionPointUser) class HelpSubmenuManager(MenuManager): """ Base class for managers of submenus of the Help menu. This class is adapted from pyface.ui.workbench.action.view_menu_manager.ViewMenuManager. """ ### IExtensionPointUser interface extension_registry = Property(Instance(IExtensionRegistry)) def _get_extension_registry(self): return self.window.application.extension_registry #### 'ActionManager' interface ############################################ # All of the groups in the manager. groups = List(Group) #### 'Private' interface ################################################## # The group containing the actual actions. _item_group = Any ########################################################################### # 'ActionManager' interface. ########################################################################### def _groups_default(self): """ Trait initializer. """ groups = [] # Add a group containing items for all contributed documents. self._item_group = self._create_item_group(self.window) groups.append(self._item_group) return groups ########################################################################### # Private interface. ########################################################################### def _clear_group(self, group): """ Remove all items in a group. """ group.destroy() group.clear() return def _create_item_group(self, window): """ Creates a group containing the items. """ group = Group() self._initialize_item_group(window, group) return group def _initialize_item_group(self, window, group): """ Initializes a group containing the items. """ raise NotImplementedError class DocumentsMenuManager(HelpSubmenuManager): """ Controls the 'Help/Documents' menu. """ #### 'ActionManager' interface ############################################ # The manager's unique identifier (if it has one). id = 'Documents' #### 'MenuManager' interface ############################################## # The menu manager's name (if the manager is a sub-menu, this is what its # label will be). name = u'&Documents' #### 'DocMenuManager' interface ########################################## # The HelpDocs for which this manager displays menu items. help_doc_list = List(IHelpDoc, allow_none=True) def _help_doc_list_default(self): return self.extension_registry.get_extensions(PKG + '.help_docs') ########################################################################### # Private interface. ########################################################################### def _initialize_item_group(self, window, group): """ Initializes a group containing the items. """ docs = self.help_doc_list docs.sort(None, lambda doc: doc.label) for doc in docs: #logger.info('Adding Helpaction for "%s", %s' % (doc.label, str(doc))) group.append( DocAction(name=doc.label, window=window) ) return class DemosMenuManager(HelpSubmenuManager): """ Controls the 'Help/Demos' menu. """ #### 'ActionManager' interface ############################################ # The manager's unique identifier (if it has one). id = 'Demos' #### 'MenuManager' interface ############################################## # The menu manager's name (if the manager is a sub-menu, this is what its # label will be). name = u'D&emos' #### 'DemoMenuManager' interface ########################################## # The HelpCode for which this manager displays menu items. help_demo_list = List(IHelpCode, allow_none=True) def _help_demo_list_default(self): return self.extension_registry.get_extensions(PKG + '.help_demos') ########################################################################### # Private interface. ########################################################################### def _initialize_item_group(self, window, group): """ Initializes a group containing the items. """ demos = self.help_demo_list demos.sort(None, lambda demo: demo.label) for demo in demos: group.append( DemoAction(name=demo.label, window=window) ) return class ExamplesMenuManager(HelpSubmenuManager): """ Controls the 'Help/Examples' menu. """ #### 'ActionManager' interface ############################################ # The manager's unique identifier (if it has one). id = 'Examples' #### 'MenuManager' interface ############################################## # The menu manager's name (if the manager is a sub-menu, this is what its # label will be). name = u'&Examples' #### 'ExampleMenuManager' interface ########################################## # The HelpCode for which this manager displays menu items. help_example_list = List(IHelpCode, allow_none=True) def _help_example_list_default(self): return self.extension_registry.get_extensions(PKG + '.help_examples') # Preferences for examples preferences = Instance(ExamplesPreferences, ()) ########################################################################### # Private interface. ########################################################################### def _initialize_item_group(self, window, group): """ Initializes a group containing the items. """ examples = self.help_example_list examples.sort(None, lambda example: example.label) for ex in examples: group.append( ExampleAction(name=ex.label, window=window, preferences=self.preferences) ) return class DownloadsMenuManager(HelpSubmenuManager): """ Controls the 'Help/Downloads' or 'Downloads'menu. """ #### 'ActionManager' interface ############################################ # The manager's unique identifier (if it has one). id = 'Downloads' #### 'MenuManager' interface ############################################## # The menu manager's name (if the manager is a sub-menu, this is what its # label will be). name = u'&Downloads' #### 'DocMenuManager' interface ########################################## # The URLs for which this manager displays menu items. help_download_list = List(IHelpDoc, allow_none=True) def _help_download_list_default(self): return self.extension_registry.get_extensions(PKG + '.help_downloads') ########################################################################### # Private interface. ########################################################################### def _initialize_item_group(self, window, group): """ Initializes a group containing the items. """ urls = self.help_download_list #docs.sort(None, lambda doc: doc.label) for url in urls: group.append( LoadURLAction(name=url.label, window=window) ) return #### EOF ######################################################################