././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.3014605 envisage-4.9.0/0000755000076500000240000000000000000000000014607 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/CHANGES.rst0000644000076500000240000002067600000000000016424 0ustar00mdickinsonstaff00000000000000==================== Envisage CHANGELOG ==================== Version 4.9.0 ============= Released: 2019-11-19 This is a minor feature release with a small handful of fixes, and a single new feature to make the ``IPythonKernelPlugin`` easier to use for applications. Features -------- - Add an option to allow the ``InternalIPKernel`` to initialise its kernel at kernel creation time. At some point in the future, this will become the default behaviour. (#227) Fixes ----- - Replace a use of the deprecated ``adapts`` function with ``register_factory``. (#234) - In the ``IPKernelApp``, correctly restore the original state of ``IPython.utils.io.std*`` streams even if those streams didn't exist originally. (#232) - Remove duplicate copyright header from autogenerated version file. (#220) Tests ----- - Remove a ``print`` call from a unit test. (#240) - Add unit tests for the ``envisage.ui.single_project`` adapters. (#235) - Add unit tests to check that ``InternalIPKernel`` doesn't affect ``sys.path``. (#233) - Fix the test suite not to write to the user's ``~/.ipython`` directory. (#231) - Fix the test suite not to write to the user's ``~/.enthought`` directory. (#230) - Remove an unused import and a useless ``tearDown`` method in the ``IPythonKernel`` tests. (#223) - Fix ``DeprecationWarning``s from uses of long-deprecated ``TestCase`` methods. (#222) - Add test eggs for Python 3.8. (#214) Build ----- - Rename changelog extension from ``.txt`` to ``.rst``. (#238) - Update EDM version used in Travis CI and Appveyor. (#236) - Add ``mock`` to test dependencies on Python 2. (#229) - Fix status badges in ``README``. (#216) Version 4.8.0 ============= Released: 2019-09-13 The main focus of this feature release is the ``IPythonKernelPlugin``, which has been updated to work with the latest IPython-related packages from PyPI, and is now much more careful about releasing resources allocated. Also in this release, a number of outdated, incomplete or otherwise nonfunctional pieces of code were removed. Features -------- - Improved ``repr`` for ``ExtensionPoint`` objects. (#142) Changes ------- - Drop support for Python versions older than 2.7 and Python 3 versions older than Python 3.5. (#139) - The ``IPythonKernelPlugin`` now releases all allocated resources (threads, file descriptors, etc.) and undoes global state changes at plugin ``stop`` time. (#188) - Suppress the Ctrl-C message printed by the IPython kernel at start time. (#182) - Add license headers to all files, and make license header statements consistent. (#192) Fixes ----- - Use a fixed pickle protocol when saving task layout state, to avoid cross-Python-version difficulties. (#179) - Fix deprecation warnings from use of ``Logger.warn``. (#178) - Fix some Python 3 syntax errors in example scripts. (#171) Removals -------- - Remove the unsupported and incomplete ``UpdateCheckerPlugin``. (#199) - Remove the ``plugin.debug`` empty submodule. (#195) - Remove the old ``IPythonShell`` plugin, which was based on pre-IPython 1.0. (#173) - Remove the non-functional ``RefreshCodePlugin``. (#202) - Remove ``project_runnable``, which was never functional. (#169) - Remove outdated debugging fallback from the ``ExtensionPoint`` source. (#167) - Remove ``FBIPlugin``. (#166) - Remove the ``remote_editor`` plugins. (#137) Documentation ------------- - Add docstrings for tasks plugin extension points. (#181) - Fix incorrect documentation for ``always_use_default_layout``. (#177) - Spell "Pyface" correctly. (#176) - NumPyDoc style fixes. (#168) - Add API documentation, with corresponding build infrastructure. (#165) - Fix invalid syntax in Tetris example. (#158) - Use the Enthought Sphinx Theme for documentation. (#157) Tests ----- - Remove dependency on the ``nose`` package, and rename test modules. All tests can now be discovered and run using ``unittest``. (#200, #194) Build ----- - Revise version-handling mechanisms and other minor details in ``setup.py`` script. (#197, #190) - Remove unused and outdated ``tox.ini`` file. (#201) - Update ``etstool.py`` to work with a non-EDM bootstrap environment on Windows. (#203) - Test against other ETS packages from source, using Travis CI cron jobs. (#162) - Fix deprecated pieces in Travis CI configuration. (#160, #159) - Update EDM version used, and clean up and simplify Travis CI and Appveyor configurations. (#152) - Usability improvements to ``etstool.py``. (#145, #148) Version 4.7.2 ============= Released: 03 May 2019 Fixes ----- * Fix some broken imports and name errors in the ``envisage.developer`` package. (#130) * Add missing test data to support running tests on Python 3.7. (#136) * Fix reversed interpretation of the ``TasksApplication.always_use_default_layout`` when creating task windows. (#144) * In the ``InternalIPKernel`` plugin, restore original standard streams (``stdout``, ``stdin``, ``stderr``) at plugin stop time. (#146) * In the ``InternalIPKernel`` plugin, fix ``ResourceWarnings`` from unclosed pipes attached to qt consoles. (#147) Version 4.7.1 ============= Released : 31 January 2019 Changes ------- * Replace use of deprecated ``HasTraits.set`` method (#118) Fixes ----- * Fix IPython GUI kernel issue when used with ipykernel 4.7.0 (#123) * Fix infinite recursion issue when harvesting extension methods (#121) Version 4.7.0 ============= Changes ------- * Update CI setup and include ``ipykernel`` in devenv (#105, #111, #114) * Use ``--gui`` rather than ``--matplotlib`` when starting IPython kernel (#101) * Downgrade level of a logging message (#95) Fixes ----- * Fix old-style relative import (#109) * Fix attractors example (#103) * Stop the IOPubThread as part of IPython kernel shutdown (#100) * Fix Sphinx conf to be able to build docs again (#91) * Fix deprecated IPython import (#92) * Fix task layout serialization under Python 3 (#90) Version 4.6.0 ============= This is an incremental release, mainly consisting of bug fixes. The most significant change is the support for IPython >= 4 in the IPython plugin. Thanks to @corranwebster, @dpinte, @itziakos, @jonathanrocher, @kamalx, @rahulporuri, @robmcmullen, @sjagoe Enhancements ------------ * IPython kernel plugin now supports IPython >= 4 (#82) * Remove usage of deprecated IPython QtConsole API (#80) * Defer selection of toolkit and avoid creating GUI applications as side-effects as much as possible (#77, #76) Fixes ----- * Fixes for tests under Python 3.5 (#86) * Work around for issue with Traits in Python 3 (#78) * Replace uses of ‘file’ and ‘execfile’ (#75) * Fix MOTD_Using_Eggs example (#66) * Fix broken and outdated links in documentation (#72) * Fix link to docs from README (#70) * Fix degenerate case where window is created with no layout (#44) Version 4.5.1 ============= Enhancements ------------ * Add tox for testing package install (#67) Fixes ----- * Include missing test files in the package data (#67) * Include missing test cases for Python 3.4 (#67) Version 4.5.0 ============= New features ------------ * IPythonKernelPlugin for Tasks: run an IPython kernel within the envisage app and expose it as a service (#54). * Envisage now supports Python 3.4 (#61). Enhancements ------------ * Allow loading plugins from an egg basket even when some eggs are invalid (#40, #46). * Add a simple ``GUIApplication`` to bootstrap basic plugin-driven applications (#34). * Split the IPython kernel and IPython menu action into two separate plugins for flexibility (#57). Fixes ----- * Use new Traits interfaces and adaptation implementation (#37). * Envisage now configures the logger with a ``NullHandler`` to avoid spurios unconfigured logger warnings (#45). * Envisage no longer swallows exceptions in plugin startup (#50). * Various fixes to continuous integration configuration (#47, #60). Version 4.4.0 ============= The major component of this feature is to work with the new ``traits.adaptation`` mechanism in place of the deprecated ``traits.protocols``, maintaining compatibility with ``traits`` version 4.4.0. This release also adds a new method to retrieve a service that is required by the application and provides documentation and test updates. New features ------------ * Added a simple GUIApplication class (673c8f6) * Added a method to get a required service (94dfdea) Enhancements ------------ * Updated to use the new traits.adaptation functionality (34fa5e6) Fixes ----- * Updated links to point to github instead of svn codebase (87cdb87) * Fixed test cases and added to Travis-CI (6c11d9f) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/LICENSE.txt0000644000076500000240000000312000000000000016426 0ustar00mdickinsonstaff00000000000000This 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/MANIFEST.in0000644000076500000240000000042000000000000016341 0ustar00mdickinsonstaff00000000000000include CHANGES.rst include LICENSE.txt include MANIFEST.in include README.rst include image_LICENSE.txt include image_LICENSE_CP.txt recursive-include envisage/tests/plugins *.py recursive-include envisage/tests/eggs *.egg recursive-include envisage/tests/bad_eggs *.egg ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.3011491 envisage-4.9.0/PKG-INFO0000644000076500000240000000672100000000000015712 0ustar00mdickinsonstaff00000000000000Metadata-Version: 2.1 Name: envisage Version: 4.9.0 Summary: Extensible application framework Home-page: http://docs.enthought.com/envisage Author: Enthought Author-email: info@enthought.com Maintainer: ETS Developers Maintainer-email: enthought-dev@enthought.com License: BSD Download-URL: https://github.com/enthought/envisage Description: ========================================== envisage: extensible application framework ========================================== .. image:: https://travis-ci.org/enthought/envisage.svg?branch=master :alt: Build Status :target: https://travis-ci.org/enthought/envisage .. image:: https://codecov.io/github/enthought/envisage/coverage.svg?branch=master :alt: Code Coverage :target: https://codecov.io/github/enthought/envisage?branch=master Documentation: https://docs.enthought.com/envisage Envisage is a Python-based framework for building extensible applications, that is, applications whose functionality can be extended by adding "plug-ins". Envisage provides a standard mechanism for features to be added to an application, whether by the original developer or by someone else. In fact, when you build an application using Envisage, the entire application consists primarily of plug-ins. In this respect, it is similar to the Eclipse and Netbeans frameworks for Java applications. Each plug-in is able to: - Advertise where and how it can be extended (its "extension points"). - Contribute extensions to the extension points offered by other plug-ins. - Create and share the objects that perform the real work of the application ("services"). The Envisage project provides the basic machinery of the Envisage framework. This project contains no plug-ins. You are free to use: - plug-ins from the EnvisagePlugins project - plug-ins from other ETS projects that expose their functionality as plug-ins - plug-ins that you create yourself Prerequisites ------------- The supported versions of Python are Python 2.7.x and Python >= 3.5. * `apptools `_ * `traits `_ Platform: UNKNOWN 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 :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: User Interfaces Requires-Python: >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.* Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571145795.0 envisage-4.9.0/README.rst0000644000076500000240000000335100000000000016300 0ustar00mdickinsonstaff00000000000000========================================== envisage: extensible application framework ========================================== .. image:: https://travis-ci.org/enthought/envisage.svg?branch=master :alt: Build Status :target: https://travis-ci.org/enthought/envisage .. image:: https://codecov.io/github/enthought/envisage/coverage.svg?branch=master :alt: Code Coverage :target: https://codecov.io/github/enthought/envisage?branch=master Documentation: https://docs.enthought.com/envisage Envisage is a Python-based framework for building extensible applications, that is, applications whose functionality can be extended by adding "plug-ins". Envisage provides a standard mechanism for features to be added to an application, whether by the original developer or by someone else. In fact, when you build an application using Envisage, the entire application consists primarily of plug-ins. In this respect, it is similar to the Eclipse and Netbeans frameworks for Java applications. Each plug-in is able to: - Advertise where and how it can be extended (its "extension points"). - Contribute extensions to the extension points offered by other plug-ins. - Create and share the objects that perform the real work of the application ("services"). The Envisage project provides the basic machinery of the Envisage framework. This project contains no plug-ins. You are free to use: - plug-ins from the EnvisagePlugins project - plug-ins from other ETS projects that expose their functionality as plug-ins - plug-ins that you create yourself Prerequisites ------------- The supported versions of Python are Python 2.7.x and Python >= 3.5. * `apptools `_ * `traits `_ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2230134 envisage-4.9.0/envisage/0000755000076500000240000000000000000000000016410 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/__init__.py0000644000076500000240000000142600000000000020524 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! try: from envisage.version import version as __version__ except ImportError: # If we get here, we're using a source tree that hasn't been created via # the setup script. __version__ = "unknown" # Per logging best practices, add a NullHandler to the root 'envisage' # logger. import logging logging.getLogger(__name__).addHandler(logging.NullHandler()) del logging ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/_compat.py0000644000076500000240000000152400000000000020406 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """Compatibility layer for Python version 2.x and 3.x. """ import sys PY_VER = sys.version_info[0] if PY_VER >= 3: import pickle from urllib.request import urlopen from urllib.error import HTTPError STRING_BASE_CLASS = str else: import cPickle as pickle from urllib2 import urlopen, HTTPError STRING_BASE_CLASS = basestring def unicode_str(x=''): return str(x) if PY_VER == 3 else unicode(x, encoding='utf-8') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/api.py0000644000076500000240000000402600000000000017535 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Envisage package Copyright 2003-2007 Enthought, Inc. """ from .i_application import IApplication from .i_extension_point import IExtensionPoint from .i_extension_point_user import IExtensionPointUser from .i_extension_provider import IExtensionProvider from .i_extension_registry import IExtensionRegistry from .i_import_manager import IImportManager from .i_plugin import IPlugin from .i_plugin_activator import IPluginActivator from .i_plugin_manager import IPluginManager from .i_service_registry import IServiceRegistry from .application import Application from .category import Category from .class_load_hook import ClassLoadHook from .egg_plugin_manager import EggPluginManager from .extension_registry import ExtensionRegistry from .extension_point import ExtensionPoint, contributes_to from .extension_point_binding import ExtensionPointBinding, bind_extension_point from .extension_provider import ExtensionProvider from .extension_point_changed_event import ExtensionPointChangedEvent from .import_manager import ImportManager from .plugin import Plugin from .plugin_activator import PluginActivator from .plugin_extension_registry import PluginExtensionRegistry from .plugin_manager import PluginManager from .provider_extension_registry import ProviderExtensionRegistry from .service import Service from .service_offer import ServiceOffer from .service_registry import NoSuchServiceError, ServiceRegistry from .twisted_application import TwistedApplication from .unknown_extension import UnknownExtension from .unknown_extension_point import UnknownExtensionPoint #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/application.py0000644000076500000240000004150300000000000021270 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An extensible, pluggable, application. """ # Standard library imports. import logging, os # Enthought library imports. from traits.etsconfig.api import ETSConfig from apptools.preferences.api import IPreferences, ScopedPreferences from apptools.preferences.api import set_default_preferences from traits.api import Delegate, Event, HasTraits, Instance, Str from traits.api import VetoableEvent, provides # Local imports. from .i_application import IApplication from .i_extension_registry import IExtensionRegistry from .i_import_manager import IImportManager from .i_plugin_manager import IPluginManager from .i_service_registry import IServiceRegistry from .application_event import ApplicationEvent from .import_manager import ImportManager # Logging. logger = logging.getLogger(__name__) @provides(IApplication) class Application(HasTraits): """ An extensible, pluggable, application. This class handles the common case for non-GUI applications, and it is intended to be subclassed to change start/stop behaviour etc. """ #### 'IApplication' interface ############################################# # The application's globally unique identifier. id = Str # The name of a directory (created for you) to which the application can # read and write non-user accessible data, i.e. configuration information, # preferences, etc. home = Str # The name of a directory (created for you upon access) to which the # application can read and write user-accessible data, e.g. projects created # by the user. user_data = Str # The root preferences node. preferences = Instance(IPreferences) #### Events #### # Fired when the application is starting. starting = VetoableEvent(ApplicationEvent) # Fired when all plugins have been started. started = Event(ApplicationEvent) # Fired when the application is stopping. stopping = VetoableEvent(ApplicationEvent) # Fired when all plugins have been stopped. stopped = Event(ApplicationEvent) #### 'IPluginManager' interface ########################################### #### Events #### # Fired when a plugin has been added. plugin_added = Delegate('plugin_manager', modify=True) # Fired when a plugin has been removed. plugin_removed = Delegate('plugin_manager', modify=True) #### 'Application' interface ############################################## # These traits allow application developers to build completely different # styles of extensible application. It allows Envisage to be used as a # framework for frameworks ;^) # # The extension registry. extension_registry = Instance(IExtensionRegistry) # The plugin manager (starts and stops plugins etc). plugin_manager = Instance(IPluginManager) # The service registry. service_registry = Instance(IServiceRegistry) #### Private interface #################################################### # The import manager. _import_manager = Instance(IImportManager, factory=ImportManager) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, plugins=None, **traits): """ Constructor. We allow the caller to specify an initial list of plugins, but the list itself is not part of the public API. To add and remove plugins after after construction, use the 'add_plugin' and 'remove_plugin' methods respectively. The application is also iterable, so to iterate over the plugins use 'for plugin in application: ...'. """ super(Application, self).__init__(**traits) # fixme: We have to initialize the application home here (i.e. we can't # wait until the 'home' trait is accessed) because the scoped # preferences uses 'ETSConfig.application' home as the name of the # default preferences file. self._initialize_application_home() # Set the default preferences node used by the preferences package. # This allows 'PreferencesHelper' and 'PreferenceBinding' instances to # be used as more convenient ways to access preferences. # # fixme: This is another sneaky global! set_default_preferences(self.preferences) # We allow the caller to specify an initial list of plugins, but the # list itself is not part of the public API. To add and remove plugins # after construction, use the 'add_plugin' and 'remove_plugin' methods # respectively. The application is also iterable, so to iterate over # the plugins use 'for plugin in application: ...'. if plugins is not None: for plugin in plugins: self.add_plugin(plugin) return ########################################################################### # 'IApplication' interface. ########################################################################### #### Trait initializers ################################################### def _home_default(self): """ Trait initializer. """ return ETSConfig.application_home def _user_data_default(self): """ Trait initializer. """ user_data = os.path.join( ETSConfig.user_data, self.id ) # Make sure it exists! if not os.path.exists(user_data): os.makedirs(user_data) return user_data def _preferences_default(self): """ Trait initializer. """ return ScopedPreferences() #### Methods ############################################################## def run(self): """ Run the application. """ if self.start(): self.stop() return ########################################################################### # 'IExtensionRegistry' interface. ########################################################################### def add_extension_point_listener(self, listener, extension_point_id=None): """ Add a listener for extensions being added/removed. """ self.extension_registry.add_extension_point_listener( listener, extension_point_id ) return def add_extension_point(self, extension_point): """ Add an extension point. """ self.extension_registry.add_extension_point(extension_point) return def get_extensions(self, extension_point_id): """ Return a list containing all contributions to an extension point. """ return self.extension_registry.get_extensions(extension_point_id) def get_extension_point(self, extension_point_id): """ Return the extension point with the specified Id. """ return self.extension_registry.get_extension_point(extension_point_id) def get_extension_points(self): """ Return all extension points that have been added to the registry. """ return self.extension_registry.get_extension_points() def remove_extension_point_listener(self,listener,extension_point_id=None): """ Remove a listener for extensions being added/removed. """ self.extension_registry.remove_extension_point_listener( listener, extension_point_id ) return def remove_extension_point(self, extension_point_id): """ Remove an extension point. """ self.extension_registry.remove_extension_point(extension_point_id) return def set_extensions(self, extension_point_id, extensions): """ Set the extensions contributed to an extension point. """ self.extension_registry.set_extensions(extension_point_id, extensions) return ########################################################################### # 'IImportManager' interface. ########################################################################### def import_symbol(self, symbol_path): """ Import the symbol defined by the specified symbol path. """ return self._import_manager.import_symbol(symbol_path) ########################################################################### # 'IPluginManager' interface. ########################################################################### def __iter__(self): """ Return an iterator over the manager's plugins. """ return iter(self.plugin_manager) def add_plugin(self, plugin): """ Add a plugin to the manager. """ self.plugin_manager.add_plugin(plugin) return def get_plugin(self, plugin_id): """ Return the plugin with the specified Id. """ return self.plugin_manager.get_plugin(plugin_id) def remove_plugin(self, plugin): """ Remove a plugin from the manager. """ self.plugin_manager.remove_plugin(plugin) return def start(self): """ Start the plugin manager. Returns True unless the start was vetoed. """ # fixme: This method is notionally on the 'IPluginManager' interface # but that interface knows nothing about the vetoable events etc and # hence doesn't have a return value. logger.debug('---------- application starting ----------') # Lifecycle event. self.starting = event = self._create_application_event() if not event.veto: # Start the plugin manager (this starts all of the manager's # plugins). self.plugin_manager.start() # Lifecycle event. self.started = self._create_application_event() logger.debug('---------- application started ----------') else: logger.debug('---------- application start vetoed ----------') return not event.veto def start_plugin(self, plugin=None, plugin_id=None): """ Start the specified plugin. """ return self.plugin_manager.start_plugin(plugin, plugin_id) def stop(self): """ Stop the plugin manager. Returns True unless the stop was vetoed. """ # fixme: This method is notionally on the 'IPluginManager' interface # but that interface knows nothing about the vetoable events etc and # hence doesn't have a return value. logger.debug('---------- application stopping ----------') # Lifecycle event. self.stopping = event = self._create_application_event() if not event.veto: # Stop the plugin manager (this stops all of the manager's # plugins). self.plugin_manager.stop() # Save all preferences. self.preferences.save() # Lifecycle event. self.stopped = self._create_application_event() logger.debug('---------- application stopped ----------') else: logger.debug('---------- application stop vetoed ----------') return not event.veto def stop_plugin(self, plugin=None, plugin_id=None): """ Stop the specified plugin. """ return self.plugin_manager.stop_plugin(plugin, plugin_id) ########################################################################### # 'IServiceRegistry' interface. ########################################################################### def get_required_service(self, protocol, query='', minimize='',maximize=''): """ Return the service that matches the specified query. Raise a 'NoSuchServiceError' exception if no such service exists. """ service = self.service_registry.get_required_service( protocol, query, minimize, maximize ) return service def get_service(self, protocol, query='', minimize='', maximize=''): """ Return at most one service that matches the specified query. """ service = self.service_registry.get_service( protocol, query, minimize, maximize ) return service def get_service_from_id(self, service_id): """ Return the service with the specified id. """ return self.service_registry.get_service_from_id(service_id) def get_service_properties(self, service_id): """ Return the dictionary of properties associated with a service. """ return self.service_registry.get_service_properties(service_id) def get_services(self, protocol, query='', minimize='', maximize=''): """ Return all services that match the specified query. """ services = self.service_registry.get_services( protocol, query, minimize, maximize ) return services def register_service(self, protocol, obj, properties=None): """ Register a service. """ service_id = self.service_registry.register_service( protocol, obj, properties ) return service_id def set_service_properties(self, service_id, properties): """ Set the dictionary of properties associated with a service. """ self.service_registry.set_service_properties(service_id, properties) return def unregister_service(self, service_id): """ Unregister a service. """ self.service_registry.unregister_service(service_id) return ########################################################################### # 'Application' interface. ########################################################################### #### Trait initializers ################################################### def _extension_registry_default(self): """ Trait initializer. """ # Do the import here to emphasize the fact that this is just the # default implementation and that the application developer is free # to override it! from .plugin_extension_registry import PluginExtensionRegistry return PluginExtensionRegistry(plugin_manager=self) def _plugin_manager_default(self): """ Trait initializer. """ # Do the import here to emphasize the fact that this is just the # default implementation and that the application developer is free # to override it! from .plugin_manager import PluginManager return PluginManager(application=self) def _service_registry_default(self): """ Trait initializer. """ # Do the import here to emphasize the fact that this is just the # default implementation and that the application developer is free # to override it! from .service_registry import ServiceRegistry return ServiceRegistry() ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ # fixme: We have this to make it easier to assign a new plugin manager # at construction time due to the fact that the plugin manager needs a # reference to the application and vice-versa, e.g. we can do # # application = Application(plugin_manager=EggPluginManager()) # # If we didn't have this then we would have to do this:- # # application = Application() # application.plugin_manager = EggPluginManager(application=application) # # Of course, it would be better if the plugin manager didn't require a # reference to the application at all (it currently uses it to set the # 'application' trait of plugin instances - but that is only done for the # same reason as this (i.e. it is nice to be able to pass plugins into the # application constructor). def _plugin_manager_changed(self, trait_name, old, new): """ Static trait change handler. """ if old is not None: old.application = None if new is not None: new.application = self return #### Methods ############################################################## def _create_application_event(self): """ Create an application event. """ return ApplicationEvent(application=self) def _initialize_application_home(self): """ Initialize the application home directory. """ ETSConfig.application_home = os.path.join( ETSConfig.application_data, self.id ) # Make sure it exists! if not os.path.exists(ETSConfig.application_home): os.makedirs(ETSConfig.application_home) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/application_event.py0000644000076500000240000000137200000000000022471 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An application event. """ # Enthought library imports. from traits.api import Instance, Vetoable class ApplicationEvent(Vetoable): """ An application event. """ # The application that the event is for. application = Instance('envisage.api.IApplication') #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/category.py0000644000076500000240000000172000000000000020577 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A definition of a category to be added to a class. """ # Enthought library imports. from traits.api import HasTraits, Str class Category(HasTraits): """ A definition of a category to be added to a class. """ #### 'Category' interface ################################################# # The name of the category class (the class that you want to add). class_name = Str # The name of the class that you want to add the category to. target_class_name = Str #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/class_load_hook.py0000644000076500000240000000665300000000000022120 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A hook to allow code be executed when a class is loaded. """ # Standard library imports. import sys # Enthought library imports. from traits.api import Callable, HasTraits, MetaHasTraits, Str class ClassLoadHook(HasTraits): """ A hook to allow code to be executed when a class is loaded. If the class is *already* loaded when the 'connect' method is called then the code is executed immediately. """ #### 'ClassLoadHook' interface ############################################ # The name of the class. When this class is loaded the 'on_class_loaded' # method is called. class_name = Str # A callable that will be executed when the class is loaded. The callable # must take a single argument which will be the loaded class. # # This is used in the default implementation of 'on_class_loaded'. If you # override that, then you don't have to set to this trait. on_load = Callable ########################################################################### # 'ClassLoadHook' interface. ########################################################################### def connect(self): """ Connect the load hook to listen for the class being loaded. """ MetaHasTraits.add_listener(self.on_class_loaded, self.class_name) # If the class has already been loaded then run the code now! cls = self._get_class(self.class_name) if cls is not None: self.on_class_loaded(cls) return def disconnect(self): """ Disconnect the load hook. """ MetaHasTraits.remove_listener(self.on_class_loaded, self.class_name) return def on_class_loaded(self, cls): """ This method is called when the class is loaded. If 'self.on_load' is not None, it calls 'self.on_load(cls)'. """ if self.on_load is not None: self.on_load(cls) return ########################################################################### # Private interface. ########################################################################### def _get_class(self, class_path): """ Returns the class defined by *class_path*. Returns **None** if the class has not yet been loaded. """ # Only check if the class name has at least a partial hierarchy. # # fixme: Comment should say why! if '.' in class_path: components = class_path.split('.') module_name = '.'.join(components[:-1]) class_name = components[-1] # The class is loaded if its module has been imported and the class # is defined in the module dictionary. module = sys.modules.get(module_name, None) if module is not None and hasattr(module, class_name): klass = getattr(module, class_name) else: klass = None else: klass = None return klass #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/composite_plugin_manager.py0000644000076500000240000001266600000000000024047 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A plugin manager composed of other plugin managers! """ # Standard library imports. import logging # Enthought library imports. from traits.api import Event, HasTraits, Instance, List, provides from traits.api import on_trait_change # Local imports. from .i_application import IApplication from .i_plugin import IPlugin from .i_plugin_manager import IPluginManager from .plugin_event import PluginEvent from .plugin_manager import PluginManager # Logging. logger = logging.getLogger(__name__) @provides(IPluginManager) class CompositePluginManager(HasTraits): """ A plugin manager composed of other plugin managers! e.g:: plugin_manager = CompositePluginManager( plugin_mangers = [ EggBasketPluginManager(...), PackagePluginManager(...), ] ) """ #### 'IPluginManager' protocol ############################################# #### Events #### # Fired when a plugin has been added to the manager. plugin_added = Event(PluginEvent) # Fired when a plugin has been removed from the manager. plugin_removed = Event(PluginEvent) #### 'CompositePluginManager' protocol ##################################### # The application that the plugin manager is part of. application = Instance(IApplication) def _application_changed(self, trait_name, old, new): for plugin_manager in self.plugin_managers: plugin_manager.application = new return # The plugin managers that make up this plugin manager! # # This is currently a list of 'PluginManager's as opposed to, the more # preferable 'IPluginManager' because the interface doesn't currently # have an 'application' trait. Should we move 'application' up to # 'IPluginManager'? plugin_managers = List(PluginManager) @on_trait_change('plugin_managers[]') def _update_application(self, obj, trait_named, removed, added): for plugin_manager in removed: plugin_manager.application = self.application for plugin_manager in added: plugin_manager.application = self.application @on_trait_change('plugin_managers:plugin_added') def _plugin_added(self, obj, trait_name, old, new): self.plugin_added = new @on_trait_change('plugin_managers:plugin_removed') def _plugin_removed(self, obj, trait_name, old, new): self.plugin_removed = new #### Private protocol ###################################################### # The plugins that the manager manages! _plugins = List(IPlugin) def __plugins_default(self): plugins = [] for plugin_manager in self.plugin_managers: for plugin in plugin_manager: plugins.append(plugin) return plugins #### 'object' protocol #################################################### def __iter__(self): """ Return an iterator over the manager's plugins. """ plugins = [] for plugin_manager in self.plugin_managers: for plugin in plugin_manager: plugins.append(plugin) return iter(plugins) #### 'IPluginManager' protocol ############################################# def add_plugin(self, plugin): """ Add a plugin to the manager. """ raise NotImplementedError def get_plugin(self, plugin_id): """ Return the plugin with the specified Id. """ for plugin in self: if plugin_id == plugin.id: break else: plugin = None return plugin def remove_plugin(self, plugin): """ Remove a plugin from the manager. """ raise NotImplementedError def start(self): """ Start the plugin manager. """ for plugin in self: self.start_plugin(plugin) return def start_plugin(self, plugin=None, plugin_id=None): """ Start the specified plugin. """ plugin = plugin or self.get_plugin(plugin_id) if plugin is not None: logger.debug('plugin %s starting', plugin.id) plugin.activator.start_plugin(plugin) logger.debug('plugin %s started', plugin.id) else: raise SystemError('no such plugin %s' % plugin_id) return def stop(self): """ Stop the plugin manager. """ # We stop the plugins in the reverse order that they were started. stop_order = list(iter(self)) stop_order.reverse() for plugin in stop_order: self.stop_plugin(plugin) return def stop_plugin(self, plugin=None, plugin_id=None): """ Stop the specified plugin. """ plugin = plugin or self.get_plugin(plugin_id) if plugin is not None: logger.debug('plugin %s stopping', plugin.id) plugin.activator.stop_plugin(plugin) logger.debug('plugin %s stopped', plugin.id) else: raise SystemError('no such plugin %s' % plugin_id) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/core_plugin.py0000644000076500000240000002334200000000000021274 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The Envisage core plugin. """ # Enthought library imports. from envisage.api import ExtensionPoint, Plugin, ServiceOffer from traits.api import List, Instance, on_trait_change, Str class CorePlugin(Plugin): """ The Envisage core plugin. The core plugin offers facilities that are generally useful when building extensible applications such as adapters, categories and hooks etc. It does not contain anything to do with user interfaces! The core plugin should be started before any other plugin. It is up to the plugin manager to do this. """ # Extension point Ids. CATEGORIES = 'envisage.categories' CLASS_LOAD_HOOKS = 'envisage.class_load_hooks' PREFERENCES = 'envisage.preferences' SERVICE_OFFERS = 'envisage.service_offers' #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = 'envisage.core' # The plugin's name (suitable for displaying to the user). name = 'Core' #### Extension points offered by this plugin ############################## # Categories are actually implemented via standard 'ClassLoadHooks', but # for (hopefully) readability and convenience we have a specific extension # point. categories = ExtensionPoint( List(Instance('envisage.category.Category')), id = CATEGORIES, desc = """ Traits categories allow you to dynamically extend a Python class with extra attributes, methods and events. Contributions to this extension point allow you to import categories *lazily* when the class to be extended is imported or created. Each contribution contains the name of the category class that you want to add (the 'class_name') and the name of the class that you want to extend (the 'target_class_name'). e.g. To add the 'FooCategory' category to the 'Foo' class:: Category( class_name = 'foo_category.FooCategory', target_class_name = 'foo.Foo' ) """ ) @on_trait_change('categories_items') def _categories_items_changed(self, event): """ React to new categories being *added*. Note that we don't currently do anything if categories are *removed*. """ self._add_category_class_load_hooks(event.added) return class_load_hooks = ExtensionPoint( List(Instance('envisage.class_load_hook.ClassLoadHook')), id = CLASS_LOAD_HOOKS, desc = """ Class load hooks allow you to be notified when any 'HasTraits' class is imported or created. See the documentation for 'ClassLoadHook' for more details. """ ) @on_trait_change('class_load_hooks_items') def _class_load_hooks_changed(self, event): """ React to new class load hooks being *added*. Note that we don't currently do anything if class load hooks are *removed*. """ self._connect_class_load_hooks(event.added) return preferences = ExtensionPoint( List(Str), id = PREFERENCES, desc = """ Preferences files allow plugins to contribute default values for user preferences. Each contributed string must be the URL of a file-like object that contains preferences values. e.g. 'pkgfile://envisage/preferences.ini' - this looks for the 'preferences.ini' file in the 'envisage' package. 'file://C:/tmp/preferences.ini' - this looks for the 'preferences.ini' file in 'C:/tmp' 'http://some.website/preferences.ini' - this looks for the 'preferences.ini' document on the 'some.website' web site! The files themselves are parsed using the excellent 'ConfigObj' package. For detailed documentation please go to:- http://www.voidspace.org.uk/python/configobj.html """ ) @on_trait_change('preferences_items') def _preferences_changed(self, event): """ React to new preferencess being *added*. Note that we don't currently do anything if preferences are *removed*. """ self._load_preferences(event.added) return service_offers = ExtensionPoint( List(ServiceOffer), id = SERVICE_OFFERS, desc = """ Services are simply objects that a plugin wants to make available to other plugins. This extension point allows you to offer services that are created 'on-demand'. e.g. my_service_offer = ServiceOffer( protocol = 'acme.IMyService', factory = an_object_or_a_callable_that_creates_one, properties = {'a dictionary' : 'that is passed to the factory'} ) See the documentation for 'ServiceOffer' for more details. """ ) @on_trait_change('service_offers_items') def _service_offers_changed(self, event): """ React to new service offers being *added*. Note that we don't currently do anything if services are *removed* as we have no facility to let users of the service know that the offer has been retracted. """ for service in event.added: self._register_service_offer(service) return #### Contributions to extension points made by this plugin ################ # None. ########################################################################### # 'IPlugin' interface. ########################################################################### def start(self): """ Start the plugin. """ # Load all contributed preferences files into the application's root # preferences node. self._load_preferences(self.preferences) # Connect all class load hooks. self._connect_class_load_hooks(self.class_load_hooks) # Add class load hooks for all of the contributed categories. The # category will be imported and added when the associated target class # is imported/created. self._add_category_class_load_hooks(self.categories) # Register all service offers. # # These services are unregistered by the default plugin activation # strategy (due to the fact that we store the service ids in this # specific trait!). self._service_ids = self._register_service_offers(self.service_offers) return ########################################################################### # Private interface. ########################################################################### def _add_category_class_load_hooks(self, categories): """ Add class load hooks for a list of categories. """ for category in categories: class_load_hook = self._create_category_class_load_hook(category) class_load_hook.connect() return def _connect_class_load_hooks(self, class_load_hooks): """ Connect all class load hooks. """ for class_load_hook in class_load_hooks: class_load_hook.connect() return def _create_category_class_load_hook(self, category): """ Create a category class load hook. """ # Local imports. from .class_load_hook import ClassLoadHook def import_and_add_category(cls): """ Import a category and add it to a class. This is a closure that binds 'self' and 'category'. """ category_cls = self.application.import_symbol(category.class_name) cls.add_trait_category(category_cls) return category_class_load_hook = ClassLoadHook( class_name = category.target_class_name, on_load = import_and_add_category ) return category_class_load_hook def _load_preferences(self, preferences): """ Load all contributed preferences into a preferences node. """ # Enthought library imports. from envisage.resource.api import ResourceManager # We add the plugin preferences to the default scope. The default scope # is a transient scope which means that (quite nicely ;^) we never # save the actual default plugin preference values. They will only get # saved if a value has been set in another (persistent) scope - which # is exactly what happens in the preferences UI. default = self.application.preferences.node('default/') # The resource manager is used to find the preferences files. resource_manager = ResourceManager() for resource_name in preferences: f = resource_manager.file(resource_name) try: default.load(f) finally: f.close() return def _register_service_offers(self, service_offers): """ Register a list of service offers. """ return list(map(self._register_service_offer, service_offers)) def _register_service_offer(self, service_offer): """ Register a service offer. """ service_id = self.application.register_service( protocol = service_offer.protocol, obj = service_offer.factory, properties = service_offer.properties ) return service_id ### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/egg_basket_plugin_manager.py0000644000076500000240000001341700000000000024133 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A plugin manager that finds plugins in eggs on the 'plugin_path'. """ import logging, pkg_resources, sys import traceback from traits.api import Callable, Directory, List, on_trait_change from .egg_utils import add_eggs_on_path, get_entry_points_in_egg_order from .plugin_manager import PluginManager logger = logging.getLogger(__name__) class EggBasketPluginManager(PluginManager): """ A plugin manager that finds plugins in eggs on the 'plugin_path'. To declare a plugin (or plugins) in your egg use an entry point in your 'setup.py' file, e.g. [envisage.plugins] acme.foo = acme.foo.foo_plugin:FooPlugin acme.foo.fred = acme.foo.fred.fred_plugin:FredPlugin The left hand side of the entry point declaration MUST be the same as the 'id' trait of the plugin (e.g. the 'FooPlugin' would have its 'id' trait set to 'acme.foo'). This allows the plugin manager to filter out plugins using the 'include' and 'exclude' lists (if specified) *without* having to import and instantiate them. """ # Entry point Id. ENVISAGE_PLUGINS_ENTRY_POINT = 'envisage.plugins' #### 'EggBasketPluginManager' protocol ##################################### # If a plugin cannot be loaded for any reason, this callable is called # with the following arguments: entry_point, exception. on_broken_plugin = Callable def _on_broken_plugin_default(self): def handle_broken_plugin(entry_point, exc): raise exc return handle_broken_plugin # If a distribution cannot be loaded for any reason # (mainly VersionConflict), this callable is called with the following # arguments: distribution, exception. on_broken_distribution = Callable # A list of directories that will be searched to find plugins. plugin_path = List(Directory) @on_trait_change('plugin_path[]') def _plugin_path_changed(self, obj, trait_name, removed, added): self._update_sys_dot_path(removed, added) self.reset_traits(['_plugins']) # Protected 'PluginManager' protocol ###################################### def __plugins_default(self): """ Trait initializer. """ plugins = self._harvest_plugins_in_eggs(self.application) logger.debug('egg basket plugin manager found plugins <%s>', plugins) return plugins #### Private protocol ##################################################### def _create_plugin_from_entry_point(self, ep, application): """ Create a plugin from an entry point. """ klass = ep.load() plugin = klass(application=application) # Warn if the entry point is an old-style one where the LHS didn't have # to be the same as the plugin Id. if ep.name != plugin.id: logger.warning( 'entry point name <%s> should be the same as the ' 'plugin id <%s>' % (ep.name, plugin.id) ) return plugin def _get_plugin_entry_points(self, working_set): """ Return all plugin entry points in the working set. """ entry_points = get_entry_points_in_egg_order( working_set, self.ENVISAGE_PLUGINS_ENTRY_POINT ) return entry_points def _harvest_plugins_in_eggs(self, application): """ Harvest plugins found in eggs on the plugin path. """ # We first add the eggs to a local working set so that when we get # the plugin entry points we don't pick up any from other eggs # installed on sys.path. plugin_working_set = pkg_resources.WorkingSet(self.plugin_path) add_eggs_on_path(plugin_working_set, self.plugin_path, self._handle_broken_distributions) # We also add the eggs to the global working set as otherwise the # plugin classes can't be imported! add_eggs_on_path(pkg_resources.working_set, self.plugin_path, self._handle_broken_distributions) plugins = [] for entry_point in self._get_plugin_entry_points(plugin_working_set): if self._include_plugin(entry_point.name): try: plugin = self._create_plugin_from_entry_point(entry_point, application) plugins.append(plugin) except Exception as exc: exc_tb = traceback.format_exc() msg = 'Error loading plugin: %s (from %s)\n%s'\ %(entry_point.name, entry_point.dist.location, exc_tb) logger.error(msg) self.on_broken_plugin(entry_point, exc) return plugins def _handle_broken_distributions(self, errors): logger.error('Error loading distributions: %s', errors) if self.on_broken_distribution is None: raise SystemError('Cannot find eggs %s' % errors) else: for dist, exc in errors.items(): self.on_broken_distribution(dist, exc) def _update_sys_dot_path(self, removed, added): """ Add/remove the given entries from sys.path. """ for dirname in removed: if dirname in sys.path: sys.path.remove(dirname) for dirname in added: if dirname not in sys.path: sys.path.append(dirname) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/egg_plugin_manager.py0000644000076500000240000001073400000000000022601 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A plugin manager that gets its plugins from Eggs. """ # Standard library imports. import logging, pkg_resources, re # Enthought library imports. from traits.api import Instance, List, Str # Local imports. from .egg_utils import get_entry_points_in_egg_order from .plugin_manager import PluginManager # Logging. logger = logging.getLogger(__name__) class EggPluginManager(PluginManager): """ A plugin manager that gets its plugins from Eggs. To declare a plugin (or plugins) in your egg use an entry point in your 'setup.py' file, e.g. [envisage.plugins] acme.foo = acme.foo.foo_plugin:FooPlugin The left hand side of the entry point declaration must be the same as the 'id' trait of the plugin (e.g. the 'FooPlugin' would have its 'id' trait set to 'acme.foo'). This allows the plugin manager to filter out plugins using the 'include' and 'exclude' lists (if specified) *without* having to import and instantiate them. """ # Entry point Id. PLUGINS = 'envisage.plugins' #### 'EggPluginManager' interface ######################################### # The working set that contains the eggs that contain the plugins that # live in the house that Jack built ;^) By default we use the global # working set. working_set = Instance(pkg_resources.WorkingSet, pkg_resources.working_set) # An optional list of the Ids of the plugins that are to be excluded by # the manager. # # Each item in the list is actually a regular expression as used by the # 're' module. exclude = List(Str) # An optional list of the Ids of the plugins that are to be included by # the manager (i.e. *only* plugins with Ids in this list will be added to # the manager). # # Each item in the list is actually a regular expression as used by the # 're' module. include = List(Str) ########################################################################### # Protected 'PluginManager' interface. ########################################################################### def __plugins_default(self): """ Trait initializer. """ plugins = [] for ep in get_entry_points_in_egg_order(self.working_set, self.PLUGINS): if self._is_included(ep.name) and not self._is_excluded(ep.name): plugin = self._create_plugin_from_ep(ep) plugins.append(plugin) logger.debug('egg plugin manager found plugins <%s>', plugins) return plugins ########################################################################### # Private interface. ########################################################################### def _create_plugin_from_ep(self, ep): """ Create a plugin from an extension point. """ klass = ep.load() plugin = klass(application=self.application) # Warn if the entry point is an old-style one where the LHS didn't have # to be the same as the plugin Id. if ep.name != plugin.id: logger.warning( 'entry point name <%s> should be the same as the ' 'plugin id <%s>' % (ep.name, plugin.id) ) return plugin def _is_excluded(self, plugin_id): """ Return True if the plugin Id is excluded. If no 'exclude' patterns are specified then this method returns False for all plugin Ids. """ if len(self.exclude) == 0: return False for pattern in self.exclude: if re.match(pattern, plugin_id) is not None: return True return False def _is_included(self, plugin_id): """ Return True if the plugin Id is included. If no 'include' patterns are specified then this method returns True for all plugin Ids. """ if len(self.include) == 0: return True for pattern in self.include: if re.match(pattern, plugin_id) is not None: return True return False #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/egg_utils.py0000644000076500000240000000674600000000000020761 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Utility functions for working with Python Eggs. """ # Standard library imports. import pkg_resources # Enthought library imports. from traits.util.toposort import topological_sort def add_eggs_on_path(working_set, path, on_error=None): """ Add all eggs found on the path to a working set. """ environment = pkg_resources.Environment(path) # 'find_plugins' identifies those distributions that *could* be added # to the working set without version conflicts or missing requirements. distributions, errors = working_set.find_plugins(environment) if len(errors) > 0: if on_error: on_error(errors) else: raise SystemError('Cannot find eggs %s' % errors) # Add the distributions to the working set (this makes any Python # modules in the eggs available for importing). for distribution in distributions: working_set.add(distribution) return def get_entry_points_in_egg_order(working_set, entry_point_name): """ Return entry points in Egg dependency order. """ # Find all distributions that actually contain contributions to the # entry point. distributions = get_distributions_with_entry_point( working_set, entry_point_name ) # Order them in dependency order (i.e. ordered by their requirements). distributions = get_distributions_in_egg_order(working_set, distributions) entry_points = [] for distribution in distributions: entry_map = distribution.get_entry_map(entry_point_name) entry_points.extend(list(entry_map.values())) return entry_points def get_distributions_with_entry_point(working_set, entry_point_name): """ Return all distributions that contribute to an entry point. """ distributions = [] for distribution in working_set: if len(distribution.get_entry_map(entry_point_name)) > 0: distributions.append(distribution) return distributions def get_distributions_in_egg_order(working_set, distributions=None): """ Return all distributions in Egg dependency order. """ # If no specific list of distributions is specified then use all # distributions in the working set. if distributions is None: distributions = working_set # Build a dependency graph. graph = {} for distribution in distributions: arcs = graph.setdefault(distribution, []) arcs.extend(get_requires(working_set, distribution)) distributions = topological_sort(graph) distributions.reverse() return distributions def get_requires(working_set, distribution): """ Return all of the other distributions that a distribution requires. """ requires = [] for requirement in distribution.requires(): required = working_set.find(requirement) # fixme: For some reason, the resolution of requirements sometimes # results in 'None' being returned instead of a distribution. if required is not None: requires.append(working_set.find(requirement)) return requires #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/extension_point.py0000644000076500000240000002237300000000000022216 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A trait type used to declare and access extension points. """ # Standard library imports. import inspect, weakref # Enthought library imports. from traits.api import List, TraitType, Undefined, provides # Local imports. from .i_extension_point import IExtensionPoint def contributes_to(id): """ A factory for extension point decorators! As an alternative to making contributions via traits, you can use this decorator to mark any method on a 'Plugin' as contributing to an extension point (note this is *only* used on 'Plugin' instances!). e.g. Using a trait you might have something like:: class MyPlugin(Plugin): messages = List(contributes_to='acme.messages') def _messages_default(self): return ['Hello', 'Hola'] whereas, using the decorator, it would be:: class MyPlugin(Plugin): @contributes_to('acme.messages') def _get_messages(self): return ['Hello', 'Hola'] There is not much in it really, but the decorator version looks a little less like 'magic' since it doesn't require the developer to know about Traits default initializers. However, if you know that you will want to dynamically change your contributions then use the trait version because all you have to do is change the value of the trait and the framework will react accordingly. """ def decorator(fn): """ A decorator for marking methods as extension contributors. """ fn.__extension_point__ = id return fn return decorator # Exception message template. INVALID_TRAIT_TYPE = 'extension points must be "List"s e.g. List, List(Int)' \ ' but a value of %s was specified.' # Even though trait types do not themselves have traits, we can still # declare that we implement an interface. @provides(IExtensionPoint) class ExtensionPoint(TraitType): """ A trait type used to declare and access extension points. Note that this is a trait *type* and hence does *NOT* have traits itself (i.e. it does *not* inherit from 'HasTraits'). """ ########################################################################### # 'ExtensionPoint' *CLASS* interface. ########################################################################### @staticmethod def bind(obj, trait_name, extension_point_id): """ Create a binding to an extension point. """ from .extension_point_binding import bind_extension_point return bind_extension_point(obj, trait_name, extension_point_id) @staticmethod def connect_extension_point_traits(obj): """ Connect all of the 'ExtensionPoint' traits on an object. """ for trait_name, trait in obj.traits(__extension_point__=True).items(): trait.trait_type.connect(obj, trait_name) return @staticmethod def disconnect_extension_point_traits(obj): """ Disconnect all of the 'ExtensionPoint' traits on an object. """ for trait_name, trait in obj.traits(__extension_point__=True).items(): trait.trait_type.disconnect(obj, trait_name) return ########################################################################### # 'object' interface. ########################################################################### def __init__(self, trait_type=List, id=None, **metadata): """ Constructor. """ # We add '__extension_point__' to the metadata to make the extension # point traits easier to find with the 'traits' and 'trait_names' # methods on 'HasTraits'. metadata['__extension_point__'] = True super(ExtensionPoint, self).__init__(**metadata) # The trait type that describes the extension point. # # If we are handed a trait type *class* e.g. List, instead of a trait # type *instance* e.g. List() or List(Int) etc, then we instantiate it. if inspect.isclass(trait_type): trait_type = trait_type() # Currently, we only support list extension points (we may in the # future want to allow other collections e.g. dictionaries etc). if not isinstance(trait_type, List): raise TypeError(INVALID_TRAIT_TYPE % trait_type) self.trait_type = trait_type # The Id of the extension point. if id is None: raise ValueError('an extension point must have an Id') self.id = id # A dictionary that is used solely to keep a reference to all extension # point listeners alive until their associated objects are garbage # collected. # # Dict(weakref.ref(Any), Dict(Str, Callable)) self._obj_to_listeners_map = weakref.WeakKeyDictionary() return def __repr__(self): """ String representation of an ExtensionPoint object """ return "ExtensionPoint(id={})".format(self.id) ########################################################################### # 'TraitType' interface. ########################################################################### def get(self, obj, trait_name): """ Trait type getter. """ extension_registry = self._get_extension_registry(obj) # Get the extensions to this extension point. extensions = extension_registry.get_extensions(self.id) # Make sure the contributions are of the appropriate type. return self.trait_type.validate(obj, trait_name, extensions) def set(self, obj, name, value): """ Trait type setter. """ extension_registry = self._get_extension_registry(obj) # Note that some extension registry implementations may not support the # setting of extension points (the default, plugin extension registry # for exxample ;^). extension_registry.set_extensions(self.id, value) return ########################################################################### # 'ExtensionPoint' interface. ########################################################################### def connect(self, obj, trait_name): """ Connect the extension point to a trait on an object. This allows the object to react when contributions are added or removed from the extension point. fixme: It would be nice to be able to make the connection automatically but we would need a slight tweak to traits to allow the trait type to be notified when a new instance that uses the trait type is created. """ def listener(extension_registry, event): """ Listener called when an extension point is changed. """ # If an index was specified then we fire an '_items' changed event. if event.index is not None: name = trait_name + '_items' old = Undefined new = event # Otherwise, we fire a normal trait changed event. else: name = trait_name old = event.removed new = event.added obj.trait_property_changed(name, old, new) return extension_registry = self._get_extension_registry(obj) # Add the listener to the extension registry. extension_registry.add_extension_point_listener(listener, self.id) # Save a reference to the listener so that it does not get garbage # collected until its associated object does. listeners = self._obj_to_listeners_map.setdefault(obj, {}) listeners[trait_name] = listener return def disconnect(self, obj, trait_name): """ Disconnect the extension point from a trait on an object. """ extension_registry = self._get_extension_registry(obj) listener = self._obj_to_listeners_map[obj].get(trait_name) if listener is not None: # Remove the listener from the extension registry. extension_registry.remove_extension_point_listener( listener, self.id ) # Clean up. del self._obj_to_listeners_map[obj][trait_name] return ########################################################################### # Private interface. ########################################################################### def _get_extension_registry(self, obj): """ Return the extension registry in effect for an object. """ extension_registry = getattr(obj, 'extension_registry', None) if extension_registry is None: raise ValueError( 'The "ExtensionPoint" trait type can only be used in ' 'objects that have a reference to an extension registry ' 'via their "extension_registry" trait. ' 'Extension point Id <%s>' % self.id ) return extension_registry #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/extension_point_binding.py0000644000076500000240000001545100000000000023707 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A binding between a trait on an object and an extension point. """ # Standard library imports. import weakref # Enthought library imports. from traits.api import Any, HasTraits, Instance, Str, Undefined # Local imports. from .i_extension_registry import IExtensionRegistry class ExtensionPointBinding(HasTraits): """ A binding between a trait on an object and an extension point. """ #### 'ExtensionPointBinding' *CLASS* interface ############################ # We keep a reference to each binding alive until its associated object # is garbage collected. _bindings = weakref.WeakKeyDictionary() #### 'ExtensionPointBinding' interface #################################### # The object that we are binding the extension point to. obj = Any # The Id of the extension point. extension_point_id = Str # The extension registry used by the binding. If this trait is not set then # the class-scope extension registry set on the 'ExtensionPoint' class is # used (and if that is not set then the binding won't work ;^) extension_registry = Instance(IExtensionRegistry) # The name of the trait that we are binding the extension point to. trait_name = Str #### Private interface #################################################### # A flag that prevents us from setting a trait twice. _event_handled = False ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Constructor. """ super(ExtensionPointBinding, self).__init__(**traits) # Initialize the object's trait from the extension point. self._set_trait(notify=False) # Wire-up the trait change and extension point handlers. self._initialize() # We keep a reference to each binding alive until its associated # object is garbage collected. bindings = ExtensionPointBinding._bindings.setdefault(self.obj, []) bindings.append(self) return ########################################################################### # 'ExtensionPointBinding' interface. ########################################################################### #### Trait initializers ################################################### def _extension_registry_default(self): """ Trait initializer. """ # fixme: Sneaky global!!!!! from .extension_point import ExtensionPoint return ExtensionPoint.extension_registry ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ def _on_trait_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ if not self._event_handled: self._set_extensions(new) return def _on_trait_items_changed(self, obj, trait_name, old, event): """ Dynamic trait change handler. """ if not self._event_handled: self._set_extensions(getattr(obj, self.trait_name)) return #### Other observer pattern listeners ##################################### def _extension_point_listener(self, extension_registry, event): """ Listener called when an extension point is changed. """ self._event_handled = True if event.index is not None: self._update_trait(event) else: self._set_trait(notify=True) self._event_handled = False return #### Methods ############################################################## 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 ) self.obj.on_trait_change( self._on_trait_items_changed, self.trait_name + '_items' ) # Listen for the extension point being changed. self.extension_registry.add_extension_point_listener( self._extension_point_listener, self.extension_point_id ) return def _set_trait(self, notify): """ Set the object's trait to the value of the extension point. """ value = self.extension_registry.get_extensions(self.extension_point_id) traits = {self.trait_name : value} self.obj.trait_set(trait_change_notify=notify, **traits) return def _update_trait(self, event): """ Update the object's trait to the value of the extension point. """ self._set_trait(notify=False) self.obj.trait_property_changed( self.trait_name + '_items', Undefined, event ) return def _set_extensions(self, extensions): """ Set the extensions to an extension point. """ self.extension_registry.set_extensions( self.extension_point_id, extensions ) return # Factory function for creating bindings. def bind_extension_point( obj, trait_name, extension_point_id, extension_registry=None ): """ Create a binding to an extension point. """ # 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 'ExtensionPointBinding' # instance is created. # # This is because we only want to set the 'extension_registry' 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 appropriate registry. # Also, if we try to set the 'extension_registry' 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, 'extension_point_id' : extension_point_id } if extension_registry is not None: traits['extension_registry'] = extension_registry return ExtensionPointBinding(**traits) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/extension_point_changed_event.py0000644000076500000240000000211100000000000025054 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An event fired when an extension point's extensions have changed. """ # Enthought library imports. from traits.api import TraitListEvent class ExtensionPointChangedEvent(TraitListEvent): """ An event fired when an extension point's extensions have changed. """ def __init__ (self, extension_point_id=None, **kw): """ Constructor. """ # The base class has the 'index', 'removed' and 'added' attributes. super(ExtensionPointChangedEvent, self).__init__(**kw) # We add the extension point Id. self.extension_point_id = extension_point_id return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/extension_provider.py0000644000076500000240000000374000000000000022714 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The default base class for extension providers. """ # Enthought library imports. from traits.api import Event, HasTraits, provides # Local imports. from .extension_point_changed_event import ExtensionPointChangedEvent from .i_extension_provider import IExtensionProvider @provides(IExtensionProvider) class ExtensionProvider(HasTraits): """ The default base class for extension providers. """ #### 'IExtensionProvider' interface ####################################### # The event fired when one of the provider's extension points has been # changed (where 'changed' means that the provider has added or removed # contributions to or from an extension point). extension_point_changed = Event(ExtensionPointChangedEvent) def get_extension_points(self): """ Return the extension points offered by the provider. """ return [] def get_extensions(self, extension_point_id): """ Return the provider's extensions to an extension point. """ return [] ##### Protected 'ExtensionProvider' interface ############################# def _fire_extension_point_changed( self, extension_point_id, added, removed, index ): """ Fire an extension point changed event. """ self.extension_point_changed = ExtensionPointChangedEvent( extension_point_id = extension_point_id, added = added, removed = removed, index = index ) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/extension_registry.py0000644000076500000240000001426300000000000022734 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A base class for extension registry implementation. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Dict, HasTraits, provides # Local imports. from .extension_point_changed_event import ExtensionPointChangedEvent from .i_extension_registry import IExtensionRegistry from . import safeweakref from .unknown_extension_point import UnknownExtensionPoint # Logging. logger = logging.getLogger(__name__) @provides(IExtensionRegistry) class ExtensionRegistry(HasTraits): """ A base class for extension registry implementation. """ ########################################################################### # Protected 'ExtensionRegistry' interface. ########################################################################### # A dictionary of extensions, keyed by extension point. _extensions = Dict # The extension points that have been added *explicitly*. _extension_points = Dict # Extension listeners. # # These are called when extensions are added to or removed from an # extension point. # # e.g. Dict(extension_point, [weakref.ref(callable)]) # # A listener is any Python callable with the following signature:- # # def listener(extension_registry, extension_point_changed_event): # ... _listeners = Dict ########################################################################### # 'IExtensionRegistry' interface. ########################################################################### def add_extension_point_listener(self, listener, extension_point_id=None): """ Add a listener for extensions being added or removed. """ listeners = self._listeners.setdefault(extension_point_id, []) listeners.append(safeweakref.ref(listener)) return def add_extension_point(self, extension_point): """ Add an extension point. """ self._extension_points[extension_point.id] = extension_point logger.debug('extension point <%s> added', extension_point.id) return def get_extensions(self, extension_point_id): """ Return the extensions contributed to an extension point. """ return self._get_extensions(extension_point_id)[:] def get_extension_point(self, extension_point_id): """ Return the extension point with the specified Id. """ return self._extension_points.get(extension_point_id) def get_extension_points(self): """ Return all extension points. """ return list(self._extension_points.values()) def remove_extension_point_listener(self,listener,extension_point_id=None): """ Remove a listener for extensions being added or removed. """ listeners = self._listeners.setdefault(extension_point_id, []) listeners.remove(safeweakref.ref(listener)) return def remove_extension_point(self, extension_point_id): """ Remove an extension point. """ self._check_extension_point(extension_point_id) # Remove the extension point. del self._extension_points[extension_point_id] # Remove any extensions to the extension point. if extension_point_id in self._extensions: old = self._extensions[extension_point_id] del self._extensions[extension_point_id] else: old = [] refs = self._get_listener_refs(extension_point_id) self._call_listeners(refs, extension_point_id, [], old, 0) logger.debug('extension point <%s> removed', extension_point_id) return def set_extensions(self, extension_point_id, extensions): """ Set the extensions contributed to an extension point. """ self._check_extension_point(extension_point_id) old = self._get_extensions(extension_point_id) self._extensions[extension_point_id] = extensions refs = self._get_listener_refs(extension_point_id) self._call_listeners(refs, extension_point_id, extensions, old, None) return ########################################################################### # Protected 'ExtensionRegistry' interface. ########################################################################### def _call_listeners(self, refs, extension_point_id, added, removed, index): """ Call listeners that are listening to an extension point. """ event = ExtensionPointChangedEvent( extension_point_id = extension_point_id, added = added, removed = removed, index = index ) for ref in refs: listener = ref() if listener is not None: listener(self, event) return def _check_extension_point(self, extension_point_id): """ Check to see if the extension point exists. Raise an 'UnknownExtensionPoint' if it does not. """ if not extension_point_id in self._extension_points: raise UnknownExtensionPoint(extension_point_id) return def _get_extensions(self, extension_point_id): """ Return the extensions for the given extension point. """ return self._extensions.setdefault(extension_point_id, []) def _get_listener_refs(self, extension_point_id): """ Get weak references to all listeners to an extension point. Returns a list containing the weak references to those listeners that are listening to this extension point specifically first, followed by those that are listening to any extension point. """ refs = [] refs.extend(self._listeners.get(extension_point_id, [])) refs.extend(self._listeners.get(None, [])) return refs #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_application.py0000644000076500000240000000436700000000000021607 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The application interface. """ # Enthought library imports. from apptools.preferences.api import IPreferences from traits.api import Event, Instance, Str, VetoableEvent # Local imports. from .i_extension_registry import IExtensionRegistry from .i_import_manager import IImportManager from .i_plugin_manager import IPluginManager from .i_service_registry import IServiceRegistry from .application_event import ApplicationEvent class IApplication( IExtensionRegistry, IImportManager, IPluginManager, IServiceRegistry ): """ The application interface. """ # The application's globally unique identifier. id = Str # The name of a directory (created for you) to which the application can # read and write non-user accessible data, i.e. configuration information, # preferences, etc. home = Str # The name of a directory (created for you upon access) to which the # application can read and write user-accessible data, e.g. projects created # by the user. user_data = Str # The root preferences node. preferences = Instance(IPreferences) #### Events #### # Fired when the application is starting. This is the first thing that # happens when the 'start' method is called. starting = VetoableEvent(ApplicationEvent) # Fired when all plugins have been started. started = Event(ApplicationEvent) # Fired when the plugin manager is stopping. This is the first thing that # happens when the 'stop' method is called. stopping = VetoableEvent(ApplicationEvent) # Fired when all plugins have been stopped. stopped = Event(ApplicationEvent) def run(self): """ Run the application. The same as:: if application.start(): application.stop() """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_extension_point.py0000644000076500000240000000273500000000000022526 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The interface for extension points. """ # Enthought library imports. from traits.api import Instance, Interface, Str, TraitType class IExtensionPoint(Interface): """ The interface for extension points. """ # A description of what the extension point is and does! (it is called # the slightly dubious, 'desc', instead of 'description', or, to be more # 'Pythonic', maybe 'doc' to match the 'desc' metadata used in traits). desc = Str # The extension point's unique identifier. # # Where 'unique' technically means 'unique within the extension registry', # but since the chances are that you will want to include extension points # from external sources, this really means 'globally unique'! Using the # Python package path might be useful here ;^) # # e.g. 'envisage.ui.workbench.views' id = Str # A trait type that describes what can be contributed to the extension # point. # # e.g. List(Str) trait_type = Instance(TraitType) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_extension_point_user.py0000644000076500000240000000167400000000000023565 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The interface for objects using the 'ExtensionPoint' trait type. """ # Enthought library imports. from traits.api import Instance, Interface # Local imports. from .i_extension_registry import IExtensionRegistry class IExtensionPointUser(Interface): """ The interface for objects using the 'ExtensionPoint' trait type. """ # The extension registry that the object's extension points are stored in. extension_registry = Instance(IExtensionRegistry) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_extension_provider.py0000644000076500000240000000260500000000000023223 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The interface for extension providers. """ # Enthought library imports. from traits.api import Event, Interface # Local imports. from .extension_point_changed_event import ExtensionPointChangedEvent class IExtensionProvider(Interface): """ The interface for extension providers. """ # The event fired when one of the provider's extension points has changed. extension_point_changed = Event(ExtensionPointChangedEvent) def get_extension_points(self): """ Return the extension points offered by the provider. Return an empty list if the provider does not offer any extension points. """ def get_extensions(self, extension_point_id): """ Return the provider's extensions to an extension point. The return value *must* be a list. Return an empty list if the provider does not contribute any extensions to the extension point. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_extension_registry.py0000644000076500000240000000557200000000000023247 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The interface for extension registries. """ # Enthought library imports. from traits.api import Interface class IExtensionRegistry(Interface): """ The interface for extension registries. """ def add_extension_point_listener(self, listener, extension_point_id=None): """ Add a listener for extensions being added or removed. A listener is any Python callable with the following signature:: def listener(extension_registry, extension_point_changed_event): ... If an extension point is specified then the listener will only be called when extensions are added to or removed from that extension point (the extension point may or may not have been added to the registry at the time of this call). If *no* extension point is specified then the listener will be called when extensions are added to or removed from *any* extension point. When extensions are added or removed all specific listeners are called first (in arbitrary order), followed by all non-specific listeners (again, in arbitrary order). """ def add_extension_point(self, extension_point): """ Add an extension point. If an extension point already exists with this Id then it is simply replaced. """ def get_extensions(self, extension_point_id): """ Return the extensions contributed to an extension point. Return an empty list if the extension point does not exist. """ def get_extension_point(self, extension_point_id): """ Return the extension point with the specified Id. Return None if no such extension point exists. """ def get_extension_points(self): """ Return all extension points that have been added to the registry. """ def remove_extension_point_listener(self,listener,extension_point_id=None): """ Remove a listener for extensions being added or removed. Raise a 'ValueError' if the listener does not exist. """ def remove_extension_point(self, extension_point_id): """ Remove an extension point. Raise an 'UnknownExtensionPoint' exception if no extension point exists with the specified Id. """ def set_extensions(self, extension_point_id, extensions): """ Set the extensions contributed to an extension point. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_import_manager.py0000644000076500000240000000327000000000000022300 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The interface for import managers. """ # Enthought library imports. from traits.api import Interface class IImportManager(Interface): """ The interface for import managers. """ def import_symbol(self, symbol_path): """ Import the symbol defined by the specified symbol path. 'symbol_path' is a string containing the path to a symbol through the Python package namespace. It can be in one of two forms: 1) 'foo.bar.baz' Which is turned into the equivalent of an import statement that looks like:: from foo.bar import baz With the value of 'baz' being returned. 2) 'foo.bar:baz' (i.e. a ':' separating the module from the symbol) Which is turned into the equivalent of:: from foo import bar eval('baz', bar.__dict__) With the result of the 'eval' being returned. The second form is recommended as it allows for nested symbols to be retreived, e.g. the symbol path 'foo.bar:baz.bling' becomes:: from foo import bar eval('baz.bling', bar.__dict__) The first form is retained for backwards compatability. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_plugin.py0000644000076500000240000000364600000000000020601 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The plugin interface. """ # Enthought library imports. from traits.api import Instance, Interface, Str # Local imports. from .i_plugin_activator import IPluginActivator class IPlugin(Interface): """ The plugin interface. """ # The activator used to start and stop the plugin. activator = Instance(IPluginActivator) # The application that the plugin is part of. application = Instance('envisage.api.IApplication') # The name of a directory (created for you) that the plugin can read and # write to at will. home = Str # The plugin's unique identifier. # # Where 'unique' technically means 'unique within the plugin manager', but # since the chances are that you will want to include plugins from external # sources, this really means 'globally unique'! Using the Python package # path might be useful here. e.g. 'envisage'. id = Str # The plugin's name (suitable for displaying to the user). name = Str def start(self): """ Start the plugin. This method is called by the framework when the application is starting up. If you want to start a plugin manually use:: application.start_plugin(plugin) """ def stop(self): """ Stop the plugin. This method is called by the framework when the application is stopping. If you want to stop a plugin manually use:: application.stop_plugin(plugin) """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_plugin_activator.py0000644000076500000240000000254700000000000022654 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The plugin activator interface. """ # Enthought library imports. from traits.api import Interface class IPluginActivator(Interface): """ The plugin activator interface. A plugin activator is really just a collection of two strategies - one to start the plugin and one to stop it. We use an activator so that the framework can implement default start and stop strategies without forcing the plugin writer to call 'super' if they override the 'start' and 'stop' methods on 'IPlugin'. I'm not sure that having to call 'super' is such a burden, but some people seem to like it this way, and it does mean one less thing for a plugin writer to have to remember to do! """ def start_plugin(self, plugin): """ Start the specified plugin. """ def stop_plugin(self, plugin): """ Stop the specified plugin. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_plugin_manager.py0000644000076500000240000000452000000000000022263 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The plugin manager interface. """ # Enthought library imports. from traits.api import Event, Interface # Local imports. from .plugin_event import PluginEvent class IPluginManager(Interface): """ The plugin manager interface. """ #### Events #### # Fired when a plugin has been added to the manager. plugin_added = Event(PluginEvent) # Fired when a plugin has been removed from the manager. plugin_removed = Event(PluginEvent) def __iter__(self): """ Return an iterator over the manager's plugins. """ def add_plugin(self, plugin): """ Add a plugin to the manager. """ def get_plugin(self, plugin_id): """ Return the plugin with the specified Id. Return None if no such plugin exists. """ def remove_plugin(self, plugin): """ Remove a plugin from the manager. """ def start(self): """ Start the plugin manager. This starts all of the manager's plugins. """ def start_plugin(self, plugin=None, plugin_id=None): """ Start the specified plugin. If a plugin is specified then start it. If no plugin is specified then the Id is used to look up the plugin and then start it. If no such plugin exists then a 'SystemError' exception is raised. """ def stop(self): """ Stop the plugin manager. This stop's all of the plugin manager's plugins (in the reverse order that they were started). """ def stop_plugin(self, plugin=None, plugin_id=None): """ Stop the specified plugin. If a plugin is specified then stop it (the Id is ignored). If no plugin is specified then the Id is used to look up the plugin and then stop it. If no such plugin exists then a 'SystemError' exception is raised. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_provider_extension_registry.py0000644000076500000240000000207000000000000025147 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The provider extension registry interface. """ # Local imports. from .i_extension_registry import IExtensionRegistry class IProviderExtensionRegistry(IExtensionRegistry): """ The provider extension registry interface. """ def add_provider(self, provider): """ Add an extension provider. """ def get_providers(self): """ Return all of the providers in the registry. """ def remove_provider(self, provider): """ Remove an extension provider. Raise a 'ValueError' if the provider is not in the registry. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_service_registry.py0000644000076500000240000001125500000000000022666 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The service registry interface. """ # Enthought library imports. from traits.api import Event, Interface class IServiceRegistry(Interface): """ The service registry interface. The service registry provides a 'Yellow Pages' style mechanism, in that services are published and looked up by protocol (meaning, *interface*, *type*, or *class* (for old-style classes!). It is called a 'Yellow Pages' mechanism because it is just like looking up a telephone number in the 'Yellow Pages' phone book. You use the 'Yellow Pages' instead of the 'White Pages' when you don't know the *name* of the person you want to call but you do know what *kind* of service you require. For example, if you have a leaking pipe, you know you need a plumber, so you pick up your 'Yellow Pages', go to the 'Plumbers' section and choose one that seems to fit the bill based on price, location, certification, etc. The service registry does exactly the same thing as the 'Yellow Pages', only with objects, and it even allows you to publish your own entries for free (unlike the *real* one)! """ # An event that is fired when a service is registered. registered = Event # An event that is fired when a service is unregistered. unregistered = Event def get_service(self, protocol, query='', minimize='', maximize=''): """ Return at most one service that matches the specified query. The protocol can be an actual class or interface, or the *name* of a class or interface in the form '.'. Return None if no such service is found. If no query is specified then a service that provides the specified protocol is returned (if one exists). NOTE: If more than one service exists that match the criteria then Don't try to guess *which* one it will return - it is random! """ def get_service_from_id(self, service_id): """ Return the service with the specified id. If no such service exists a 'ValueError' exception is raised. """ def get_services(self, protocol, query='', minimize='', maximize=''): """ Return all services that match the specified query. The protocol can be an actual class or interface, or the *name* of a class or interface in the form '.'. If no services match the query, then an empty list is returned. If no query is specified then all services that provide the specified protocol are returned (if any exist). """ def get_service_properties(self, service_id): """ Return the dictionary of properties associated with a service. If no such service exists a 'ValueError' exception is raised. The properties returned are 'live' i.e. changing them immediately changes the service registration. """ def register_service(self, protocol, obj, properties=None): """ Register a service. The protocol can be an actual class or interface, or the *name* of a class or interface in the form:: 'foo.bar.baz' Which is turned into the equivalent of an import statement that looks like:: from foo.bar import baz Return a service Id that can be used to unregister the service and to get/set any service properties. If 'obj' does not implement the specified protocol then it is treated as a 'service factory' that will be called the first time a service of the appropriate type is requested. A 'service factory' is simply a callable that takes the properties specified here as keyword arguments and returns an object. For *really* lazy loading, the factory can also be specified as a string which is used to import the callable. """ def set_service_properties(self, service_id, properties): """ Set the dictionary of properties associated with a service. If no such service exists a 'ValueError' exception is raised. """ def unregister_service(self, service_id): """ Unregister a service. If no such service exists a 'ValueError' exception is raised. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/i_service_user.py0000644000076500000240000000162500000000000021774 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The interface for objects using the 'Service' trait type. """ # Enthought library imports. from traits.api import Instance, Interface # Local imports. from .i_service_registry import IServiceRegistry class IServiceUser(Interface): """ The interface for objects using the 'Service' trait type. """ # The service registry that the object's services are stored in. service_registry = Instance(IServiceRegistry) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/import_manager.py0000644000076500000240000000503700000000000021773 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The default import manager implementation. """ # Enthought library imports. from traits.api import HasTraits, provides # Local imports. from .i_import_manager import IImportManager @provides(IImportManager) class ImportManager(HasTraits): """ The default import manager implementation. Its just a guess, but I think using an import manager to do all imports will make debugging easier (as opposed to just letting imports happen from all over the place). """ ########################################################################### # 'IImportManager' interface. ########################################################################### def import_symbol(self, symbol_path): """ Import the symbol defined by the specified symbol path. """ if ':' in symbol_path: module_name, symbol_name = symbol_path.split(':') module = self._import_module(module_name) symbol = eval(symbol_name, module.__dict__) else: 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) # Event notification. self.symbol_imported = symbol return symbol ########################################################################### # Private interface. ########################################################################### def _import_module(self, module_name): """ Import the module with the specified (and possibly dotted) name. Returns the imported module. This method is copied from the documentation of the '__import__' function in the Python Library Reference Manual. """ module = __import__(module_name) components = module_name.split('.') for component in components[1:]: module = getattr(module, component) return module #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/package_plugin_manager.py0000644000076500000240000001222100000000000023423 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A plugin manager that finds plugins in packages on the 'plugin_path'. """ import logging, sys from apptools.io import File from traits.api import Directory, List, on_trait_change from .plugin_manager import PluginManager logger = logging.getLogger(__name__) class PackagePluginManager(PluginManager): """ A plugin manager that finds plugins in packages on the 'plugin_path'. All items in 'plugin_path' are directory names and they are all added to 'sys.path' (if not already present). Each directory is then searched for plugins as follows:- a) If the package contains a 'plugins.py' module, then we import it and look for a callable 'get_plugins' that takes no arguments and returns a list of plugins (i.e. instances that implement 'IPlugin'!). b) If the package contains any modules named in the form 'xxx_plugin.py' then the module is imported and if it contains a callable 'XXXPlugin' it is called with no arguments and it must return a single plugin. """ # Plugin manifest. PLUGIN_MANIFEST = 'plugins.py' #### 'PackagePluginManager' protocol ####################################### # A list of directories that will be searched to find plugins. plugin_path = List(Directory) @on_trait_change('plugin_path[]') def _plugin_path_changed(self, obj, trait_name, removed, added): self._update_sys_dot_path(removed, added) self.reset_traits(['_plugins']) #### Protected 'PluginManager' protocol ################################### def __plugins_default(self): """ Trait initializer. """ plugins = [ plugin for plugin in self._harvest_plugins_in_packages() if self._include_plugin(plugin.id) ] logger.debug('package plugin manager found plugins <%s>', plugins) return plugins #### Private protocol ##################################################### def _get_plugins_module(self, package_name): """ Import 'plugins.py' from the package with the given name. If the package does not exist, or does not contain 'plugins.py' then return None. """ try: module = __import__(package_name + '.plugins', fromlist=['plugins']) except ImportError: module = None return module # smell: Looooong and ugly! def _harvest_plugins_in_package(self, package_name, package_dirname): """ Harvest plugins found in the given package. """ # If the package contains a 'plugins.py' module, then we import it and # look for a callable 'get_plugins' that takes no arguments and returns # a list of plugins (i.e. instances that implement 'IPlugin'!). plugins_module = self._get_plugins_module(package_name) if plugins_module is not None: factory = getattr(plugins_module, 'get_plugins', None) if factory is not None: plugins = factory() # Otherwise, look for any modules in the form 'xxx_plugin.py' and # see if they contain a callable in the form 'XXXPlugin' and if they # do, call it with no arguments to get a plugin! else: plugins = [] logger.debug('Looking for plugins in %s' % package_dirname) for child in File(package_dirname).children or []: if child.ext == '.py' and child.name.endswith('_plugin'): module = __import__( package_name + '.' + child.name, fromlist=[child.name] ) atoms = child.name.split('_') capitalized = [atom.capitalize() for atom in atoms] factory_name = ''.join(capitalized) factory = getattr(module, factory_name, None) if factory is not None: plugins.append(factory()) return plugins def _harvest_plugins_in_packages(self): """ Harvest plugins found in packages on the plugin path. """ plugins = [] for dirname in self.plugin_path: for child in File(dirname).children or []: if child.is_package: plugins.extend( self._harvest_plugins_in_package( child.name, child.path ) ) return plugins def _update_sys_dot_path(self, removed, added): """ Add/remove the given entries from sys.path. """ for dirname in removed: if dirname in sys.path: sys.path.remove(dirname) for dirname in added: if dirname not in sys.path: sys.path.append(dirname) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugin.py0000644000076500000240000003542500000000000020271 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The default implementation of the 'IPlugin' interface. """ # Standard library imports. import inspect, logging, os from os.path import exists, join # Enthought library imports. from traits.api import Instance, List, Property, Str, provides from traits.util.camel_case import camel_case_to_words # Local imports. from .extension_point import ExtensionPoint from .extension_provider import ExtensionProvider from .i_application import IApplication from .i_extension_point_user import IExtensionPointUser from .i_extension_registry import IExtensionRegistry from .i_plugin import IPlugin from .i_plugin_activator import IPluginActivator from .i_service_registry import IServiceRegistry from .i_service_user import IServiceUser from .plugin_activator import PluginActivator # Logging. logger = logging.getLogger(__name__) @provides(IPlugin, IExtensionPointUser, IServiceUser) class Plugin(ExtensionProvider): """ The default implementation of the 'IPlugin' interface. This class is intended to be subclassed for each plugin that you create. """ #### 'IPlugin' interface ################################################## # The activator used to start and stop the plugin. # # By default the *same* activator instance is used for *all* plugins of # this type. activator = Instance(IPluginActivator, PluginActivator()) # The application that the plugin is part of. application = Instance(IApplication) # The name of a directory (created for you) that the plugin can read and # write to at will. home = Str # The plugin's unique identifier. # # If no identifier is specified then the module and class name of the # plugin are used to create an Id with the form 'module_name.class_name'. id = Str # The plugin's name (suitable for displaying to the user). # # If no name is specified then the plugin's class name is used with an # attempt made to turn camel-case class names into words separated by # spaces (e.g. if the class name is 'MyPlugin' then the name would be # 'My Plugin'). Of course, if you really care about the actual name, then # just set it! name = Str #### 'IExtensionPointUser' interface ###################################### # The extension registry that the object's extension points are stored in. extension_registry = Property(Instance(IExtensionRegistry)) #### 'IServiceUser' interface ############################################# # The service registry that the object's services are stored in. service_registry = Property(Instance(IServiceRegistry)) #### Private interface #################################################### # The Ids of the services that were automatically registered. _service_ids = List ########################################################################### # 'IExtensionPointUser' interface. ########################################################################### def _get_extension_registry(self): """ Trait property getter. """ return self.application ########################################################################### # 'IServiceUser' interface. ########################################################################### def _get_service_registry(self): """ Trait property getter. """ return self.application ########################################################################### # 'IExtensionProvider' interface. ########################################################################### def get_extension_points(self): """ Return the extension points offered by the provider. """ extension_points = [ trait.trait_type for trait in self.traits(__extension_point__=True).values() ] return extension_points def get_extensions(self, extension_point_id): """ Return the provider's extensions to an extension point. """ # Each class can have at most *one* trait that contributes to a # particular extension point. # # fixme: We make this restriction in case that in future we can wire up # the list traits directly. If we don't end up doing that then it is # fine to allow mutiple traits! trait_names = self.trait_names(contributes_to=extension_point_id) # FIXME: This is a temporary fix, which was necessary due to the # namespace refactor, but should be removed at some point. if len(trait_names) == 0: old_id = 'enthought.' + extension_point_id trait_names = self.trait_names(contributes_to=old_id) # if trait_names: # print 'deprecated:', old_id if len(trait_names) == 0: # If there is no contributing trait then look for any decorated # methods. extensions = self._harvest_methods(extension_point_id) # FIXME: This is a temporary fix, which was necessary due to the # namespace refactor, but should be removed at some point. if not extensions: old_id = 'enthought.' + extension_point_id extensions = self._harvest_methods(old_id) # if extensions: # print 'deprecated:', old_id elif len(trait_names) == 1: extensions = self._get_extensions_from_trait(trait_names[0]) else: raise self._create_multiple_traits_exception(extension_point_id) return extensions ########################################################################### # 'IPlugin' interface. ########################################################################### #### Trait initializers ################################################### def _home_default(self): """ Trait initializer. """ # Each plugin gets a sub-directory of a 'plugins' directory in # 'application.home'. # # i.e. .../my.application.id/plugins/ plugins_dir = join(self.application.home, 'plugins') if not exists(plugins_dir): os.mkdir(plugins_dir) # Now create the 'home' directory of this plugin. home_dir = join(plugins_dir, self.id) if not exists(home_dir): os.mkdir(home_dir) return home_dir def _id_default(self): """ Trait initializer. """ id = '%s.%s' % (type(self).__module__, type(self).__name__) logger.warning('plugin %s has no Id - using <%s>' % (self, id)) return id def _name_default(self): """ Trait initializer. """ name = camel_case_to_words(type(self).__name__) logger.warning('plugin %s has no name - using <%s>' % (self, name)) return name #### Methods ############################################################## def start(self): """ Start the plugin. This method will *always* be empty so that you never have to call 'super(xxx, self).start()' if you provide an implementation in a derived class. The framework does what it needs to do when it starts a plugin by means of the plugin's activator. """ pass def stop(self): """ Stop the plugin. This method will *always* be empty so that you never have to call 'super(xxx, self).stop()' if you provide an implementation in a derived class. The framework does what it needs to do when it stops a plugin by means of the plugin's activator. """ pass ########################################################################### # 'Plugin' interface. ########################################################################### def connect_extension_point_traits(self): """ Connect all of the plugin's extension points. This means that the plugin will be notified if and when contributions are add or removed. """ ExtensionPoint.connect_extension_point_traits(self) return def disconnect_extension_point_traits(self): """ Disconnect all of the plugin's extension points.""" ExtensionPoint.disconnect_extension_point_traits(self) return def register_services(self): """ Register the services offered by the plugin. """ for trait_name, trait in self.traits(service=True).items(): logger.warning( 'DEPRECATED: Do not use the "service=True" metadata anymore. ' 'Services should now be offered using the service ' 'offer extension point (envisage.service_offers) ' 'from the core plugin. ' 'Plugin %s trait <%s>' % (self, trait_name) ) # Register a service factory for the trait. service_id = self._register_service_factory(trait_name, trait) # We save the service Id so that so that we can unregister the # service when the plugin is stopped. self._service_ids.append(service_id) return def unregister_services(self): """ Unregister any service offered by the plugin. """ # Unregister the services in the reverse order that we registered # them. service_ids = self._service_ids[:] service_ids.reverse() for service_id in service_ids: self.application.unregister_service(service_id) # Just in case the plugin is started again! self._service_ids = [] return ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ def _anytrait_changed(self, trait_name, old, new): """ Static trait change handler. """ # Ignore the '_items' part of the trait name (if it is there!), and get # the actual trait. base_trait_name = trait_name.split('_items')[0] trait = self.trait(base_trait_name) # If the trait is one that contributes to an extension point then fire # an appropriate 'extension point changed' event. if trait.contributes_to is not None: if trait_name.endswith('_items'): added = new.added removed = new.removed index = new.index else: added = new removed = old index = slice(0, max(len(old), len(new))) # Let the extension registry know about the change. self._fire_extension_point_changed( trait.contributes_to, added, removed, index ) return #### Methods ############################################################## def _create_multiple_traits_exception(self, extension_point_id): """ Create the exception raised when multiple traits are found. """ exception = ValueError( 'multiple traits for extension point <%s> in plugin <%s>' % ( extension_point_id, self.id ) ) return exception def _get_extensions_from_trait(self, trait_name): """ Return the extensions contributed via the specified trait. """ try: extensions = getattr(self, trait_name) except: logger.exception( 'getting extensions from %s, trait <%s>' % (self, trait_name) ) raise return extensions def _get_service_protocol(self, trait): """ Determine the protocol to register a service trait with. """ # If a specific protocol was specified then use it. if trait.service_protocol is not None: protocol = trait.service_protocol # Otherwise, use the type of the objects that can be assigned to the # trait. # # fixme: This works for 'Instance' traits, but what about 'AdaptsTo' # and 'AdaptedTo' traits? else: # Note that in traits the protocol can be an actual class or # interfacem or the *name* of a class or interface. This allows # us to lazy load them! protocol = trait.trait_type.klass return protocol def _harvest_methods(self, extension_point_id): """ Harvest all method-based contributions. """ extensions = [] # Using inspect.getmembers(self) here will cause an infinite recursion, # so use an internal HasTraits method for inspecting the MRO of the # instance's type to find all methods instead. for name in self._each_trait_method(self): value = getattr(self, name) if self._is_extension_method(value, extension_point_id): result = value() if not isinstance(result, list): result = [result] extensions.extend(result) return extensions def _is_extension_method(self, value, extension_point_id): """ Return True if the value is an extension method. i.e. If the method is one that makes a contribution to the extension point. Currently there is exactly one way to make a method make a contribution, and that is to mark it using the 'contributes_to' decorator, e.g:: @contributes_to('acme.motd.messages') def get_messages(self): ... messages = [...] ... return messages """ is_extension_method = inspect.ismethod(value) \ and extension_point_id == getattr(value,'__extension_point__',None) return is_extension_method def _register_service_factory(self, trait_name, trait): """ Register a service factory for the specified trait. """ # Determine the protocol that the service should be registered with. protocol = self._get_service_protocol(trait) # Register a factory for the service so that it will be lazily loaded # the first time somebody asks for a service with the same protocol # (this could obviously be a lambda function, but I thought it best to # be more explicit 8^). def factory(**properties): """ A service factory. """ return getattr(self, trait_name) return self.application.register_service(protocol, factory) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugin_activator.py0000644000076500000240000000330700000000000022337 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The default plugin activator. """ # Enthought library imports. from traits.api import HasTraits, provides # Local imports. from .i_plugin_activator import IPluginActivator @provides(IPluginActivator) class PluginActivator(HasTraits): """ The default plugin activator. """ ########################################################################### # 'IPluginActivator' interface. ########################################################################### def start_plugin(self, plugin): """ Start the specified plugin. """ # Connect all of the plugin's extension point traits so that the plugin # will be notified if and when contributions are added or removed. plugin.connect_extension_point_traits() # Register all services. plugin.register_services() # Plugin specific start. plugin.start() return def stop_plugin(self, plugin): """ Stop the specified plugin. """ # Plugin specific stop. plugin.stop() # Unregister all service. plugin.unregister_services() # Disconnect all of the plugin's extension point traits. plugin.disconnect_extension_point_traits() return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugin_event.py0000644000076500000240000000133200000000000021460 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A plugin event. """ # Enthought library imports. from traits.api import Instance, Vetoable class PluginEvent(Vetoable): """ A plugin event. """ # The plugin that the event is for. plugin = Instance('envisage.api.IPlugin') #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugin_extension_registry.py0000644000076500000240000000470600000000000024313 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An extension registry that uses plugins as extension providers. """ # Enthought library imports. from traits.api import Instance, on_trait_change # Local imports. from .i_plugin_manager import IPluginManager from .provider_extension_registry import ProviderExtensionRegistry class PluginExtensionRegistry(ProviderExtensionRegistry): """ An extension registry that uses plugins as extension providers. The application's plugins are used as the registries providers so adding or removing a plugin affects the extension points and extensions etc. """ #### 'PluginExtensionRegistry' interface ################################## # The plugin manager that has the plugins we are after! plugin_manager = Instance(IPluginManager) ########################################################################### # 'PluginExtensionRegistry' interface. ########################################################################### #### Trait change handlers ################################################ def _plugin_manager_changed(self, trait_name, old, new): """ Static trait change handler. """ # In practise I can't see why you would ever want (or need) to change # the registry's plugin manager on the fly, but hey... Hence, 'old' # will probably always be 'None'! if old is not None: for plugin in old: self.remove_provider(plugin) if new is not None: for plugin in new: self.add_provider(plugin) return @on_trait_change('plugin_manager:plugin_added') def _on_plugin_added(self, obj, trait_name, old, event): """ Dynamic trait change handler. """ self.add_provider(event.plugin) return @on_trait_change('plugin_manager:plugin_removed') def _on_plugin_removed(self, obj, trait_name, old, event): """ Dynamic trait change handler. """ self.remove_provider(event.plugin) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugin_manager.py0000644000076500000240000001653700000000000021766 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A simple plugin manager implementation. """ from fnmatch import fnmatch import logging from traits.api import Event, HasTraits, Instance, List, Str, provides from .i_application import IApplication from .i_plugin import IPlugin from .i_plugin_manager import IPluginManager from .plugin_event import PluginEvent logger = logging.getLogger(__name__) @provides(IPluginManager) class PluginManager(HasTraits): """ A simple plugin manager implementation. This implementation manages an explicit collection of plugin instances, e.g:: plugin_manager = PluginManager( plugins = [ MyPlugin(), YourPlugin() ] ) Plugins can be added and removed after construction time via the methods 'add_plugin' and 'remove_plugin'. """ #### 'IPluginManager' protocol ############################################# # Fired when a plugin has been added to the manager. plugin_added = Event(PluginEvent) # Fired when a plugin has been removed from the manager. plugin_removed = Event(PluginEvent) #### 'PluginManager' protocol ############################################## # The application that the plugin manager is part of. application = Instance(IApplication) def _application_changed(self, trait_name, old, new): """ Static trait change handler. """ self._update_plugin_application([], self._plugins) return # An optional list of the Ids of the plugins that are to be excluded by # the manager. # # Each item in the list is actually an 'fnmatch' expression. exclude = List(Str) # An optional list of the Ids of the plugins that are to be included by # the manager (i.e. *only* plugins with Ids in this list will be added to # the manager). # # Each item in the list is actually an 'fnmatch' expression. include = List(Str) #### 'object' protocol ##################################################### def __init__(self, plugins=None, **traits): """ Constructor. We allow the caller to specify an initial list of plugins, but the list itself is not part of the public API. To add and remove plugins after construction, use the 'add_plugin' and 'remove_plugin' methods respectively. The manager is also iterable, so to iterate over the plugins use 'for plugin in plugin_manager'. """ super(PluginManager, self).__init__(**traits) if plugins is not None: self._plugins = plugins return def __iter__(self): """ Return an iterator over the manager's plugins. """ plugins = [ plugin for plugin in self._plugins if self._include_plugin(plugin.id) ] return iter(plugins) #### 'IPluginManager' protocol ############################################# def add_plugin(self, plugin): """ Add a plugin to the manager. """ self._plugins.append(plugin) self.plugin_added = PluginEvent(plugin=plugin) return def get_plugin(self, plugin_id): """ Return the plugin with the specified Id. """ for plugin in self._plugins: if plugin_id == plugin.id: if not self._include_plugin(plugin.id): plugin = None break else: plugin = None return plugin def remove_plugin(self, plugin): """ Remove a plugin from the manager. """ self._plugins.remove(plugin) self.plugin_removed = PluginEvent(plugin=plugin) return def start(self): """ Start the plugin manager. """ for plugin in self._plugins: self.start_plugin(plugin) return def start_plugin(self, plugin=None, plugin_id=None): """ Start the specified plugin. """ plugin = plugin or self.get_plugin(plugin_id) if plugin is not None: logger.debug('plugin %s starting', plugin.id) plugin.activator.start_plugin(plugin) logger.debug('plugin %s started', plugin.id) else: raise SystemError('no such plugin %s' % plugin_id) return def stop(self): """ Stop the plugin manager. """ # We stop the plugins in the reverse order that they were started. stop_order = self._plugins[:] stop_order.reverse() for plugin in stop_order: self.stop_plugin(plugin) return def stop_plugin(self, plugin=None, plugin_id=None): """ Stop the specified plugin. """ plugin = plugin or self.get_plugin(plugin_id) if plugin is not None: logger.debug('plugin %s stopping', plugin.id) plugin.activator.stop_plugin(plugin) logger.debug('plugin %s stopped', plugin.id) else: raise SystemError('no such plugin %s' % plugin_id) return #### Protected 'PluginManager' ############################################# # The plugins that the manager manages! _plugins = List(IPlugin) def __plugins_changed(self, trait_name, old, new): """ Static trait change handler. """ self._update_plugin_application(old, new) return def __plugins_items_changed(self, trait_name, old, new): """ Static trait change handler. """ self._update_plugin_application(new.removed, new.added) return def _include_plugin(self, plugin_id): """ Return True if the plugin should be included. This is just shorthand for:- if self._is_included(plugin_id) and not self._is_excluded(plugin_id): ... """ return self._is_included(plugin_id) and not self._is_excluded(plugin_id) #### Private protocol ###################################################### def _is_excluded(self, plugin_id): """ Return True if the plugin Id is excluded. If no 'exclude' patterns are specified then this method returns False for all plugin Ids. """ if len(self.exclude) == 0: return False for pattern in self.exclude: if fnmatch(plugin_id, pattern): return True return False def _is_included(self, plugin_id): """ Return True if the plugin Id is included. If no 'include' patterns are specified then this method returns True for all plugin Ids. """ if len(self.include) == 0: return True for pattern in self.include: if fnmatch(plugin_id, pattern): return True return False def _update_plugin_application(self, removed, added): """ Update the 'application' trait of plugins added/removed. """ for plugin in removed: plugin.application = None for plugin in added: plugin.application = self.application return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.230432 envisage-4.9.0/envisage/plugins/0000755000076500000240000000000000000000000020071 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/__init__.py0000644000076500000240000000000000000000000022170 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2308688 envisage-4.9.0/envisage/plugins/event_manager/0000755000076500000240000000000000000000000022704 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/event_manager/__init__.py0000644000076500000240000000000000000000000025003 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/event_manager/plugin.py0000644000076500000240000000257300000000000024563 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ This module provides a plugin which adds an EventManager to application. If the application does not already have an evt_mgr attribute which is an instance of EventManager, the plugin creates a new EventManager instance, creates a service to offer the event manager and sets the evt_mgr instance of the application to the created event manager. """ # Enthought library imports. from envisage.api import Plugin, ServiceOffer from traits.api import List, Any class EventManagerPlugin(Plugin): """ Plugin to add event manager to the application. """ id = 'envisage.event_manager' SERVICE_OFFERS = 'envisage.service_offers' service_offers = List(contributes_to=SERVICE_OFFERS) def _service_offers_default(self): from encore.events.api import BaseEventManager, get_event_manager evt_mgr_service_offer = ServiceOffer( protocol = BaseEventManager, factory = get_event_manager, ) return [evt_mgr_service_offer] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.233221 envisage-4.9.0/envisage/plugins/ipython_kernel/0000755000076500000240000000000000000000000023123 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/ipython_kernel/__init__.py0000644000076500000240000000000000000000000025222 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/ipython_kernel/actions.py0000644000076500000240000000150500000000000025136 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from pyface.tasks.action.api import TaskAction from traits.api import Instance from .internal_ipkernel import InternalIPKernel class StartQtConsoleAction(TaskAction): """ Open in a separate window a Qt console attached to a, existing kernel. """ id = 'ipython_qtconsole' name = 'IPython Console' kernel = Instance(InternalIPKernel) def perform(self, event=None): self.kernel.new_qt_console() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/ipython_kernel/api.py0000644000076500000240000000075600000000000024256 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .ipython_kernel_plugin import IPythonKernelPlugin, IPYTHON_KERNEL_PROTOCOL # noqa ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/ipython_kernel/heartbeat.py0000644000076500000240000000340000000000000025431 0ustar00mdickinsonstaff00000000000000""" A simple ping-pong style heartbeat that runs in a thread. Modified from upstream to enable the heartbeat thread to be shut down cleanly. """ # We're currently using ipykernel v4.10.1. In that version, there's no way to # cleanly shut down the Heartbeat thread. However, ipykernel v5.x allows the # thread to be shut down by terminating the corresponding context. The relevant # code is here: # # https://github.com/ipython/ipykernel/blob/18f2ef77b6a72109a1e50d8229e7216f1cfc2e39/ipykernel/heartbeat.py#L103-L111 # # The key change is to explicitly catch the termination attempt and close # the socket. Without this, the Context.term call from the main thread # will hang: the open socket prevents termination. # # This version of Heartbeat subclasses the upstream version to introduce the # minimal changes necessary to make shutdown feasible with the v4.10.1 code. # # Once we're using ipykernel 5.x, this module can be removed and we can # revert to using the upstream Heartbeat class. from __future__ import absolute_import, print_function, unicode_literals import ipykernel.heartbeat import zmq class Heartbeat(ipykernel.heartbeat.Heartbeat): """ A simple ping-pong style heartbeat that runs in a thread. Modified from upstream to enable the thread to be shut down cleanly. """ def run(self): try: super(Heartbeat, self).run() except zmq.ZMQError as e: # We expect to get here on normal context termination, but # not otherwise; re-raise if the error is not the expected one # (but close the socket anyway, to allow the context termination # to complete without blocking the main thread). self.socket.close() if e.errno != zmq.ETERM: raise ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/plugins/ipython_kernel/internal_ipkernel.py0000644000076500000240000001213200000000000027201 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ This code has been inspired from the IPython repository https://github.com/ipython/ipython/blob/2.x/examples/Embedding/internal_ipkernel.py """ from __future__ import absolute_import, print_function, unicode_literals import atexit import warnings import ipykernel.connect import six from envisage.plugins.ipython_kernel.kernelapp import IPKernelApp from traits.api import Any, HasStrictTraits, Instance, List # Allow unregistration of atexit handlers registered by IPython machinery. if six.PY2: def atexit_unregister(func): # Replace the contents, not the list itself, in case anyone else # is keeping references to it. Also use 'not thing == func' instead # of 'thing != func' to match the semantics of the Python 3 code. atexit._exithandlers[:] = list( handler for handler in atexit._exithandlers if not handler[0] == func ) else: from atexit import unregister as atexit_unregister def _gui_kernel(gui_backend): """ Launch and return an IPython kernel GUI with support. Parameters ---------- gui_backend -- string or None The GUI mode used to initialize the GUI mode. For options, see the `ipython --gui` help pages. If None, the kernel is initialized without GUI support. """ kernel = IPKernelApp.instance() argv = ['python'] if gui_backend is not None: argv.append('--gui={}'.format(gui_backend)) kernel.initialize(argv) return kernel class InternalIPKernel(HasStrictTraits): """ Represents an IPython kernel and the consoles attached to it. """ #: The IPython kernel. ipkernel = Instance(IPKernelApp) #: A list of connected Qt consoles. consoles = List() #: The kernel namespace. #: Use `Any` instead of `Dict` because this is an IPython dictionary #: object. namespace = Any() #: The values used to initialize the kernel namespace. #: This is a list of tuples (name, value). initial_namespace = List() def init_ipkernel(self, gui_backend=None): """ Initialize the IPython kernel. Parameters ---------- gui_backend -- string, optional The GUI mode used to initialize the GUI event loop integration. For options, see the `ipython --gui` help pages. If not given, no event loop integration is set up. .. note:: Use of this argument is deprecated! """ # For backwards compatibility, we allow a kernel to be initialized # twice, and we ignore the second initialization, but warn. if self.ipkernel is not None: warnings.warn( ( "The IPython kernel has already been initialized. A " "second call to init_ipkernel has no effect. In the " "future, a second initialization may become an error." ), DeprecationWarning, ) return if gui_backend is not None: warnings.warn( ( "The gui_backend argument is deprecated. " "Integration with a GUI event loop can be " "achieved by setting the 'eventloop' attribute on the " "kernel instance" ), DeprecationWarning, ) # Start IPython kernel with GUI event loop support self.ipkernel = _gui_kernel(gui_backend) # This application will also act on the shell user namespace self.namespace = self.ipkernel.shell.user_ns self.namespace.update(dict(self.initial_namespace)) def new_qt_console(self): """ Start a new qtconsole connected to our kernel. """ console = ipykernel.connect.connect_qtconsole( self.ipkernel.connection_file, argv=['--no-confirm-exit'], ) self.consoles.append(console) return console def cleanup_consoles(self): """ Kill all existing consoles. """ for c in self.consoles: c.kill() c.wait() c.stdout.close() c.stderr.close() self.consoles = [] def shutdown(self): """ Shut the kernel down. Existing IPython consoles are killed first. """ if self.ipkernel is not None: self.cleanup_consoles() self.ipkernel.close() # ipkernel.close is only registered for ipykernel 5.1.2 and later, # but unregistering something that wasn't registered is safe. atexit_unregister(self.ipkernel.close) self.ipkernel = None # Remove stored singleton to facilitate garbage collection. IPKernelApp.clear_instance() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/plugins/ipython_kernel/ipython_kernel_plugin.py0000644000076500000240000000741300000000000030112 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An IPython kernel plugin. """ import logging import warnings # Enthought library imports. from envisage.api import ( bind_extension_point, ExtensionPoint, Plugin, ServiceOffer) from traits.api import Bool, Instance, List # Extension point IDs. SERVICE_OFFERS = 'envisage.service_offers' IPYTHON_NAMESPACE = 'ipython_plugin.namespace' # Protocol for the contributed service offer. IPYTHON_KERNEL_PROTOCOL = 'envisage.plugins.ipython_kernel.internal_ipkernel.InternalIPKernel' # noqa: E501 logger = logging.getLogger(__name__) class IPythonKernelPlugin(Plugin): """ An IPython kernel plugin. """ #: The plugin unique identifier. id = 'envisage.plugins.ipython_kernel' #: The plugin name (suitable for displaying to the user). name = 'IPython embedded kernel plugin' #: Extension point for objects contributed to the IPython kernel namespace. kernel_namespace = ExtensionPoint( List, id=IPYTHON_NAMESPACE, desc=""" Variables to add to the IPython kernel namespace. This is a list of tuples (name, value). """ ) #: Service offers contributed by this plugin. service_offers = List(contributes_to=SERVICE_OFFERS) #: Whether to initialize the kernel when the service is created. #: The default is ``False```, for backwards compatibility. It will change #: to ``True`` in a future version of Envisage. External users wanting #: to use the future behaviour now should pass ``init_ipkernel=True`` #: when creating the plugin. init_ipkernel = Bool(False) def stop(self): """ Stop the plugin. """ self._destroy_kernel() # Private traits and methods #: The InternalIPKernel instance provided by the service. _kernel = Instance(IPYTHON_KERNEL_PROTOCOL) def _create_kernel(self): from .internal_ipkernel import InternalIPKernel # This shouldn't happen with a normal lifecycle, but add a warning # just in case. if self._kernel is not None: warnings.warn( "A kernel already exists. " "No new kernel will be created.", RuntimeWarning, ) return logger.debug("Creating the embedded IPython kernel") kernel = self._kernel = InternalIPKernel() bind_extension_point(kernel, 'initial_namespace', IPYTHON_NAMESPACE, self.application) if self.init_ipkernel: kernel.init_ipkernel() else: warnings.warn( ( "In the future, the IPython kernel will be initialized " "automatically at creation time. To enable this " "future behaviour now, create the plugin using " "IPythonKernelPlugin(init_ipkernel=True)" ), DeprecationWarning, ) return kernel def _destroy_kernel(self): """ Destroy any existing kernel. """ if self._kernel is None: return logger.debug("Shutting down the embedded IPython kernel") self._kernel.shutdown() self._kernel = None def _service_offers_default(self): ipython_kernel_service_offer = ServiceOffer( protocol=IPYTHON_KERNEL_PROTOCOL, factory=self._create_kernel, ) return [ipython_kernel_service_offer] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/ipython_kernel/ipython_kernel_ui_plugin.py0000644000076500000240000000377700000000000030620 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An IPython kernel plugin. """ # Enthought library imports. from envisage.plugins.ipython_kernel.ipython_kernel_plugin import ( IPYTHON_KERNEL_PROTOCOL) from envisage.ui.tasks.api import TaskExtension from envisage.api import Plugin from traits.api import List from pyface.tasks.action.api import SGroup, SchemaAddition TASK_EXTENSIONS = 'envisage.ui.tasks.task_extensions' class IPythonKernelUIPlugin(Plugin): """ Contributes UI actions on top of the IPython Kernel Plugin. """ #### 'IPlugin' interface ################################################## # The plugin unique identifier. id = 'envisage.plugins.ipython_kernel_ui' # The plugin name (suitable for displaying to the user). name = 'IPython embedded kernel UI plugin' #### Contributions to extension points made by this plugin ################ contributed_task_extensions = List(contributes_to=TASK_EXTENSIONS) #### Trait initializers ################################################### def _contributed_task_extensions_default(self): from .actions import StartQtConsoleAction def menu_factory(): kernel = self.application.get_service(IPYTHON_KERNEL_PROTOCOL) return SGroup( StartQtConsoleAction(kernel=kernel), id='ipython' ) return [ TaskExtension( actions=[ SchemaAddition( path='MenuBar/View', factory=menu_factory, id='IPythonSchema', ), ] ) ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/plugins/ipython_kernel/kernelapp.py0000644000076500000240000003065500000000000025467 0ustar00mdickinsonstaff00000000000000""" This module contains an extended version of the upstream ipykernel IPKernelApp. The main reason for extending is to support clean shutdown. """ from __future__ import absolute_import, print_function, unicode_literals import atexit import logging import os import sys import ipykernel.ipkernel import ipykernel.kernelapp import ipykernel.zmqshell import IPython.utils.io import six import zmq from envisage.plugins.ipython_kernel.heartbeat import Heartbeat # The IPython machinery registers various atexit cleanup handlers. We # need to be able to do cleanup *before* process exit time, and some # of the registered handlers are not idempotent, and so cause errors # at process exit time. Those handlers also interfere with timely garbage # collection by holding onto references to otherwise dead objects. So we # deliberately unregister all registered handlers. if six.PY2: def atexit_unregister(func): # Replace the contents, not the list itself, in case anyone else # is keeping references to it. Also use 'not thing == func' instead # of 'thing != func' to match the semantics of the Python 3 code. atexit._exithandlers[:] = list( handler for handler in atexit._exithandlers if not handler[0] == func ) else: from atexit import unregister as atexit_unregister # Sentinel object used to represent a missing attribute. _MISSING = object() class IPKernelApp(ipykernel.kernelapp.IPKernelApp): """ Patched version of the IPKernelApp, mostly to support clean shutdown. """ # Methods overridden from the base class ################################## def init_heartbeat(self): """start the heart beating Overridden from the base class in order to swap in our own Heartbeat class in place of the official one. Our Heartbeat class is modified to allow the heartbeat thread to be shut down cleanly. This override can be removed once we're on ipkernel 5.x. """ # heartbeat doesn't share context, because it mustn't be blocked # by the GIL, which is accessed by libzmq when freeing zero-copy # messages hb_ctx = zmq.Context() self.heartbeat = Heartbeat( hb_ctx, (self.transport, self.ip, self.hb_port), ) self.hb_port = self.heartbeat.port self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port) self.heartbeat.start() def patch_io(self): """Patch important libraries that can't handle sys.stdout forwarding. Overridden from the base class to do nothing. The base class method monkeypatches faulthandler so that calling faulthandler.enable() while streams are redirected doesn't fail. This override bypasses that patching. Users of the Envisage plugin are advised to enable faulthandler (if desired) as part of application setup, before the plugin is started. Related: https://github.com/ipython/ipykernel/issues/91 """ pass def configure_tornado_logger(self): """ Configure tornado logging. Adds a NullHandler to the tornado root logger, if there are no handlers already present on that logger. If no tornado handler is present at the time the IO loop is started, tornado will call logging.basicConfig, which isn't what we want to happen. Overridden from the base class, which unconditionally adds a new StreamHandler every time. See also: - https://github.com/tornadoweb/tornado/blob/v6.0.3/tornado/ioloop.py#L427-L445 # noqa: E501 - https://github.com/tornadoweb/tornado/pull/741 """ logger = logging.getLogger("tornado") if not logger.handlers: logger.addHandler(logging.NullHandler()) def log_connection_info(self): """ Display connection info, and store ports. Overridden to not write information to __stdout__. We don't usually want this in applications that embed an IPython kernel (as opposed to the case where IPython effectively *is* the application). """ basename = os.path.basename(self.connection_file) if (basename == self.connection_file or os.path.dirname(self.connection_file) == self.connection_dir): # use shortname tail = basename else: tail = self.connection_file lines = [ "To connect another client to this kernel, use:", " --existing %s" % tail, ] # log connection info # info-level, so often not shown. # frontends should use the %connect_info magic # to see the connection info for line in lines: self.log.info(line) self.ports = dict( shell=self.shell_port, iopub=self.iopub_port, stdin=self.stdin_port, hb=self.hb_port, control=self.control_port, ) # Methods extending the base class methods ################################ def init_crash_handler(self): """ Set up a suitable exception hook. Extended to keep track of the original sys.excepthook value, so that it can be restored later. """ self._original_sys_excepthook = sys.excepthook super(IPKernelApp, self).init_crash_handler() def init_io(self): """ Redirect input streams and set a display hook. Extended to store the original sys attributes so that they can be restored later. """ if self.outstream_class: self._original_sys_stdout = sys.stdout self._original_sys_stderr = sys.stderr if self.displayhook_class: self._original_sys_displayhook = sys.displayhook super(IPKernelApp, self).init_io() def init_kernel(self): """ Create the kernel object itself. Extended to store the original values of IPython.utils.io.stdout and IPython.utils.io.stderr, so that they can be restored later. """ self._original_ipython_utils_io_stdout = getattr( IPython.utils.io, "stdout", _MISSING) self._original_ipython_utils_io_stderr = getattr( IPython.utils.io, "stderr", _MISSING) super(IPKernelApp, self).init_kernel() # New methods, mostly to control shutdown ################################# def close(self): """ Undo the effects of the initialize method: - free resources allocated during initialization - undo changes to global state """ # Note: the upstream ipykernel introduced its own close method in # v5.1.2, along with an atexit handler for that method. See # https://github.com/ipython/ipykernel/pull/412. For ipykernel versions # of 5.1.2 or later, this method overrides the base class version. self.close_shell() self.close_kernel() self.close_io() self.close_heartbeat() self.close_sockets() self.cleanup_connection_file() atexit_unregister(self.cleanup_connection_file) self.close_crash_handler() self.close_profile_dir() self.cleanup_singletons() def close_shell(self): """ Clean up resources allocated by the shell. """ shell = self.kernel.shell # Clean up script magics object, which is contained in a reference # cycle, and has a __del__ method, preventing its removal in Python 2. magics_manager = shell.magics_manager script_magics = magics_manager.registry["ScriptMagics"] script_magics.kill_bg_processes() atexit_unregister(script_magics.kill_bg_processes) script_magics.magics.clear() script_magics.shell = None script_magics.parent = None # The shell's cleanup method restores the sys.module changes. shell.cleanup() # The atexit_operations method ends the history manager session, # but doesn't stop the history manager's save_thread, so we need # to do that separately. shell.atexit_operations() atexit_unregister(shell.atexit_operations) shell.history_manager.save_thread.stop() atexit_unregister(shell.history_manager.save_thread.stop) # Rely on garbage collection to clean up the file connection. shell.history_manager.db.close() # Remove some references to avoid keeping objects alive unnecessarily. del shell.configurables[:] del shell.sys_excepthook del shell._orig_sys_module_state del shell._orig_sys_modules_main_mod def close_kernel(self): """ Undo setup from init_kernel. """ # Unhook listeners, and close kernel streams (which also closes # the corresponding zmq.Socket objects). kernel = self.kernel while kernel.shell_streams: stream = kernel.shell_streams.pop() stream.stop_on_recv() stream.close() # Remove selected references to allow effective garbage collection. kernel.stdin_socket = None # Undo changes to IPython.utils.io made at shell creation time. # The values written by the shell keep references that prevent # proper garbage collection from taking place. if self._original_ipython_utils_io_stderr is _MISSING: del IPython.utils.io.stderr else: IPython.utils.io.stderr = self._original_ipython_utils_io_stderr del self._original_ipython_utils_io_stderr if self._original_ipython_utils_io_stdout is _MISSING: del IPython.utils.io.stdout else: IPython.utils.io.stdout = self._original_ipython_utils_io_stdout del self._original_ipython_utils_io_stdout def close_io(self): """ Undo the effects of init_io. Restores sys module attributes altered by init_io. """ if self.displayhook_class: sys.displayhook = self._original_sys_displayhook del self._original_sys_displayhook if self.outstream_class: sys.stderr.close() sys.stderr = self._original_sys_stderr del self._original_sys_stderr sys.stdout.close() sys.stdout = self._original_sys_stdout del self._original_sys_stdout def close_iopub(self): """ Close iopub-related resources. """ iopub_socket = self.iopub_thread.socket # Remove the atexit handler that's registered. self.iopub_thread.stop() atexit_unregister(self.iopub_thread.stop) iopub_socket.close() def close_crash_handler(self): """ Undo the global state change from init_crash_handler. Restore the sys.excepthook attribute. """ sys.excepthook = self._original_sys_excepthook del self._original_sys_excepthook def close_heartbeat(self): """ Stop the heartbeat thread, by terminating the corresponding zmq.Context. """ # This should interrupt the zmq.device call. self.heartbeat.context.term() self.heartbeat.join() def close_sockets(self): """ Unbind, close and destroy sockets created by init_sockets. """ self.close_iopub() for channel in ('shell', 'control', 'stdin'): self.log.debug("Closing %s channel", channel) socket = getattr(self, channel + "_socket", None) if socket and not socket.closed: socket.close() # ipykernel 5.1.2 and later creates its own context. Earlier versions # use the shared zmq context. Ref: ipython/ipykernel#412. if hasattr(self, "context"): self.log.debug("Terminating zmq context") self.context.term() self.log.debug("Terminated zmq context") def close_profile_dir(self): """ Undo changes made in init_profile_dir. """ ipython_dir_entry = os.path.abspath(self.ipython_dir) if ipython_dir_entry in sys.path: sys.path.remove(ipython_dir_entry) def cleanup_singletons(self): """ Clear SingletonConfigurable instances. """ # These instances will otherwise hinder garbage collection, # and prevent a clean recreation of a new kernel app. ipykernel.zmqshell.ZMQInteractiveShell.clear_instance() ipykernel.ipkernel.IPythonKernel.clear_instance() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2340462 envisage-4.9.0/envisage/plugins/ipython_kernel/tests/0000755000076500000240000000000000000000000024265 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/ipython_kernel/tests/__init__.py0000644000076500000240000000000000000000000026364 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/plugins/ipython_kernel/tests/test_internal_ipkernel.py0000644000076500000240000002464300000000000031414 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from __future__ import absolute_import, print_function, unicode_literals import atexit import gc try: # Python 3: mock part of standard library. from unittest import mock except ImportError: # Python 2: use 3rd-party mock import mock import os import shutil import sys import tempfile import threading import unittest import warnings try: import ipykernel # noqa: F401 except ImportError: ipykernel_available = False else: ipykernel_available = True if ipykernel_available: import ipykernel.iostream import ipykernel.ipkernel import ipykernel.kernelapp import ipykernel.zmqshell import IPython.utils.io import tornado.ioloop import zmq from envisage.plugins.ipython_kernel.internal_ipkernel import ( InternalIPKernel) @unittest.skipUnless(ipykernel_available, "skipping tests that require the ipykernel package") class TestInternalIPKernel(unittest.TestCase): def setUp(self): # Make sure that IPython-related files are written to a temporary # directory instead of the home directory. tmpdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tmpdir) self._old_ipythondir = os.environ.get("IPYTHONDIR") os.environ["IPYTHONDIR"] = tmpdir def tearDown(self): # Restore previous state of the IPYTHONDIR environment variable. old_ipythondir = self._old_ipythondir if old_ipythondir is None: del os.environ["IPYTHONDIR"] else: os.environ["IPYTHONDIR"] = old_ipythondir def test_lifecycle(self): kernel = InternalIPKernel() self.assertIsNone(kernel.ipkernel) kernel.init_ipkernel(gui_backend=None) self.assertIsNotNone(kernel.ipkernel) self.assertIsInstance(kernel.ipkernel, ipykernel.kernelapp.IPKernelApp) kernel.new_qt_console() kernel.new_qt_console() self.assertEqual(len(kernel.consoles), 2) kernel.shutdown() self.assertIsNone(kernel.ipkernel) self.assertEqual(len(kernel.consoles), 0) def test_initial_namespace(self): kernel = InternalIPKernel(initial_namespace=[('x', 42.1)]) kernel.init_ipkernel(gui_backend=None) self.assertIn('x', kernel.namespace) self.assertEqual(kernel.namespace['x'], 42.1) kernel.shutdown() def test_shutdown_restores_output_streams(self): original_stdin = sys.stdin original_stdout = sys.stdout original_stderr = sys.stderr self.create_and_destroy_kernel() self.assertIs(sys.stdin, original_stdin) self.assertIs(sys.stdout, original_stdout) self.assertIs(sys.stderr, original_stderr) def test_shutdown_restores_sys_modules_main(self): original_sys_modules_main = sys.modules["__main__"] self.create_and_destroy_kernel() self.assertIs(sys.modules["__main__"], original_sys_modules_main) def test_shutdown_restores_displayhook_and_excepthook(self): original_displayhook = sys.displayhook original_excepthook = sys.excepthook self.create_and_destroy_kernel() self.assertIs(sys.displayhook, original_displayhook) self.assertIs(sys.excepthook, original_excepthook) def test_shutdown_restores_sys_path(self): original_sys_path = sys.path[:] self.create_and_destroy_kernel() self.assertEqual(sys.path, original_sys_path) def test_shutdown_closes_console_pipes(self): kernel = InternalIPKernel() kernel.init_ipkernel(gui_backend=None) console = kernel.new_qt_console() self.assertFalse(console.stdout.closed) self.assertFalse(console.stderr.closed) kernel.shutdown() self.assertTrue(console.stdout.closed) self.assertTrue(console.stderr.closed) def test_ipython_util_io_globals_restored(self): original_io_stdin = IPython.utils.io.stdin original_io_stdout = IPython.utils.io.stdout original_io_stderr = IPython.utils.io.stderr self.create_and_destroy_kernel() self.assertIs(IPython.utils.io.stdin, original_io_stdin) self.assertIs(IPython.utils.io.stdout, original_io_stdout) self.assertIs(IPython.utils.io.stderr, original_io_stderr) def test_ipython_util_io_globals_restored_if_they_dont_exist(self): # Regression test for enthought/envisage#218 original_io_stdin = IPython.utils.io.stdin original_io_stdout = IPython.utils.io.stdout original_io_stderr = IPython.utils.io.stderr del IPython.utils.io.stdin del IPython.utils.io.stdout del IPython.utils.io.stderr try: self.create_and_destroy_kernel() self.assertFalse(hasattr(IPython.utils.io, "stdin")) self.assertFalse(hasattr(IPython.utils.io, "stdout")) self.assertFalse(hasattr(IPython.utils.io, "stderr")) finally: IPython.utils.io.stdin = original_io_stdin IPython.utils.io.stdout = original_io_stdout IPython.utils.io.stderr = original_io_stderr def test_io_pub_thread_stopped(self): self.create_and_destroy_kernel() io_pub_threads = self.objects_of_type(ipykernel.iostream.IOPubThread) for thread in io_pub_threads: self.assertFalse(thread.thread.is_alive()) def test_no_threads_leaked(self): threads_before = threading.active_count() self.create_and_destroy_kernel() threads_after = threading.active_count() self.assertEqual(threads_before, threads_after) def test_no_new_atexit_handlers(self): # Caution: this is a rather fragile and indirect test. We want # to know that all cleanup has happened when shutting down the # kernel, with none of that cleanup deferred to atexit handlers. # # Since we have no direct way to get hold of the atexit handlers on # Python 3, we instead use the number of referents from the # atexit module as a proxy. # # If this test starts failing, try adding a warmup cycle. If the # first call to self.create_and_destroy_kernel adds new referents, # that's not a big deal. But if every call consistently adds new # referents, then there's something to be fixed. atexit_handlers_before = len(gc.get_referents(atexit)) self.create_and_destroy_kernel() atexit_handlers_after = len(gc.get_referents(atexit)) self.assertEqual(atexit_handlers_before, atexit_handlers_after) def test_zmq_sockets_closed(self): # Previously, tests were leaking file descriptors linked to # zmq.Socket objects. Check that all extant sockets are closed. self.create_and_destroy_kernel() sockets = self.objects_of_type(zmq.Socket) self.assertTrue(all(socket.closed for socket in sockets)) def test_ipykernel_live_objects(self): # Check that all IPython-related objects have been cleaned up # as expected. self.create_and_destroy_kernel() # Remove anything kept alive by cycles. (There are too many cycles # to break them individually.) gc.collect() shells = self.objects_of_type(ipykernel.zmqshell.ZMQInteractiveShell) self.assertEqual(shells, []) kernels = self.objects_of_type(ipykernel.ipkernel.IPythonKernel) self.assertEqual(kernels, []) kernel_apps = self.objects_of_type(ipykernel.kernelapp.IPKernelApp) self.assertEqual(kernel_apps, []) def test_initialize_twice(self): # Trying to re-initialize an already initialized IPKernelApp can # happen right now as a result of refactoring, but eventually # it should be an error. For now, it's a warning. kernel = InternalIPKernel() self.assertIsNone(kernel.ipkernel) kernel.init_ipkernel(gui_backend=None) try: self.assertIsNotNone(kernel.ipkernel) ipkernel = kernel.ipkernel with warnings.catch_warnings(record=True) as warn_msgs: warnings.simplefilter("always", category=DeprecationWarning) kernel.init_ipkernel(gui_backend=None) # Check that the existing kernel has not been replaced. self.assertIs(ipkernel, kernel.ipkernel) finally: kernel.shutdown() # Check that we got the expected warning message. self.assertEqual(len(warn_msgs), 1) message = str(warn_msgs[0].message) self.assertIn("already been initialized", message) def test_init_ipkernel_with_explicit_gui_backend(self): loop = tornado.ioloop.IOLoop.current() # Kernel initialization adds an "enter_eventloop" call to the # ioloop event loop queue. Mock to avoid modifying the actual event # loop. with mock.patch.object(loop, "add_callback") as mock_add_callback: with warnings.catch_warnings(record=True) as warn_msgs: warnings.simplefilter("always", category=DeprecationWarning) # Use of gui_backend is deprecated. kernel = InternalIPKernel() kernel.init_ipkernel(gui_backend="qt4") kernel.shutdown() mock_add_callback.reset_mock() # Check that we got the expected warning message. matching_messages = [ msg for msg in warn_msgs if "gui_backend argument is deprecated" in str(msg.message) ] self.assertEqual(len(matching_messages), 1) # Helper functions. def objects_of_type(self, type): """ Find and return a list of all currently tracked instances of the given type. """ return [ obj for obj in gc.get_objects() if isinstance(obj, type) ] def create_and_destroy_kernel(self): """ Set up a new kernel with two associated consoles, then shut everything down. """ kernel = InternalIPKernel() kernel.init_ipkernel(gui_backend=None) kernel.new_qt_console() kernel.new_qt_console() kernel.shutdown() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/plugins/ipython_kernel/tests/test_ipython_kernel_plugin.py0000644000076500000240000001600000000000000032303 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! import contextlib import os try: # Python 3: mock available in std. lib. from unittest import mock except ImportError: # Python 2: use 3rd party mock library import mock import shutil import tempfile import unittest import warnings from traits.api import List from envisage._compat import STRING_BASE_CLASS from envisage.api import Application, Plugin from envisage.core_plugin import CorePlugin from envisage.tests.ets_config_patcher import ETSConfigPatcher # Skip these tests unless ipykernel is available. try: import ipykernel # noqa: F401 except ImportError: ipykernel_available = False else: ipykernel_available = True if ipykernel_available: from envisage.plugins.ipython_kernel.internal_ipkernel import ( InternalIPKernel) from envisage.plugins.ipython_kernel.ipython_kernel_plugin import ( IPYTHON_KERNEL_PROTOCOL, IPYTHON_NAMESPACE, IPythonKernelPlugin) @unittest.skipUnless(ipykernel_available, "skipping tests that require the ipykernel package") class TestIPythonKernelPlugin(unittest.TestCase): def setUp(self): ets_config_patcher = ETSConfigPatcher() ets_config_patcher.start() self.addCleanup(ets_config_patcher.stop) # Make sure that IPython-related files are written to a temporary # directory instead of the home directory. tmpdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tmpdir) self._old_ipythondir = os.environ.get("IPYTHONDIR") os.environ["IPYTHONDIR"] = tmpdir def tearDown(self): # Restore previous state of the IPYTHONDIR environment variable. old_ipythondir = self._old_ipythondir if old_ipythondir is None: del os.environ["IPYTHONDIR"] else: os.environ["IPYTHONDIR"] = old_ipythondir def test_import_from_api(self): # Regression test for enthought/envisage#108 from envisage.plugins.ipython_kernel.api import IPYTHON_KERNEL_PROTOCOL self.assertIsInstance(IPYTHON_KERNEL_PROTOCOL, STRING_BASE_CLASS) def test_kernel_service(self): # See that we can get the IPython kernel service when the plugin is # there. with self.running_app() as app: kernel = app.get_service(IPYTHON_KERNEL_PROTOCOL) self.assertIsInstance(kernel, InternalIPKernel) self.assertIsNotNone(kernel.ipkernel) # After application stop, the InternalIPKernel object should # also have been shut down. self.assertIsNone(kernel.ipkernel) def test_kernel_namespace_extension_point(self): class NamespacePlugin(Plugin): kernel_namespace = List(contributes_to=IPYTHON_NAMESPACE) def _kernel_namespace_default(self): return [('y', 'hi')] plugins = [ IPythonKernelPlugin(init_ipkernel=True), NamespacePlugin(), ] with self.running_app(plugins=plugins) as app: kernel = app.get_service(IPYTHON_KERNEL_PROTOCOL) self.assertIn('y', kernel.namespace) self.assertEqual(kernel.namespace['y'], 'hi') def test_get_service_twice(self): with self.running_app() as app: kernel1 = app.get_service(IPYTHON_KERNEL_PROTOCOL) kernel2 = app.get_service(IPYTHON_KERNEL_PROTOCOL) self.assertIs(kernel1, kernel2) def test_service_not_used(self): # If the service isn't used, no kernel should be created. from envisage.plugins.ipython_kernel import internal_ipkernel kernel_instances = [] class TrackingInternalIPKernel(internal_ipkernel.InternalIPKernel): def __init__(self, *args, **kwargs): super(TrackingInternalIPKernel, self).__init__(*args, **kwargs) kernel_instances.append(self) patcher = mock.patch.object( internal_ipkernel, "InternalIPKernel", TrackingInternalIPKernel, ) with patcher: kernel_plugin = IPythonKernelPlugin(init_ipkernel=True) with self.running_app(plugins=[kernel_plugin]): pass self.assertEqual(kernel_instances, []) def test_service_used(self): # This is a complement to the test_service_not_used test. It's mostly # here as a double check on the somewhat messy test machinery used in # test_service_not_used: if the assumptions (e.g., on the location that # InternalIPKernel is imported from) in test_service_not_used break, # then this test will likely break too. from envisage.plugins.ipython_kernel import internal_ipkernel kernel_instances = [] class TrackingInternalIPKernel(internal_ipkernel.InternalIPKernel): def __init__(self, *args, **kwargs): super(TrackingInternalIPKernel, self).__init__(*args, **kwargs) kernel_instances.append(self) patcher = mock.patch.object( internal_ipkernel, "InternalIPKernel", TrackingInternalIPKernel, ) with patcher: kernel_plugin = IPythonKernelPlugin(init_ipkernel=True) with self.running_app(plugins=[kernel_plugin]) as app: app.get_service(IPYTHON_KERNEL_PROTOCOL) self.assertEqual(len(kernel_instances), 1) def test_no_init(self): # Testing deprecated behaviour where the kernel is not initialized. plugins = [IPythonKernelPlugin()] with self.running_app(plugins=plugins) as app: with warnings.catch_warnings(record=True) as warn_msgs: warnings.simplefilter("always", category=DeprecationWarning) app.get_service(IPYTHON_KERNEL_PROTOCOL) matching_messages = [ msg for msg in warn_msgs if isinstance(msg.message, DeprecationWarning) if "kernel will be initialized" in str(msg.message) ] self.assertEqual(len(matching_messages), 1) @contextlib.contextmanager def running_app(self, plugins=None): """ Returns a context manager that provides a running application. Parameters ---------- plugins : list of Plugin, optional Plugins to use in the application, other than the CorePlugin (which is always included). If not given, an IPythonKernelPlugin is instantiated and used. """ if plugins is None: plugins = [IPythonKernelPlugin(init_ipkernel=True)] app = Application(plugins=[CorePlugin()] + plugins, id='test') app.start() try: yield app finally: app.stop() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2350407 envisage-4.9.0/envisage/plugins/python_shell/0000755000076500000240000000000000000000000022601 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/python_shell/__init__.py0000644000076500000240000000000000000000000024700 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/python_shell/api.py0000644000076500000240000000067700000000000023736 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .i_python_shell import IPythonShell ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/python_shell/i_python_shell.py0000644000076500000240000000217200000000000026175 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A simple interface for the Python shell. """ # Enthought library imports. from traits.api import Interface class IPythonShell(Interface): """ A simple interface for the Python shell. """ def bind(self, name, value): """ Binds a name to a value in the interpreter's namespace. """ def execute_command(self, command, hidden=True): """ Execute a command in the interpreter. """ def execute_file(self, path, hidden=True): """ Execute a file in the interpreter. """ def lookup(self, name): """ Returns the value bound to a name in the interpreter's namespace. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/python_shell/python_shell_plugin.py0000644000076500000240000000523200000000000027243 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The interactive Python shell plugin. """ # Enthought library imports. from envisage.api import ExtensionPoint, Plugin from traits.api import Dict, List, Str class PythonShellPlugin(Plugin): """ The interactive Python shell plugin. """ # Extension point Ids. BINDINGS = 'envisage.plugins.python_shell.bindings' COMMANDS = 'envisage.plugins.python_shell.commands' VIEWS = 'envisage.ui.workbench.views' #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = 'envisage.plugins.python_shell' # The plugin's name (suitable for displaying to the user). name = 'Python Shell' #### Extension points offered by this plugin ############################## bindings = ExtensionPoint( List(Dict), id=BINDINGS, desc=""" This extension point allows you to contribute name/value pairs that will be bound when the interactive Python shell is started. e.g. Each item in the list is a dictionary of name/value pairs:: {'x' : 10, 'y' : ['a', 'b', 'c']} """ ) commands = ExtensionPoint( List(Str), id=COMMANDS, desc=""" This extension point allows you to contribute commands that are executed when the interactive Python shell is started. e.g. Each item in the list is a string of arbitrary Python code:: 'import os, sys' 'from traits.api import *' Yes, I know this is insecure but it follows the usual Python rule of 'we are all consenting adults'. """ ) #### Contributions to extension points made by this plugin ################ # Bindings. contributed_bindings = List(contributes_to=BINDINGS) def _contributed_bindings_default(self): """ Trait initializer. """ return [{'application' : self.application}] # Views. contributed_views = List(contributes_to=VIEWS) def _contributed_views_default(self): """ Trait initializer. """ # Local imports. from .view.python_shell_view import PythonShellView from .view.namespace_view import NamespaceView return [PythonShellView, NamespaceView] #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2360277 envisage-4.9.0/envisage/plugins/python_shell/view/0000755000076500000240000000000000000000000023553 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/python_shell/view/__init__.py0000644000076500000240000000000000000000000025652 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/python_shell/view/api.py0000644000076500000240000000070500000000000024700 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .python_shell_view import PythonShellView ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/python_shell/view/namespace_view.py0000644000076500000240000001143000000000000027112 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A view containing the contents of a Python shell namespace. """ import types # Enthought library imports. from envisage.plugins.python_shell.api import IPythonShell from envisage.plugins.python_shell.view.python_shell_view import PythonShellView from pyface.workbench.api import View from traits.api import HasTraits, Str, Property, List, Instance, \ DelegatesTo, cached_property from traitsui.api import Item, TableEditor, VGroup from traitsui.api import View as TraitsView from traitsui.table_column import ObjectColumn from traitsui.table_filter import RuleTableFilter from traitsui.table_filter import MenuFilterTemplate from traitsui.table_filter import EvalFilterTemplate from traitsui.table_filter import RuleFilterTemplate # Table editor definition: filters = [EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate] table_editor = TableEditor( columns = [ ObjectColumn(name='name'), ObjectColumn(name='type'), ObjectColumn(name='module'), ], editable = False, deletable = False, sortable = True, sort_model = False, filters = filters, search = RuleTableFilter(), ) def type_to_str(obj): """ Make a string out `obj`'s type robustly. """ typ = type(obj) if typ.__name__ == 'vtkobject' or typ is types.InstanceType: typ = obj.__class__ if type.__module__ == '__builtin__': # Make things like int and str easier to read. return typ.__name__ else: name = '%s.%s' % (typ.__module__, typ.__name__) return name def module_to_str(obj): """ Return the string representation of *obj*'s ``__module__`` attribute, or an empty string if there is no such attribute. """ if hasattr(obj, '__module__'): return str(obj.__module__) else: return '' class NamespaceView(View): """ A view containing the contents of the Python shell namespace. """ #### 'IView' interface #################################################### # The part's globally unique identifier. id = 'enthought.plugins.python_shell.view.namespace_view' # The view's name. name = 'Namespace' # The default position of the view relative to the item specified in the # 'relative_to' trait. position = 'left' #### 'NamespaceView' interface ############################################ # The bindings in the namespace. This is a list of HasTraits objects with # 'name', 'type' and 'module' string attributes. bindings = Property(List, depends_on=['namespace']) shell_view = Instance(PythonShellView) namespace = DelegatesTo('shell_view') # The default traits UI view. traits_view = TraitsView( VGroup( Item( 'bindings', id = 'table', editor = table_editor, springy = True, resizable = True, ), show_border = True, show_labels = False ), resizable = True, ) ########################################################################### # 'View' interface. ########################################################################### def create_control(self, parent): """ Creates the toolkit-specific control that represents the view. 'parent' is the toolkit-specific control that is the view's parent. """ self.ui = self.edit_traits(parent=parent, kind='subpanel') self.shell_view = self.window.application.get_service(IPythonShell) # 'shell_view' is an instance of the class PythonShellView from the module # envisage.plugins.python_shell.view.python_shell_view. return self.ui.control ########################################################################### # 'NamespaceView' interface. ########################################################################### #### Properties ########################################################### @cached_property def _get_bindings(self): """ Property getter. """ if self.shell_view is None: return [] class item(HasTraits): name = Str type = Str module = Str data = [item(name=name, type=type_to_str(value), module=module_to_str(value)) for name, value in self.shell_view.namespace.items()] return data ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/python_shell/view/python_shell_view.py0000644000076500000240000002012000000000000027662 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A view containing an interactive Python shell. """ # Standard library imports. import logging, sys # Enthought library imports. from envisage.api import IExtensionRegistry from envisage.api import ExtensionPoint from envisage.plugins.python_shell.api import IPythonShell from pyface.api import PythonShell from pyface.workbench.api import View from traits.api import Any, Event, Instance, Property, DictStrAny, provides # Setup a logger for this module. logger = logging.getLogger(__name__) class PseudoFile ( object ): """ Simulates a normal File object. """ def __init__(self, write): self.write = write def readline(self): pass def writelines(self, lines): for line in lines: self.write(line) def flush(self): pass def isatty(self): return 1 @provides(IPythonShell) class PythonShellView(View): """ A view containing an interactive Python shell. """ #### 'IView' interface #################################################### # The part's globally unique identifier. id = 'envisage.plugins.python_shell_view' # The part's name (displayed to the user). name = 'Python' # The default position of the view relative to the item specified in the # 'relative_to' trait. position = 'bottom' #### 'PythonShellView' interface ########################################## # The interpreter's namespace. namespace = Property(DictStrAny) # The names bound in the interpreter's namespace. names = Property # Original value for 'sys.stdout': original_stdout = Any # Stdout text is posted to this event stdout_text = Event #### 'IExtensionPointUser' interface ###################################### # The extension registry that the object's extension points are stored in. extension_registry = Property(Instance(IExtensionRegistry)) #### Private interface #################################################### # Bindings. _bindings = ExtensionPoint(id='envisage.plugins.python_shell.bindings') # Commands. _commands = ExtensionPoint(id='envisage.plugins.python_shell.commands') ########################################################################### # 'IExtensionPointUser' interface. ########################################################################### def _get_extension_registry(self): """ Trait property getter. """ return self.window.application ########################################################################### # 'View' interface. ########################################################################### def create_control(self, parent): """ Creates the toolkit-specific control that represents the view. """ self.shell = shell = PythonShell(parent) shell.on_trait_change(self._on_key_pressed, 'key_pressed') shell.on_trait_change(self._on_command_executed, 'command_executed') # Write application standard out to this shell instead of to DOS window self.on_trait_change( self._on_write_stdout, 'stdout_text', dispatch='ui' ) self.original_stdout = sys.stdout sys.stdout = PseudoFile(self._write_stdout) # Namespace contributions. for bindings in self._bindings: for name, value in bindings.items(): self.bind(name, value) for command in self._commands: self.execute_command(command) # We take note of the starting set of names and types bound in the # interpreter's namespace so that we can show the user what they have # added or removed in the namespace view. self._namespace_types = set((name, type(value)) for name, value in \ self.namespace.items()) # Register the view as a service. app = self.window.application self._service_id = app.register_service(IPythonShell, self) return self.shell.control def destroy_control(self): """ Destroys the toolkit-specific control that represents the view. """ super(PythonShellView, self).destroy_control() # Unregister the view as a service. self.window.application.unregister_service(self._service_id) # Remove the sys.stdout handlers. self.on_trait_change( self._on_write_stdout, 'stdout_text', remove=True ) # Restore the original stdout. sys.stdout = self.original_stdout return ########################################################################### # 'PythonShellView' interface. ########################################################################### #### Properties ########################################################### def _get_namespace(self): """ Property getter. """ return self.shell.interpreter().locals def _get_names(self): """ Property getter. """ return list(self.shell.interpreter().locals.keys()) #### Methods ############################################################## def bind(self, name, value): """ Binds a name to a value in the interpreter's namespace. """ self.shell.bind(name, value) return def execute_command(self, command, hidden=True): """ Execute a command in the interpreter. """ return self.shell.execute_command(command, hidden) def execute_file(self, path, hidden=True): """ Execute a command in the interpreter. """ return self.shell.execute_file(path, hidden) def lookup(self, name): """ Returns the value bound to a name in the interpreter's namespace. """ return self.shell.interpreter().locals[name] ########################################################################### # Private interface. ########################################################################### def _write_stdout(self, text): """ Handles text written to stdout. """ self.stdout_text = text return #### Trait change handlers ################################################ def _on_command_executed(self, shell): """ Dynamic trait change handler. """ if self.control is not None: # Get the set of tuples of names and types in the current namespace. namespace_types = set((name, type(value)) for name, value in \ self.namespace.items()) # Figure out the changes in the namespace, if any. added = namespace_types.difference(self._namespace_types) removed = self._namespace_types.difference(namespace_types) # Cache the new list, to use for comparison next time. self._namespace_types = namespace_types # Fire events if there are change. if len(added) > 0 or len(removed) > 0: self.trait_property_changed('namespace', {}, self.namespace) self.trait_property_changed('names', [], self.names) return def _on_key_pressed(self, event): """ Dynamic trait change handler. """ if event.alt_down and event.key_code == 317: zoom = self.shell.control.GetZoom() if zoom != 20: self.shell.control.SetZoom(zoom+1) elif event.alt_down and event.key_code == 319: zoom = self.shell.control.GetZoom() if zoom != -10: self.shell.control.SetZoom(zoom-1) return def _on_write_stdout(self, text): """ Dynamic trait change handler. """ self.shell.control.write(text) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2365482 envisage-4.9.0/envisage/plugins/tasks/0000755000076500000240000000000000000000000021216 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/tasks/__init__.py0000644000076500000240000000000000000000000023315 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/tasks/python_shell_plugin.py0000644000076500000240000001003700000000000025657 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Module defining a simple Python shell Envisage tasks plugin. This plugin provides a task with a simple Python shell. This shouldn't be confused with a more full-featured shell, such as those provided by IPython. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Str, List, Dict, Instance, Property from pyface.tasks.contrib.python_shell import PythonShellTask from envisage.api import Plugin, ExtensionPoint, IExtensionRegistry from envisage.ui.tasks.api import TaskFactory logger = logging.getLogger() BINDINGS = 'envisage.plugins.python_shell.bindings' COMMANDS = 'envisage.plugins.python_shell.commands' class EnvisagePythonShellTask(PythonShellTask): """ Subclass of PythonShellTask that gets its bindings and commands from an Envisage ExtensionPoint """ id='envisage.plugins.tasks.python_shell_task' # ExtensionPointUser interface extension_registry = Property(Instance(IExtensionRegistry)) # The list of bindings for the shell bindings = ExtensionPoint(id=BINDINGS) # The list of commands to run on shell startup commands = ExtensionPoint(id=COMMANDS) # property getter/setters def _get_extension_registry(self): if self.window is not None: return self.window.application return None class PythonShellPlugin(Plugin): """ A tasks plugin to display a simple Python shell to the user. """ # Extension point IDs. BINDINGS = BINDINGS COMMANDS = COMMANDS TASKS = 'envisage.ui.tasks.tasks' #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = 'envisage.plugins.tasks.python_shell_plugin' # The plugin's name (suitable for displaying to the user). name = 'Python Shell' #### Extension points exposed by this plugin ############################## bindings = ExtensionPoint( List(Dict), id=BINDINGS, desc=""" This extension point allows you to contribute name/value pairs that will be bound when the interactive Python shell is started. e.g. Each item in the list is a dictionary of name/value pairs:: {'x' : 10, 'y' : ['a', 'b', 'c']} """ ) commands = ExtensionPoint( List(Str), id=COMMANDS, desc=""" This extension point allows you to contribute commands that are executed when the interactive Python shell is started. e.g. Each item in the list is a string of arbitrary Python code:: 'import os, sys' 'from traits.api import *' Yes, I know this is insecure but it follows the usual Python rule of 'we are all consenting adults'. """ ) #### Contributions to extension points made by this plugin ################ # Bindings. contributed_bindings = List(contributes_to=BINDINGS) tasks = List(contributes_to=TASKS) ########################################################################### # Protected interface. ########################################################################### def start(self): logger.debug('started python shell plugin') def _contributed_bindings_default(self): """ By default we expose the Envisage application object to the namespace """ return [{'application' : self.application}] def _tasks_default(self): return [ TaskFactory( id='envisage.plugins.tasks.python_shell_task', name='Python Shell', factory=EnvisagePythonShellTask ), ] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2378075 envisage-4.9.0/envisage/plugins/text_editor/0000755000076500000240000000000000000000000022423 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/text_editor/__init__.py0000644000076500000240000000000000000000000024522 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/text_editor/actions.py0000644000076500000240000000276000000000000024442 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! import logging from apptools.io.api import File from pyface.api import FileDialog, OK from pyface.action.api import Action from traits.api import Any from .editor.text_editor import TextEditor logger = logging.getLogger(__name__) class NewFileAction(Action): """ Open a new file in the text editor. """ tooltip = "Create a new file for editing" description = "Create a new file for editing" # The WorkbenchWindow the action is attached to. window = Any() def perform(self, event=None): logger.info('NewFileAction.perform()') self.window.workbench.edit(File(''), kind=TextEditor, use_existing=False) class OpenFileAction(Action): """ Open an existing file in the text editor. """ tooltip = "Open a file for editing" description = "Open a file for editing" def perform(self, event=None): logger.info('OpenFileAction.perform()') dialog = FileDialog(parent=self.window.control, title='Open File') if dialog.open() == OK: self.window.workbench.edit(File(dialog.path), kind=TextEditor) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/text_editor/api.py0000644000076500000240000000077100000000000023553 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .text_editor_action_set import TextEditorActionSet from .editor.text_editor import TextEditor ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2386315 envisage-4.9.0/envisage/plugins/text_editor/editor/0000755000076500000240000000000000000000000023711 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/text_editor/editor/__init__.py0000644000076500000240000000000000000000000026010 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/text_editor/editor/text_editor.py0000644000076500000240000001532500000000000026623 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A text editor. """ # Standard library imports. from os.path import basename # Enthought library imports. from pyface.workbench.api import TraitsUIEditor from pyface.api import FileDialog, CANCEL from traits.api import Code, Instance from traitsui.api import CodeEditor, Group, Item, View from traitsui.key_bindings import KeyBinding, KeyBindings from traitsui.menu import NoButtons # Local imports. from .text_editor_handler import TextEditorHandler def _id_generator(): """ A generator that returns the next number for untitled files. """ i = 1 while True: yield(i) i += 1 return _id_generator = _id_generator() class TextEditor(TraitsUIEditor): """ A text editor. """ #### 'TextEditor' interface ############################################### # The key bindings used by the editor. key_bindings = Instance(KeyBindings) # The text being edited. text = Code ########################################################################### # 'IEditor' interface. ########################################################################### def save(self): """ Saves the text to disk. """ # If the file has not yet been saved then prompt for the file name. if len(self.obj.path) == 0: self.save_as() else: f = open(self.obj.path, 'w') f.write(self.text) f.close() # We have just saved the file so we ain't dirty no more! self.dirty = False return def save_as(self): """ Saves the text to disk after prompting for the file name. """ dialog = FileDialog( parent = self.window.control, action = 'save as', default_filename = self.name, wildcard = FileDialog.WILDCARD_PY ) if dialog.open() != CANCEL: # Update the editor. self.id = dialog.path self.name = basename(dialog.path) # Update the resource. self.obj.path = dialog.path # Save it! self.save() return ########################################################################### # 'TraitsUIEditor' interface. ########################################################################### def create_ui(self, parent): """ Creates the traits UI that represents the editor. """ ui = self.edit_traits( parent=parent, view=self._create_traits_ui_view(), kind='subpanel' ) return ui ########################################################################### # 'TextEditor' interface. ########################################################################### def run(self): """ Runs the file as Python. """ # The file must be saved first! self.save() # Execute the code. if len(self.obj.path) > 0: view = self.window.get_view_by_id( 'envisage.plugins.python_shell_view' ) if view is not None: view.execute_command( 'exec(open(r"%s").read())' % self.obj.path, hidden=False ) return def select_line(self, lineno): """ Selects the specified line. """ self.ui.info.text.selected_line = lineno return ########################################################################### # Private interface. ########################################################################### #### Trait initializers ################################################### def _key_bindings_default(self): """ Trait initializer. """ key_bindings = KeyBindings( KeyBinding( binding1 = 'Ctrl-s', description = 'Save the file', method_name = 'save' ), KeyBinding( binding1 = 'Ctrl-r', description = 'Run the file', method_name = 'run' ) ) return key_bindings #### Trait change handlers ################################################ def _obj_changed(self, new): """ Static trait change handler. """ # The path will be the empty string if we are editing a file that has # not yet been saved. if len(new.path) == 0: self.id = self._get_unique_id() self.name = self.id else: self.id = new.path self.name = basename(new.path) f = open(new.path, 'r') self.text = f.read() f.close() return def _text_changed(self, trait_name, old, new): """ Static trait change handler. """ if self.traits_inited(): self.dirty = True return def _dirty_changed(self, dirty): """ Static trait change handler. """ if len(self.obj.path) > 0: if dirty: self.name = basename(self.obj.path) + '*' else: self.name = basename(self.obj.path) return #### Methods ############################################################## def _create_traits_ui_view(self): """ Create the traits UI view used by the editor. fixme: We create the view dynamically to allow the key bindings to be created dynamically (we don't use this just yet, but obviously plugins need to be able to contribute new bindings). """ view = View( Group( Item( 'text', editor=CodeEditor(key_bindings=self.key_bindings) ), show_labels = False ), id = 'envisage.editor.text_editor', handler = TextEditorHandler(), kind = 'live', resizable = True, width = 1.0, height = 1.0, buttons = NoButtons, ) return view def _get_unique_id(self, prefix='Untitled '): """ Return a unique id for a new file. """ id = prefix + str(next(_id_generator)) while self.window.get_editor_by_id(id) is not None: id = prefix + str(next(_id_generator)) return id #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/text_editor/editor/text_editor_handler.py0000644000076500000240000000241000000000000030307 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The traits UI handler for the text editor. """ # Enthought library imports. from traitsui.api import Handler class TextEditorHandler(Handler): """ The traits UI handler for the text editor. """ ########################################################################### # 'TextEditorHandler' interface. ########################################################################### # fixme: We need to work out how to create these 'dispatch' methods # dynamically! Plugins will want to add bindings to the editor to bind # a key to an action. def run(self, info): """ Run the text as Python code. """ info.object.run() return def save(self, info): """ Save the text to disk. """ info.object.save() return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/text_editor/text_editor_action_set.py0000644000076500000240000000232200000000000027536 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from envisage.ui.action.api import Action, ActionSet, Group class TextEditorActionSet(ActionSet): """ The default action set for the Text Editor plugin. """ groups = [ Group( id = "TextFileGroup", path = "MenuBar/File", before = "ExitGroup", ) ] actions = [ Action( id = "NewFileAction", name = "New Text File", class_name='envisage.plugins.text_editor.actions.NewFileAction', group='TextFileGroup', path="MenuBar/File", ), Action( id = 'OpenFile', name = "Open Text File...", class_name='envisage.plugins.text_editor.actions.OpenFileAction', group='TextFileGroup', path="MenuBar/File", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/plugins/text_editor/text_editor_plugin.py0000644000076500000240000000224700000000000026712 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Text Editor plugin for the Workbench UI. """ # Enthought library imports. from traits.api import List from envisage.api import Plugin # The plugin's globally unique identifier (also used as the prefix for all # identifiers defined in this module). ID = "envisage.plugins.text_editor" class TextEditorPlugin(Plugin): """ Text Editor plugin for the Workbench UI. """ name = 'Text Editor plugin' #### Contributions made by this plugin ##################################### ACTION_SETS = 'envisage.ui.workbench.action_sets' action_sets = List(contributes_to=ACTION_SETS) def _action_sets_default(self): from envisage.plugins.text_editor.text_editor_action_set import \ TextEditorActionSet return [TextEditorActionSet] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/provider_extension_registry.py0000644000076500000240000002614200000000000024645 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An extension registry implementation with multiple providers. """ # Standard library imports. import logging # Enthought library imports. from traits.api import List, provides, on_trait_change # Local imports. from .extension_registry import ExtensionRegistry from .i_extension_provider import IExtensionProvider from .i_provider_extension_registry import IProviderExtensionRegistry # Logging. logger = logging.getLogger(__name__) @provides(IProviderExtensionRegistry) class ProviderExtensionRegistry(ExtensionRegistry): """ An extension registry implementation with multiple providers. """ #### Protected 'ProviderExtensionRegistry' interface ###################### # The extension providers that populate the registry. _providers = List(IExtensionProvider) ########################################################################### # 'IExtensionRegistry' interface. ########################################################################### def set_extensions(self, extension_point_id, extensions): """ Set the extensions to an extension point. """ raise SystemError('extension points cannot be set') ########################################################################### # 'ProviderExtensionRegistry' interface. ########################################################################### def add_provider(self, provider): """ Add an extension provider. """ events = self._add_provider(provider) for extension_point_id, (refs, added, index) in events.items(): self._call_listeners(refs, extension_point_id, added, [], index) return def get_providers(self): """ Return all of the providers in the registry. """ return self._providers[:] def remove_provider(self, provider): """ Remove an extension provider. Raise a 'ValueError' if the provider is not in the registry. """ events = self._remove_provider(provider) for extension_point_id, (refs, removed, index) in events.items(): self._call_listeners(refs, extension_point_id, [], removed, index) return ########################################################################### # Protected 'ExtensionRegistry' interface. ########################################################################### def _get_extensions(self, extension_point_id): """ Return the extensions for the given extension point. """ # If we don't know about the extension point then it sure ain't got # any extensions! if not extension_point_id in self._extension_points: logger.warning( 'getting extensions of unknown extension point <%s>' \ % extension_point_id ) extensions = [] # Has this extension point already been accessed? elif extension_point_id in self._extensions: extensions = self._extensions[extension_point_id] # If not, then ask each provider for its contributions to the extension # point. else: extensions = self._initialize_extensions(extension_point_id) self._extensions[extension_point_id] = extensions # We store the extensions as a list of lists, with each inner list # containing the contributions from a single provider. Here we just # concatenate them into a single list. # # You could use a list comprehension, here:- # # all = [x for y in extensions for x in y] # # But I'm sure that that makes it any clearer ;^) all = [] for extensions_of_single_provider in extensions: all.extend(extensions_of_single_provider) return all ########################################################################### # Protected 'ProviderExtensionRegistry' interface. ########################################################################### def _add_provider(self, provider): """ Add a new provider. """ # Add the provider's extension points. self._add_provider_extension_points(provider) # Add the provider's extensions. events = self._add_provider_extensions(provider) # And finally, tag it into the list of providers. self._providers.append(provider) return events def _add_provider_extensions(self, provider): """ Add a provider's extensions to the registry. """ # Each provider can contribute to multiple extension points, so we # build up a dictionary of the 'ExtensionPointChanged' events that we # need to fire. events = {} # Does the provider contribute any extensions to an extension point # that has already been accessed? for extension_point_id, extensions in self._extensions.items(): new = provider.get_extensions(extension_point_id) # We only need fire an event for this extension point if the # provider contributes any extensions. if len(new) > 0: index = sum(map(len, extensions)) refs = self._get_listener_refs(extension_point_id) events[extension_point_id] = (refs, new[:], index) extensions.append(new) return events def _add_provider_extension_points(self, provider): """ Add a provider's extension points to the registry. """ for extension_point in provider.get_extension_points(): self._extension_points[extension_point.id] = extension_point return def _remove_provider(self, provider): """ Remove a provider. """ # Remove the provider's extensions. events = self._remove_provider_extensions(provider) # Remove the provider's extension points. self._remove_provider_extension_points(provider, events) # And finally take it out of the list of providers. self._providers.remove(provider) return events def _remove_provider_extensions(self, provider): """ Remove a provider's extensions from the registry. """ # Each provider can contribute to multiple extension points, so we # build up a dictionary of the 'ExtensionPointChanged' events that we # need to fire. events = {} # Find the index of the provider in the provider list. Its # contributions are at the same index in the extensions list of lists. index = self._providers.index(provider) # Does the provider contribute any extensions to an extension point # that has already been accessed? for extension_point_id, extensions in self._extensions.items(): old = extensions[index] # We only need fire an event for this extension point if the # provider contributed any extensions. if len(old) > 0: offset = sum(map(len, extensions[:index])) refs = self._get_listener_refs(extension_point_id) events[extension_point_id] = (refs, old[:], offset) del extensions[index] return events def _remove_provider_extension_points(self, provider, events): """ Remove a provider's extension points from the registry. """ for extension_point in provider.get_extension_points(): # Remove the extension point. del self._extension_points[extension_point.id] return ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ @on_trait_change('_providers:extension_point_changed') def _providers_extension_point_changed(self, obj, trait_name, old, event): """ Dynamic trait change handler. """ logger.debug('provider <%s> extension point changed', obj) extension_point_id = event.extension_point_id # If the extension point has not yet been accessed then we don't fire a # changed event. # # This is because we only access extension points lazily and so we # can't tell what has actually changed because we have nothing to # compare it to! if not extension_point_id in self._extensions: return # This is a list of lists where each inner list contains the # contributions made to the extension point by a single provider. # # fixme: This causes a problem if the extension point has not yet been # accessed! The tricky thing is that if it hasn't been accessed yet # how do we know what has changed?!? Maybe we should just return an # empty list instead of barfing! extensions = self._extensions[extension_point_id] # Find the index of the provider in the provider list. Its # contributions are at the same index in the extensions list of lists. provider_index = self._providers.index(obj) # Get the updated list from the provider. extensions[provider_index] = obj.get_extensions(extension_point_id) # Find where the provider's contributions are in the whole 'list'. offset = sum(map(len, extensions[:provider_index])) # Translate the event index from one that refers to the list of # contributions from the provider, to the list of contributions from # all providers. index = self._translate_index(event.index, offset) # Find out who is listening. refs = self._get_listener_refs(extension_point_id) # Let any listeners know that the extensions have been added. self._call_listeners( refs, extension_point_id, event.added, event.removed, index ) return #### Methods ############################################################## def _initialize_extensions(self, extension_point_id): """ Initialize the extensions to an extension point. """ # We store the extensions as a list of lists, with each inner list # containing the contributions from a single provider. extensions = [] for provider in self._providers: extensions.append(provider.get_extensions(extension_point_id)[:]) logger.debug('extensions to <%s> <%s>', extension_point_id, extensions) return extensions def _translate_index(self, index, offset): """ Translate an event index by the given offset. """ if isinstance(index, slice): index = slice(index.start+offset, index.stop+offset, index.step) else: index = index + offset return index #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2420125 envisage-4.9.0/envisage/resource/0000755000076500000240000000000000000000000020237 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/__init__.py0000644000076500000240000000000000000000000022336 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/api.py0000644000076500000240000000142200000000000021361 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .i_resource_protocol import IResourceProtocol from .i_resource_manager import IResourceManager from .file_resource_protocol import FileResourceProtocol from .http_resource_protocol import HTTPResourceProtocol from .no_such_resource_error import NoSuchResourceError from .package_resource_protocol import PackageResourceProtocol from .resource_manager import ResourceManager ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/file_resource_protocol.py0000644000076500000240000000307100000000000025361 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A resource protocol for a local file system. """ # Standard library imports. import errno # Enthought library imports. from traits.api import HasTraits, provides # Local imports. from .i_resource_protocol import IResourceProtocol from .no_such_resource_error import NoSuchResourceError @provides(IResourceProtocol) class FileResourceProtocol(HasTraits): """ A resource protocol for a local file system. """ ########################################################################### # 'IResourceProtocol' interface. ########################################################################### def file(self, address): """ Return a readable file-like object for the specified address. """ # Opened in binary mode to be consistent with package resources. This # means, for example, that line-endings will not be converted. try: f = open(address, 'rb') except IOError as e: if e.errno == errno.ENOENT: raise NoSuchResourceError(address) else: raise return f #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/http_resource_protocol.py0000644000076500000240000000267500000000000025432 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A resource protocol for HTTP documents. """ # Enthought library imports. from traits.api import HasTraits, provides # Local imports. from .i_resource_protocol import IResourceProtocol from .no_such_resource_error import NoSuchResourceError @provides(IResourceProtocol) class HTTPResourceProtocol(HasTraits): """ A resource protocol for HTTP documents. """ ########################################################################### # 'IResourceProtocol' interface. ########################################################################### def file(self, address): """ Return a readable file-like object for the specified address. """ # Do the import here 'cos I'm not sure how much this will actually # be used. from .._compat import urlopen, HTTPError try: f = urlopen('http://' + address) except HTTPError: raise NoSuchResourceError('http:://' + address) return f #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/i_resource_manager.py0000644000076500000240000000216200000000000024443 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The resource manager interface. """ # Enthought library imports. from traits.api import Instance, Interface # Local imports. from .i_resource_protocol import IResourceProtocol class IResourceManager(Interface): """ The resource manager interface. """ # The protocols used by the manager to resolve resource URLs. resource_protocols = Instance(IResourceProtocol) def file(self, url): """ Return a readable file-like object for the specified url. Raise a 'NoSuchResourceError' if the resource does not exist. e.g.:: manager.file('pkgfile://acme.ui.workbench/preferences.ini') """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/i_resource_protocol.py0000644000076500000240000000173100000000000024673 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The interface for protocols that handle resource URLs. """ # Enthought library imports. from traits.api import Interface class IResourceProtocol(Interface): """ The interface for protocols that handle resource URLs. """ def file(self, address): """ Return a readable file-like object for the specified address. Raise a 'NoSuchResourceError' if the resource does not exist. e.g.:: protocol.file('acme.ui.workbench/preferences.ini') """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/no_such_resource_error.py0000644000076500000240000000144400000000000025372 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The exception raised when trying to open a non-existent resource. """ class NoSuchResourceError(Exception): """ The exception raised when trying to open a non-existent resource. """ def __init__(self, message=''): """ Constructor. """ Exception.__init__(self, message) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/package_resource_protocol.py0000644000076500000240000000363500000000000026043 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A resource protocol for package resources. """ # Standard library imports. import errno, pkg_resources # Enthought library imports. from traits.api import HasTraits, provides # Local imports. from .i_resource_protocol import IResourceProtocol from .no_such_resource_error import NoSuchResourceError @provides(IResourceProtocol) class PackageResourceProtocol(HasTraits): """ A resource protocol for package resources. This protocol uses 'pkg_resources' to find and access resources. An address for this protocol is a string in the form:: 'package/resource' e.g:: 'acme.ui.workbench/preferences.ini' """ ########################################################################### # 'IResourceProtocol' interface. ########################################################################### def file(self, address): """ Return a readable file-like object for the specified address. """ first_forward_slash = address.index('/') package = address[:first_forward_slash] resource_name = address[first_forward_slash+1:] try: f = pkg_resources.resource_stream(package, resource_name) except IOError as e: if e.errno == errno.ENOENT: raise NoSuchResourceError(address) else: raise except ImportError: raise NoSuchResourceError(address) return f #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/resource_manager.py0000644000076500000240000000450700000000000024140 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The default resource manager. """ # Enthought library imports. from traits.api import Dict, HasTraits, Str, provides # Local imports. from .i_resource_manager import IResourceManager from .i_resource_protocol import IResourceProtocol @provides(IResourceManager) class ResourceManager(HasTraits): """ The default resource manager. """ #### 'IResourceManager' interface ######################################### # The protocols used by the manager to resolve resource URLs. resource_protocols = Dict(Str, IResourceProtocol) ########################################################################### # 'IResourceManager' interface. ########################################################################### #### Trait initializers ################################################### def _resource_protocols_default(self): """ Trait initializer. """ # We do the import(s) here in case somebody wants a resource manager # that doesn't use the default protocol(s). from .file_resource_protocol import FileResourceProtocol from .http_resource_protocol import HTTPResourceProtocol from .package_resource_protocol import PackageResourceProtocol resource_protocols = { 'file' : FileResourceProtocol(), 'http' : HTTPResourceProtocol(), 'pkgfile' : PackageResourceProtocol() } return resource_protocols #### Methods ############################################################## def file(self, url): """ Return a readable file-like object for the specified url. """ protocol_name, address = url.split('://') protocol = self.resource_protocols.get(protocol_name) if protocol is None: raise ValueError('unknown protocol in URL %s' % url) return protocol.file(address) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2424598 envisage-4.9.0/envisage/resource/tests/0000755000076500000240000000000000000000000021401 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/resource/tests/__init__.py0000644000076500000240000000000000000000000023500 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/resource/tests/test_resource_manager.py0000644000076500000240000001105000000000000026330 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the resource manager. """ # Standard library imports. import unittest from io import StringIO # Major package imports. from pkg_resources import resource_filename # Enthought library imports. from envisage.resource.api import ResourceManager from envisage.resource.api import NoSuchResourceError from envisage._compat import HTTPError, unicode_str import envisage._compat url_library = envisage._compat # This module's package. PKG = 'envisage.resource.tests' # mimics `urlopen` for some tests. # In setUp it replaces `urlopen` for some tests, # and in tearDown, the regular `urlopen` is put back into place. def stubout_urlopen(url): if 'bogus' in url: raise HTTPError(url, '404', 'No such resource', '', None) elif 'localhost' in url: return StringIO(unicode_str('This is a test file.\n')) else: raise ValueError('Unexpected URL %r in stubout_urlopen' % url) class ResourceManagerTestCase(unittest.TestCase): """ Tests for the resource manager. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.stored_urlopen = url_library.urlopen url_library.urlopen = stubout_urlopen def tearDown(self): """ Called immediately after each test method has been called. """ url_library.urlopen = self.stored_urlopen ########################################################################### # Tests. ########################################################################### def test_file_resource(self): """ file resource """ rm = ResourceManager() # Get the filename of the 'api.py' file. filename = resource_filename('envisage.resource', 'api.py') # Open a file resource. f = rm.file('file://' + filename) self.assertNotEqual(f, None) contents = f.read() f.close() # Open the api file via the file system. with open(filename, 'rb') as g: self.assertEqual(g.read(), contents) def test_no_such_file_resource(self): """ no such file resource """ rm = ResourceManager() # Open a file resource. with self.assertRaises(NoSuchResourceError): rm.file("file://../bogus.py") def test_package_resource(self): """ package resource """ rm = ResourceManager() # Open a package resource. f = rm.file('pkgfile://envisage.resource/api.py') self.assertNotEqual(f, None) contents = f.read() f.close() # Get the filename of the 'api.py' file. filename = resource_filename('envisage.resource', 'api.py') # Open the api file via the file system. g = open(filename, 'rb') self.assertEqual(g.read(), contents) g.close() def test_no_such_package_resource(self): """ no such package resource """ rm = ResourceManager() # Open a package resource. with self.assertRaises(NoSuchResourceError): rm.file("pkgfile://envisage.resource/bogus.py") with self.assertRaises(NoSuchResourceError): rm.file("pkgfile://completely.bogus/bogus.py") def test_http_resource(self): """ http resource """ # Open an HTTP document resource. rm = ResourceManager() f = rm.file('http://localhost:1234/file.dat') self.assertNotEqual(f, None) contents = f.read() f.close() self.assertEqual(contents, 'This is a test file.\n') def test_no_such_http_resource(self): """ no such http resource """ # Open an HTTP document resource. rm = ResourceManager() with self.assertRaises(NoSuchResourceError): rm.file("http://localhost:1234/bogus.dat") def test_unknown_protocol(self): """ unknown protocol """ # Open an HTTP document resource. rm = ResourceManager() with self.assertRaises(ValueError): rm.file("bogus://foo/bar/baz") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/safeweakref.py0000644000076500000240000000762700000000000021261 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An implementation of weak references that works for bound methods. This code is based on the code in the Python Cookbook, but you can call `ref` for objects that are *not* bound methods too, in which case it just returns a standard `weakref.ref`. Weak references to bound methods are cached so that `ref(x) is ref(x)` as for standard weakrefs, and the `ref` class defined here is therefore intended to be used as a drop-in replacement for 'weakref.ref'. """ # Standard library imports. import sys, weakref # Because this module is intended as a drop-in replacement for weakref, we # import everything from that module here (so the user can do things like # "import safeweakref as weakref" etc). from weakref import * __all__ = weakref.__all__ if hasattr(weakref, "WeakMethod"): pass else: # Backport WeakMethod from Python 3 class WeakMethod(weakref.ref): """ A custom `weakref.ref` subclass which simulates a weak reference to a bound method, working around the lifetime problem of bound methods. """ __slots__ = "_func_ref", "_meth_type", "_alive", "__weakref__" def __new__(cls, meth, callback=None): try: obj = meth.__self__ func = meth.__func__ except AttributeError: raise TypeError( "argument should be a bound method, not {}" .format(type(meth)) ) def _cb(arg): # The self-weakref trick is needed to avoid creating a reference # cycle. self = self_wr() if self._alive: self._alive = False if callback is not None: callback(self) self = weakref.ref.__new__(cls, obj, _cb) self._func_ref = weakref.ref(func, _cb) self._meth_type = type(meth) self._alive = True self_wr = weakref.ref(self) return self def __call__(self): obj = weakref.ref.__call__(self) func = self._func_ref() if obj is None or func is None: return None return self._meth_type(func, obj) def __eq__(self, other): if isinstance(other, WeakMethod): if not self._alive or not other._alive: return self is other return weakref.ref.__eq__(self, other) and self._func_ref == other._func_ref return False def __ne__(self, other): if isinstance(other, WeakMethod): if not self._alive or not other._alive: return self is not other return weakref.ref.__ne__(self, other) or self._func_ref != other._func_ref return True __hash__ = weakref.ref.__hash__ class ref(object): """ An implementation of weak references that works for bound methods and \ caches them. """ _cache = weakref.WeakKeyDictionary() def __new__(cls, obj, callback=None): if getattr(obj, "__self__", None) is not None: # Bound method # Caching func_cache = cls._cache.setdefault(obj.__self__, {}) self = func_cache.get(obj.__func__) if self is None: self = WeakMethod(obj, callback) func_cache[obj.__func__] = self return self else: return weakref.ref(obj, callback) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/service.py0000644000076500000240000000565400000000000020434 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A trait type used to access services. """ # Standard library imports. import logging # Enthought library imports. from traits.api import TraitType # Logging. logger = logging.getLogger(__name__) class Service(TraitType): """ A trait type used to access services. Note that this is a trait *type* and hence does *NOT* have traits itself (i.e. it does *not* inherit from 'HasTraits'). """ ########################################################################### # 'object' interface. ########################################################################### def __init__( self, protocol=None, query='', minimize='', maximize='', **metadata ): """ Constructor. """ super(Service, self).__init__(**metadata) # The protocol that the service must provide. self._protocol = protocol # The optional query. self._query = query # The optional name of the trait/property to minimize. self._minimize = minimize # The optional name of the trait/property to maximize. self._maximize = maximize return ########################################################################### # 'TraitType' interface. ########################################################################### def get(self, obj, trait_name): """ Trait type getter. """ service_registry = self._get_service_registry(obj) obj = service_registry.get_service( self._protocol, self._query, self._minimize, self._maximize ) return obj def set(self, obj, name, value): """ Trait type setter. """ raise SystemError('Service traits cannot be set') ########################################################################### # Private interface. ########################################################################### def _get_service_registry(self, obj): """ Return the service registry in effect for an object. """ service_registry = getattr(obj, 'service_registry', None) if service_registry is None: raise ValueError( 'The "Service" trait type can only be used within objects ' \ 'that have a reference to a service registry via their ' \ '"service_registry" trait. \n' \ 'Object %s\nService protocol %s' % (obj, self._protocol) ) return service_registry #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/service_offer.py0000644000076500000240000000301400000000000021601 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An offer to provide a service. """ # Enthought library imports. from traits.api import Callable, Dict, Either, HasTraits, Str, Type class ServiceOffer(HasTraits): """ An offer to provide a service. """ #### 'ServiceOffer' interface ############################################# # The protocol that the service provides. # # This can be an actual class or interface, or a string that can be used to # import a class or interface. # # e.g. 'foo.bar.baz.Baz' is turned into 'from foo.bar.baz import Baz' protocol = Either(Str, Type) # A callable (or a string that can be used to import a callable) that is # the factory that creates the actual service object. # # e.g:: # # callable(**properties) -> Any # # e.g. 'foo.bar.baz.Baz' is turned into 'from foo.bar.baz import Baz' factory = Either(Str, Callable) # An optional set of properties to associate with the service offer. # # This dictionary is passed as keyword arguments to the factory. properties = Dict #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/service_registry.py0000644000076500000240000002177400000000000022365 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The service registry. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Dict, Event, HasTraits, Int, provides # Local imports. from .i_service_registry import IServiceRegistry from .import_manager import ImportManager from ._compat import STRING_BASE_CLASS # Logging. logger = logging.getLogger(__name__) class NoSuchServiceError(Exception): """ Raised when a required service is not found. """ @provides(IServiceRegistry) class ServiceRegistry(HasTraits): """ The service registry. """ #### IServiceRegistry interface ########################################## # An event that is fired when a service is registered. registered = Event # An event that is fired when a service is unregistered. unregistered = Event #### Private interface ################################################### # The services in the registry. # # { service_id : (protocol_name, obj, properties) } # # where: # # 'protocol_name' is the (possible dotted) name of the interface, type or # class that the object is registered against. # # 'obj' is the object that is registered (any old, Python object!). # # 'properties' is the arbitrary dictionary of properties that were # registered with the object. _services = Dict # The next service Id (service Ids are never persisted between process # invocations so this is simply an ever increasing integer!). _service_id = Int ########################################################################### # 'IServiceRegistry' interface. ########################################################################### def get_required_service(self, protocol, query='', minimize='',maximize=''): """ Return the service that matches the specified query. Raise a 'NoSuchServiceError' exception if no such service exists. """ service = self.get_service(protocol, query, minimize, maximize) if service is None: raise NoSuchServiceError(protocol) return service def get_service(self, protocol, query='', minimize='', maximize=''): """ Return at most one service that matches the specified query. """ services = self.get_services(protocol, query, minimize, maximize) if len(services) > 0: service = services[0] else: service = None return service def get_service_from_id(self, service_id): """ Return the service with the specified id. """ try: protocol, obj, properties = self._services[service_id] except KeyError: raise ValueError('no service with id <%d>' % service_id) return obj def get_services(self, protocol, query='', minimize='', maximize=''): """ Return all services that match the specified query. """ services = [] for service_id, (name, obj, properties) in self._services.items(): if self._get_protocol_name(protocol) == name: # If the protocol is a string then we need to import it! if isinstance(protocol, STRING_BASE_CLASS): actual_protocol = ImportManager().import_symbol(protocol) # Otherwise, it is an actual protocol, so just use it! else: actual_protocol = protocol # If the registered service is actually a factory then use it # to create the actual object. obj = self._resolve_factory( actual_protocol, name, obj, properties, service_id ) # If a query was specified then only add the service if it # matches it! if len(query) == 0 or self._eval_query(obj, properties, query): services.append(obj) # Are we minimizing or maximising anything? If so then sort the list # of services by the specified attribute/property. if minimize != '': services.sort(key=lambda x: getattr(x, minimize)) elif maximize != '': services.sort(key=lambda x: getattr(x, maximize), reverse=True) return services def get_service_properties(self, service_id): """ Return the dictionary of properties associated with a service. """ try: protocol, obj, properties = self._services[service_id] properties = properties.copy() except KeyError: raise ValueError('no service with id <%d>' % service_id) return properties def register_service(self, protocol, obj, properties=None): """ Register a service. """ protocol_name = self._get_protocol_name(protocol) # Make sure each service gets its own properties dictionary. if properties is None: properties = {} service_id = self._next_service_id() self._services[service_id] = (protocol_name, obj, properties) self.registered = service_id logger.debug('service <%d> registered %s', service_id, protocol_name) return service_id def set_service_properties(self, service_id, properties): """ Set the dictionary of properties associated with a service. """ try: protocol, obj, old_properties = self._services[service_id] self._services[service_id] = protocol, obj, properties.copy() except KeyError: raise ValueError('no service with id <%d>' % service_id) return def unregister_service(self, service_id): """ Unregister a service. """ try: protocol, obj, properties = self._services.pop(service_id) self.unregistered = service_id logger.debug('service <%d> unregistered', service_id) except KeyError: raise ValueError('no service with id <%d>' % service_id) return ########################################################################### # Private interface. ########################################################################### def _create_namespace(self, service, properties): """ Create a namespace in which to evaluate a query. """ namespace = {} namespace.update(service.__dict__) namespace.update(properties) return namespace def _eval_query(self, service, properties, query): """ Evaluate a query over a single service. Return True if the service matches the query, otherwise return False. """ namespace = self._create_namespace(service, properties) try: result = eval(query, namespace) except: result = False return result def _get_protocol_name(self, protocol_or_name): """ Returns the full class name for a protocol. """ if isinstance(protocol_or_name, STRING_BASE_CLASS): name = protocol_or_name else: name = '%s.%s' % ( protocol_or_name.__module__, protocol_or_name.__name__ ) return name def _is_service_factory(self, protocol, obj): """ Is the object a factory for services supporting the protocol? """ # fixme: Should we have a formal notion of service factory with an # appropriate API, or is this good enough? An API might have lifecycle # methods to both create and destroy the service?!? return not isinstance(obj, protocol) def _next_service_id(self): """ Returns the next service ID. """ self._service_id += 1 return self._service_id def _resolve_factory(self, protocol, name, obj, properties, service_id): """ If 'obj' is a factory then use it to create the actual service. """ # Is the registered service actually a service *factory*? if self._is_service_factory(protocol, obj): # A service factory is any callable that takes two arguments, the # first is the protocol, the second is the (possibly empty) # dictionary of properties that were registered with the service. # # If the factory is specified as a symbol path then import it. if isinstance(obj, STRING_BASE_CLASS): obj = ImportManager().import_symbol(obj) obj = obj(**properties) # The resulting service object replaces the factory in the cache # (i.e. the factory will not get called again unless it is # unregistered first). self._services[service_id] = (name, obj, properties) return obj #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2513835 envisage-4.9.0/envisage/tests/0000755000076500000240000000000000000000000017552 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/__init__.py0000644000076500000240000000000000000000000021651 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2530868 envisage-4.9.0/envisage/tests/bad_eggs/0000755000076500000240000000000000000000000021305 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/bad_eggs/acme.bad-0.1a1-py2.7.egg0000644000076500000240000000657700000000000025035 0ustar00mdickinsonstaff00000000000000PK@>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKC*؎acme/__init__.pycEA 0ED.\ynXB4JiJ;o=pRc0~:Iu@Q<)PI\ǧ!kx;: @p`]<,*p{aBBOOs?|hF)(CiVT.f[3h[&5ƒrO;c~cbPK@qqxacme/bad/__init__.pyA 0E9ŀ hEt]J=A1 X<y<_+Cy`xǺ] >:!.,?_| [NRm3Tڅ7.[<;3?PKCUkzacme/bad/__init__.pycEA 0ED.\yn(bq! tB %ڴ!IA7 8)1O?~$M:(B\($.TQrSpx!8.phV80୐SAԃn4BVeUb4LHaU1ugz5ĨPKC*acme/bad/bad_plugin.pyMj0:Š,ltB7jYRS+K6)4i}zJ)zYjB6ס7Pq}q393wrG=8.ZDPFD{5e¶)b&TO40C-"H:NcodMZMw)d5=pq7)+eSM܈*ؕGo=nZe r$-\%0D0 w?Ýr%PKCN $Qacme/bad/bad_plugin.pycSM@Gdee3zEV$\BOw $0# ?oЪJvuċ~TU'ޫtf_|SO.L6"AyMmE4411lWXm!i{M!|gs=Nf7iLo:y5>HaVo@Ub>CWT!l  ByŔPZfџ ͭB7ŲZHTOpeF{DzSi^pKxY#1Ưtn][k&6W =EX,Vv>ouչ''g ^/H*G G]P $" `RQ1E}؎ܸh}'ٌ/,ˍnmY'mgv NMWaLVjľhbSD=wq92\e2+ }溯ue~kƇ〕80KoPKC߳EGG-INFO/top_level.txtKLMPKC2EGG-INFO/zip-safePK@>"wacme/__init__.pyPKC*؎acme/__init__.pycPK@qqxacme/bad/__init__.pyPKCUkz0acme/bad/__init__.pycPKC*acme/bad/bad_plugin.pyPKCN $Qvacme/bad/bad_plugin.pycPKC2EGG-INFO/dependency_links.txtPKCjF]4KEGG-INFO/entry_points.txtPKCiBEGG-INFO/namespace_packages.txtPKC˒EGG-INFO/PKG-INFOPKCZ LEGG-INFO/requires.txtPKCΑKEGG-INFO/SOURCES.txtPKC߳K EGG-INFO/top_level.txtPKC2 EGG-INFO/zip-safePK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/bad_eggs/acme.bad-0.1a1-py3.5.egg0000644000076500000240000000702600000000000025022 0ustar00mdickinsonstaff00000000000000PKvLD>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPK%J6- (acme/__pycache__/__init__.cpython-35.pyc2$ 0TfF,FT&1!XoAvz|Qjq~iQrj&-܂[)9Eyɩ8 E@/̜ ]C=3 x3tT}y@ *oq槔ڱ r4PKvLDqqxacme/bad/__init__.pyA 0E9ŀ hEt]J=A1 X<y<_+Cy`xǺ] >:!.,?_| [NRm3Tڅ7.[<;3?PKvLD*acme/bad/bad_plugin.pyMj0:Š,ltB7jYRS+K6)4i}zJ)zYjB6ס7Pq}q393wrG=8.ZDPFD{5e¶)b&TO40C-"H:NcodMZMw)d5=pq7)+eSM܈*ؕGo=nZe r$-\%0D0 w?Ýr%PK%Jb>,acme/bad/__pycache__/__init__.cpython-35.pycMN 0|("tp/pQ 8 }z䙰{g樜QU!uȤ22CDuVL$}C2]8r}'x@,J΅/؎^) @H&AXTԹzyۘruC4/^79EPK%J2EGG-INFO/dependency_links.txtPK%JjF]4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVN)`X5PK%JiEGG-INFO/namespace_packages.txtKLMJzI)\PK%J˒EGG-INFO/PKG-INFOE 0D| 6+ J5 E]_6Л<E] \0A C)^ʂi T |A]!K(Y Z_=r|P6 $u~BOl 8M*&)}m@+PK%J ` EGG-INFO/requires.txtKLMKPK%JΑKEGG-INFO/SOURCES.txtuA0EޅrƘѸT40bh\I3I+[(X8aWjA]y BH=gB&q&Xb.#Au!O0KoPK%J߳EGG-INFO/top_level.txtKLMPK%J2EGG-INFO/zip-safePKvLD>"wacme/__init__.pyPK%J6- (acme/__pycache__/__init__.cpython-35.pycPKvLDqqxacme/bad/__init__.pyPKvLD*Iacme/bad/bad_plugin.pyPK%Jb>,acme/bad/__pycache__/__init__.cpython-35.pycPK%JGL7.acme/bad/__pycache__/bad_plugin.cpython-35.pycPK%J2EGG-INFO/dependency_links.txtPK%JjF]4K(EGG-INFO/entry_points.txtPK%JiEGG-INFO/namespace_packages.txtPK%J˒EGG-INFO/PKG-INFOPK%J ` EGG-INFO/requires.txtPK%JΑKEGG-INFO/SOURCES.txtPK%J߳ EGG-INFO/top_level.txtPK%J2 EGG-INFO/zip-safePK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/bad_eggs/acme.bad-0.1a1-py3.6.egg0000644000076500000240000000701600000000000025022 0ustar00mdickinsonstaff00000000000000PKKL*EGG-INFO/PKG-INFOM0D~m`ەL4j5Z.ЄےR]#TA<1 ;s1 !oؿd9+G"}חfO(zhpF1>(w)Ǧ+~tNqjƟ!'vݐe L}Lb]LOfaA}|PKKLΑKEGG-INFO/SOURCES.txtuA0EޅrƘѸT40bh\I3I+[(X8aWjA]y BH=gB&q&Xb.#Au!O0KoPKKL2EGG-INFO/dependency_links.txtPKKLjF]4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVN)`X5PKKLiEGG-INFO/namespace_packages.txtKLMJzI)\PKKL ` EGG-INFO/requires.txtKLMKPKKL߳EGG-INFO/top_level.txtKLMPKKL2EGG-INFO/zip-safePKeJL>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKKL(acme/__pycache__/__init__.cpython-36.pyc3c% 0 Tf,TfFF`M[EEɩ~̷3s Jo &$%$&sW2MJ3sRR2Kr+t t+,LSsSe+aRjr"ȽPKeJLqqxacme/bad/__init__.pyA 0E9ŀ hEt]J=A1 X<y<_+Cy`xǺ] >:!.,?_| [NRm3Tڅ7.[<;3?PKeJL*acme/bad/bad_plugin.pyMj0:Š,ltB7jYRS+K6)4i}zJ)zYjB6ס7Pq}q393wrG=8.ZDPFD{5e¶)b&TO40C-"H:NcodMZMw)d5=pq7)+eSM܈*ؕGo=nZe r$-\%0D0 w?Ýr%PKKLD,acme/bad/__pycache__/__init__.cpython-36.pyc3c% 0 Tf,TfFF`M[EEɩ~̷3s Jo &$%$&sW2M̓J3sRR2Kr+t t+,LSsSSe+aRjr&PKKLp G.acme/bad/__pycache__/bad_plugin.cpython-36.pycR0qKC:\ TBHe3%R'=bg!&v&&PzH~g?<;:&?׀o8^.Q(L1q9W< U@▀&P%FۓxƧN2۬ui#޶(P?bU0m U#%A@s$jN Jm00]Nvʯ~"ÊoƗqu\#*ِ5Q<(҂V$|ؤVueQ]=}Ϯ/z|>ț)?t_~l–%@PI '(1 q%c/=, =C+E9V릯u^&xɺN5K,xrﮁ PIyȹRHv;I *&L?կIbSuRl݊t+P~PKKL*EGG-INFO/PKG-INFOPKKLΑKEGG-INFO/SOURCES.txtPKKL2EGG-INFO/dependency_links.txtPKKLjF]4KEGG-INFO/entry_points.txtPKKLi6EGG-INFO/namespace_packages.txtPKKL ` EGG-INFO/requires.txtPKKL߳EGG-INFO/top_level.txtPKKL2EGG-INFO/zip-safePKeJL>"w,acme/__init__.pyPKKL(acme/__pycache__/__init__.cpython-36.pycPKeJLqqxacme/bad/__init__.pyPKeJL*racme/bad/bad_plugin.pyPKKLD,acme/bad/__pycache__/__init__.cpython-36.pycPKKLp G.acme/bad/__pycache__/bad_plugin.cpython-36.pycPK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/bad_eggs/acme.bad-0.1a1-py3.7.egg0000644000076500000240000000702300000000000025021 0ustar00mdickinsonstaff00000000000000PKDKN˒EGG-INFO/PKG-INFOE 0D| 6+ J5 E]_6Л<E] \0A C)^ʂi T |A]!K(Y Z_=r|P6 $u~BOl 8M*&)}m@+PKDKNΑKEGG-INFO/SOURCES.txtuA0EޅrƘѸT40bh\I3I+[(X8aWjA]y BH=gB&q&Xb.#Au!O0KoPKDKN2EGG-INFO/dependency_links.txtPKDKNjF]4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVN)`X5PKDKNiEGG-INFO/namespace_packages.txtKLMJzI)\PKDKN ` EGG-INFO/requires.txtKLMKPKDKN߳EGG-INFO/top_level.txtKLMPKDKN2EGG-INFO/zip-safePKz*M>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKDKN\"(acme/__pycache__/__init__.cpython-37.pycsbݵӢ%3 f vb! ʐ̸1i!##C c0&-ނҢb?M[\E%SRsRsS Soqă9+X_&I9)I)%zz&ff&ɹ@2fTO)Ic`PKz*Mqqxacme/bad/__init__.pyA 0E9ŀ hEt]J=A1 X<y<_+Cy`xǺ] >:!.,?_| [NRm3Tڅ7.[<;3?PKz*M*acme/bad/bad_plugin.pyMj0:Š,ltB7jYRS+K6)4i}zJ)zYjB6ס7Pq}q393wrG=8.ZDPFD{5e¶)b&TO40C-"H:NcodMZMw)d5=pq7)+eSM܈*ؕGo=nZe r$-\%0D0 w?Ýr%PKDKNf,acme/bad/__pycache__/__init__.cpython-37.pycsbݵӢ3 f vb! ʐ̸1i!##C c0&-ނҢb?M[\E%SRsRsS Soqă9+X_I9)I)%zz&ff&ɹI)@2TO)IchPKDKN*EL.acme/bad/__pycache__/bad_plugin.cpython-37.pycRA0'qKˠa*b3bRFF#*RUHӕYXn)&8h`5sIvW`wJ޳=O|N{y$@4cQ˸-/W|n x2~IF8Z3TۓHu~#9">t:g' 50&28D  ı{MoaGel^e&^jhJ;旎sN?EaDXoFUTa]8Z#քKp / Ziҷ_Xe0^BRVqWyy}z$>^_جcj\WA⏜Ib1:ɋ;Ff 4b}ol؇4%Pj3TW*ˡN(i |%`=Gg{{wUXE(>炊@63 r`6WI&Ebݕ~euRce]ublijݎvK`PKDKN˒EGG-INFO/PKG-INFOPKDKNΑKEGG-INFO/SOURCES.txtPKDKN2EGG-INFO/dependency_links.txtPKDKNjF]4KEGG-INFO/entry_points.txtPKDKNi*EGG-INFO/namespace_packages.txtPKDKN ` uEGG-INFO/requires.txtPKDKN߳EGG-INFO/top_level.txtPKDKN2EGG-INFO/zip-safePKz*M>"w acme/__init__.pyPKDKN\"(acme/__pycache__/__init__.cpython-37.pycPKz*Mqqxacme/bad/__init__.pyPKz*M*jacme/bad/bad_plugin.pyPKDKNf,acme/bad/__pycache__/__init__.cpython-37.pycPKDKN*EL.acme/bad/__pycache__/bad_plugin.cpython-37.pycPK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571145795.0 envisage-4.9.0/envisage/tests/bad_eggs/acme.bad-0.1a1-py3.8.egg0000644000076500000240000000737700000000000025036 0ustar00mdickinsonstaff00000000000000PK*D0O˒EGG-INFO/PKG-INFOE 0D| 6+ J5 E]_6Л<E] \0A C)^ʂi T |A]!K(Y Z_=r|P6 $u~BOl 8M*&)}m@+PK*D0OΑKEGG-INFO/SOURCES.txtuA0EޅrƘѸT40bh\I3I+[(X8aWjA]y BH=gB&q&Xb.#Au!O0KoPK*D0O2EGG-INFO/dependency_links.txtPK*D0OjF]4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVN)`X5PK*D0OiEGG-INFO/namespace_packages.txtKLMJzI)\PK*D0O ` EGG-INFO/requires.txtKLMKPK*D0O߳EGG-INFO/top_level.txtKLMPK*D0O2EGG-INFO/zip-safePK[A0O>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPK*D0OT;7(acme/__pycache__/__init__.cpython-38.pyc b:X F hXH2043.`LeZȐ̠t ;=(8(9OW||fnA~QI|-Ģ[`N|J"V`IRifN~RJfq^nbr~qnY~jz~brn>Lz8lrSJsR؀f|9PK[A0Oqqxacme/bad/__init__.pyA 0E9ŀ hEt]J=A1 X<y<_+Cy`xǺ] >:!.,?_| [NRm3Tڅ7.[<;3?PK[A0O Ywacme/bad/bad_plugin.pySM0W !TܲJ{ؖT  9pl㏖{&N)"/23~acrD%gc{ :G$J80^xd$z{ `|X>'*4LS |ج< FD U^);k&PѻDm=6hZn <:X(5Y|~<9-m}xSAl0PRHA`2X|KWl4eRN;KU?j1pV{ b\8C9StxB tif.X42Vi,&gzˀօLӂbE8ufVOui@V lW+ІLz8lrSJsR؀|9PK*D0O XYS.acme/bad/__pycache__/bad_plugin.cpython-38.pycώ0mIgy˪HRYBHV!mXB1%R'Bp}RxNxPv+.XIƙ7]Ʒ} Aw ҌIF1R/71O}&CpM 0ʓ 27G색GE^&':x<<.C$u In| 55;uga(& zÙ>J*Ҷ*WtW.fa Ea`Z,M~K,gupIYř^ttry~FC\lp֠RMG Ѐmیwsɋ[LfS;N\?n7)b:BΌRuW,uqOkP).` X- Qz({ B6AϹ" BI3OI&Eb$d::|]_"6˱w;^tOQ~PK*D0O˒EGG-INFO/PKG-INFOPK*D0OΑKEGG-INFO/SOURCES.txtPK*D0O2EGG-INFO/dependency_links.txtPK*D0OjF]4KEGG-INFO/entry_points.txtPK*D0Oi*EGG-INFO/namespace_packages.txtPK*D0O ` uEGG-INFO/requires.txtPK*D0O߳EGG-INFO/top_level.txtPK*D0O2EGG-INFO/zip-safePK[A0O>"w acme/__init__.pyPK*D0OT;7(acme/__pycache__/__init__.cpython-38.pycPK[A0Oqqxacme/bad/__init__.pyPK[A0O Ywkacme/bad/bad_plugin.pyPK*D0O[e,acme/bad/__pycache__/__init__.cpython-38.pycPK*D0O XYS.acme/bad/__pycache__/bad_plugin.cpython-38.pycPK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/bar_category.py0000644000076500000240000000120600000000000022564 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A test class used to test categories. """ # Enthought library imports. from traits.api import HasTraits, Int class BarCategory(HasTraits): y = Int #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2593768 envisage-4.9.0/envisage/tests/eggs/0000755000076500000240000000000000000000000020477 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.bar-0.1a1-py2.7.egg0000644000076500000240000000651500000000000024235 0ustar00mdickinsonstaff00000000000000PKga@{acme/__init__.pyS֥*RVp/,L(QHT0200WHTp+/(x%:(+ĩ"^܂x xmEɩz)9Eyɩ`v|&/PKa@ acme/__init__.pycE 0 S"<z k!tmu+kxy>lDh:վqsdRڙv:vMQŎ%E}x5:??/1~1!ZXUՍ0iͮpġi%I*;|&TOQR&i<4 D@b;ӝSԋC]cNxA\DDPK~a@Ӯ{$acme/bar/__init__.pyNA 0 a-xٳCm7\BHH( Y%" ]۾`<}Qp#GOٿQY [U3k~XfbF8C҈:W3PKa@acme/bar/__init__.pycE 0 D< z еaT׭ٷ5}!?O)Ww(U:†A-S .U2Œz sUh}Dk5^zAZ N*ٔf*#ldz-ZmBfuʒh?Ns:QO=>|PKa@2EGG-INFO/dependency_links.txtPKa@?]f4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNE`X5PKa@?EGG-INFO/namespace_packages.txtKLMJzIE\PKa@ jEGG-INFO/PKG-INFOMA 0D!0fRAQP7$[ bdOE jP!r* ~x g@(hqvcB.oۙK$p7aD@]E&׾80=&DZPKa@Z EGG-INFO/requires.txtKLMKPKa@t-KEGG-INFO/SOURCES.txtu0 !c"F⹙PQV.OC{GY, X`йPjEFb[Y1!kX@Fb%lbz0f Yv%i>0qoPKa@߳EGG-INFO/top_level.txtKLMPK|a@EGG-INFO/zip-safePKga@{acme/__init__.pyPKa@ acme/__init__.pycPKBnP?"acme/bar/bar_plugin.pyPKa@H٪acme/bar/bar_plugin.pycPK~a@Ӯ{$acme/bar/__init__.pyPKa@acme/bar/__init__.pycPKa@2aEGG-INFO/dependency_links.txtPKa@?]f4KEGG-INFO/entry_points.txtPKa@? EGG-INFO/namespace_packages.txtPKa@ jUEGG-INFO/PKG-INFOPKa@Z EGG-INFO/requires.txtPKa@t-KVEGG-INFO/SOURCES.txtPKa@߳ EGG-INFO/top_level.txtPK|a@S EGG-INFO/zip-safePK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.bar-0.1a1-py3.5.egg0000644000076500000240000000675600000000000024243 0ustar00mdickinsonstaff00000000000000PK\r@>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPK%J4]\Q(acme/__pycache__/__init__.cpython-35.pycM1 @DF Q$ Olv?!Mn4^i[oaʁf,`^xß<瓳Y dpc@Pdp =45ӂ9)bZv.I\V\i o @m3Y6R\Ԧ_o(]]Lys(vpyXjٕt ?_PK\r@qqxacme/bar/__init__.pyA 0E9ŀ hEt]J=A1 X<y<_+Cy`xǺ] >:!.,?_| [NRm3Tڅ7.[<;3?PKWV@Aacme/bar/bar_plugin.pyRj0+ (HP-%UBئIދ;3;ZJ){vU(uMJ^p_ k\ Sz!VNe'dj^cTdʳ">4T^VԘDŊ Ad+%x&%Z{BpG1$koF5r'^T&l-KWU;G;|IKbg'v,ǵL: c0EぁlFr%PK%Ja,acme/bar/__pycache__/__init__.cpython-35.pycMN 0|(E(⢃_FA_HG6$tkKY 'Nq,ƫI{^0!-de*I]Ss!5LO RMIQ;PK%Jb}b.acme/bar/__pycache__/bar_plugin.cpython-35.pyc͊@o?1 #Up3d14 "2bV^eST6t2]ću+=2+ur;7pђH 4dPhnBiBeAf{+-6|Re ҁiO'_ }5jQ,'fa.1;l1&3;+@np9.y20;xH~#̦]tkOJBsa[DtÕ|@ dA4n &lz"%hmKoC|c}ϙ ͭ46Kۋܣ$z}yUUI>j%˹s4F!I-u@r6BL QB؎ ag;YV,T"BdAEѵÂ+uW{~vi넝PK%J2EGG-INFO/dependency_links.txtPK%J?]f4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNE`X5PK%J?EGG-INFO/namespace_packages.txtKLMJzIE\PK%J}h6EGG-INFO/PKG-INFOE 0D| 6+  &%okWg3g 15Wb5K(ٿŚBa>:\Ц>Dw~y@7h|8/ N΢OhԬd{6_mPK%J ` EGG-INFO/requires.txtKLMKPK%Jt-KEGG-INFO/SOURCES.txtu0 !c"F⹙PQV.OC{GY, X`йPjEFb[Y1!kX@Fb%lbz0f Yv%i>0qoPK%J߳EGG-INFO/top_level.txtKLMPK%J2EGG-INFO/zip-safePK\r@>"wacme/__init__.pyPK%J4]\Q(acme/__pycache__/__init__.cpython-35.pycPK\r@qqxacme/bar/__init__.pyPKWV@AGacme/bar/bar_plugin.pyPK%Ja,acme/bar/__pycache__/__init__.cpython-35.pycPK%Jb}b.acme/bar/__pycache__/bar_plugin.cpython-35.pycPK%J2EGG-INFO/dependency_links.txtPK%J?]f4KEGG-INFO/entry_points.txtPK%J?lEGG-INFO/namespace_packages.txtPK%J}h6EGG-INFO/PKG-INFOPK%J ` uEGG-INFO/requires.txtPK%Jt-KEGG-INFO/SOURCES.txtPK%J߳u EGG-INFO/top_level.txtPK%J2 EGG-INFO/zip-safePK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.bar-0.1a1-py3.6.egg0000644000076500000240000000675100000000000024237 0ustar00mdickinsonstaff00000000000000PKL2>EGG-INFO/PKG-INFOM0D~m`ەL$j5Z/ЄےR]#!x`w2cC(_ؿd9F"w}ԗfO(zhpF1>(w)Ǧ+^: lu_ϐ;YnHC۪`&>o.&Uܦ~q0fx >L>PKLt-KEGG-INFO/SOURCES.txtu0 !c"F⹙PQV.OC{GY, X`йPjEFb[Y1!kX@Fb%lbz0f Yv%i>0qoPKL2EGG-INFO/dependency_links.txtPKL?]f4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNE`X5PKL?EGG-INFO/namespace_packages.txtKLMJzIE\PKL ` EGG-INFO/requires.txtKLMKPKL߳EGG-INFO/top_level.txtKLMPKL2EGG-INFO/zip-safePKeJL>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKL(acme/__pycache__/__init__.cpython-36.pyc3c% 0 Tf,TfFF`M[EEɩ~̷3s Jo &$%$&sW2MJ3sRR2Kr+t t+,LSsSe+aRjr"ȽPKeJLqqxacme/bar/__init__.pyA 0E9ŀ hEt]J=A1 X<y<_+Cy`xǺ] >:!.,?_| [NRm3Tڅ7.[<;3?PKeJLAacme/bar/bar_plugin.pyRj0+ (HP-%UBئIދ;3;ZJ){vU(uMJ^p_ k\ Sz!VNe'dj^cTdʳ">4T^VԘDŊ Ad+%x&%Z{BpG1$koF5r'^T&l-KWU;G;|IKbg'v,ǵL: c0EぁlFr%PKLҏԵ,acme/bar/__pycache__/__init__.cpython-36.pyc3c% 0 Tf,TfFF`M[EEɩ~̷3s Jo &$%$&sW2M̓J3sRR2Kr+t t+,LSsSe+aRjr&PKL`?..acme/bar/__pycache__/bar_plugin.cpython-36.pycRM@"xpXM1fM1ٞ2c%ݬ'<MEyQ?B~z9G" P`-drpJ.9%ppp5u I4ԜFu^Սiϑ˪*C[@DRf@LrsK7X }GM؈4+uH4+ZG0BUQoGWmjڨETͧu~c,ZmwbyX7mRYܜ=;O&g7/&jr1؆c(Ԡ_^٤G=97L9"U}t=FƷ\aDyxgq Vi օRi1L|ʐc-&n} cLKAPr.'3ǸP&]$sc߉ Zm_pvRyt6kkyHC/PKL2>EGG-INFO/PKG-INFOPKLt-KEGG-INFO/SOURCES.txtPKL2EGG-INFO/dependency_links.txtPKL?]f4KEGG-INFO/entry_points.txtPKL?5EGG-INFO/namespace_packages.txtPKL ` EGG-INFO/requires.txtPKL߳EGG-INFO/top_level.txtPKL2EGG-INFO/zip-safePKeJL>"w+acme/__init__.pyPKL(acme/__pycache__/__init__.cpython-36.pycPKeJLqqxacme/bar/__init__.pyPKeJLAqacme/bar/bar_plugin.pyPKLҏԵ,acme/bar/__pycache__/__init__.cpython-36.pycPKL`?..acme/bar/__pycache__/bar_plugin.cpython-36.pycPK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.bar-0.1a1-py3.7.egg0000644000076500000240000000675600000000000024245 0ustar00mdickinsonstaff00000000000000PKDKN}h6EGG-INFO/PKG-INFOE 0D| 6+  &%okWg3g 15Wb5K(ٿŚBa>:\Ц>Dw~y@7h|8/ N΢OhԬd{6_mPKDKNt-KEGG-INFO/SOURCES.txtu0 !c"F⹙PQV.OC{GY, X`йPjEFb[Y1!kX@Fb%lbz0f Yv%i>0qoPKDKN2EGG-INFO/dependency_links.txtPKDKN?]f4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNE`X5PKDKN?EGG-INFO/namespace_packages.txtKLMJzIE\PKDKN ` EGG-INFO/requires.txtKLMKPKDKN߳EGG-INFO/top_level.txtKLMPKDKN2EGG-INFO/zip-safePKz*M>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKDKN\"(acme/__pycache__/__init__.cpython-37.pycsbݵӢ%3 f vb! ʐ̸1i!##C c0&-ނҢb?M[\E%SRsRsS Soqă9+X_&I9)I)%zz&ff&ɹ@2fTO)Ic`PKz*Mqqxacme/bar/__init__.pyA 0E9ŀ hEt]J=A1 X<y<_+Cy`xǺ] >:!.,?_| [NRm3Tڅ7.[<;3?PKz*MAacme/bar/bar_plugin.pyRj0+ (HP-%UBئIދ;3;ZJ){vU(uMJ^p_ k\ Sz!VNe'dj^cTdʳ">4T^VԘDŊ Ad+%x&%Z{BpG1$koF5r'^T&l-KWU;G;|IKbg'v,ǵL: c0EぁlFr%PKDKNxnp,acme/bar/__pycache__/__init__.cpython-37.pycsbݵӢ3 f vb! ʐ̸1i!##C c0&-ނҢb?M[\E%SRsRsS Soqă9+X_I9)I)%zz&ff&ɹIE@2TO)IchPKDKNN`3.acme/bar/__pycache__/bar_plugin.cpython-37.pycRMn@Ip)*;Vʋ:UB)"f5,F,ٱ;D*g3poYqVq(E0Ҽ~ _,% Ii$;Sp)yM٭#]IoN{NsVzߖ7F'el';Ƣ(̀JJVX >҃v[\lD:^$f^ʯ#~&ÊQoMbA-UΧ~m,(JMVwbyXg9M\$iYߜ]㳛Ku9rd]Еݪ>nGUǣ#`d: $[,eC{OJY͢a+Z%V*U-N~+ee`+W7Tw ]{2-,rsA'D7E9.RIM1I gGuF+om4pPKDKN}h6EGG-INFO/PKG-INFOPKDKNt-KEGG-INFO/SOURCES.txtPKDKN2EGG-INFO/dependency_links.txtPKDKN?]f4KEGG-INFO/entry_points.txtPKDKN?)EGG-INFO/namespace_packages.txtPKDKN ` tEGG-INFO/requires.txtPKDKN߳EGG-INFO/top_level.txtPKDKN2EGG-INFO/zip-safePKz*M>"wacme/__init__.pyPKDKN\"(acme/__pycache__/__init__.cpython-37.pycPKz*Mqqxacme/bar/__init__.pyPKz*MAiacme/bar/bar_plugin.pyPKDKNxnp,acme/bar/__pycache__/__init__.cpython-37.pycPKDKNN`3.acme/bar/__pycache__/bar_plugin.cpython-37.pycPK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571145796.0 envisage-4.9.0/envisage/tests/eggs/acme.bar-0.1a1-py3.8.egg0000644000076500000240000000733200000000000024235 0ustar00mdickinsonstaff00000000000000PKD0O}h6EGG-INFO/PKG-INFOE 0D| 6+  &%okWg3g 15Wb5K(ٿŚBa>:\Ц>Dw~y@7h|8/ N΢OhԬd{6_mPKD0Ot-KEGG-INFO/SOURCES.txtu0 !c"F⹙PQV.OC{GY, X`йPjEFb[Y1!kX@Fb%lbz0f Yv%i>0qoPKD0O2EGG-INFO/dependency_links.txtPKD0O?]f4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNE`X5PKD0O?EGG-INFO/namespace_packages.txtKLMJzIE\PKD0O ` EGG-INFO/requires.txtKLMKPKD0O߳EGG-INFO/top_level.txtKLMPKD0O2EGG-INFO/zip-safePK[A0O>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKD0OT;7(acme/__pycache__/__init__.cpython-38.pyc b:X F hXH2043.`LeZȐ̠t ;=(8(9OW||fnA~QI|-Ģ[`N|J"V`IRifN~RJfq^nbr~qnY~jz~brn>Lz8lrSJsR؀f|9PK[A0Oqqxacme/bar/__init__.pyA 0E9ŀ hEt]J=A1 X<y<_+Cy`xǺ] >:!.,?_| [NRm3Tڅ7.[<;3?PK[A0OѮ cacme/bar/bar_plugin.pySM0WL{JHU[ {5:(c'lPwWj%{ϓ!cX{rrp3}x3:M t&p}zbJAУ;`Ő^zG:se" @: j!k<:_G ']ڥozŸ\ h Hm Tg:QO^-dF{+kkA`D1Edt ~R¤"5؇`oL3,pD!zw}`,j- ? ;(ܕ`Uܑ_, "{:N k\𬨝iAzCƭ )Z<8.R{ ɶz>ƷzRB\%K7Z 3fVtwaj*)J^5ȶd1%4Yw9;X yrqBRP`zQ`)ȑP۸br\}t`oV3,<\)/PKD0ON"s,acme/bar/__pycache__/__init__.cpython-38.pyc b:XiF hXH2043.`LeZȐ̠t ;=(8(9OW||fnA~QI|-Ģ[`N|J"V`ERifN~RJfq^nbr~qnY~jz~brn~Rb>ІLz8lrSJsR؀|9PKD0On/%+.acme/bar/__pycache__/bar_plugin.cpython-38.pycj0%Y4nJuZc0hvcB230eJ^W;rІLOG>\?^&~:C2Q[;SW['v"NR̖ts>weQd!#Mbx4v\۞ 3 SVB%fʌzEw&߉$P1#⫧UL֘g$ ɚ5~elQ5FtyM{5[ gVunNϢ y1|h љr (5n;5YG=zN4E x:1a+G{`.r24BʅʵMGʼef뮔Km;P$X}`Ʒ"?4wa'ք ]mg2Lϸ_Ju8S6*ߍ7wi PKD0O}h6EGG-INFO/PKG-INFOPKD0Ot-KEGG-INFO/SOURCES.txtPKD0O2EGG-INFO/dependency_links.txtPKD0O?]f4KEGG-INFO/entry_points.txtPKD0O?)EGG-INFO/namespace_packages.txtPKD0O ` tEGG-INFO/requires.txtPKD0O߳EGG-INFO/top_level.txtPKD0O2EGG-INFO/zip-safePK[A0O>"wacme/__init__.pyPKD0OT;7(acme/__pycache__/__init__.cpython-38.pycPK[A0Oqqxacme/bar/__init__.pyPK[A0OѮ cjacme/bar/bar_plugin.pyPKD0ON"s,acme/bar/__pycache__/__init__.cpython-38.pycPKD0On/%+.acme/bar/__pycache__/bar_plugin.cpython-38.pycPK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.baz-0.1a1-py2.7.egg0000644000076500000240000000651700000000000024247 0ustar00mdickinsonstaff00000000000000PKa@{acme/__init__.pyS֥*RVp/,L(QHT0200WHTp+/(x%:(+ĩ"^܂x xmEɩz)9Eyɩ`v|&/PKa@q+acme/__init__.pycE 0 S"<z k!tmu+kxy|:/M`HŸ?ٻ? ZF2bG 4j 2Khƈ<ҝ {(x)fSe4l ~ԶRiG{ɕ172ݦ/PKBnP?#acme/baz/baz_plugin.pyMj0aPN6:@@]\@Ƕ@TW,!I!4-x3>=ڲ @)xVW~4|2Ҳh'C44xL 0V)ׂY :MgTU(^P\:4-qyh* ^ MmgX= =s:A+caLXdIi})PT9+I0K+lE_D~Q7*|c6a;bNo n9ly#I Dy#&.PKa@eacme/baz/baz_plugin.pycj@gWlB =$zZBIGSֹTڻq$Kx-5OZhgFvC{h`?I3)dW ObJTFB-PJGPGP`%\c6L b@NQ6?Ի+/ B}\m22z()K/P," ZF`'[=?< Ԭ 臘Nz$0o^zF7 D0+΂ބ<`$rKj7DOYtٶ묹[uA `(|anܮVs#4j߸s2)Ldg;gkbG><-}ZVUqдf[S8дKǤTMQ A>TQ*(H4q wt O땝)a]cNLJ.ai|"NPKa@{acme/baz/__init__.pyS֥*RVp/,L(QHT0200WHTp+/(x%:(+ĩ"^܂x xmEɩz)9Eyɩ`v|&/PKa@Ѓacme/baz/__init__.pycEA 0E'-p.t+ m PMѭf"83ß?,ޯQt$54faJXܵ8yTi_0֍> W4jT(i180.gS?†N׼&fݖcq,V>8kwvSEO!_PKa@2EGG-INFO/dependency_links.txtPKa@ 4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNU`X5PKa@EGG-INFO/namespace_packages.txtKLMJzIU\PKa@3]EGG-INFO/PKG-INFOMA 0D!fRFomII"t5fx#!B0Z(dΙB%|9[\PpV'"%Ofb;8)SōyWºoqH㉳5ɵKJMC?XbPKa@8@ EGG-INFO/requires.txtKLMKJ,PKa@0^[KEGG-INFO/SOURCES.txtu0 !c"F⹙PQ*.OC{(Fm,I~bйPjEF_b[!!4ĚK6a.ij<>gBRwt >:PKa@߳EGG-INFO/top_level.txtKLMPK}a@EGG-INFO/zip-safePKa@{acme/__init__.pyPKa@q+acme/__init__.pycPKBnP?#~acme/baz/baz_plugin.pyPKa@eacme/baz/baz_plugin.pycPKa@{acme/baz/__init__.pyPKa@Ѓacme/baz/__init__.pycPKa@2bEGG-INFO/dependency_links.txtPKa@ 4KEGG-INFO/entry_points.txtPKa@ EGG-INFO/namespace_packages.txtPKa@3]VEGG-INFO/PKG-INFOPKa@8@ EGG-INFO/requires.txtPKa@0^[KXEGG-INFO/SOURCES.txtPKa@߳ EGG-INFO/top_level.txtPK}a@U EGG-INFO/zip-safePK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.baz-0.1a1-py3.5.egg0000644000076500000240000000675700000000000024254 0ustar00mdickinsonstaff00000000000000PK\r@>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKɔ%J4]\Q(acme/__pycache__/__init__.cpython-35.pycM1 @DF Q$ Olv?!Mn4^i[oaʁf,`^xß<瓳Y dpc@Pdp =45ӂ9)bZv.I\V\i o @m3Y6R\Ԧ_o(]]Lys(vpyXjٕt ?_PK\r@>"wacme/baz/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKWV@rhacme/baz/baz_plugin.pyRj0+ (HP-%EB%6M-^tݙRJ#b`UJ)!d{z>'''<'3^tȅo Wk%$Ylw-&2@ꀮ5fo5f"`IT$-W_rL=90efa8Z_9xσy!dpa@Agbp^.Uk$F+ "YCX M٤m^(6hM0Ys2.&e;]PKɔ%Jwb.acme/baz/__pycache__/baz_plugin.cpython-35.pyc͊@o?1 #Up3d14 "2bV^eSSeH:JZn|a|l] 7zdVTuw*9;:o~cx! *A р\BҀV LRN'_yPU~Sևaθ";P$bF#D',M0$a{.Em:'J-n &3+!3<4MUXv[0 YvCRZ P| /E޴QfUs}q4~6ᓧ\bBկP!Yīպ1g4GSSշLFxJYwT6ygG?Y봔e%6yMZvlErtj1vWɽ#L2-g;,1_?MQZn&Zm3,Rw[l2N؉PKɔ%J2EGG-INFO/dependency_links.txtPKɔ%J 4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNU`X5PKɔ%JEGG-INFO/namespace_packages.txtKLMJzIU\PKɔ%J,EGG-INFO/PKG-INFOE 0D| 6+ J5 E]_mMJ zX839a'$7 zx) Pq0o YBɚLU|l .hS;7eg~X kxbGkIm*Va4=/ZPKɔ%JMh: EGG-INFO/requires.txtKLMKJ,PKɔ%J0^[KEGG-INFO/SOURCES.txtu0 !c"F⹙PQ*.OC{(Fm,I~bйPjEF_b[!!4ĚK6a.ij<>gBRwt >:PKɔ%J߳EGG-INFO/top_level.txtKLMPKɔ%J2EGG-INFO/zip-safePK\r@>"wacme/__init__.pyPKɔ%J4]\Q(acme/__pycache__/__init__.cpython-35.pycPK\r@>"wacme/baz/__init__.pyPKWV@rhFacme/baz/baz_plugin.pyPKɔ%Jl2,acme/baz/__pycache__/__init__.cpython-35.pycPKɔ%Jwb.acme/baz/__pycache__/baz_plugin.cpython-35.pycPKɔ%J2EGG-INFO/dependency_links.txtPKɔ%J 4KEGG-INFO/entry_points.txtPKɔ%JlEGG-INFO/namespace_packages.txtPKɔ%J,EGG-INFO/PKG-INFOPKɔ%JMh: vEGG-INFO/requires.txtPKɔ%J0^[KEGG-INFO/SOURCES.txtPKɔ%J߳v EGG-INFO/top_level.txtPKɔ%J2 EGG-INFO/zip-safePK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.baz-0.1a1-py3.6.egg0000644000076500000240000000675300000000000024251 0ustar00mdickinsonstaff00000000000000PK酪L6EGG-INFO/PKG-INFOM0D~m`ەL4j5Z/ЄےRvj3'3'⎡)ˌi T |›L搳r 0*~}~h󄢃'b[r{gX mKxbGkih]-6Ťnf/-PK酪L0^[KEGG-INFO/SOURCES.txtu0 !c"F⹙PQ*.OC{(Fm,I~bйPjEF_b[!!4ĚK6a.ij<>gBRwt >:PK酪L2EGG-INFO/dependency_links.txtPK酪L 4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNU`X5PK酪LEGG-INFO/namespace_packages.txtKLMJzIU\PK酪LMh: EGG-INFO/requires.txtKLMKJ,PK酪L߳EGG-INFO/top_level.txtKLMPK酪L2EGG-INFO/zip-safePKeJL>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPK酪L(acme/__pycache__/__init__.cpython-36.pyc3c% 0 Tf,TfFF`M[EEɩ~̷3s Jo &$%$&sW2MJ3sRR2Kr+t t+,LSsSe+aRjr"ȽPKeJL>"wacme/baz/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKeJLrhacme/baz/baz_plugin.pyRj0+ (HP-%EB%6M-^tݙRJ#b`UJ)!d{z>'''<'3^tȅo Wk%$Ylw-&2@ꀮ5fo5f"G" P`9-dr'w .9%ppp5u $\|eҞUYeՍhϑ˲C[@DRf@LrsK7X }GMhE:^&,+ZG0BUQë&1MؠV*y: U6u;1\n&K&.oΞӳS5jbCM0]jЯ>wnlRǣk ~_ENHY1[`뿑-{!`'|?QF)ytBuRh@MnXO$&RPHXƱ{wXxp7>?%APr.'3Ǹ˱^_guqReƾ4&ɚīmtז1;PK酪L6EGG-INFO/PKG-INFOPK酪L0^[KEGG-INFO/SOURCES.txtPK酪L2EGG-INFO/dependency_links.txtPK酪L 4KEGG-INFO/entry_points.txtPK酪L6EGG-INFO/namespace_packages.txtPK酪LMh: EGG-INFO/requires.txtPK酪L߳EGG-INFO/top_level.txtPK酪L2EGG-INFO/zip-safePKeJL>"w,acme/__init__.pyPK酪L(acme/__pycache__/__init__.cpython-36.pycPKeJL>"wacme/baz/__init__.pyPKeJLrhqacme/baz/baz_plugin.pyPK酪L ',acme/baz/__pycache__/__init__.cpython-36.pycPK酪L!..acme/baz/__pycache__/baz_plugin.cpython-36.pycPK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.baz-0.1a1-py3.7.egg0000644000076500000240000000675700000000000024256 0ustar00mdickinsonstaff00000000000000PKDKN,EGG-INFO/PKG-INFOE 0D| 6+ J5 E]_mMJ zX839a'$7 zx) Pq0o YBɚLU|l .hS;7eg~X kxbGkIm*Va4=/ZPKDKN0^[KEGG-INFO/SOURCES.txtu0 !c"F⹙PQ*.OC{(Fm,I~bйPjEF_b[!!4ĚK6a.ij<>gBRwt >:PKDKN2EGG-INFO/dependency_links.txtPKDKN 4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNU`X5PKDKNEGG-INFO/namespace_packages.txtKLMJzIU\PKDKNMh: EGG-INFO/requires.txtKLMKJ,PKDKN߳EGG-INFO/top_level.txtKLMPKDKN2EGG-INFO/zip-safePKz*M>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKDKN\"(acme/__pycache__/__init__.cpython-37.pycsbݵӢ%3 f vb! ʐ̸1i!##C c0&-ނҢb?M[\E%SRsRsS Soqă9+X_&I9)I)%zz&ff&ɹ@2fTO)Ic`PKz*M>"wacme/baz/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKz*Mrhacme/baz/baz_plugin.pyRj0+ (HP-%EB%6M-^tݙRJ#b`UJ)!d{z>'''<'3^tȅo Wk%$Ylw-&2@ꀮ5fo5f"҃v[\4"Nr-f^ʯ#~&ÊQ7:6uP#* yZ e&t;1<[n KH:㤨n.Σեj5n Z#by֪Q5Q7$'(-0qyu?}`P<V<~Awi9ёPKDKN,EGG-INFO/PKG-INFOPKDKN0^[KEGG-INFO/SOURCES.txtPKDKN2EGG-INFO/dependency_links.txtPKDKN 4KEGG-INFO/entry_points.txtPKDKN*EGG-INFO/namespace_packages.txtPKDKNMh: uEGG-INFO/requires.txtPKDKN߳EGG-INFO/top_level.txtPKDKN2EGG-INFO/zip-safePKz*M>"w acme/__init__.pyPKDKN\"(acme/__pycache__/__init__.cpython-37.pycPKz*M>"wacme/baz/__init__.pyPKz*Mrhiacme/baz/baz_plugin.pyPKDKNoM,acme/baz/__pycache__/__init__.cpython-37.pycPKDKNN 3.acme/baz/__pycache__/baz_plugin.cpython-37.pycPK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571145796.0 envisage-4.9.0/envisage/tests/eggs/acme.baz-0.1a1-py3.8.egg0000644000076500000240000000733300000000000024246 0ustar00mdickinsonstaff00000000000000PKD0O,EGG-INFO/PKG-INFOE 0D| 6+ J5 E]_mMJ zX839a'$7 zx) Pq0o YBɚLU|l .hS;7eg~X kxbGkIm*Va4=/ZPKD0O0^[KEGG-INFO/SOURCES.txtu0 !c"F⹙PQ*.OC{(Fm,I~bйPjEF_b[!!4ĚK6a.ij<>gBRwt >:PKD0O2EGG-INFO/dependency_links.txtPKD0O 4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Rb- EVNU`X5PKD0OEGG-INFO/namespace_packages.txtKLMJzIU\PKD0OMh: EGG-INFO/requires.txtKLMKJ,PKD0O߳EGG-INFO/top_level.txtKLMPKD0O2EGG-INFO/zip-safePK[A0O>"wacme/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPKD0OT;7(acme/__pycache__/__init__.cpython-38.pyc b:X F hXH2043.`LeZȐ̠t ;=(8(9OW||fnA~QI|-Ģ[`N|J"V`IRifN~RJfq^nbr~qnY~jz~brn>Lz8lrSJsR؀f|9PK[A0O>"wacme/baz/__init__.pyA 0yŀpEYþb㠲1 7Ѩ /-&}׽a`y g |<K X~1HFuX)ĩ60z{pXPK[A0Oo₅ cacme/baz/baz_plugin.pySM0WL{JHU[ xVA]iWŗ$~3=?O0Zaa>l'X7&}A l~CW r+l҃7u8r@֙((aQ t]cL{;GXeitd9O$ KCcQSnU A9J*/m=Jnw'5.xV4 =!Vv|-T|΍QD])=d[=jhl)CZPڎҥyxX+SzM+0tUDrp%d[u%4Yw9;XytqLRP`zQ`)ȑP۸br\}t`}~l~kPKD0Ok?,acme/baz/__pycache__/__init__.cpython-38.pyc b:X F hXH2043.`LeZȐ̠t ;=(8(9OW||fnA~QI|-Ģ[`N|J"V`ERifN~RJfq^nbr~qnY~jz~brn~Rb>ІLz8lrSJsR؀|9PKD0Oys,+.acme/baz/__pycache__/baz_plugin.cpython-38.pycߊ@g&dMW/Y*l u]dHsH$5^/WʭW^y&nƁLN8/~~JlB~q]P`YĨy)yOح\{7f_Tpr7'A2ci|XˢȒC4ۡ(D hd= <^!x4"Nrf{(Ž3&)N$qoWu<$a0L׬u+P`,1Rkpݫ*`4ju}Qbos_PKD0O,EGG-INFO/PKG-INFOPKD0O0^[KEGG-INFO/SOURCES.txtPKD0O2EGG-INFO/dependency_links.txtPKD0O 4KEGG-INFO/entry_points.txtPKD0O*EGG-INFO/namespace_packages.txtPKD0OMh: uEGG-INFO/requires.txtPKD0O߳EGG-INFO/top_level.txtPKD0O2EGG-INFO/zip-safePK[A0O>"w acme/__init__.pyPKD0OT;7(acme/__pycache__/__init__.cpython-38.pycPK[A0O>"wacme/baz/__init__.pyPK[A0Oo₅ cjacme/baz/baz_plugin.pyPKD0Ok?,acme/baz/__pycache__/__init__.cpython-38.pycPKD0Oys,+.acme/baz/__pycache__/baz_plugin.cpython-38.pycPK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.foo-0.1a1-py2.7.egg0000644000076500000240000000630700000000000024253 0ustar00mdickinsonstaff00000000000000PKa@|Z|&acme/__init__.pyNA 0 a "\bIE1 ({3aa_&0&PѶOnй4poqZ8#}~T$DU߀ǚ_8ֺ陬Fgf+<5?wPKa@?ъacme/__init__.pycE 0 SC;x d*B0Vr I'EnZάex Ujp) wSf(\, n1nxԤZEqҌX͠eMÌ$9{4\s:6|PKBnP?"acme/foo/foo_plugin.pyAj0EaPJ6:@B ]\@G,\k[㐤^ObAj2˳l`obcK :cymY& 5SCbNK-C:Mlggm7}MCDJ8 ղr4LV<@gC M$EEj ˲Ezs:AW(}nHm[Hp7KֹK򠚓B "W>(_%b̦쎾 qNo n=1vޜXwĺ;8& ǐ ŃγPKa@Bdacme/foo/foo_plugin.pycA{f:a/;A 8t'6̤tGOc*{&Qzp/V@b*TFB+PI'&ХP`%1g[UFQs-._{Y~㶅x~6h'cJ|bQ0$*8ܢ~GUc'#(ӿ1UggkW&_ѣD % !,J.("6XDL=$s.-^651v65`uI` OPn5B}pgOvYiׇGNϥk"ЛК2-4~O0ǧhcl] ;7Ư&~g8%lR~P0EQBe*+Q8{5Tq1p9`=Fr%T8K~PKa@|Z|&acme/foo/__init__.pyNA 0 a "\bIE1 ({3aa_&0&PѶOnй4poqZ8#}~T$DU߀ǚ_8ֺ陬Fgf+<5?wPKa@جǿacme/foo/__init__.pycEA 0ED \y ݊xpd,Ѧ I>^ 8)13bn~N! cg70K W]d%{%s}AYFrh= )UGIJtJX? {JjQSzsX5 x\PKa@2EGG-INFO/dependency_links.txtPKa@>4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Z~- EVn`X5PKa@̯uEGG-INFO/namespace_packages.txtKLMJzi\PKa@GtEGG-INFO/PKG-INFOM 0DC>JmlIӃoZzx3< > {a= Ka(pabms]@!E=A{[)L q4.T3$Q>4eW0XR8=#mPKa@w,EGG-INFO/SOURCES.txtu 0 K8<%Kœ!9|w3,V'lI~,`}j.9u>u(r@$s%C0]ZI cſHLv@ ~PKa@߳EGG-INFO/top_level.txtKLMPK}a@EGG-INFO/zip-safePKa@|Z|&acme/__init__.pyPKa@?ъacme/__init__.pycPKBnP?"acme/foo/foo_plugin.pyPKa@Bdacme/foo/foo_plugin.pycPKa@|Z|&acme/foo/__init__.pyPKa@جǿacme/foo/__init__.pycPKa@2aEGG-INFO/dependency_links.txtPKa@>4KEGG-INFO/entry_points.txtPKa@̯u EGG-INFO/namespace_packages.txtPKa@GtUEGG-INFO/PKG-INFOPKa@w,EGG-INFO/SOURCES.txtPKa@߳EGG-INFO/top_level.txtPK}a@ EGG-INFO/zip-safePK nC ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.foo-0.1a1-py3.5.egg0000644000076500000240000000655100000000000024253 0ustar00mdickinsonstaff00000000000000PK\r@־øxacme/__init__.pyA 0E9ŀ "ݸ. O1bL$ ޾g>WͭQCOf@=W[Xr`1 &e'l.@<7&\C=GM sGvX-K%ׄWd./cguE)Lכ$J>tS\(*:j?Zv%'PK\r@־øxacme/foo/__init__.pyA 0E9ŀ "ݸ. O1bL$ ޾g>WͭQC"0&`gi-8{ޅ'CAxw>lZGRmiؖ-@%_8Ff~Lae7p>FF6!URPF?@{XˋUQBMWYgO1?ObZh4~}}D3X9~?cUI3cm)QA|LpHhy/Jrލ8XίVYq9*GcBbG$ ~f2M0vN;чNR4BY]hth~ܕG`]Z:bG_PKϔ%J2EGG-INFO/dependency_links.txtPKϔ%J>4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Z~- EVn`X5PKϔ%J̯uEGG-INFO/namespace_packages.txtKLMJzi\PKϔ%J%EGG-INFO/PKG-INFOE 0D| 5' J5 E=/qْ"֞ o.1p4RRX 4ߕJDmȄ-h7厓8vy#Op72D[\1spYo*QR{1zȞPKϔ%Jw,EGG-INFO/SOURCES.txtu 0 K8<%Kœ!9|w3,V'lI~,`}j.9u>u(r@$s%C0]ZI cſHLv@ ~PKϔ%J߳EGG-INFO/top_level.txtKLMPKϔ%J2EGG-INFO/zip-safePK\r@־øxacme/__init__.pyPKϔ%J(acme/__pycache__/__init__.cpython-35.pycPK\r@־øxacme/foo/__init__.pyPKWV@QbIacme/foo/foo_plugin.pyPKϔ%J1X5,acme/foo/__pycache__/__init__.cpython-35.pycPKϔ%J[b.acme/foo/__pycache__/foo_plugin.cpython-35.pycPKϔ%J2EGG-INFO/dependency_links.txtPKϔ%J>4KEGG-INFO/entry_points.txtPKϔ%J̯unEGG-INFO/namespace_packages.txtPKϔ%J%EGG-INFO/PKG-INFOPKϔ%Jw,wEGG-INFO/SOURCES.txtPKϔ%J߳3 EGG-INFO/top_level.txtPKϔ%J2n EGG-INFO/zip-safePK  ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.foo-0.1a1-py3.6.egg0000644000076500000240000000654400000000000024256 0ustar00mdickinsonstaff00000000000000PKLKDEGG-INFO/PKG-INFOM0E~m`ەL4j5zRhtH) ^*nrɽLꉱu&, PWe:\ȇ=ˊjfH G#aʡnJb5҇3cg0ж,DKӷqHcY3*4PKLw,EGG-INFO/SOURCES.txtu 0 K8<%Kœ!9|w3,V'lI~,`}j.9u>u(r@$s%C0]ZI cſHLv@ ~PKL2EGG-INFO/dependency_links.txtPKL>4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Z~- EVn`X5PKL̯uEGG-INFO/namespace_packages.txtKLMJzi\PKL߳EGG-INFO/top_level.txtKLMPKL2EGG-INFO/zip-safePKeJL־øxacme/__init__.pyA 0E9ŀ "ݸ. O1bL$ ޾g>WͭQCWͭQCgu|21$"_ƿnBCw5۲9&5*=`k;*~7R@!h=hhx(ZdRLw;| /D`XUI|dHl2MXnSU\Z߉(*/`tyݤe67t|zr#X\rKﴟ\K4`]ôWc,b{=&3a}7b='(&V(JT;T4*ʊ~*f`=gsCإt>炊@n2M{&N*ac V!V\lb۶G4PKLKDEGG-INFO/PKG-INFOPKLw,EGG-INFO/SOURCES.txtPKL2EGG-INFO/dependency_links.txtPKL>4KEGG-INFO/entry_points.txtPKL̯u/EGG-INFO/namespace_packages.txtPKL߳zEGG-INFO/top_level.txtPKL2EGG-INFO/zip-safePKeJL־øxacme/__init__.pyPKL(acme/__pycache__/__init__.cpython-36.pycPKeJL־øxacme/foo/__init__.pyPKeJLQb.acme/foo/foo_plugin.pyPKLִ{,|acme/foo/__pycache__/__init__.cpython-36.pycPKLj..{acme/foo/__pycache__/foo_plugin.cpython-36.pycPK  ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/eggs/acme.foo-0.1a1-py3.7.egg0000644000076500000240000000655000000000000024254 0ustar00mdickinsonstaff00000000000000PKDKN%EGG-INFO/PKG-INFOE 0D| 5' J5 E=/qْ"֞ o.1p4RRX 4ߕJDmȄ-h7厓8vy#Op72D[\1spYo*QR{1zȞPKDKNw,EGG-INFO/SOURCES.txtu 0 K8<%Kœ!9|w3,V'lI~,`}j.9u>u(r@$s%C0]ZI cſHLv@ ~PKDKN2EGG-INFO/dependency_links.txtPKDKN>4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Z~- EVn`X5PKDKN̯uEGG-INFO/namespace_packages.txtKLMJzi\PKDKN߳EGG-INFO/top_level.txtKLMPKDKN2EGG-INFO/zip-safePKz*M־øxacme/__init__.pyA 0E9ŀ "ݸ. O1bL$ ޾g>WͭQCWͭQCut<58"_FoLmw_Sdl+;(̀JʠFX  >҃v\E:6f^ʯ#~&ÊQGMjA Uͧa0U%uqXd_WMRoNNͳsu>b\ѕ۪>~G U/["&aS 2c6~ 2XyoTx ijJV#>ҢJ$v֝J஁ N\PMfq`ˑ^~tʭ&r4oO/6_mZGtPKDKN%EGG-INFO/PKG-INFOPKDKNw,EGG-INFO/SOURCES.txtPKDKN2zEGG-INFO/dependency_links.txtPKDKN>4KEGG-INFO/entry_points.txtPKDKN̯u#EGG-INFO/namespace_packages.txtPKDKN߳nEGG-INFO/top_level.txtPKDKN2EGG-INFO/zip-safePKz*M־øxacme/__init__.pyPKDKNiL(acme/__pycache__/__init__.cpython-37.pycPKz*M־øx|acme/foo/__init__.pyPKz*MQb&acme/foo/foo_plugin.pyPKDKN[͹,tacme/foo/__pycache__/__init__.cpython-37.pycPKDKN |3.wacme/foo/__pycache__/foo_plugin.cpython-37.pycPK  ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571145796.0 envisage-4.9.0/envisage/tests/eggs/acme.foo-0.1a1-py3.8.egg0000644000076500000240000000712400000000000024253 0ustar00mdickinsonstaff00000000000000PKD0O%EGG-INFO/PKG-INFOE 0D| 5' J5 E=/qْ"֞ o.1p4RRX 4ߕJDmȄ-h7厓8vy#Op72D[\1spYo*QR{1zȞPKD0Ow,EGG-INFO/SOURCES.txtu 0 K8<%Kœ!9|w3,V'lI~,`}j.9u>u(r@$s%C0]ZI cſHLv@ ~PKD0O2EGG-INFO/dependency_links.txtPKD0O>4KEGG-INFO/entry_points.txtRԼT̼Xpbrn^Z~- EVn`X5PKD0O̯uEGG-INFO/namespace_packages.txtKLMJzi\PKD0O߳EGG-INFO/top_level.txtKLMPKD0O2EGG-INFO/zip-safePK[A0O־øxacme/__init__.pyA 0E9ŀ "ݸ. O1bL$ ޾g>WͭQCLz8lrSJsR؀f|9PK[A0O־øxacme/foo/__init__.pyA 0E9ŀ "ݸ. O1bL$ ޾g>WͭQCІLz8lrSJsR؀|9PKD0O~}+.acme/foo/__pycache__/foo_plugin.cpython-38.pycj0%Y4nJuZ(c 0hvcQ330eJ^W;rІLOG>\?^&{~:ErZQ[;SSW;'v$NxqXY6عx;Vyf[E"`1W.8w46< @ >h1┥HBEZSE7wdD`GNL֘g$ Ɋ5~e lR #U*^s/Y+Ȫ:*TW'gչ< l6ΆnUǣ[>c]> 3NEN( ]NFH9O %eӑаmݕf䛎/% Vv. PԚ7A˹LwkV%3%efS$ߎ׶۴wi PKD0O%EGG-INFO/PKG-INFOPKD0Ow,EGG-INFO/SOURCES.txtPKD0O2zEGG-INFO/dependency_links.txtPKD0O>4KEGG-INFO/entry_points.txtPKD0O̯u#EGG-INFO/namespace_packages.txtPKD0O߳nEGG-INFO/top_level.txtPKD0O2EGG-INFO/zip-safePK[A0O־øxacme/__init__.pyPKD0Ozb(acme/__pycache__/__init__.cpython-38.pycPK[A0O־øx}acme/foo/__init__.pyPK[A0O֔m c'acme/foo/foo_plugin.pyPKD0O`8,dacme/foo/__pycache__/__init__.cpython-38.pycPKD0O~}+.hacme/foo/__pycache__/foo_plugin.cpython-38.pycPK  ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/ets_config_patcher.py0000644000076500000240000000371600000000000023761 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! import os import shutil import tempfile class ETSConfigPatcher(object): """ Object that patches the directories in ETSConfig, to avoid having tests write to the home directory. """ def __init__(self): from traits.etsconfig.api import ETSConfig self.etsconfig = ETSConfig self.tmpdir = None self.old_application_data = None self.old_application_home = None self.old_user_data = None def start(self): tmpdir = self.tmpdir = tempfile.mkdtemp() self.old_application_data = self.etsconfig._application_data self.etsconfig._application_data = os.path.join( tmpdir, "application_data") self.old_application_home = self.etsconfig._application_home self.etsconfig._application_home = os.path.join( tmpdir, "application_home") self.old_user_data = self.etsconfig._user_data self.etsconfig._user_data = os.path.join(tmpdir, "user_home") def stop(self): if self.old_user_data is not None: self.etsconfig._user_data = self.old_user_data self.old_user_data = None if self.old_application_home is not None: self.etsconfig._application_home = self.old_application_home self.old_application_home = None if self.old_application_data is not None: self.etsconfig._application_data = self.old_application_data self.old_application_data = None if self.tmpdir is not None: shutil.rmtree(self.tmpdir) self.tmpdir = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/event_tracker.py0000644000076500000240000000606300000000000022765 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Used to track events in tests. """ # Enthought library imports. from traits.api import HasTraits, List, Str, Tuple class EventTracker(HasTraits): """ Used to track traits events. """ # The traits events that have fired. # # This is a list of tuples in the form:- # # (obj, trait_name, old, new) events = List(Tuple) # The names of the traits events that have fired. # # This is useful if you just care about the order of the events, not the # contents. event_names = List(Str) # The trait event subscriptions used by the tracker. # # This is a list of tuples in the form:- # # (obj, trait_name) # # Where 'obj' is the object to listen to, and 'trait_name' is the name of # the trait to listen to, or None to listen for all trait events. subscriptions = List(Tuple) ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ def _subscriptions_changed(self, old, new): """ Static trait change handler. """ for subscription in old: self._remove_subscription(subscription) for subscription in new: self._add_subscription(subscription) return def _subscriptions_items_changed(self, event): """ Static trait change handler. """ for subscription in event.removed: self._remove_subscription(subscription) for subscription in event.added: self._add_subscription(subscription) return def _listener(self, obj, trait_name, old, new): """ Dynamic trait change listener. """ self.events.append((obj, trait_name, old, new)) self.event_names.append(trait_name) return #### Methods ############################################################## def _add_subscription(self, subscription): """ Add a subscription. """ obj, trait_name = subscription if trait_name is not None: obj.on_trait_change(self._listener, trait_name) else: obj.on_trait_change(self._listener) return def _remove_subscription(self, subscription): """ Remove a subscription. """ obj, trait_name = subscription if trait_name is not None: obj.on_trait_change(self._listener, trait_name, remove=True) else: obj.on_trait_change(self._listener, remove=True) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/foo.py0000644000076500000240000000130600000000000020707 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A test class used in the service registry tests! """ # Enthought library imports. from traits.api import HasTraits, provides # Local imports. from .i_foo import IFoo @provides(IFoo) class Foo(HasTraits): pass #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/i_foo.py0000644000076500000240000000116500000000000021222 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A test class used to test adapters. """ # Enthought library imports. from traits.api import Interface class IFoo(Interface): pass #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/mutable_extension_registry.py0000644000076500000240000000473500000000000025612 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A mutable, manually populated extension registry used for testing. """ # Enthought library imports. from envisage.api import ExtensionRegistry, UnknownExtension class MutableExtensionRegistry(ExtensionRegistry): """ A mutable, manually populated extension registry used for testing. """ ########################################################################### # 'MutableExtensionRegistry' interface. ########################################################################### def add_extension(self, extension_point_id, extension): """ Contribute an extension to an extension point. """ self.add_extensions(extension_point_id, [extension]) return def add_extensions(self, extension_point_id, extensions): """ Contribute a list of extensions to an extension point. """ self._check_extension_point(extension_point_id) old = self._get_extensions(extension_point_id) index = len(old) old.extend(extensions) # Let any listeners know that the extensions have been added. refs = self._get_listener_refs(extension_point_id) self._call_listeners(refs, extension_point_id, extensions, [], index) return def remove_extension(self, extension_point_id, extension): """ Remove a contribution from an extension point. """ self.remove_extensions(extension_point_id, [extension]) return def remove_extensions(self, extension_point_id, extensions): """ Remove a list of contributions from an extension point. """ for extension in extensions: try: self._get_extensions(extension_point_id).remove(extension) except ValueError: raise UnknownExtension(extension_point_id, extension) # Let any listeners know that the extensions have been removed. refs = self._get_listener_refs(extension_point_id) self._call_listeners(refs, extension_point_id, [], extensions, None) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.203482 envisage-4.9.0/envisage/tests/plugins/0000755000076500000240000000000000000000000021233 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2606454 envisage-4.9.0/envisage/tests/plugins/banana/0000755000076500000240000000000000000000000022453 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/plugins/banana/__init__.py0000644000076500000240000000000000000000000024552 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/plugins/banana/banana_plugin.py0000644000076500000240000000222300000000000025622 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The 'Banana' plugin """ from envisage.api import Plugin from traits.api import Bool class BananaPlugin(Plugin): """ The 'Banana' plugin """ #### 'IPlugin' protocol #################################################### # The plugin's unique identifier. id = 'banana' def start(self): """ Start the plugin. """ self.started = True self.stopped = False return def stop(self): """ Stop the plugin. """ self.started = False self.stopped = True return #### 'BananaPlugin' protocol ############################################### started = Bool(False) stopped = Bool(False) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/plugins/banana/plugins.py0000644000076500000240000000124500000000000024510 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Plugin finder for the current package. """ def get_plugins(): """ Get the plugins from this package. """ from .banana_plugin import BananaPlugin return [BananaPlugin()] #### EOF ####################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2621138 envisage-4.9.0/envisage/tests/plugins/orange/0000755000076500000240000000000000000000000022506 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/plugins/orange/__init__.py0000644000076500000240000000000000000000000024605 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/plugins/orange/orange_plugin.py0000644000076500000240000000222300000000000025710 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The 'Orange' plugin """ from envisage.api import Plugin from traits.api import Bool class OrangePlugin(Plugin): """ The 'Orange' plugin """ #### 'IPlugin' protocol #################################################### # The plugin's unique identifier. id = 'orange' def start(self): """ Start the plugin. """ self.started = True self.stopped = False return def stop(self): """ Stop the plugin. """ self.started = False self.stopped = True return #### 'BananaPlugin' protocol ############################################### started = Bool(False) stopped = Bool(False) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/plugins/orange/plugins.py0000644000076500000240000000124500000000000024543 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Plugin finder for the current package. """ def get_plugins(): """ Get the plugins from this package. """ from .orange_plugin import OrangePlugin return [OrangePlugin()] #### EOF ####################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2627518 envisage-4.9.0/envisage/tests/plugins/pear/0000755000076500000240000000000000000000000022162 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/plugins/pear/__init__.py0000644000076500000240000000000000000000000024261 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/plugins/pear/pear_plugin.py0000644000076500000240000000221300000000000025037 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The 'Pear' plugin """ from envisage.api import Plugin from traits.api import Bool class PearPlugin(Plugin): """ The 'Pear' plugin """ #### 'IPlugin' protocol #################################################### # The plugin's unique identifier. id = 'pear' def start(self): """ Start the plugin. """ self.started = True self.stopped = False return def stop(self): """ Stop the plugin. """ self.started = False self.stopped = True return #### 'BananaPlugin' protocol ############################################### started = Bool(False) stopped = Bool(False) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/preferences.ini0000644000076500000240000000003000000000000022545 0ustar00mdickinsonstaff00000000000000[enthought.test] x = 42 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_application.py0000644000076500000240000003267500000000000023503 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for applications and plugins. """ # Standard library imports. import os import shutil import tempfile import unittest # Enthought library imports. from traits.etsconfig.api import ETSConfig from envisage.api import Application, ExtensionPoint from envisage.api import Plugin, PluginManager from traits.api import Bool, Int, List # Local imports. from envisage.tests.event_tracker import EventTracker from envisage.tests.ets_config_patcher import ETSConfigPatcher 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 def vetoer(event): """ A function that will veto an event. """ event.veto = True class TestApplication(Application): """ The type of application used in the tests. """ id = 'test' class SimplePlugin(Plugin): """ A simple plugin. """ #### 'SimplePlugin' interface ############################################# started = Bool(False) stopped = Bool(False) ########################################################################### # 'IPlugin' interface. ########################################################################### def start(self): """ Start the plugin. """ self.started = True self.stopped = False def stop(self): """ Stop the plugin. """ self.started = False self.stopped = True class BadPlugin(Plugin): """ A plugin that just causes trouble ;^). """ ########################################################################### # 'IPlugin' interface. ########################################################################### def start(self): """ Start the plugin. """ raise 1/0 def stop(self): """ Stop the plugin. """ raise 1/0 class PluginA(Plugin): """ A plugin that offers an extension point. """ id = 'A' x = ExtensionPoint(List, id='a.x') class PluginB(Plugin): """ A plugin that contributes to an extension point. """ id = 'B' x = List(Int, [1, 2, 3], contributes_to='a.x') class PluginC(Plugin): """ Another plugin that contributes to an extension point! """ id = 'C' x = List(Int, [98, 99, 100], contributes_to='a.x') class ApplicationTestCase(unittest.TestCase): """ Tests for applications and plugins. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ # Make sure that the listener contents get cleand up before each test. listener.obj = None listener.trait_name = None listener.old = None listener.new = None ets_config_patcher = ETSConfigPatcher() ets_config_patcher.start() self.addCleanup(ets_config_patcher.stop) def test_home(self): """ home """ application = TestApplication() # Make sure we get the right default value. self.assertEqual(ETSConfig.application_home, application.home) # Delete the directory. shutil.rmtree(application.home) # Create a new application. application = TestApplication() # Make sure the directory got created. self.assertTrue(os.path.exists(application.home)) # Delete the directory. shutil.rmtree(application.home) def test_no_plugins(self): """ no plugins """ application = TestApplication() tracker = EventTracker( subscriptions = [ (application, 'starting'), (application, 'started'), (application, 'stopping'), (application, 'stopped') ] ) # Start the application. started = application.start() self.assertEqual(True, started) self.assertEqual(['starting', 'started'], tracker.event_names) # Stop the application. stopped = application.stop() self.assertEqual(True, stopped) self.assertEqual( ['starting', 'started', 'stopping', 'stopped'], tracker.event_names ) def test_veto_starting(self): """ veto starting """ application = TestApplication() # This listener will veto the 'starting' event. application.on_trait_change(vetoer, 'starting') tracker = EventTracker( subscriptions = [ (application, 'starting'), (application, 'started'), (application, 'stopping'), (application, 'stopped') ] ) # Start the application. started = application.start() self.assertEqual(False, started) self.assertTrue('started' not in tracker.event_names) def test_veto_stopping(self): """ veto stopping """ application = TestApplication() # This listener will veto the 'stopping' event. application.on_trait_change(vetoer, 'stopping') tracker = EventTracker( subscriptions = [ (application, 'starting'), (application, 'started'), (application, 'stopping'), (application, 'stopped') ] ) # Start the application. started = application.start() self.assertEqual(['starting', 'started'], tracker.event_names) self.assertEqual(True, started) # Stop the application. stopped = application.stop() self.assertEqual(False, stopped) self.assertTrue('stopped' not in tracker.event_names) def test_start_and_stop_errors(self): """ start and stop errors """ simple_plugin = SimplePlugin() bad_plugin = BadPlugin() application = TestApplication(plugins=[simple_plugin, bad_plugin]) # Try to start the application - the bad plugin should barf. with self.assertRaises(ZeroDivisionError): application.start() # Try to stop the application - the bad plugin should barf. with self.assertRaises(ZeroDivisionError): application.stop() # Try to start a non-existent plugin. with self.assertRaises(SystemError): application.start_plugin(plugin_id="bogus") # Try to stop a non-existent plugin. with self.assertRaises(SystemError): application.stop_plugin(plugin_id="bogus") def test_extension_point(self): """ extension point """ a = PluginA() b = PluginB() c = PluginC() application = TestApplication(plugins=[a, b, c]) application.start() # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) # Make sure we can get the contributions via the plugin. extensions = a.x self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) def test_add_extension_point_listener(self): """ add extension point listener """ a = PluginA() b = PluginB() c = PluginC() # Start off with just two of the plugins. application = TestApplication(plugins=[a, b]) application.start() def listener(extension_registry, event): """ An extension point listener. """ listener.extension_point_id = event.extension_point_id listener.added = event.added listener.removed = event.removed # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') self.assertEqual(3, len(extensions)) self.assertEqual([1, 2, 3], extensions) # Add the listener. application.add_extension_point_listener(listener, 'a.x') # Now add the other plugin. application.add_plugin(c) # Make sure the listener was called. self.assertEqual('a.x', listener.extension_point_id) self.assertEqual([], listener.removed) self.assertEqual([98, 99, 100], listener.added) def test_remove_extension_point_listener(self): """ remove extension point listener """ a = PluginA() b = PluginB() c = PluginC() # Start off with just one of the plugins. application = TestApplication(plugins=[a]) application.start() def listener(extension_registry, event): """ An extension point listener. """ listener.extension_point_id = event.extension_point_id listener.added = event.added listener.removed = event.removed # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') self.assertEqual(0, len(extensions)) # Add the listener. application.add_extension_point_listener(listener, 'a.x') # Now add one of the other plugins. application.add_plugin(b) # Make sure the listener was called. self.assertEqual('a.x', listener.extension_point_id) self.assertEqual([], listener.removed) self.assertEqual([1, 2, 3], listener.added) # Now remove the listener. listener.extension_point_id = None application.remove_extension_point_listener(listener, 'a.x') # Now add the final plugin. application.add_plugin(c) # Make sure the listener was *not* called. self.assertEqual(None, listener.extension_point_id) def test_add_plugin(self): """ add plugin """ a = PluginA() b = PluginB() c = PluginC() # Start off with just two of the plugins. application = TestApplication(plugins=[a, b]) application.start() # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') self.assertEqual(3, len(extensions)) self.assertEqual([1, 2, 3], extensions) # Make sure we can get the contributions via the plugin. extensions = a.x self.assertEqual(3, len(extensions)) self.assertEqual([1, 2, 3], extensions) # Now add the other plugin. application.add_plugin(c) # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) # Make sure we can get the contributions via the plugin. extensions = a.x self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) def test_get_plugin(self): """ get plugin """ a = PluginA() b = PluginB() c = PluginC() # Start off with just two of the plugins. application = TestApplication(plugins=[a, b, c]) application.start() # Make sure we can get the plugins. self.assertEqual(a, application.get_plugin('A')) self.assertEqual(b, application.get_plugin('B')) self.assertEqual(c, application.get_plugin('C')) # Make sure we can't get one that isn't there ;^) self.assertEqual(None, application.get_plugin('BOGUS')) def test_remove_plugin(self): """ remove plugin """ a = PluginA() b = PluginB() c = PluginC() application = TestApplication(plugins=[a, b, c]) application.start() # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) # Make sure we can get the contributions via the plugin. extensions = a.x self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) # Now remove one plugin. application.remove_plugin(b) # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') self.assertEqual(3, len(extensions)) self.assertEqual([98, 99, 100], extensions) # Make sure we can get the contributions via the plugin. extensions = a.x self.assertEqual(3, len(extensions)) self.assertEqual([98, 99, 100], extensions) def test_set_plugin_manager_at_contruction_time(self): """ set plugin manager at construction time""" a = PluginA() b = PluginB() c = PluginC() # Start off with just two of the plugins. application = TestApplication( plugin_manager=PluginManager(plugins=[a, b, c]) ) application.start() # Make sure we can get the plugins. self.assertEqual(a, application.get_plugin('A')) self.assertEqual(b, application.get_plugin('B')) self.assertEqual(c, application.get_plugin('C')) # Make sure we can't get one that isn't there ;^) self.assertEqual(None, application.get_plugin('BOGUS')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_class_load_hook.py0000644000076500000240000000563200000000000024315 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for class load hooks. """ from envisage.api import ClassLoadHook from traits.api import HasTraits from traits.testing.unittest_tools import unittest # This module's package. PKG = 'envisage.tests' class ClassLoadHookTestCase(unittest.TestCase): """ Tests for class load hooks. """ def test_connect(self): """ connect """ def on_class_loaded(cls): """ Called when a class is loaded. """ on_class_loaded.cls = cls # To register with 'MetaHasTraits' we use 'module_name.class_name'. hook = ClassLoadHook( class_name = ClassLoadHookTestCase.__module__ + '.Foo', on_load = on_class_loaded ) hook.connect() class Foo(HasTraits): pass self.assertEqual(Foo, on_class_loaded.cls) def test_class_already_loaded(self): """ class already loaded """ def on_class_loaded(cls): """ Called when a class is loaded. """ on_class_loaded.cls = cls # To register with 'MetaHasTraits' we use 'module_name.class_name'. hook = ClassLoadHook( class_name = self._get_full_class_name(ClassLoadHookTestCase), on_load = on_class_loaded ) hook.connect() # Make sure the 'on_load' got called immediately because the class is # already loaded. self.assertEqual(ClassLoadHookTestCase, on_class_loaded.cls) def test_disconnect(self): """ disconnect """ def on_class_loaded(cls): """ Called when a class is loaded. """ on_class_loaded.cls = cls # To register with 'MetaHasTraits' we use 'module_name.class_name'. hook = ClassLoadHook( class_name = ClassLoadHookTestCase.__module__ + '.Foo', on_load = on_class_loaded ) hook.connect() class Foo(HasTraits): pass self.assertEqual(Foo, on_class_loaded.cls) # 'Reset' the listener, on_class_loaded.cls = None # Now disconnect. hook.disconnect() class Foo(HasTraits): pass self.assertEqual(None, on_class_loaded.cls) ########################################################################### # Private interface. ########################################################################### def _get_full_class_name(self, cls): """ Return the full (possibly) dotted name of a class. """ return cls.__module__ + '.' + cls.__name__ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_composite_plugin_manager.py0000644000076500000240000001414400000000000026241 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the composite plugin manager. """ from envisage.application import Application from envisage.composite_plugin_manager import CompositePluginManager from envisage.plugin_manager import PluginManager from envisage.plugin import Plugin from traits.api import Bool from traits.testing.unittest_tools import unittest class SimplePlugin(Plugin): """ A simple plugin. """ #### 'SimplePlugin' protocol ############################################### started = Bool(False) stopped = Bool(False) #### 'IPlugin' protocol ################################################### def start(self): """ Start the plugin. """ self.started = True self.stopped = False def stop(self): """ Stop the plugin. """ self.started = False self.stopped = True class CustomException(Exception): """ Custom exception used for testing purposes. """ pass class RaisingPluginManager(PluginManager): """ A PluginManager that raises on iteration. """ def __iter__(self): raise CustomException("Something went wrong.") class CompositePluginManagerTestCase(unittest.TestCase): """ Tests for the composite plugin manager. """ def test_find_no_plugins_if_there_are_no_plugin_managers(self): plugin_manager = CompositePluginManager() ids = [plugin.id for plugin in plugin_manager] self.assertEqual(0, len(ids)) def test_find_no_plugins_if_there_are_no_plugins_in_plugin_managers(self): plugin_manager = CompositePluginManager( plugin_managers=[PluginManager(), PluginManager()] ) ids = [plugin.id for plugin in plugin_manager] self.assertEqual(0, len(ids)) def test_find_plugins_in_a_single_plugin_manager(self): plugin_manager = CompositePluginManager( plugin_managers=[ PluginManager( plugins=[SimplePlugin(id='red'), SimplePlugin(id='yellow')] ) ] ) ids = [plugin.id for plugin in plugin_manager] self.assertEqual(2, len(ids)) self.assertIn('red', ids) self.assertIn('yellow', ids) self._test_start_and_stop(plugin_manager, ['red', 'yellow']) def test_find_plugins_in_a_multiple_plugin_managers(self): plugin_manager = CompositePluginManager( plugin_managers=[ PluginManager( plugins=[SimplePlugin(id='red'), SimplePlugin(id='yellow')] ), PluginManager( plugins=[SimplePlugin(id='green')] ) ] ) ids = [plugin.id for plugin in plugin_manager] self.assertEqual(3, len(ids)) self.assertIn('red', ids) self.assertIn('yellow', ids) self.assertIn('green', ids) self._test_start_and_stop(plugin_manager, ['red', 'yellow', 'green']) def test_application_gets_propogated_to_plugin_managers(self): application = Application() composite_plugin_manager = CompositePluginManager( application = application, plugin_managers = [PluginManager(), PluginManager()] ) for plugin_manager in composite_plugin_manager.plugin_managers: self.assertEqual(application, plugin_manager.application) def test_propogate_plugin_added_or_remove_events_from_plugin_managers(self): a = PluginManager() b = PluginManager() composite_plugin_manager = CompositePluginManager( plugin_managers = [a, b] ) composite_plugin_manager._plugins def added(obj, trait_name, old, new): added.count += 1 added.count = 0 composite_plugin_manager.on_trait_change(added, 'plugin_added') def removed(obj, trait_name, old, new): removed.count += 1 removed.count = 0 composite_plugin_manager.on_trait_change(removed, 'plugin_removed') a.add_plugin(Plugin(id='foo')) self.assertEqual(1, self._plugin_count(composite_plugin_manager)) a.remove_plugin(a.get_plugin('foo')) self.assertEqual(0, self._plugin_count(composite_plugin_manager)) def test_correct_exception_propagated_from_plugin_manager(self): plugin_manager = CompositePluginManager( plugin_managers=[RaisingPluginManager()] ) with self.assertRaises(CustomException): plugin_manager.start() #### Private protocol ##################################################### def _plugin_count(self, plugin_manager): """ Return how many plugins the plugin manager contains. """ count = 0 for plugin in plugin_manager: count += 1 return count def _test_start_and_stop(self, plugin_manager, expected): """ Make sure the plugin manager starts and stops the expected plugins. """ # Make sure the plugin manager found only the required plugins. self.assertEqual(expected, [plugin.id for plugin in plugin_manager]) # Start the plugin manager. This starts all of the plugin manager's # plugins. plugin_manager.start() # Make sure all of the the plugins were started. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.started) # Stop the plugin manager. This stops all of the plugin manager's # plugins. plugin_manager.stop() # Make sure all of the the plugins were stopped. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.stopped) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_core_plugin.py0000644000076500000240000002513600000000000023500 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the core plugin. """ # Major package imports. from pkg_resources import resource_filename # Enthought library imports. from envisage.api import Application, Category, ClassLoadHook, Plugin from envisage.api import ServiceOffer from traits.api import HasTraits, Int, Interface, List from traits.testing.unittest_tools import unittest # This module's package. PKG = 'envisage.tests' class TestApplication(Application): """ The type of application used in the tests. """ id = 'core.plugin.test' class CorePluginTestCase(unittest.TestCase): """ Tests for the core plugin. """ def test_service_offers(self): """ service offers """ from envisage.core_plugin import CorePlugin class IMyService(Interface): pass class PluginA(Plugin): id = 'A' service_offers = List( contributes_to='envisage.service_offers' ) def _service_offers_default(self): """ Trait initializer. """ service_offers = [ ServiceOffer( protocol=IMyService, factory=self._my_service_factory ) ] return service_offers def _my_service_factory(self, **properties): """ Service factory. """ return 42 core = CorePlugin() a = PluginA() application = TestApplication(plugins=[core, a]) application.start() # Lookup the service. self.assertEqual(42, application.get_service(IMyService)) # Stop the core plugin. application.stop_plugin(core) # Make sure th service has gone. self.assertEqual(None, application.get_service(IMyService)) def test_dynamically_added_service_offer(self): """ dynamically added service offer """ from envisage.core_plugin import CorePlugin class IMyService(Interface): pass class PluginA(Plugin): id = 'A' service_offers = List( contributes_to='envisage.service_offers' ) def _service_offers_default(self): """ Trait initializer. """ service_offers = [ ServiceOffer( protocol=IMyService, factory=self._my_service_factory ) ] return service_offers def _my_service_factory(self, **properties): """ Service factory. """ return 42 core = CorePlugin() a = PluginA() # Start off with just the core plugin. application = TestApplication(plugins=[core]) application.start() # Make sure the service does not exist! service = application.get_service(IMyService) self.assertIsNone(service) # Make sure the service offer exists... extensions = application.get_extensions('envisage.service_offers') self.assertEqual(0, len(extensions)) # Now add a plugin that contains a service offer. application.add_plugin(a) # Make sure the service offer exists... extensions = application.get_extensions('envisage.service_offers') self.assertEqual(1, len(extensions)) # ... and that the core plugin responded to the new service offer and # published it in the service registry. service = application.get_service(IMyService) self.assertEqual(42, service) def test_categories(self): """ categories """ from envisage.core_plugin import CorePlugin class PluginA(Plugin): id = 'A' categories = List(contributes_to='envisage.categories') def _categories_default(self): """ Trait initializer. """ bar_category = Category( class_name = PKG + '.bar_category.BarCategory', target_class_name = CorePluginTestCase.__module__ + '.Bar' ) return [bar_category] core = CorePlugin() a = PluginA() application = TestApplication(plugins=[core, a]) application.start() # Create the target class. class Bar(HasTraits): x = Int # Make sure the category was imported and added. # # fixme: The following assertion was commented out. Please don't do # that! If a test fails we need to work out why - otherwise you have # just completely removed the benefits of having tests in the first # place! This test works for me on Python 2.4! self.assertTrue('y' in Bar.class_traits()) def test_dynamically_added_category(self): """ dynamically added category """ from envisage.core_plugin import CorePlugin class PluginA(Plugin): id = 'A' categories = List(contributes_to='envisage.categories') def _categories_default(self): """ Trait initializer. """ bar_category = Category( class_name = PKG + '.bar_category.BarCategory', target_class_name = CorePluginTestCase.__module__ + '.Bar' ) return [bar_category] core = CorePlugin() a = PluginA() # Start with just the core plugin. application = TestApplication(plugins=[core]) application.start() # Now add a plugin that contains a category. application.add_plugin(a) # Create the target class. class Bar(HasTraits): x = Int # Make sure the category was imported and added. self.assertTrue('y' in Bar.class_traits()) def test_class_load_hooks(self): """ class load hooks """ from envisage.core_plugin import CorePlugin def on_class_loaded(cls): """ Called when a class has been loaded. """ on_class_loaded.cls = cls class PluginA(Plugin): id = 'A' class_load_hooks = List( [ ClassLoadHook( class_name = CorePluginTestCase.__module__ + '.Baz', on_load = on_class_loaded, ) ], contributes_to='envisage.class_load_hooks' ) core = CorePlugin() a = PluginA() application = TestApplication(plugins=[core, a]) application.start() # Make sure we ignore a class that we are not interested in! class Bif(HasTraits): pass # Make sure the class load hook was *ignored*. self.assertTrue(not hasattr(on_class_loaded, 'cls')) # Create the target class. class Baz(HasTraits): pass # Make sure the class load hook was called. # # fixme: The following assertion was commented out. Please don't do # that! If a test fails we need to work out why - otherwise you have # just completely removed the benefits of having tests in the first # place! This test works for me on Python 2.4! self.assertEqual(Baz, on_class_loaded.cls) def test_dynamically_added_class_load_hooks(self): """ dynamically class load hooks """ from envisage.core_plugin import CorePlugin def on_class_loaded(cls): """ Called when a class has been loaded. """ on_class_loaded.cls = cls class PluginA(Plugin): id = 'A' class_load_hooks = List( [ ClassLoadHook( class_name = CorePluginTestCase.__module__ + '.Baz', on_load = on_class_loaded, ) ], contributes_to='envisage.class_load_hooks' ) core = CorePlugin() a = PluginA() # Start with just the core plugin. application = TestApplication(plugins=[core]) application.start() # Now add a plugin that contains a class load hook. application.add_plugin(a) # Make sure we ignore a class that we are not interested in! class Bif(HasTraits): pass # Make sure the class load hook was *ignored*. self.assertTrue(not hasattr(on_class_loaded, 'cls')) # Create the target class. class Baz(HasTraits): pass # Make sure the class load hook was called. self.assertEqual(Baz, on_class_loaded.cls) def test_preferences(self): """ preferences """ # The core plugin is the plugin that offers the preferences extension # point. from envisage.core_plugin import CorePlugin class PluginA(Plugin): id = 'A' preferences = List(contributes_to='envisage.preferences') def _preferences_default(self): """ Trait initializer. """ return ['file://' + resource_filename(PKG, 'preferences.ini')] core = CorePlugin() a = PluginA() application = TestApplication(plugins=[core, a]) application.run() # Make sure we can get one of the preferences. self.assertEqual('42', application.preferences.get('enthought.test.x')) def test_dynamically_added_preferences(self): """ dynamically added preferences """ # The core plugin is the plugin that offers the preferences extension # point. from envisage.core_plugin import CorePlugin class PluginA(Plugin): id = 'A' preferences = List(contributes_to='envisage.preferences') def _preferences_default(self): """ Trait initializer. """ return ['file://' + resource_filename(PKG, 'preferences.ini')] core = CorePlugin() a = PluginA() # Start with just the core plugin. application = TestApplication(plugins=[core]) application.start() # Now add a plugin that contains a preference. application.add_plugin(a) # Make sure we can get one of the preferences. self.assertEqual('42', application.preferences.get('enthought.test.x')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_egg_based.py0000644000076500000240000000462200000000000023067 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Base class for Egg-based test cases. """ import pkg_resources from os.path import dirname, join from traits.testing.unittest_tools import unittest class EggBasedTestCase(unittest.TestCase): """ Base class for Egg-based test cases. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ # The location of the 'eggs' directory. self.egg_dir = join(dirname(__file__), 'eggs') def _add_egg(self, filename, working_set=None): """ Create and add a distribution from the specified '.egg'. """ if working_set is None: working_set = pkg_resources.working_set # The eggs must be in our egg directory! filename = join(dirname(__file__), 'eggs', filename) # Create a distribution for the egg. distributions = pkg_resources.find_distributions(filename) # Add the distributions to the working set (this makes any Python # modules in the eggs available for importing). for distribution in distributions: working_set.add(distribution) def _add_eggs_on_path(self, path, working_set=None): """ Add all eggs found on the path to a working set. """ if working_set is None: working_set = pkg_resources.working_set environment = pkg_resources.Environment(path) # 'find_plugins' identifies those distributions that *could* be added # to the working set without version conflicts or missing requirements. distributions, errors = working_set.find_plugins(environment) # Py2 tests was checking that len(errors) > 0. This did not work on # Py3. Test changed to check the len(distributions) if len(distributions) == 0: raise SystemError('Cannot find eggs %s' % errors) # Add the distributions to the working set (this makes any Python # modules in the eggs available for importing). for distribution in distributions: working_set.add(distribution) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_egg_basket_plugin_manager.py0000644000076500000240000002344100000000000026332 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the 'Egg Basket' plugin manager. """ import glob import sys from os.path import basename, dirname, join import pkg_resources import shutil import tempfile from envisage.egg_basket_plugin_manager import EggBasketPluginManager from traits.testing.unittest_tools import unittest class EggBasketPluginManagerTestCase(unittest.TestCase): """ Tests for the 'Egg Basket' plugin manager. """ #### 'unittest.TestCase' protocol ######################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ # The location of the 'eggs' test data directory. self.eggs_dir = join(dirname(__file__), 'eggs') self.bad_eggs_dir = join(dirname(__file__), 'bad_eggs') def tearDown(self): """ Called immediately after each test method has been called. """ # Undo any side-effects: egg_basket_plugin_manager modifies sys.path. sys_path = [] for path in sys.path: if self.bad_eggs_dir not in path: sys_path.append(path) sys.path = sys_path # `envisage.egg_utils.get_entry_points_in_egg_order` modifies the # global working set. pkg_resources.working_set = pkg_resources.WorkingSet() #### Tests ################################################################ def test_find_plugins_in_eggs_on_the_plugin_path(self): plugin_manager = EggBasketPluginManager( plugin_path=[self.eggs_dir] ) ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 3) self.assertIn('acme.foo', ids) self.assertIn('acme.bar', ids) self.assertIn('acme.baz', ids) def test_only_find_plugins_whose_ids_are_in_the_include_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. include = ['acme.foo', 'acme.bar'] plugin_manager = EggBasketPluginManager( plugin_path = [self.eggs_dir], include = include ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['acme.foo', 'acme.bar'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_only_find_plugins_matching_a_wildcard_in_the_include_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. include = ['acme.b*'] plugin_manager = EggBasketPluginManager( plugin_path = [self.eggs_dir], include = include ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['acme.bar', 'acme.baz'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_ignore_plugins_whose_ids_are_in_the_exclude_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. exclude = ['acme.foo', 'acme.baz'] plugin_manager = EggBasketPluginManager( plugin_path = [self.eggs_dir], exclude = exclude ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['acme.bar'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_ignore_plugins_matching_a_wildcard_in_the_exclude_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. exclude = ['acme.b*'] plugin_manager = EggBasketPluginManager( plugin_path = [self.eggs_dir], exclude = exclude ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['acme.foo'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_reflect_changes_to_the_plugin_path(self): plugin_manager = EggBasketPluginManager() ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 0) plugin_manager.plugin_path.append(self.eggs_dir) ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 3) self.assertIn('acme.foo', ids) self.assertIn('acme.bar', ids) self.assertIn('acme.baz', ids) del plugin_manager.plugin_path[0] ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 0) def test_ignore_broken_plugins_raises_exceptions_by_default(self): plugin_manager = EggBasketPluginManager( plugin_path = [self.bad_eggs_dir, self.eggs_dir], ) with self.assertRaises(ImportError): list(plugin_manager) def test_ignore_broken_plugins_loads_good_plugins(self): data = {'count': 0} def on_broken_plugin(ep, exc): data['count'] += 1 data['entry_point'] = ep data['exc'] = exc plugin_manager = EggBasketPluginManager( plugin_path = [self.bad_eggs_dir, self.eggs_dir], on_broken_plugin = on_broken_plugin, ) ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 3) self.assertIn('acme.foo', ids) self.assertIn('acme.bar', ids) self.assertIn('acme.baz', ids) self.assertEqual(data['count'], 1) self.assertEqual(data['entry_point'].name, 'acme.bad') exc = data['exc'] self.assertTrue(isinstance(exc, ImportError)) def test_ignore_broken_distributions_raises_exceptions_by_default(self): plugin_manager = EggBasketPluginManager( plugin_path = [self.bad_eggs_dir, self._create_broken_distribution_eggdir('acme.foo*.egg')], ) with self.assertRaises(SystemError): iter(plugin_manager) def test_ignore_broken_distributions_loads_good_distributions(self): data = {'count':0} def on_broken_distribution(dist, exc): data['count'] += 1 data['distribution'] = dist data['exc'] = exc plugin_manager = EggBasketPluginManager( plugin_path = [self.eggs_dir, self._create_broken_distribution_eggdir('acme.foo*.egg')], on_broken_distribution = on_broken_distribution, ) ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 3) self.assertIn('acme.foo', ids) self.assertIn('acme.bar', ids) self.assertIn('acme.baz', ids) self.assertEqual(data['count'], 1) self.assertEqual(data['distribution'].project_name, 'acme.foo') exc = data['exc'] self.assertTrue(isinstance(exc, pkg_resources.VersionConflict)) #### Private protocol ##################################################### def _test_start_and_stop(self, plugin_manager, expected): """ Make sure the plugin manager starts and stops the expected plugins. """ # Make sure the plugin manager found only the required plugins. self.assertEqual(expected, [plugin.id for plugin in plugin_manager]) # Start the plugin manager. This starts all of the plugin manager's # plugins. plugin_manager.start() # Make sure all of the the plugins were started. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.started) # Stop the plugin manager. This stops all of the plugin manager's # plugins. plugin_manager.stop() # Make sure all of the the plugins were stopped. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.stopped) def _create_broken_distribution_eggdir(self, egg_pat, replacement=None): """ Copy a good egg to a different version egg name in a new temp dir and return the new directory. Parameters ---------- egg_pat: a glob pattern for the egg in `self.egg_dir` eg 'foo.bar*.egg' replacement: a string replacement for the version part of egg name. If None, '1' is appended to the original version. Returns ------- The newly created dir where the new broken egg is copied. Adding this dir to plugin_path will cause VersionConflict on trying to load distributions. """ tmpdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tmpdir) # Copy the egg to the temp dir and rename it eggs = glob.glob(join(self.eggs_dir, egg_pat)) for egg in eggs: egg_name = basename(egg) split_name = egg_name.split('-') if replacement is None: split_name[1] += '1' else: split_name[1] = replacement new_name = '-'.join(split_name) shutil.copy(egg, join(tmpdir, new_name)) return tmpdir ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_egg_plugin_manager.py0000644000076500000240000001466200000000000025006 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the Egg plugin manager. """ # Enthought library imports. from envisage.api import EggPluginManager # Local imports. from .test_egg_based import EggBasedTestCase from traits.testing.unittest_tools import unittest class EggPluginManagerTestCase(EggBasedTestCase): """ Tests for the Egg plugin manager. """ ########################################################################### # Tests. ########################################################################### # fixme: Depending how many eggs are on sys.path, this test may take too # long to be part of the TDD cycle. def test_no_include_or_exclude(self): """ no include or exclude """ # Add all of the eggs in the egg basket. self._add_eggs_on_path([self.egg_dir]) # Make sure that the plugin manager only includes those plugins. plugin_manager = EggPluginManager() # We don't know how many plugins we will actually get - it depends on # what eggs are on sys.path! What we *do* know however is the the 3 # 'acme' test eggs should be in there! ids = [plugin.id for plugin in plugin_manager] self.assertTrue('acme.foo' in ids) self.assertTrue('acme.bar' in ids) self.assertTrue('acme.baz' in ids) return def test_include_specific(self): """ include specific """ # Add all of the eggs in the egg basket. self._add_eggs_on_path([self.egg_dir]) # The Ids of the plugins that we expect the plugin manager to find. expected = ['acme.foo', 'acme.bar'] # We explicitly limit the plugins to be just the 'acme' test plugins # because otherwise the egg plugin manager will pick up *every* plugin # in *every* egg on sys.path! include = ['acme\.foo', 'acme\.bar'] # Make sure that the plugin manager only includes those plugins. plugin_manager = EggPluginManager(include=include) # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) return def test_include_multiple(self): """ include multiple """ # Add all of the eggs in the egg basket. self._add_eggs_on_path([self.egg_dir]) # The Ids of the plugins that we expect the plugin manager to find. expected = ['acme.foo', 'acme.bar', 'acme.baz'] # We explicitly limit the plugins to be just the 'acme' test plugins # because otherwise the egg plugin manager will pick up *every* plugin # in *every* egg on sys.path! include = ['acme.*'] # Make sure that the plugin manager only includes those plugins. plugin_manager = EggPluginManager(include=include) # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) return def test_exclude_specific(self): """ exclude specific """ # Add all of the eggs in the egg basket. self._add_eggs_on_path([self.egg_dir]) # The Ids of the plugins that we expect the plugin manager to find. expected = ['acme.bar'] # We explicitly limit the plugins to be just the 'acme' test plugins # because otherwise the egg plugin manager will pick up *every* plugin # in *every* egg on sys.path! include = ['acme.*'] # Now exclude all but 'acme.bar'... exclude = ['acme\.foo', 'acme\.baz'] # Make sure that the plugin manager excludes the specified plugins. plugin_manager = EggPluginManager(include=include, exclude=exclude) # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) return def test_exclude_multiple(self): """ exclude multiple """ # Add all of the eggs in the egg basket. self._add_eggs_on_path([self.egg_dir]) # The Ids of the plugins that we expect the plugin manager to find. expected = ['acme.foo'] # We explicitly limit the plugins to be just the 'acme' test plugins # because otherwise the egg plugin manager will pick up *every* plugin # in *every* egg on sys.path! include = ['acme.*'] # Now exclude every plugin that starts with 'acme.b'. exclude = ['acme\.b.*'] # Make sure that the plugin manager excludes the specified plugins. plugin_manager = EggPluginManager(include=include, exclude=exclude) # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) return ########################################################################### # Private interface. ########################################################################### def _test_start_and_stop(self, plugin_manager, expected): """ Make sure the plugin manager starts and stops the expected plugins. """ # Make sure the plugin manager found only the required plugins. self.assertEqual(expected, [plugin.id for plugin in plugin_manager]) # Start the plugin manager. This starts all of the plugin manager's # plugins. plugin_manager.start() # Make sure all of the the plugins were started. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.started) # Stop the plugin manager. This stops all of the plugin manager's # plugins. plugin_manager.stop() # Make sure all of the the plugins were stopped. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.stopped) return ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_extension_point.py0000644000076500000240000001741700000000000024422 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for extension points. """ # Enthought library imports. from envisage.api import Application, ExtensionPoint from envisage.api import ExtensionRegistry from traits.api import HasTraits, Int, List, TraitError from traits.testing.unittest_tools import unittest class TestBase(HasTraits): """ Base class for all test classes that use the 'ExtensionPoint' type. """ extension_registry = None class ExtensionPointTestCase(unittest.TestCase): """ Tests for extension points. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ # We do all of the testing via the application to make sure it offers # the same interface! self.registry = Application(extension_registry=ExtensionRegistry()) # Set the extension registry used by the test classes. TestBase.extension_registry = self.registry def test_invalid_extension_point_type(self): """ invalid extension point type """ # Extension points currently have to be 'List's of something! The # default is a list of anything. with self.assertRaises(TypeError): ExtensionPoint(Int, "my.ep") def test_no_reference_to_extension_registry(self): """ no reference to extension registry """ registry = self.registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Set the extensions. registry.set_extensions('my.ep', 'xxx') # Declare a class that consumes the extension. class Foo(HasTraits): x = ExtensionPoint(List(Int), id='my.ep') # We should get an exception because the object does not have an # 'extension_registry' trait. f = Foo() with self.assertRaises(ValueError): getattr(f, "x") def test_extension_point_changed(self): """ extension point changed """ registry = self.registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Declare a class that consumes the extension. class Foo(TestBase): x = ExtensionPoint(id='my.ep') def _x_changed(self): """ Static trait change handler. """ self.x_changed_called = True f = Foo() # Connect the extension points on the object so that it can listen # for changes. ExtensionPoint.connect_extension_point_traits(f) # Set the extensions. registry.set_extensions('my.ep', [42, 'a string', True]) # Make sure that instances of the class pick up the extensions. self.assertEqual(3, len(f.x)) self.assertEqual([42, 'a string', True], f.x) # Make sure the trait change handler was called. self.assertTrue(f.x_changed_called) # Reset the change handler flag. f.x_changed_called = False # Disconnect the extension points on the object. ExtensionPoint.disconnect_extension_point_traits(f) # Set the extensions. registry.set_extensions('my.ep', [98, 99, 100]) # Make sure the trait change handler was *not* called. self.assertEqual(False, f.x_changed_called) def test_untyped_extension_point(self): """ untyped extension point """ registry = self.registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Set the extensions. registry.set_extensions('my.ep', [42, 'a string', True]) # Declare a class that consumes the extension. class Foo(TestBase): x = ExtensionPoint(id='my.ep') # Make sure that instances of the class pick up the extensions. f = Foo() self.assertEqual(3, len(f.x)) self.assertEqual([42, 'a string', True], f.x) g = Foo() self.assertEqual(3, len(g.x)) self.assertEqual([42, 'a string', True], g.x) def test_typed_extension_point(self): """ typed extension point """ registry = self.registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Set the extensions. registry.set_extensions('my.ep', [42, 43, 44]) # Declare a class that consumes the extension. class Foo(TestBase): x = ExtensionPoint(List(Int), id='my.ep') # Make sure that instances of the class pick up the extensions. f = Foo() self.assertEqual(3, len(f.x)) self.assertEqual([42, 43, 44], f.x) g = Foo() self.assertEqual(3, len(g.x)) self.assertEqual([42, 43, 44], g.x) def test_invalid_extension_point(self): """ invalid extension point """ registry = self.registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Set the extensions. registry.set_extensions('my.ep', 'xxx') # Declare a class that consumes the extension. class Foo(TestBase): x = ExtensionPoint(List(Int), id='my.ep') # Make sure we get a trait error because the type of the extension # doesn't match that of the extension point. f = Foo() with self.assertRaises(TraitError): getattr(f, "x") def test_extension_point_with_no_id(self): """ extension point with no Id """ def factory(): class Foo(TestBase): x = ExtensionPoint(List(Int)) with self.assertRaises(ValueError): factory() def test_set_untyped_extension_point(self): """ set untyped extension point """ registry = self.registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Declare a class that consumes the extension. class Foo(TestBase): x = ExtensionPoint(id='my.ep') # Make sure that when we set the trait the extension registry gets # updated. f = Foo() f.x = [42] self.assertEqual([42], registry.get_extensions('my.ep')) def test_set_typed_extension_point(self): """ set typed extension point """ registry = self.registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Declare a class that consumes the extension. class Foo(TestBase): x = ExtensionPoint(List(Int), id='my.ep') # Make sure that when we set the trait the extension registry gets # updated. f = Foo() f.x = [42] self.assertEqual([42], registry.get_extensions('my.ep')) def test_extension_point_str_representation(self): """ test the string representation of the extension point """ ep_repr = "ExtensionPoint(id={})" ep = self._create_extension_point('my.ep') self.assertEqual(ep_repr.format('my.ep'), str(ep)) self.assertEqual(ep_repr.format('my.ep'), repr(ep)) ########################################################################### # Private interface. ########################################################################### def _create_extension_point(self, id, trait_type=List, desc=''): """ Create an extension point. """ return ExtensionPoint(id=id, trait_type=trait_type, desc=desc) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_extension_point_binding.py0000644000076500000240000001630000000000000026102 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for extension point bindings. """ # Enthought library imports. from envisage.api import ExtensionPoint from envisage.api import bind_extension_point from traits.api import HasTraits, List from traits.testing.unittest_tools import unittest # Local imports. from envisage.tests.mutable_extension_registry import ( MutableExtensionRegistry ) 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 class ExtensionPointBindingTestCase(unittest.TestCase): """ Tests for extension point binding. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ self.extension_registry = MutableExtensionRegistry() # Use the extension registry for all extension points and bindings. ExtensionPoint.extension_registry = self.extension_registry def test_untyped_extension_point(self): """ untyped extension point """ registry = self.extension_registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Add an extension. registry.add_extension('my.ep', 42) # Declare a class that consumes the extension. class Foo(HasTraits): x = List f = Foo() f.on_trait_change(listener) # Make some bindings. bind_extension_point(f, 'x', 'my.ep') # Make sure that the object was initialized properly. self.assertEqual(1, len(f.x)) self.assertEqual(42, f.x[0]) # Add another extension. registry.add_extension('my.ep', 'a string') # Make sure that the object picked up the new extension... self.assertEqual(2, len(f.x)) self.assertTrue(42 in f.x) self.assertTrue('a string' in f.x) # ... and that the correct trait change event was fired. self.assertEqual(f, listener.obj) self.assertEqual('x_items', listener.trait_name) self.assertEqual(1, len(listener.new.added)) self.assertTrue('a string' in listener.new.added) def test_set_extensions_via_trait(self): """ set extensions via trait """ registry = self.extension_registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Add an extension. registry.add_extension('my.ep', 42) # Declare a class that consumes the extension. class Foo(HasTraits): x = List f = Foo() f.on_trait_change(listener) # Make some bindings. bind_extension_point(f, 'x', 'my.ep') # Make sure that the object was initialized properly. self.assertEqual(1, len(f.x)) self.assertEqual(42, f.x[0]) # Set the extensions. f.x = ['a string'] # Make sure that the object picked up the new extension... self.assertEqual(1, len(f.x)) self.assertTrue('a string' in f.x) self.assertEqual(1, len(registry.get_extensions('my.ep'))) self.assertTrue('a string' in registry.get_extensions('my.ep')) # ... and that the correct trait change event was fired. self.assertEqual(f, listener.obj) self.assertEqual('x', listener.trait_name) self.assertEqual(1, len(listener.new)) self.assertTrue('a string' in listener.new) def test_set_extensions_via_registry(self): """ set extensions via registry """ registry = self.extension_registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Add an extension. registry.add_extension('my.ep', 42) # Declare a class that consumes the extension. class Foo(HasTraits): x = List f = Foo() f.on_trait_change(listener) # Make some bindings. bind_extension_point(f, 'x', 'my.ep') # Make sure that the object was initialized properly. self.assertEqual(1, len(f.x)) self.assertEqual(42, f.x[0]) # Set the extensions. registry.set_extensions('my.ep', ['a string']) # Make sure that the object picked up the new extension... self.assertEqual(1, len(f.x)) self.assertTrue('a string' in f.x) # ... and that the correct trait change event was fired. self.assertEqual(f, listener.obj) self.assertEqual('x', listener.trait_name) self.assertEqual(1, len(listener.new)) self.assertTrue('a string' in listener.new) def test_explicit_extension_registry(self): """ explicit extension registry """ registry = self.extension_registry # Add an extension point. registry.add_extension_point(self._create_extension_point('my.ep')) # Add an extension. registry.add_extension('my.ep', 42) # Declare a class that consumes the extension. class Foo(HasTraits): x = List f = Foo() f.on_trait_change(listener) # Create an empty extension registry use that in the binding. extension_registry = MutableExtensionRegistry() # Make some bindings. bind_extension_point(f, 'x', 'my.ep', extension_registry) # Make sure that we pick up the empty extension registry and not the # default one. self.assertEqual(0, len(f.x)) def test_should_be_able_to_bind_multiple_traits_on_a_single_object(self): registry = self.extension_registry # Add 2 extension points. registry.add_extension_point(self._create_extension_point('my.ep')) registry.add_extension_point(self._create_extension_point('another.ep')) # Declare a class that consumes both of the extension points. class Foo(HasTraits): x = List y = List f = Foo() # Bind two different traits on the object to the extension points. bind_extension_point(f, 'x', 'my.ep', registry) bind_extension_point(f, 'y', 'another.ep', registry) self.assertEqual(0, len(f.x)) self.assertEqual(0, len(f.y)) # Add some contributions to the extension points. registry.add_extension('my.ep', 42) registry.add_extensions('another.ep', [98, 99, 100]) # Make sure both traits were bound correctly. self.assertEqual(1, len(f.x)) self.assertEqual(3, len(f.y)) ########################################################################### # Private interface. ########################################################################### def _create_extension_point(self, id, trait_type=List, desc=''): """ Create an extension point. """ return ExtensionPoint(id=id, trait_type=trait_type, desc=desc) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_extension_point_changed.py0000644000076500000240000002666600000000000026101 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the events fired when extension points are changed. """ # Enthought library imports. from traits.testing.unittest_tools import unittest # Local imports. from envisage.tests.test_application import ( PluginA, PluginB, PluginC, TestApplication, listener ) class ExtensionPointChangedTestCase(unittest.TestCase): """ Tests for the events fired when extension points are changed. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ # Make sure that the listener contents get cleand up before each test. listener.obj = None listener.trait_name = None listener.old = None listener.new = None def test_set_extension_point(self): """ set extension point """ a = PluginA() application = TestApplication(plugins=[a]) application.start() # Try to set the extension point. with self.assertRaises(SystemError): setattr(a, "x", [1, 2, 3]) def test_append(self): """ append """ a = PluginA(); a.on_trait_change(listener, 'x_items') b = PluginB() c = PluginC() application = TestApplication(plugins=[a, b, c]) application.start() # fixme: If the extension point has not been accessed then the # provider extension registry can't work out what has changed, so it # won't fire a changed event. self.assertEqual([1, 2, 3, 98, 99, 100], a.x) # Append a contribution. b.x.append(4) # Make sure we pick up the new contribution via the application. extensions = application.get_extensions('a.x') extensions.sort() self.assertEqual(7, len(extensions)) self.assertEqual([1, 2, 3, 4, 98, 99, 100], extensions) # Make sure we pick up the new contribution via the plugin. extensions = a.x[:] extensions.sort() self.assertEqual(7, len(extensions)) self.assertEqual([1, 2, 3, 4, 98, 99, 100], extensions) # Make sure we got a trait event telling us that the contributions # to the extension point have been changed. self.assertEqual(a, listener.obj) self.assertEqual('x_items', listener.trait_name) self.assertEqual([4], listener.new.added) self.assertEqual([], listener.new.removed) self.assertEqual(3, listener.new.index) def test_remove(self): """ remove """ a = PluginA(); a.on_trait_change(listener, 'x_items') b = PluginB() c = PluginC() application = TestApplication(plugins=[a, b, c]) application.start() # fixme: If the extension point has not been accessed then the # provider extension registry can't work out what has changed, so it # won't fire a changed event. self.assertEqual([1, 2, 3, 98, 99, 100], a.x) # Remove a contribution. b.x.remove(3) # Make sure we pick up the correct contributions via the application. extensions = application.get_extensions('a.x') extensions.sort() self.assertEqual(5, len(extensions)) self.assertEqual([1, 2, 98, 99, 100], extensions) # Make sure we pick up the correct contributions via the plugin. extensions = a.x[:] extensions.sort() self.assertEqual(5, len(extensions)) self.assertEqual([1, 2, 98, 99, 100], extensions) # Make sure we got a trait event telling us that the contributions # to the extension point have been changed. self.assertEqual(a, listener.obj) self.assertEqual('x_items', listener.trait_name) self.assertEqual([], listener.new.added) self.assertEqual([3], listener.new.removed) self.assertEqual(2, listener.new.index) def test_assign_empty_list(self): """ assign empty list """ a = PluginA(); a.on_trait_change(listener, 'x_items') b = PluginB() c = PluginC() application = TestApplication(plugins=[a, b, c]) application.start() # fixme: If the extension point has not been accessed then the # provider extension registry can't work out what has changed, so it # won't fire a changed event. self.assertEqual([1, 2, 3, 98, 99, 100], a.x) # Assign an empty list to one of the plugin's contributions. b.x = [] # Make sure we pick up the correct contribution via the application. extensions = application.get_extensions('a.x') extensions.sort() self.assertEqual(3, len(extensions)) self.assertEqual([98, 99, 100], extensions) # Make sure we pick up the correct contribution via the plugin. extensions = a.x[:] extensions.sort() self.assertEqual(3, len(extensions)) self.assertEqual([98, 99, 100], extensions) # Make sure we got a trait event telling us that the contributions # to the extension point have been changed. self.assertEqual(a, listener.obj) self.assertEqual('x_items', listener.trait_name) self.assertEqual([], listener.new.added) self.assertEqual([1, 2, 3], listener.new.removed) self.assertEqual(0, listener.new.index.start) self.assertEqual(3, listener.new.index.stop) def test_assign_empty_list_no_event(self): """ assign empty list no event """ a = PluginA(); a.on_trait_change(listener, 'x_items') b = PluginB() c = PluginC() application = TestApplication(plugins=[a, b, c]) application.start() # Assign an empty list to one of the plugin's contributions. b.x = [] # Make sure we pick up the correct contribution via the application. extensions = application.get_extensions('a.x') extensions.sort() self.assertEqual(3, len(extensions)) self.assertEqual([98, 99, 100], extensions) # Make sure we pick up the correct contribution via the plugin. extensions = a.x[:] extensions.sort() self.assertEqual(3, len(extensions)) self.assertEqual([98, 99, 100], extensions) # We shouldn't get a trait event here because we haven't accessed the # extension point yet! self.assertEqual(None, listener.obj) def test_assign_non_empty_list(self): """ assign non-empty list """ a = PluginA(); a.on_trait_change(listener, 'x_items') b = PluginB() c = PluginC() application = TestApplication(plugins=[a, b, c]) application.start() # fixme: If the extension point has not been accessed then the # provider extension registry can't work out what has changed, so it # won't fire a changed event. self.assertEqual([1, 2, 3, 98, 99, 100], a.x) # Assign a non-empty list to one of the plugin's contributions. b.x = [2, 4, 6, 8] # Make sure we pick up the new contribution via the application. extensions = application.get_extensions('a.x') extensions.sort() self.assertEqual(7, len(extensions)) self.assertEqual([2, 4, 6, 8, 98, 99, 100], extensions) # Make sure we pick up the new contribution via the plugin. extensions = a.x[:] extensions.sort() self.assertEqual(7, len(extensions)) self.assertEqual([2, 4, 6, 8, 98, 99, 100], extensions) # Make sure we got a trait event telling us that the contributions # to the extension point have been changed. self.assertEqual(a, listener.obj) self.assertEqual('x_items', listener.trait_name) self.assertEqual([2, 4, 6, 8], listener.new.added) self.assertEqual([1, 2, 3], listener.new.removed) self.assertEqual(0, listener.new.index.start) self.assertEqual(4, listener.new.index.stop) def test_add_plugin(self): """ add plugin """ a = PluginA(); a.on_trait_change(listener, 'x_items') b = PluginB() c = PluginC() # Start off with just two of the plugins. application = TestApplication(plugins=[a, b]) application.start() # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') extensions.sort() self.assertEqual(3, len(extensions)) self.assertEqual([1, 2, 3], extensions) # Make sure we can get the contributions via the plugin. extensions = a.x[:] extensions.sort() self.assertEqual(3, len(extensions)) self.assertEqual([1, 2, 3], extensions) # Now add the other plugin. application.add_plugin(c) # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') extensions.sort() self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) # Make sure we can get the contributions via the plugin. extensions = a.x[:] extensions.sort() self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) # Make sure we got a trait event telling us that the contributions # to the extension point have been changed. self.assertEqual(a, listener.obj) self.assertEqual('x_items', listener.trait_name) self.assertEqual([98, 99, 100], listener.new.added) self.assertEqual([], listener.new.removed) self.assertEqual(3, listener.new.index) def test_remove_plugin(self): """ remove plugin """ a = PluginA(); a.on_trait_change(listener, 'x_items') b = PluginB() c = PluginC() application = TestApplication(plugins=[a, b, c]) application.start() # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') extensions.sort() self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) # Make sure we can get the contributions via the plugin. extensions = a.x[:] extensions.sort() self.assertEqual(6, len(extensions)) self.assertEqual([1, 2, 3, 98, 99, 100], extensions) # Now remove one plugin. application.remove_plugin(b) # Make sure we can get the contributions via the application. extensions = application.get_extensions('a.x') extensions.sort() self.assertEqual(3, len(extensions)) self.assertEqual([98, 99, 100], extensions) # Make sure we can get the contributions via the plugin. extensions = a.x[:] extensions.sort() self.assertEqual(3, len(extensions)) self.assertEqual([98, 99, 100], extensions) # Make sure we got a trait event telling us that the contributions # to the extension point have been changed. self.assertEqual(a, listener.obj) self.assertEqual('x_items', listener.trait_name) self.assertEqual([], listener.new.added) self.assertEqual([1, 2, 3], listener.new.removed) self.assertEqual(0, listener.new.index) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_extension_registry.py0000644000076500000240000001204700000000000025133 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the base extension registry. """ # Enthought library imports. from envisage.api import Application, ExtensionPoint from envisage.api import ExtensionRegistry, UnknownExtensionPoint from traits.api import List from traits.testing.unittest_tools import unittest class ExtensionRegistryTestCase(unittest.TestCase): """ Tests for the base extension registry. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ # We do all of the testing via the application to make sure it offers # the same interface! self.registry = Application(extension_registry=ExtensionRegistry()) def test_empty_registry(self): """ empty registry """ registry = self.registry # Make sure there are no extensions. extensions = registry.get_extensions('my.ep') self.assertEqual(0, len(extensions)) # Make sure there are no extension points. extension_points = registry.get_extension_points() self.assertEqual(0, len(extension_points)) def test_add_extension_point(self): """ add extension point """ registry = self.registry # Add an extension *point*. registry.add_extension_point(self._create_extension_point('my.ep')) # Make sure there's NO extensions. extensions = registry.get_extensions('my.ep') self.assertEqual(0, len(extensions)) # Make sure there's one and only one extension point. extension_points = registry.get_extension_points() self.assertEqual(1, len(extension_points)) self.assertEqual('my.ep', extension_points[0].id) def test_get_extension_point(self): """ get extension point """ registry = self.registry # Add an extension *point*. registry.add_extension_point(self._create_extension_point('my.ep')) # Make sure we can get it. extension_point = registry.get_extension_point('my.ep') self.assertNotEqual(None, extension_point) self.assertEqual('my.ep', extension_point.id) def test_remove_empty_extension_point(self): """ remove empty_extension point """ registry = self.registry # Add an extension point... registry.add_extension_point(self._create_extension_point('my.ep')) # ...and remove it! registry.remove_extension_point('my.ep') # Make sure there are no extension points. extension_points = registry.get_extension_points() self.assertEqual(0, len(extension_points)) def test_remove_non_empty_extension_point(self): """ remove non-empty extension point """ registry = self.registry # Add an extension point... registry.add_extension_point(self._create_extension_point('my.ep')) # ... with some extensions... registry.set_extensions('my.ep', [42]) # ...and remove it! registry.remove_extension_point('my.ep') # Make sure there are no extension points. extension_points = registry.get_extension_points() self.assertEqual(0, len(extension_points)) # And that the extensions are gone too. self.assertEqual([], registry.get_extensions('my.ep')) def test_remove_non_existent_extension_point(self): """ remove non existent extension point """ registry = self.registry with self.assertRaises(UnknownExtensionPoint): registry.remove_extension_point("my.ep") def test_remove_non_existent_listener(self): """ remove non existent listener """ registry = self.registry def listener(registry, extension_point, added, removed, index): """ Called when an extension point has changed. """ self.listener_called = (registry, extension_point, added, removed) with self.assertRaises(ValueError): registry.remove_extension_point_listener(listener) def test_set_extensions(self): """ set extensions """ registry = self.registry # Add an extension *point*. registry.add_extension_point(self._create_extension_point('my.ep')) # Set some extensions. registry.set_extensions('my.ep', [1, 2, 3]) # Make sure we can get them. self.assertEqual([1, 2, 3], registry.get_extensions('my.ep')) ########################################################################### # Private interface. ########################################################################### def _create_extension_point(self, id, trait_type=List, desc=''): """ Create an extension point. """ return ExtensionPoint(id=id, trait_type=trait_type, desc=desc) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_import_manager.py0000644000076500000240000000315100000000000024167 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the import manager. """ # Enthought library imports. from envisage.api import Application, ImportManager from traits.testing.unittest_tools import unittest class ImportManagerTestCase(unittest.TestCase): """ Tests for the import manager. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ # We do all of the testing via the application to make sure it offers # the same interface! self.import_manager = Application(import_manager=ImportManager()) def test_import_dotted_symbol(self): """ import dotted symbol """ import tarfile symbol = self.import_manager.import_symbol('tarfile.TarFile') self.assertEqual(symbol, tarfile.TarFile) def test_import_nested_symbol(self): """ import nested symbol """ import tarfile symbol = self.import_manager.import_symbol('tarfile:TarFile.open') self.assertEqual(symbol, tarfile.TarFile.open) def test_import_dotted_module(self): """ import dotted module """ symbol = self.import_manager.import_symbol( 'envisage.api:ImportManager' ) self.assertEqual(symbol, ImportManager) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_package_plugin_manager.py0000644000076500000240000001301700000000000025630 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the 'Package' plugin manager. """ from os.path import dirname, join from envisage.package_plugin_manager import PackagePluginManager from traits.testing.unittest_tools import unittest class PackagePluginManagerTestCase(unittest.TestCase): """ Tests for the 'Package' plugin manager. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ # The location of the 'plugins' test data directory. self.plugins_dir = join(dirname(__file__), 'plugins') def test_find_plugins_in_packages_on_the_plugin_path(self): plugin_manager = PackagePluginManager(plugin_path=[self.plugins_dir]) ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 3) self.assertIn('banana', ids) self.assertIn('orange', ids) self.assertIn('pear', ids) def test_only_find_plugins_whose_ids_are_in_the_include_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. include = ['orange', 'pear'] plugin_manager = PackagePluginManager( plugin_path = [self.plugins_dir], include = include ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['orange', 'pear'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_only_find_plugins_matching_a_wildcard_in_the_include_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. include = ['*r*'] plugin_manager = PackagePluginManager( plugin_path = [self.plugins_dir], include = include ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['orange', 'pear'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_ignore_plugins_whose_ids_are_in_the_exclude_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. exclude = ['orange', 'pear'] plugin_manager = PackagePluginManager( plugin_path = [self.plugins_dir], exclude = exclude ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['banana'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_ignore_plugins_matching_a_wildcard_in_the_exclude_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. exclude = ['*r*'] plugin_manager = PackagePluginManager( plugin_path = [self.plugins_dir], exclude = exclude ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['banana'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_reflect_changes_to_the_plugin_path(self): plugin_manager = PackagePluginManager() ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 0) plugin_manager.plugin_path.append(self.plugins_dir) ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 3) self.assertIn('banana', ids) self.assertIn('orange', ids) self.assertIn('pear', ids) del plugin_manager.plugin_path[0] ids = [plugin.id for plugin in plugin_manager] self.assertEqual(len(ids), 0) #### Private protocol ##################################################### def _test_start_and_stop(self, plugin_manager, expected): """ Make sure the plugin manager starts and stops the expected plugins. """ # Make sure the plugin manager found only the required plugins. self.assertEqual( list(sorted(expected)), list(sorted(plugin.id for plugin in plugin_manager)) ) # Start the plugin manager. This starts all of the plugin manager's # plugins. plugin_manager.start() # Make sure all of the the plugins were started. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.started) # Stop the plugin manager. This stops all of the plugin manager's # plugins. plugin_manager.stop() # Make sure all of the the plugins were stopped. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.stopped) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_plugin.py0000644000076500000240000003061500000000000022466 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for plugins. """ # Standard library imports. from os.path import exists, join # Enthought library imports. from envisage.api import Application, ExtensionPoint from envisage.api import IPluginActivator, Plugin, contributes_to from envisage.tests.ets_config_patcher import ETSConfigPatcher from traits.api import HasTraits, Instance, Int, Interface, List from traits.api import provides from traits.testing.unittest_tools import unittest 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 class TestApplication(Application): """ The type of application used in the tests. """ id = 'test' class PluginTestCase(unittest.TestCase): """ Tests for plugins. """ def setUp(self): ets_config_patcher = ETSConfigPatcher() ets_config_patcher.start() self.addCleanup(ets_config_patcher.stop) def test_id_policy(self): """ id policy """ # If no Id is specified then use 'module_name.class_name'. p = Plugin() self.assertEqual('envisage.plugin.Plugin', p.id) # If an Id is specified make sure we use it! p = Plugin(id='wilma') self.assertEqual('wilma', p.id) # Make sure setting the name doesn't interfere with the Id. p = Plugin(name='fred', id='wilma') self.assertEqual('wilma', p.id) self.assertEqual('fred', p.name) def test_name_policy(self): """ name policy """ # If name is specified then use the plugin's class name. p = Plugin() self.assertEqual('Plugin', p.name) # If a name is specified make sure we use it! p = Plugin(name='wilma') self.assertEqual('wilma', p.name) # Try a camel case plugin class. class ThisIsMyPlugin(Plugin): pass p = ThisIsMyPlugin() self.assertEqual('This Is My Plugin', p.name) def test_plugin_activator(self): """ plugin activator. """ @provides(IPluginActivator) class NullPluginActivator(HasTraits): """ A plugin activator that does nothing! """ def start_plugin(self, plugin): """ Start a plugin. """ self.started = plugin def stop_plugin(self, plugin): """ Stop a plugin. """ self.stopped = plugin class PluginA(Plugin): id = 'A' class PluginB(Plugin): id = 'B' plugin_activator = NullPluginActivator() a = PluginA(activator=plugin_activator) b = PluginB() application = TestApplication(plugins=[a, b]) application.start() # Make sure A's plugin activator was called. self.assertEqual(a, plugin_activator.started) # Stop the application. application.stop() # Make sure A's plugin activator was called. self.assertEqual(a, plugin_activator.stopped) def test_service(self): """ service """ class Foo(HasTraits): pass class Bar(HasTraits): pass class Baz(HasTraits): pass class PluginA(Plugin): id = 'A' foo = Instance(Foo, (), service=True) bar = Instance(Bar, (), service=True) baz = Instance(Baz, (), service=True) a = PluginA() application = TestApplication(plugins=[a]) application.start() # Make sure the services were registered. self.assertNotEqual(None, application.get_service(Foo)) self.assertEqual(a.foo, application.get_service(Foo)) self.assertNotEqual(None, application.get_service(Bar)) self.assertEqual(a.bar, application.get_service(Bar)) self.assertNotEqual(None, application.get_service(Baz)) self.assertEqual(a.baz, application.get_service(Baz)) application.stop() # Make sure the service was unregistered. self.assertEqual(None, application.get_service(Foo)) self.assertEqual(None, application.get_service(Bar)) self.assertEqual(None, application.get_service(Baz)) def test_service_protocol(self): """ service protocol """ class IFoo(Interface): pass class IBar(Interface): pass @provides(IFoo, IBar) class Foo(HasTraits): pass class PluginA(Plugin): id = 'A' foo = Instance(Foo, (), service=True, service_protocol=IBar) a = PluginA() application = TestApplication(plugins=[a]) application.start() # Make sure the service was registered with the 'IBar' protocol. self.assertNotEqual(None, application.get_service(IBar)) self.assertEqual(a.foo, application.get_service(IBar)) application.stop() # Make sure the service was unregistered. self.assertEqual(None, application.get_service(IBar)) def test_multiple_trait_contributions(self): """ multiple trait contributions """ class PluginA(Plugin): id = 'A' x = ExtensionPoint(List, id='x') class PluginB(Plugin): id = 'B' x = List([1, 2, 3], contributes_to='x') y = List([4, 5, 6], contributes_to='x') a = PluginA() b = PluginB() application = TestApplication(plugins=[a, b]) # We should get an error because the plugin has multiple traits # contributing to the same extension point. with self.assertRaises(ValueError): application.get_extensions("x") def test_exception_in_trait_contribution(self): """ exception in trait contribution """ class PluginA(Plugin): id = 'A' x = ExtensionPoint(List, id='x') class PluginB(Plugin): id = 'B' x = List(contributes_to='x') def _x_default(self): """ Trait initializer. """ raise 1/0 a = PluginA() b = PluginB() application = TestApplication(plugins=[a, b]) # We should get an when we try to get the contributions to the # extension point. with self.assertRaises(ZeroDivisionError): application.get_extensions("x") def test_contributes_to(self): """ contributes to """ class PluginA(Plugin): id = 'A' x = ExtensionPoint(List, id='x') class PluginB(Plugin): id = 'B' x = List([1, 2, 3], contributes_to='x') a = PluginA() b = PluginB() application = TestApplication(plugins=[a, b]) # We should get an error because the plugin has multiple traits # contributing to the same extension point. self.assertEqual([1, 2, 3], application.get_extensions('x')) def test_contributes_to_decorator(self): """ contributes to decorator """ class PluginA(Plugin): id = 'A' x = ExtensionPoint(List, id='x') class PluginB(Plugin): id = 'B' @contributes_to('x') def _x_contributions(self): return [1, 2, 3] a = PluginA() b = PluginB() application = TestApplication(plugins=[a, b]) self.assertEqual([1, 2, 3], application.get_extensions('x')) def test_contributes_to_decorator_ignored_if_trait_present(self): """ contributes to decorator ignored if trait present """ class PluginA(Plugin): id = 'A' x = ExtensionPoint(List, id='x') class PluginB(Plugin): id = 'B' x = List([1, 2, 3], contributes_to='x') @contributes_to('x') def _x_contributions(self): return [4, 5, 6] a = PluginA() b = PluginB() application = TestApplication(plugins=[a, b]) self.assertEqual([1, 2, 3], application.get_extensions('x')) def test_add_plugins_to_empty_application(self): """ add plugins to empty application """ class PluginA(Plugin): id = 'A' x = ExtensionPoint(List(Int), id='x') def _x_items_changed(self, event): self.added = event.added self.removed = event.removed class PluginB(Plugin): id = 'B' x = List(Int, [1, 2, 3], contributes_to='x') class PluginC(Plugin): id = 'C' x = List(Int, [4, 5, 6], contributes_to='x') a = PluginA() b = PluginB() c = PluginC() # Create an empty application. application = TestApplication() application.start() # Add the plugin that offers the extension point. application.add_plugin(a) ####################################################################### # fixme: Currently, we connect up extension point traits when the # plugin is started. Is this right? Should we start plugins by default # when we add them (and maybe have the ability to add a plugin without # starting it?). # # I think we should start the plugin, otherwise you have the wierdness # that the extension contributed by the plugin are available after # the call to 'add_plugin', but the plugin isn't started?!? ####################################################################### application.start_plugin(a) ####################################################################### # fixme: Currently, we only fire changed events if an extension point # has already been accessed! Is this right? ####################################################################### self.assertEqual([], a.x) # Add a plugin that contributes to the extension point. application.add_plugin(b) # Make sure that we pick up B's extensions and that the appropriate # trait event was fired. self.assertEqual([1, 2, 3], a.x) self.assertEqual([1, 2, 3], a.added) # Add another plugin that contributes to the extension point. application.add_plugin(c) self.assertEqual([1, 2, 3, 4, 5, 6], a.x) self.assertEqual([4, 5, 6], a.added) # Remove the first contributing plugin. application.remove_plugin(b) self.assertEqual([4, 5, 6], a.x) self.assertEqual([1, 2, 3], a.removed) # Remove the second contributing plugin. application.remove_plugin(c) self.assertEqual([], a.x) self.assertEqual([4, 5, 6], a.removed) def test_home(self): """ home """ class PluginA(Plugin): id = 'A' class PluginB(Plugin): id = 'B' a = PluginA() b = PluginB() application = TestApplication(plugins=[a, b]) # Make sure that each plugin gets its own directory. self.assertEqual(join(application.home, 'plugins', a.id), a.home) self.assertEqual(join(application.home, 'plugins', b.id), b.home) # Make sure that the directories got created. self.assertTrue(exists(a.home)) self.assertTrue(exists(b.home)) # Create a new application with plugins with the same Id to make sure # that it all works when the directories already exist. a = PluginA() b = PluginB() application = TestApplication(plugins=[a, b]) # Make sure that each plugin gets its own directory. self.assertEqual(join(application.home, 'plugins', a.id), a.home) self.assertEqual(join(application.home, 'plugins', b.id), b.home) # Make sure the directories got created. self.assertTrue(exists(a.home)) self.assertTrue(exists(b.home)) def test_no_recursion(self): """ Regression test for #119. """ class PluginA(Plugin): id = 'A' x = ExtensionPoint(List, id='bob') application = Application(plugins=[PluginA()]) application.get_extensions('bob') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_plugin_manager.py0000644000076500000240000001775300000000000024170 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the plugin manager. """ # Enthought library imports. from envisage.api import Plugin, PluginManager from traits.api import Bool from traits.testing.unittest_tools import unittest class SimplePlugin(Plugin): """ A simple plugin. """ #### 'SimplePlugin' interface ############################################# started = Bool(False) stopped = Bool(False) ########################################################################### # 'IPlugin' interface. ########################################################################### def start(self): """ Start the plugin. """ self.started = True self.stopped = False def stop(self): """ Stop the plugin. """ self.started = False self.stopped = True class BadPlugin(Plugin): """ A plugin that just causes trouble ;^). """ ########################################################################### # 'IPlugin' interface. ########################################################################### def start(self): """ Start the plugin. """ raise 1/0 def stop(self): """ Stop the plugin. """ raise 1/0 class PluginManagerTestCase(unittest.TestCase): """ Tests for the plugin manager. """ def test_get_plugin(self): """ get plugin """ simple_plugin = SimplePlugin() plugin_manager = PluginManager(plugins=[simple_plugin]) # Get the plugin. plugin = plugin_manager.get_plugin(simple_plugin.id) self.assertEqual(plugin, simple_plugin) # Try to get a non-existent plugin. self.assertEqual(None, plugin_manager.get_plugin('bogus')) def test_iteration_over_plugins(self): """ iteration over plugins """ simple_plugin = SimplePlugin() bad_plugin = BadPlugin() plugin_manager = PluginManager(plugins=[simple_plugin, bad_plugin]) # Iterate over the plugin manager's plugins. plugins = [] for plugin in plugin_manager: plugins.append(plugin) self.assertEqual([simple_plugin, bad_plugin], plugins) def test_start_and_stop(self): """ start and stop """ simple_plugin = SimplePlugin() plugin_manager = PluginManager(plugins=[simple_plugin]) # Start the plugin manager. This starts all of the plugin manager's # plugins. plugin_manager.start() # Make sure the plugin was started. self.assertEqual(True, simple_plugin.started) # Stop the plugin manager. This stops all of the plugin manager's # plugins. plugin_manager.stop() # Make sure the plugin was stopped. self.assertEqual(True, simple_plugin.stopped) def test_start_and_stop_errors(self): """ start and stop errors """ simple_plugin = SimplePlugin() bad_plugin = BadPlugin() plugin_manager = PluginManager(plugins=[simple_plugin, bad_plugin]) # Start the plugin manager. This starts all of the plugin manager's # plugins. with self.assertRaises(ZeroDivisionError): plugin_manager.start() # Stop the plugin manager. This stops all of the plugin manager's # plugins. with self.assertRaises(ZeroDivisionError): plugin_manager.stop() # Try to start a non-existent plugin. with self.assertRaises(SystemError): plugin_manager.start_plugin(plugin_id="bogus") # Try to stop a non-existent plugin. with self.assertRaises(SystemError): plugin_manager.stop_plugin(plugin_id="bogus") def test_only_include_plugins_whose_ids_are_in_the_include_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. include = ['foo', 'bar'] plugin_manager = PluginManager( include = include, plugins = [ SimplePlugin(id='foo'), SimplePlugin(id='bar'), SimplePlugin(id='baz') ] ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['foo', 'bar'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_only_include_plugins_matching_a_wildcard_in_the_include_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. include = ['b*'] plugin_manager = PluginManager( include = include, plugins = [ SimplePlugin(id='foo'), SimplePlugin(id='bar'), SimplePlugin(id='baz') ] ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['bar', 'baz'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_ignore_plugins_whose_ids_are_in_the_exclude_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. exclude = ['foo', 'baz'] plugin_manager = PluginManager( exclude = exclude, plugins = [ SimplePlugin(id='foo'), SimplePlugin(id='bar'), SimplePlugin(id='baz') ] ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['bar'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) def test_ignore_plugins_matching_a_wildcard_in_the_exclude_list(self): # Note that the items in the list use the 'fnmatch' syntax for matching # plugins Ids. exclude = ['b*'] plugin_manager = PluginManager( exclude = exclude, plugins = [ SimplePlugin(id='foo'), SimplePlugin(id='bar'), SimplePlugin(id='baz') ] ) # The Ids of the plugins that we expect the plugin manager to find. expected = ['foo'] # Make sure the plugin manager found only the required plugins and that # it starts and stops them correctly.. self._test_start_and_stop(plugin_manager, expected) #### Private protocol ##################################################### def _test_start_and_stop(self, plugin_manager, expected): """ Make sure the plugin manager starts and stops the expected plugins. """ # Make sure the plugin manager found only the required plugins. self.assertEqual(expected, [plugin.id for plugin in plugin_manager]) # Start the plugin manager. This starts all of the plugin manager's # plugins. plugin_manager.start() # Make sure all of the the plugins were started. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.started) # Stop the plugin manager. This stops all of the plugin manager's # plugins. plugin_manager.stop() # Make sure all of the the plugins were stopped. for id in expected: plugin = plugin_manager.get_plugin(id) self.assertNotEqual(None, plugin) self.assertEqual(True, plugin.stopped) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_provider_extension_registry.py0000644000076500000240000004401000000000000027040 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the provider extension registry. """ # Standard imports import unittest # Enthought library imports. from envisage.api import ExtensionPoint, ExtensionProvider from envisage.api import ProviderExtensionRegistry from traits.api import Int, List # Local imports. from .test_extension_registry import ExtensionRegistryTestCase class ProviderExtensionRegistryTestCase(ExtensionRegistryTestCase): """ Tests for the provider extension registry. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ self.registry = ProviderExtensionRegistry() def test_providers(self): """ providers """ registry = self.registry # Some providers. class ProviderA(ExtensionProvider): """ An extension provider. """ def get_extension_points(self): """ Return the extension points offered by the provider. """ return [ExtensionPoint(List, 'x')] def get_extensions(self, extension_point): """ Return the provider's contributions to an extension point. """ if extension_point == 'x': extensions = [42, 43] else: extensions = [] return extensions class ProviderB(ExtensionProvider): """ An extension provider. """ def get_extensions(self, extension_point): """ Return the provider's contributions to an extension point. """ if extension_point == 'x': extensions = [44, 45, 46] else: extensions = [] return extensions class ProviderC(ExtensionProvider): """ An empty provider! """ # Add the providers to the registry. registry.add_provider(ProviderA()) registry.add_provider(ProviderB()) registry.add_provider(ProviderC()) # The provider's extensions should now be in the registry. extensions = registry.get_extensions('x') self.assertEqual(5, len(extensions)) self.assertEqual(list(range(42, 47)), extensions) # Make sure there's one and only one extension point. extension_points = registry.get_extension_points() self.assertEqual(1, len(extension_points)) self.assertEqual('x', extension_points[0].id) def test_provider_extensions_changed(self): """ provider extensions changed """ registry = self.registry # Some providers. class ProviderA(ExtensionProvider): """ An extension provider. """ x = List(Int) def get_extension_points(self): """ Return the extension points offered by the provider. """ return [ExtensionPoint(List, 'my.ep')] def get_extensions(self, extension_point_id): """ Return the provider's contributions to an extension point. """ if extension_point_id == 'my.ep': return self.x else: extensions = [] return extensions def _x_changed(self, old, new): """ Static trait change handler. """ self._fire_extension_point_changed( 'my.ep', new, old, slice(0, len(old)) ) def _x_items_changed(self, event): """ Static trait change handler. """ self._fire_extension_point_changed( 'my.ep', event.added, event.removed, event.index ) class ProviderB(ExtensionProvider): """ An extension provider. """ x = List(Int) def get_extensions(self, extension_point_id): """ Return the provider's contributions to an extension point. """ if extension_point_id == 'my.ep': return self.x else: extensions = [] return extensions def _x_changed(self, old, new): """ Static trait change handler. """ self._fire_extension_point_changed( 'my.ep', new, old, slice(0, len(old)) ) def _x_items_changed(self, event): """ Static trait change handler. """ self._fire_extension_point_changed( 'my.ep', event.added, event.removed, event.index ) # Add the providers to the registry. a = ProviderA(x=[42]) b = ProviderB(x=[99, 100]) registry.add_provider(a) registry.add_provider(b) # The provider's extensions should now be in the registry. extensions = registry.get_extensions('my.ep') self.assertEqual(3, len(extensions)) self.assertEqual([42, 99, 100], extensions) # Add an extension listener to the registry. def listener(registry, event): """ A useful trait change handler for testing! """ listener.registry = registry listener.extension_point = event.extension_point_id listener.added = event.added listener.removed = event.removed listener.index = event.index registry.add_extension_point_listener(listener, 'my.ep') # Add a new extension via the provider. a.x.append(43) # Make sure the listener got called. self.assertEqual('my.ep', listener.extension_point) self.assertEqual([43], listener.added) self.assertEqual([], listener.removed) self.assertEqual(1, listener.index) # Now we should get the new extension. extensions = registry.get_extensions('my.ep') self.assertEqual(4, len(extensions)) self.assertEqual([42, 43, 99, 100], extensions) # Insert a new extension via the other provider. b.x.insert(0, 98) # Make sure the listener got called. self.assertEqual('my.ep', listener.extension_point) self.assertEqual([98], listener.added) self.assertEqual([], listener.removed) self.assertEqual(2, listener.index) # Now we should get the new extension. extensions = registry.get_extensions('my.ep') self.assertEqual(5, len(extensions)) self.assertEqual([42, 43, 98, 99, 100], extensions) # Completely change a provider's extensions. b.x = [1, 2] # Make sure the listener got called. self.assertEqual('my.ep', listener.extension_point) self.assertEqual([1, 2], listener.added) self.assertEqual([98, 99, 100], listener.removed) self.assertEqual(2, listener.index.start) self.assertEqual(5, listener.index.stop) # Now we should get the new extension. extensions = registry.get_extensions('my.ep') self.assertEqual(4, len(extensions)) self.assertEqual([42, 43, 1, 2], extensions) def test_add_provider(self): """ add provider """ registry = self.registry # A provider. class ProviderA(ExtensionProvider): """ An extension provider. """ def get_extension_points(self): """ Return the extension points offered by the provider. """ return [ExtensionPoint(List, 'x')] def get_extensions(self, extension_point): """ Return the provider's contributions to an extension point. """ if extension_point == 'x': return [42] else: extensions = [] return extensions def _x_items_changed(self, event): """ Static trait change handler. """ self._fire_extension_point_changed( 'x', event.added, event.removed, event.index ) # Add the provider to the registry. registry.add_provider(ProviderA()) # The provider's extensions should now be in the registry. extensions = registry.get_extensions('x') self.assertEqual(1, len(extensions)) self.assertTrue(42 in extensions) # Add an extension listener to the registry. def listener(registry, event): """ A useful trait change handler for testing! """ listener.registry = registry listener.extension_point = event.extension_point_id listener.added = event.added listener.removed = event.removed listener.index = event.index registry.add_extension_point_listener(listener, 'x') # Add a new provider. class ProviderB(ExtensionProvider): """ An extension provider. """ def get_extensions(self, extension_point): """ Return the provider's contributions to an extension point. """ if extension_point == 'x': extensions = [43, 44] else: extensions = [] return extensions registry.add_provider(ProviderB()) # Make sure the listener got called. self.assertEqual('x', listener.extension_point) self.assertEqual([43, 44], listener.added) self.assertEqual([], listener.removed) # Now we should get the new extensions. extensions = registry.get_extensions('x') self.assertEqual(3, len(extensions)) self.assertTrue(42 in extensions) self.assertTrue(43 in extensions) self.assertTrue(44 in extensions) def test_get_providers(self): """ get providers """ registry = self.registry # Some providers. class ProviderA(ExtensionProvider): """ An extension provider. """ class ProviderB(ExtensionProvider): """ An extension provider. """ a = ProviderA() b = ProviderB() # Add the provider to the registry. registry.add_provider(a) registry.add_provider(b) # Make sure we can get them. self.assertEqual([a, b], registry.get_providers()) def test_remove_provider(self): """ remove provider """ registry = self.registry # Some providers. class ProviderA(ExtensionProvider): """ An extension provider. """ def get_extension_points(self): """ Return the extension points offered by the provider. """ return [ExtensionPoint(List, 'x'), ExtensionPoint(List, 'y')] def get_extensions(self, extension_point): """ Return the provider's contributions to an extension point. """ if extension_point == 'x': return [42] else: extensions = [] return extensions def _x_items_changed(self, event): """ Static trait change handler. """ self._fire_extension_point_changed( 'x', event.added, event.removed, event.index ) class ProviderB(ExtensionProvider): """ An extension provider. """ def get_extensions(self, extension_point): """ Return the provider's contributions to an extension point. """ if extension_point == 'x': extensions = [43, 44] else: extensions = [] return extensions # Add the providers to the registry. a = ProviderA() b = ProviderB() registry.add_provider(a) registry.add_provider(b) # The provider's extensions should now be in the registry. extensions = registry.get_extensions('x') self.assertEqual(3, len(extensions)) self.assertTrue(42 in extensions) self.assertTrue(43 in extensions) self.assertTrue(44 in extensions) # Add an extension listener to the registry. def listener(registry, event): """ A useful trait change handler for testing! """ listener.registry = registry listener.extension_point = event.extension_point_id listener.added = event.added listener.removed = event.removed registry.add_extension_point_listener(listener, 'x') # Remove one of the providers. registry.remove_provider(b) # Make sure the listener got called. self.assertEqual('x', listener.extension_point) self.assertEqual([], listener.added) self.assertEqual([43, 44], listener.removed) # Make sure we don't get the removed extensions. extensions = registry.get_extensions('x') self.assertEqual(1, len(extensions)) self.assertTrue(42 in extensions) # Now remove the provider that declared the extension point. registry.remove_provider(a) # Make sure the extension point is gone. self.assertEqual(None, registry.get_extension_point('x')) # Make sure we don't get the removed extensions. extensions = registry.get_extensions('x') self.assertEqual(0, len(extensions)) # Make sure the listener got called. self.assertEqual('x', listener.extension_point) self.assertEqual([], listener.added) self.assertEqual([42], listener.removed) def test_remove_provider_with_no_contributions(self): """ remove provider with no contributions """ registry = self.registry # Some providers. class ProviderA(ExtensionProvider): """ An extension provider. """ def get_extension_points(self): """ Return the extension points offered by the provider. """ return [ExtensionPoint(List, 'x'), ExtensionPoint(List, 'y')] def get_extensions(self, extension_point): """ Return the provider's contributions to an extension point. """ return [] # Add the provider to the registry. a = ProviderA() registry.add_provider(a) # The provider's extensions should now be in the registry. extensions = registry.get_extensions('x') self.assertEqual(0, len(extensions)) # Add an extension listener to the registry. def listener(registry, event): """ A useful trait change handler for testing! """ listener.registry = registry listener.extension_point = event.extension_point_id listener.added = event.added listener.removed = event.removed registry.add_extension_point_listener(listener, 'x') # Remove the provider that declared the extension point. registry.remove_provider(a) # Make sure the extension point is gone. self.assertEqual(None, registry.get_extension_point('x')) # Make sure we don't get the removed extensions. extensions = registry.get_extensions('x') self.assertEqual(0, len(extensions)) # Make sure the listener did not get called (since the provider did # not make any contributions anyway!). self.assertEqual(None, getattr(listener, 'registry', None)) def test_remove_non_existent_provider(self): """ remove provider """ registry = self.registry # Some providers. class ProviderA(ExtensionProvider): """ An extension provider. """ pass a = ProviderA() # Remove one of the providers. with self.assertRaises(ValueError): registry.remove_provider(a) # Overriden to test differing behavior between the provider registry and # the base class. def test_set_extensions(self): """ set extensions """ registry = self.registry # Add an extension *point*. registry.add_extension_point(self._create_extension_point('my.ep')) # Set some extensions. with self.assertRaises(SystemError): registry.set_extensions("my.ep", [1, 2, 3]) def test_remove_non_empty_extension_point(self): """ remove non-empty extension point """ registry = self.registry # Some providers. class ProviderA(ExtensionProvider): """ An extension provider. """ def get_extension_points(self): """ Return the extension points offered by the provider. """ return [ExtensionPoint(List, 'x')] def get_extensions(self, extension_point): """ Return the provider's contributions to an extension point. """ if extension_point == 'x': extensions = [42, 43] else: extensions = [] return extensions # Add the provider to the registry. registry.add_provider(ProviderA()) # The provider's extensions should now be in the registry. extensions = registry.get_extensions('x') self.assertEqual(2, len(extensions)) self.assertEqual(list(range(42, 44)), extensions) # Make sure there's one and only one extension point. extension_points = registry.get_extension_points() self.assertEqual(1, len(extension_points)) self.assertEqual('x', extension_points[0].id) # Remove the extension point. registry.remove_extension_point('x') # Make sure there are no extension points. extension_points = registry.get_extension_points() self.assertEqual(0, len(extension_points)) # And that the extensions are gone too. self.assertEqual([], registry.get_extensions('x')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_safeweakref.py0000644000076500000240000000721200000000000023450 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for safe weakrefs. """ # Standard library imports. import weakref # Enthought library imports. from envisage.safeweakref import ref from traits.api import HasTraits from traits.testing.unittest_tools import unittest class SafeWeakrefTestCase(unittest.TestCase): """ Tests for safe weakrefs. """ def test_can_create_weakref_to_bound_method(self): class Foo(HasTraits): def method(self): self.method_called = True f = Foo() # Get a weak reference to a bound method. r = ref(f.method) self.assertNotEqual(None, r()) # Make sure we can call it. r()() self.assertTrue(f.method_called) # Delete the object to delete the method! del f # The reference should now return None. self.assertEqual(None, r()) def test_two_weakrefs_to_bound_method_are_identical(self): class Foo(HasTraits): def method(self): pass f = Foo() self.assertIs(ref(f.method), ref(f.method)) def test_internal_cache_is_weak_too(self): # smell: Fragile test because we are reaching into the internals of the # object under test. # # I can't see a (clean!) way around this without adding something to # the public API that would only exist for testing, but in terms of # 'bang for the buck' I think this is good enough despite the # fragility. cache = ref._cache class Foo(HasTraits): def method(self): pass f = Foo() # Get the length of the cache before we do anything. len_cache = len(cache) # Create a weak reference to the bound method and make sure that # exactly one item has been added to the cache. r = ref(f.method) self.assertEqual(len_cache + 1, len(cache)) # Delete the instance! del f # Our `ref` should now reference nothing... self.assertEqual(None, r()) # ... and the cache should be back to its original size! self.assertEqual(len_cache, len(cache)) def test_two_weakrefs_to_bound_method_are_equal(self): class Foo(HasTraits): def method(self): pass f = Foo() # Make sure that two references to the same method compare as equal. r1 = ref(f.method) r2 = ref(f.method) self.assertEqual(r1, r2) # Make sure that a reference compares as unequal to non-references! self.assertTrue(not r1 == 99) def test_two_weakrefs_to_bound_method_hash_equally(self): class Foo(HasTraits): def method(self): pass f = Foo() # Make sure we can hash the references. r1 = ref(f.method) r2 = ref(f.method) self.assertEqual(hash(r1), hash(r2)) # Make sure we can hash non-bound methods. r1 = ref(Foo) r2 = ref(Foo) self.assertEqual(hash(r1), hash(r2)) def test_get_builtin_weakref_for_non_bound_method(self): class Foo(HasTraits): pass f = Foo() # Get a weak reference to something that is not a bound method. r = ref(f) self.assertEqual(weakref.ref, type(r)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_service.py0000644000076500000240000000402500000000000022624 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the 'Service' trait type. """ # Enthought library imports. from envisage.api import Application, Plugin, Service from traits.api import HasTraits, Instance from traits.testing.unittest_tools import unittest class TestApplication(Application): """ The type of application used in the tests. """ id = 'test' class ServiceTestCase(unittest.TestCase): """ Tests for the 'Service' trait type. """ def test_service_trait_type(self): """ service trait type""" class Foo(HasTraits): pass class PluginA(Plugin): id = 'A' foo = Instance(Foo, (), service=True) class PluginB(Plugin): id = 'B' foo = Service(Foo) a = PluginA() b = PluginB() application = TestApplication(plugins=[a, b]) application.start() # Make sure the services were registered. self.assertEqual(a.foo, b.foo) # Stop the application. application.stop() # Make sure the service was unregistered. self.assertEqual(None, b.foo) # You can't set service traits! with self.assertRaises(SystemError): setattr(b, "foo", "bogus") def test_service_trait_type_with_no_service_registry(self): """ service trait type with no service registry """ class Foo(HasTraits): pass class Bar(HasTraits): foo = Service(Foo) # We should get an exception because the object does not have an # 'service_registry' trait. b = Bar() with self.assertRaises(ValueError): getattr(b, "foo") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_service_registry.py0000644000076500000240000004045000000000000024556 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the service registry. """ # Standard library imports. import sys # Enthought library imports. from envisage.api import Application, ServiceRegistry, NoSuchServiceError from traits.api import HasTraits, Int, Interface, provides from traits.testing.unittest_tools import unittest # This module's package. PKG = 'envisage.tests' def service_factory(**properties): """ A factory for foos. """ return HasTraits(**properties) class ServiceRegistryTestCase(unittest.TestCase): """ Tests for the service registry. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ # We do all of the testing via the application to make sure it offers # the same interface! self.service_registry = Application(service_registry=ServiceRegistry()) # module 'foo' need to be cleared out when this test is run, # because other tests also import foo. if PKG + '.foo' in sys.modules: del sys.modules[PKG + '.foo'] ########################################################################### # Tests. ########################################################################### def test_should_get_required_service(self): class Foo(HasTraits): price = Int foo = Foo() # Register a service factory. self.service_registry.register_service(Foo, foo) service = self.service_registry.get_required_service(Foo) self.assertIs(foo, service) def test_should_get_exception_if_required_service_is_missing(self): class IFoo(Interface): price = Int with self.assertRaises(NoSuchServiceError): self.service_registry.get_required_service(IFoo) def test_imported_service_factory(self): """ imported service factory """ class IFoo(Interface): price = Int # Register a service factory. self.service_registry.register_service( HasTraits, PKG + '.test_service_registry.service_factory', {'price' : 100} ) # Create a query that matches the registered object. service = self.service_registry.get_service(HasTraits, 'price <= 100') self.assertNotEqual(None, service) self.assertEqual(HasTraits, type(service)) # This shows that the properties were passed in to the factory. self.assertEqual(100, service.price) # Make sure that the object created by the factory is cached (i.e. we # get the same object back from now on!). service2 = self.service_registry.get_service(HasTraits, 'price <= 100') self.assertTrue(service is service2) def test_function_service_factory(self): """ function service factory """ class IFoo(Interface): price = Int @provides(IFoo) class Foo(HasTraits): price = Int def foo_factory(**properties): """ A factory for foos. """ return Foo(**properties) # Register a service factory. self.service_registry.register_service( IFoo, foo_factory, {'price' : 100} ) # Create a query that matches the registered object. service = self.service_registry.get_service(IFoo, 'price <= 100') self.assertNotEqual(None, service) self.assertEqual(Foo, type(service)) # Make sure that the object created by the factory is cached (i.e. we # get the same object back from now on!). service2 = self.service_registry.get_service(IFoo, 'price <= 100') self.assertTrue(service is service2) def test_lazy_function_service_factory(self): """ lazy function service factory """ # Register a service factory by name. def foo_factory(**properties): """ A factory for foos. """ from envisage.tests.foo import Foo foo_factory.foo = Foo() return foo_factory.foo i_foo = PKG + '.i_foo.IFoo' foo = PKG + '.foo' self.service_registry.register_service(i_foo, foo_factory) # Get rid of the 'foo' module (used in other tests). if foo in sys.modules: del sys.modules[foo] # Make sure that we haven't imported the 'foo' module. self.assertTrue(foo not in sys.modules) # Look up a non-existent service. services = self.service_registry.get_services('bogus.IBogus') # Make sure that we *still* haven't imported the 'foo' module. self.assertTrue(foo not in sys.modules) # Look it up again. services = self.service_registry.get_services(i_foo) self.assertEqual([foo_factory.foo], services) self.assertTrue(foo in sys.modules) # Clean up! del sys.modules[foo] def test_lazy_bound_method_service_factory(self): """ lazy bound method service factory """ i_foo = PKG + '.i_foo.IFoo' foo = PKG + '.foo' class ServiceProvider(HasTraits): """ A class that provides a service. This is used to make sure a bound method can be used as a service factory. """ # Register a service factory by name. def foo_factory(self, **properties): """ A factory for foos. """ from envisage.tests.foo import Foo self.foo = Foo() return self.foo sp = ServiceProvider() self.service_registry.register_service(i_foo, sp.foo_factory) # Get rid of the 'foo' module (used in other tests). if foo in sys.modules: del sys.modules[foo] # Make sure that we haven't imported the 'foo' module. self.assertTrue(foo not in sys.modules) # Look up a non-existent service. services = self.service_registry.get_services('bogus.IBogus') # Make sure that we *still* haven't imported the 'foo' module. self.assertTrue(foo not in sys.modules) # Look up the service. services = self.service_registry.get_services(i_foo) self.assertEqual([sp.foo], services) self.assertTrue(foo in sys.modules) # Clean up! del sys.modules[foo] def test_get_services(self): """ get services """ class IFoo(Interface): pass @provides(IFoo) class Foo(HasTraits): pass # Register two services. foo = Foo() self.service_registry.register_service(IFoo, foo) foo = Foo() self.service_registry.register_service(IFoo, foo) # Look it up again. services = self.service_registry.get_services(IFoo) self.assertEqual(2, len(services)) class IBar(Interface): pass # Lookup a non-existent service. services = self.service_registry.get_services(IBar) self.assertEqual([], services) def test_get_services_with_strings(self): """ get services with strings """ from envisage.tests.foo import Foo # Register a couple of services using a string protocol name. protocol_name = 'envisage.tests.foo.IFoo' self.service_registry.register_service(protocol_name, Foo()) self.service_registry.register_service(protocol_name, Foo()) # Look them up using the same string! services = self.service_registry.get_services(protocol_name) self.assertEqual(2, len(services)) def test_get_services_with_query(self): """ get services with query """ class IFoo(Interface): price = Int @provides(IFoo) class Foo(HasTraits): price = Int # Register two services. # # This one shows how the object's attributes are used when evaluating # a query. foo = Foo(price=100) self.service_registry.register_service(IFoo, foo) # This one shows how properties can be specified that *take precedence* # over the object's attributes when evaluating a query. goo = Foo(price=10) self.service_registry.register_service(IFoo, goo, {'price' : 200}) # Create a query that doesn't match any registered object. services = self.service_registry.get_services(IFoo, 'color == "red"') self.assertEqual([], services) # Create a query that matches one of the registered objects. services = self.service_registry.get_services(IFoo, 'price <= 100') self.assertEqual([foo], services) # Create a query that matches both registered objects. services = self.service_registry.get_services(IFoo, 'price >= 100') self.assertTrue(foo in services) self.assertTrue(goo in services) self.assertEqual(2, len(services)) class IBar(Interface): pass # Lookup a non-existent service. services = self.service_registry.get_services(IBar, 'price <= 100') self.assertEqual([], services) def test_get_service(self): """ get service """ class IFoo(Interface): pass @provides(IFoo) class Foo(HasTraits): pass # Register a couple of services. foo = Foo() self.service_registry.register_service(IFoo, foo) goo = Foo() self.service_registry.register_service(IFoo, goo) # Look up one of them! service = self.service_registry.get_service(IFoo) self.assertTrue(foo is service or goo is service) class IBar(Interface): pass # Lookup a non-existent service. service = self.service_registry.get_service(IBar) self.assertEqual(None, service) def test_get_service_with_query(self): """ get service with query """ class IFoo(Interface): price = Int @provides(IFoo) class Foo(HasTraits): price = Int # Register two services. # # This one shows how the object's attributes are used when evaluating # a query. foo = Foo(price=100) self.service_registry.register_service(IFoo, foo) # This one shows how properties can be specified that *take precedence* # over the object's attributes when evaluating a query. goo = Foo(price=10) self.service_registry.register_service(IFoo, goo, {'price' : 200}) # Create a query that doesn't match any registered object. service = self.service_registry.get_service(IFoo, 'price < 100') self.assertEqual(None, service) # Create a query that matches one of the registered objects. service = self.service_registry.get_service(IFoo, 'price <= 100') self.assertEqual(foo, service) # Create a query that matches both registered objects. service = self.service_registry.get_service(IFoo, 'price >= 100') self.assertTrue(foo is service or goo is service) class IBar(Interface): pass # Lookup a non-existent service. service = self.service_registry.get_service(IBar, 'price <= 100') self.assertEqual(None, service) def test_get_and_set_service_properties(self): """ get and set service properties """ class IFoo(Interface): price = Int @provides(IFoo) class Foo(HasTraits): price = Int # Register two services. # # This one has no properties. foo = Foo(price=100) foo_id = self.service_registry.register_service(IFoo, foo) # This one has properties. goo = Foo(price=10) goo_id = self.service_registry.register_service( IFoo, goo, {'price' : 200} ) # Get the properties. foo_properties = self.service_registry.get_service_properties(foo_id) self.assertEqual({}, foo_properties) goo_properties = self.service_registry.get_service_properties(goo_id) self.assertEqual(200, goo_properties['price']) # Update the properties. foo_properties['price'] = 300 goo_properties['price'] = 500 # Set the properties. self.service_registry.set_service_properties(foo_id, foo_properties) self.service_registry.set_service_properties(goo_id, goo_properties) # Get the properties again. foo_properties = self.service_registry.get_service_properties(foo_id) self.assertEqual(300, foo_properties['price']) goo_properties = self.service_registry.get_service_properties(goo_id) self.assertEqual(500, goo_properties['price']) # Try to get the properties of a non-existent service. with self.assertRaises(ValueError): self.service_registry.get_service_properties(-1) # Try to set the properties of a non-existent service. with self.assertRaises(ValueError): self.service_registry.set_service_properties(-1, {}) def test_unregister_service(self): """ unregister service """ class IFoo(Interface): price = Int @provides(IFoo) class Foo(HasTraits): price = Int # Register two services. # # This one shows how the object's attributes are used when evaluating # a query. foo = Foo(price=100) foo_id = self.service_registry.register_service(IFoo, foo) # This one shows how properties can be specified that *take precedence* # over the object's attributes when evaluating a query. goo = Foo(price=10) goo_id = self.service_registry.register_service( IFoo, goo, {'price' : 200} ) # Create a query that doesn't match any registered object. service = self.service_registry.get_service(IFoo, 'price < 100') self.assertEqual(None, service) # Create a query that matches one of the registered objects. service = self.service_registry.get_service(IFoo, 'price <= 100') self.assertEqual(foo, service) # Create a query that matches both registered objects. service = self.service_registry.get_service(IFoo, 'price >= 100') self.assertTrue(foo is service or goo is service) #### Now do some unregistering! #### # Unregister 'foo'. self.service_registry.unregister_service(foo_id) # This query should no longer match any of the registered objects. service = self.service_registry.get_service(IFoo, 'price <= 100') self.assertEqual(None, service) # Unregister 'goo'. self.service_registry.unregister_service(goo_id) # This query should no longer match any of the registered objects. service = self.service_registry.get_service(IFoo, 'price >= 100') self.assertEqual(None, service) # Try to unregister a non-existent service. with self.assertRaises(ValueError): self.service_registry.unregister_service(-1) def test_minimize_and_maximize(self): """ minimize and maximize """ class IFoo(Interface): price = Int @provides(IFoo) class Foo(HasTraits): price = Int # Register some objects with various prices. x = Foo(price=10) y = Foo(price=5) z = Foo(price=100) for foo in [x, y, z]: self.service_registry.register_service(IFoo, foo) # Find the service with the lowest price. service = self.service_registry.get_service(IFoo, minimize='price') self.assertNotEqual(None, service) self.assertEqual(Foo, type(service)) self.assertEqual(y, service) # Find the service with the highest price. service = self.service_registry.get_service(IFoo, maximize='price') self.assertNotEqual(None, service) self.assertEqual(Foo, type(service)) self.assertEqual(z, service) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/tests/test_slice.py0000644000076500000240000001174200000000000022267 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests to help find out how trait list events work. These tests exist because when we are using the 'ExtensionPoint' trait type we try to mimic trait list events when extensions are added or removed. """ # Enthought library imports. from traits.api import HasTraits, List from traits.testing.unittest_tools import unittest # The starting list for all tests. TEST_LIST = [7, 9, 2, 3, 4, 1, 6, 5, 8, 0] def listener(obj, trait_name, old, event): """ Recreate a list operation from a trait list event. """ clone = TEST_LIST[:] # If nothing was added then this is a 'del' or 'remove' operation. if len(event.added) == 0: if isinstance(event.index, slice): del clone[event.index] else: # workaroud for traits bug in Python 3 # https://github.com/enthought/traits/issues/334 index = event.index if event.index is not None else 0 del clone[index:index + len(event.removed)] # If nothing was removed then it is an 'append', 'insert' or 'extend' # operation. elif len(event.removed) == 0: if isinstance(event.index, slice): clone[event.index] = event.added[0] else: clone.insert(event.index, event.added[0]) # Otherwise, it is an assigment ('sort' and 'reverse' fall into this # category). else: if isinstance(event.index, slice): clone[event.index] = event.added[0] else: clone[event.index : event.index + len(event.added)] = event.added listener.clone = clone class SliceTestCase(unittest.TestCase): """ Tests to help find out how trait list events work. """ def setUp(self): """ Prepares the test fixture before each test method is called. """ class Foo(HasTraits): l = List self.f = Foo(l=TEST_LIST) self.f.on_trait_change(listener, 'l_items') def test_append(self): """ append """ self.f.l.append(99) # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_insert(self): """ insert """ self.f.l.insert(3, 99) # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_extend(self): """ extend """ self.f.l.append([99, 100]) # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_remove(self): """ remove """ self.f.l.remove(5) # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_reverse(self): """ reverse """ self.f.l.reverse() # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_sort(self): """ sort """ self.f.l.sort() # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_pop(self): """ remove """ self.f.l.pop() # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_del_all(self): """ del all """ del self.f.l[:] # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_assign_item(self): """ assign item """ self.f.l[3] = 99 # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_del_item(self): """ del item """ del self.f.l[3] # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_assign_slice(self): """ assign slice """ self.f.l[2:4] = [88, 99] # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_del_slice(self): """ del slice """ del self.f.l[2:5] # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_assign_extended_slice(self): """ assign extended slice """ self.f.l[2:6:2] = [88, 99] # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) def test_del_extended_slice(self): """ del extended slice """ del self.f.l[2:6:2] # Make sure we successfully recreated the operation. self.assertEqual(self.f.l, listener.clone) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/tests/test_version.py0000644000076500000240000000376500000000000022663 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the envisage.__version__ attribute and the envisage.version module contents. """ from __future__ import absolute_import, print_function, unicode_literals import unittest import pkg_resources import six import envisage class TestVersion(unittest.TestCase): def test_dunder_version(self): self.assertIsInstance(envisage.__version__, six.text_type) # Round-trip through parse_version; this verifies not only # that the version is valid, but also that it's properly normalised # according to the PEP 440 rules. parsed_version = pkg_resources.parse_version(envisage.__version__) self.assertEqual(six.text_type(parsed_version), envisage.__version__) def test_version_version(self): # Importing inside the test to ensure that we get a test error # in the case where the version module does not exist. from envisage.version import version self.assertIsInstance(version, six.text_type) parsed_version = pkg_resources.parse_version(version) self.assertEqual(six.text_type(parsed_version), version) def test_version_git_revision(self): from envisage.version import git_revision self.assertIsInstance(git_revision, six.text_type) # Check the form of the revision. Could use a regex, but that seems # like overkill. self.assertEqual(len(git_revision), 40) self.assertLessEqual(set(git_revision), set("0123456789abcdef")) def test_versions_match(self): import envisage.version self.assertEqual(envisage.version.version, envisage.__version__) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/twisted_application.py0000644000076500000240000000326100000000000023032 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A non-GUI application with a twisted reactor event loop. Nothing is imported from twisted until the application is started so this module can safely live in the Envisage core without twisted being installed. """ # Standard library imports. import logging # Enthought library imports. from envisage.api import Application # Logging. logger = logging.getLogger(__name__) class TwistedApplication(Application): """ A non-GUI application with a twisted reactor event loop. """ def start(self): """ Start the application. """ started = super(TwistedApplication, self).start() # Don't start the event loop if the start was vetoed. if started: from twisted.internet import reactor logger.debug('---------- reactor starting ----------') reactor.run() return started def stop(self): """ Stop the application. """ stopped = super(TwistedApplication, self).stop() # Don't stop the event loop if the stop was vetoed. if stopped: from twisted.internet import reactor logger.debug('---------- reactor stopping ----------') reactor.stop() return stopped #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2635043 envisage-4.9.0/envisage/ui/0000755000076500000240000000000000000000000017025 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/__init__.py0000644000076500000240000000000000000000000021124 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2678342 envisage-4.9.0/envisage/ui/action/0000755000076500000240000000000000000000000020302 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/__init__.py0000644000076500000240000000000000000000000022401 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/abstract_action_manager_builder.py0000644000076500000240000003617300000000000027226 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Builds menus, menu bars and tool bars from action sets. """ # Enthought library imports. from pyface.action.api import ActionManager, MenuManager from traits.api import HasTraits, Instance, List, provides # Local imports. from .action_set import ActionSet from .action_set_manager import ActionSetManager from .group import Group from .i_action_manager_builder import IActionManagerBuilder @provides(IActionManagerBuilder) class AbstractActionManagerBuilder(HasTraits): """ Builds menus, menu bars and tool bars from action sets. This class *must* be subclassed, and the following methods implemented:: _create_action _create_group _create_menu_manager """ #### 'IActionManagerBuilder' interface #################################### # The action sets used by the builder. action_sets = List(ActionSet) #### Private interface #################################################### _action_set_manager = Instance(ActionSetManager, ()) ########################################################################### # 'IActionManagerBuilder' interface. ########################################################################### def create_menu_bar_manager(self, root): """ Create a menu bar manager from the builder's action sets. """ menu_bar_manager = self._create_menu_bar_manager() self.initialize_action_manager(menu_bar_manager, root) return menu_bar_manager # fixme: V3 refactor loooong (and confusing) method! def create_tool_bar_managers(self, root): """ Creates all tool bar managers from the builder's action sets. """ ######################################## # New style (i.e multi) tool bars. ######################################## tool_bar_managers = [] for tool_bar in self._action_set_manager.get_tool_bars(root): # Get all of the groups for the tool bar. groups = [] for group in self._action_set_manager.get_groups(root): if group.path.startswith('%s/%s' % (root, tool_bar.name)): group.path = '/'.join(action.path.split('/')[1:]) groups.append(group) # Get all of the actions for the tool bar. actions = [] for action in self._action_set_manager.get_actions(root): if action.path.startswith('%s/%s' % (root, tool_bar.name)): action.path = '/'.join(action.path.split('/')[1:]) actions.append(action) # We don't add the tool bar if it is empty! if len(groups) + len(actions) > 0: tool_bar_manager = self._create_tool_bar_manager(tool_bar) # Add all groups and menus. self._add_groups_and_menus(tool_bar_manager, groups) # Add all of the actions ot the menu manager. self._add_actions(tool_bar_manager, actions) # Include the tool bar! tool_bar_managers.append(tool_bar_manager) ###################################################################### # Scoop up old groups and actions for the old style (single) tool bar. ###################################################################### # Get all of the groups for the tool bar. groups = [] for group in self._action_set_manager.get_groups(root): if group.path == root: groups.append(group) # Get all of the actions for the tool bar. actions = [] for action in self._action_set_manager.get_actions(root): if action.path == root: actions.append(action) # We don't add the tool bar if it is empty! if len(groups) + len(actions) > 0: from .tool_bar import ToolBar tool_bar_manager = self._create_tool_bar_manager( ToolBar(name='Tool Bar', path=root, _action_set_=None) ) # Add all groups and menus. self._add_groups_and_menus(tool_bar_manager, groups) # Add all of the actions ot the menu manager. self._add_actions(tool_bar_manager, actions) # Include the tool bar! tool_bar_managers.insert(0, tool_bar_manager) return tool_bar_managers def initialize_action_manager(self, action_manager, root): """ Initialize an action manager from the builder's action sets. """ # Get all of the groups and menus for the specified root (for toolbars # there will **only** be groups). groups_and_menus = self._action_set_manager.get_groups(root) groups_and_menus.extend(self._action_set_manager.get_menus(root)) # Add all groups and menus. self._add_groups_and_menus(action_manager, groups_and_menus) # Get all actions for the specified root. actions = self._action_set_manager.get_actions(root) # Add all of the actions ot the menu manager. self._add_actions(action_manager, actions) return ########################################################################### # Protected 'AbstractActionManagerBuilder' interface. ########################################################################### def _create_action(self, action_definition): """ Creates an action implementation from a definition. """ raise NotImplementedError def _create_group(self, group_definition): """ Creates a group implementation from a definition. """ raise NotImplementedError def _create_menu_manager(self, menu_manager_definition): """ Creates a menu manager implementation from a definition. """ raise NotImplementedError def _create_menu_bar_manager(self): """ Creates a menu bar manager implementation. """ raise NotImplementedError def _create_tool_bar_manager(self, tool_bar_definition): """ Creates a tool bar manager implementation from a definition. """ raise NotImplementedError ########################################################################### # Private interface. ########################################################################### #### Trait change handler ################################################# def _action_sets_changed(self, old, new): """ Static trait change handler. """ self._action_set_manager.action_sets = new return #### Methods ############################################################## def _add_actions(self, action_manager, actions): """ Add the specified actions to an action manager. """ while len(actions) > 0: start = len(actions) for action in actions[:]: # Resolve the action's path to find the action manager that it # should be added to. # # If any of the menus in path are missing then this creates # them automatically (think 'mkdirs'!). target = self._make_submenus(action_manager, action.path) # Attempt to place the action. # # If the action needs to be placed 'before' or 'after' some # other action, but the other action has not yet been added # then we will try again later! if self._add_action(target, action): actions.remove(action) end = len(actions) # If we didn't succeed in placing *any* actions then we must have a # problem! if start == end: raise ValueError('Could not place %s' % actions) return def _add_action(self, action_manager, action): """ Add an action to an action manager. Return True if the action was added successfully. Return False if the action needs to be placed 'before' or 'after' some other action, but the other action has not yet been added. """ group = self._find_group(action_manager, action.group) if group is None: msg = 'No such group (%s) for %s' % (action.group, action) raise ValueError(msg) if len(action.before) > 0: item = group.find(action.before) if item is None: return False index = group.items.index(item) elif len(action.after) > 0: item = group.find(action.after) if item is None: return False index = group.items.index(item) + 1 else: index = len(group.items) group.insert(index, self._create_action(action)) return True def _add_groups_and_menus(self, action_manager, groups_and_menus): """ Add the specified groups and menus to an action manager. """ # The reason we put the groups and menus together is that as we iterate # over the list trying to add them, we might need to add a group before # we can add a menu and we might need to add a menu before we can add a # group! Hence, we take multiple passes over the list and we only barf # if, in any single iteration, we cannot add anything. while len(groups_and_menus) > 0: start = len(groups_and_menus) for item in groups_and_menus[:]: # Resolve the path to find the menu manager that we are about # to add the sub-menu or group to. target = self._find_action_manager(action_manager, item.path) if target is not None: # Attempt to place a group. if isinstance(item, Group): if self._add_group(target, item): groups_and_menus.remove(item) # Attempt to place a menu. elif self._add_menu(target, item): groups_and_menus.remove(item) end = len(groups_and_menus) # If we didn't succeed in adding *any* menus or groups then we # must have a problem! if start == end: raise ValueError('Could not place %s' % groups_and_menus) return def _add_group(self, action_manager, group): """ Add a group to an action manager. Return True if the group was added successfully. Return False if the group needs to be placed 'before' or 'after' some other group, but the other group has not yet been added. """ # Does the group already exist in the menu? If not then add it, # otherwise do nothing. if action_manager.find_group(group.id) is None: if len(group.before) > 0: item = action_manager.find_group(group.before) if item is None: return False index = action_manager.groups.index(item) elif len(group.after) > 0: item = action_manager.find_group(group.after) if item is None: return False index = action_manager.groups.index(item) + 1 else: # If the menu manger has an 'additions' group then make sure # that it is always the last one! In Pyface, the 'additions' # groups is created by default, so unless someone has # explicitly removed it, it *will* be there! additions = action_manager.find_group('additions') if additions is not None: index = action_manager.groups.index(additions) else: index = len(action_manager.groups) action_manager.insert(index, self._create_group(group)) return True def _add_menu(self, menu_manager, menu): """ Add a menu manager to a errr, menu manager. Return True if the menu was added successfully. Return False if the menu needs to be placed 'before' or 'after' some other item, but the other item has not yet been added. """ group = self._find_group(menu_manager, menu.group) if group is None: return False if len(menu.before) > 0: item = group.find(menu.before) if item is None: return False index = group.items.index(item) elif len(menu.after) > 0: item = group.find(menu.after) if item is None: return False index = group.items.index(item) + 1 else: index = len(group.items) # If the menu does *not* already exist in the group then add it. menu_item = group.find(menu.id) if menu_item is None: group.insert(index, self._create_menu_manager(menu)) # Otherwise, add all of the new menu's groups to the existing one. else: for group in menu.groups: self._add_group(menu_item, group) return True def _find_group(self, action_manager, id): """ Find the group with the specified ID. """ if len(id) > 0: group = action_manager.find_group(id) else: group = action_manager.find_group('additions') return group def _find_action_manager(self, action_manager, path): """ Return the action manager at the specified path. Returns None if the action manager cannot be found. """ components = path.split('/') if len(components) == 1: action_manager = action_manager else: action_manager = action_manager.find_item('/'.join(components[1:])) return action_manager def _make_submenus(self, menu_manager, path): """ Retutn the menu manager identified by the path. Make any intermediate menu-managers that are missing. """ components = path.split('/') # We skip the first component, because if the path is of length 1, then # the target menu manager is the menu manager passed in. for component in components[1:]: item = menu_manager.find_item(component) # If the menu manager does *not* contain an item with this ID then # create a sub-menu automatically. if item is None: item = MenuManager(id=component, name=component) menu_manager.append(item) # If the menu manager *does* already contain an item with this ID # then make sure it is a menu and not an action! elif not isinstance(item, ActionManager): msg = '%s is not a menu in path %s' % (item, path) raise ValueError(msg) menu_manager = item return menu_manager #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/action.py0000644000076500000240000000246400000000000022137 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The *definition* of an action in a tool bar or menu. """ # Enthought library imports. from traits.api import Str # Local imports. from .location import Location class Action(Location): """ The *definition* of an action in a tool bar or menu. """ #### Action implementation ################################################ # The action's name (appears on menus and toolbars etc). name = Str # The name of the class that implements the action. class_name = Str ########################################################################### # 'object' interface ########################################################################### def __str__(self): """ Return the 'informal' string representation of the object. """ return 'Action(%s)' % self.name __repr__ = __str__ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/action_set.py0000644000076500000240000000706500000000000023014 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action set is a collection of menus, groups, and actions. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Bool, Dict, HasTraits, List, Str, provides from traits.util.camel_case import camel_case_to_words # Local imports. from .action import Action from .group import Group from .menu import Menu from .tool_bar import ToolBar from .i_action_set import IActionSet # Logging. logger = logging.getLogger(__name__) @provides(IActionSet) class ActionSet(HasTraits): """ An action set is a collection of menus, groups, and actions. """ # The action set's globally unique identifier. id = Str # The action set's name. # # fixme: This is not currently used, but in future it will be the name that # is shown to the user when they are customizing perspectives by adding or # removing action sets etc. name = Str # The actions in this set. actions = List(Action) # The groups in this set. groups = List(Group) # The menus in this set. menus = List(Menu) # The tool bars in this set. tool_bars = List(ToolBar) # Are the actions and menus in this set enabled (if they are disabled they # will be greyed out). Tool bars are generally not greyed out themselves, # but the actions within them are. enabled = Bool(True) # Are the actions, menus and tool bars in this set visible? visible = Bool(True) # A mapping from human-readable names to globally unique IDs. # # This mapping is used when interpreting the first item in a location path # (i.e., the **path** trait of a **Location** instance). # # When the path is intepreted, the first component (i.e., the first item # before any '/') is checked to see if it is in the mapping, and if so it # is replaced with the value in the map. # # This technique allows paths to start with human readable names, as # opposed to IDs (which are required in order to manage the namespace of # all action sets). # # For example, in the Envisage workbench, the menu bar ID is: # # ``'envisage.workbench.menubar'`` # # Without aliases, you must specify a location like this: # # ``Location(path='envisage.workbench.menubar/ASubMenu/AGroup')`` # # This is a bit long-winded! Instead, you can define an alias: # # ``aliases = { 'MenuBar' : 'envisage.workbench.menubar' }`` # # In that case, you can specify a location like this: # # ``Location(path='MenuBar/ASubMenu/AGroup')`` # aliases = Dict(Str, Str) #### Trait initializers ################################################### def _id_default(self): """ Trait initializer. """ id = '%s.%s' % (type(self).__module__, type(self).__name__) logger.warning('action set %s has no Id - using <%s>' % (self, id)) return id def _name_default(self): """ Trait initializer. """ name = camel_case_to_words(type(self).__name__) logger.warning('action set %s has no name - using <%s>' % (self, name)) return name #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/action_set_manager.py0000644000076500000240000000744400000000000024507 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Manages a collection of action sets. """ # Enthought library imports. from traits.api import HasTraits, List # Local imports. from .action_set import ActionSet class ActionSetManager(HasTraits): """ Manages a collection of action sets. """ #### 'ActionSetManager' interface ######################################### # The action sets that this manager manages. action_sets = List(ActionSet) ########################################################################### # 'ActionSetManager' interface. ########################################################################### def get_actions(self, root): """ Return all action definitions for a root. """ return self._get_items(self.action_sets, 'actions', root) def get_groups(self, root): """ Return all group definitions for a root. """ return self._get_items(self.action_sets, 'groups', root) def get_menus(self, root): """ Return all menu definitions for a root. """ return self._get_items(self.action_sets, 'menus', root) def get_tool_bars(self, root): """ Return all tool bar definitions for a root. """ return self._get_items(self.action_sets, 'tool_bars', root) ########################################################################### # 'Private' interface. ########################################################################### def _get_items(self, action_sets, attribute_name, root): """ Return all actions, groups or menus for a particular root. e.g. To get all of the groups:: self._get_items(action_sets, 'groups', root) """ items = [] for action_set in action_sets: for item in getattr(action_set, attribute_name): if self._get_root(item.path, action_set.aliases) == root: items.append(item) # fixme: Hacky, but the model needs to maintain the # action set that contributed the item. item._action_set_ = action_set # fixme: Even hackier if this is a menu then we need to # tag the action set onto all of the groups. if attribute_name in ['menus', 'toolbars']: for group in item.groups: group._action_set_ = action_set return items def _get_root(self, path, aliases): """ Return the effective root for a path. If the first component of the path matches an alias, then we return the value of the alias. e.g. If the aliases are:: {'MenuBar' : 'envisage.ui.workbench.menubar'} and the path is:: 'MenuBar/File/New' Then the effective root is:: 'envisage.ui.workbench.menubar' If the first component of the path does *not* match an alias, then it is returned as is. e.g. If the aliases are:: {'ToolBar' : 'envisage.ui.workbench.toolbar'} and the path is:: 'MenuBar/File/New' Then the effective root is:: 'MenuBar' """ components = path.split('/') if components[0] in aliases: root = aliases[components[0]] else: root = components[0] return root #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/api.py0000644000076500000240000000131600000000000021426 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .i_action_set import IActionSet from .i_action_manager_builder import IActionManagerBuilder from .abstract_action_manager_builder import AbstractActionManagerBuilder from .action import Action from .action_set import ActionSet from .group import Group from .menu import Menu from .tool_bar import ToolBar ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/group.py0000644000076500000240000000265500000000000022020 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The *definition* of a group in a tool bar or menu. """ # Enthought library imports. from traits.api import Bool, Str # Local imports. from .location import Location class Group(Location): """ The *definition* of a group in a tool bar or menu. """ # The group's unique identifier (unique within the tool bar, menu bar or # menu that the group is to be added to). id = Str # Does this group require a separator? separator = Bool(True) # The optional name of a class that implements the group. The class must # support the **pyface.action.Group** protocol. class_name = Str ########################################################################### # 'object' interface ########################################################################### def __str__(self): """ Return the 'informal' string representation of the object. """ return 'Group(%s)' % self.id __repr__ = __str__ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/i_action_manager_builder.py0000644000076500000240000000220400000000000025637 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The interface for action manager builders. """ # Enthought library imports. from traits.api import Interface, List # Local imports. from .action_set import ActionSet class IActionManagerBuilder(Interface): """ The interface for action manager builders. An action manager builder populates action managers (i.e. menus, menu bars and tool bars) from the menus, groups and actions defined in its action sets. """ # The action sets used by the builder. action_sets = List(ActionSet) def initialize_action_manager(self, action_manager, root): """ Initialize an action manager from the builder's action sets. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/i_action_set.py0000644000076500000240000000501400000000000023314 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The action set interface. """ # Enthought library imports. from traits.api import Dict, Interface, List, Str # Local imports. from .action import Action from .group import Group from .menu import Menu from .tool_bar import ToolBar class IActionSet(Interface): """ The action set interface. An action set is a collection of menus, groups, and actions. """ # The action set's globally unique identifier. id = Str # The action set's name. # # fixme: This is not currently used, but in future it will be the name that # is shown to the user when they are customizing perspectives by adding or # removing action sets etc. name = Str # The actions in this set. actions = List(Action) # The groups in this set. groups = List(Group) # The menus in this set. menus = List(Menu) # The tool bars in this set. tool_bars = List(ToolBar) # A mapping from human-readable names to globally unique IDs. # # This mapping is used when interpreting the first item in a location path # (i.e., the **path** trait of a **Location** instance). # # When the path is intepreted, the first component (i.e., the first item # before any '/') is checked to see if it is in the mapping, and if so it # is replaced with the value in the map. # # This technique allows paths to start with human readable names, as # opposed to IDs (which are required in order to manage the namespace of # all action sets). # # For example, in the Envisage workbench, the menu bar ID is: # # ``'envisage.workbench.menubar'`` # # Without aliases, you must specify a location like this: # # ``Location(path='envisage.workbench.menubar/ASubMenu/AGroup')`` # # This is a bit long-winded! Instead, you can define an alias: # # ``aliases = { 'MenuBar' : 'envisage.workbench.menubar' }`` # # In that case, you can specify a location like this: # # ``Location(path='MenuBar/ASubMenu/AGroup')`` # aliases = Dict(Str, Str) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/location.py0000644000076500000240000000330600000000000022466 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The location of a group, menu, or action, within an action hierarchy. """ # Enthought library imports. from traits.api import HasTraits, Str class Location(HasTraits): """ The location of a group, menu, or action, within an action hierarchy. """ # A forward-slash-separated path through the action hierarchy to the menu # to add the action, group or menu to. # # Examples # -------- # # * To add an item to the menu bar: ``path = "MenuBar"`` # # * To add an item to the tool bar: ``path = "ToolBar"`` # # * To add an item to a sub-menu: ``path = "MenuBar/File/New"`` # path = Str #### Placement of the action within the menu specified by the path ######## # The ID of the group to add the action or menu to (you can't have nested # groups). group = Str # The item appears after the item with this ID. # # - for groups, this is the ID of another group. # - for menus and actions, this is the ID of another menu or action. after = Str # The action appears before the item with this ID. # # - for groups, this is the ID of another group. # - for menus and actions, this is the ID of another menu or action. before = Str #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/menu.py0000644000076500000240000000536600000000000021632 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The *definition* of a menu in a menu bar or menu. """ # Enthought library imports. from traits.api import Instance, List, Str # Local imports. from .group import Group from .location import Location from ..._compat import STRING_BASE_CLASS class CGroup(Instance): """ A trait type for a 'Group' or anything that can be cast to a 'Group'. Currently, the only cast allowed is from string -> Group using the string as the group's ID. """ ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **kw): """ Constructor. """ super(CGroup, self).__init__(klass=Group, **kw) return ########################################################################### # 'TraitType' interface. ########################################################################### def validate(self, object, name, value): """ Validate a value. """ if isinstance(value, STRING_BASE_CLASS): value = Group(id=value) return super(CGroup, self).validate(object, name, value) class Menu(Location): """ The *definition* of a menu in a menu bar or menu. """ # The menu's unique identifier (unique within the group that the menu is to # be added to). id = Str # The menu name (appears on the menu bar or menu). name = Str # The groups in the menu. groups = List(CGroup) # The optional name of a class that implements the menu. The class must # support the **pyface.action.MenuManager** protocol. class_name = Str ########################################################################### # 'object' interface ########################################################################### def __str__(self): """ Return the 'informal' string representation of the object. """ return 'Menu(%s)' % self.name __repr__ = __str__ ########################################################################### # 'Menu' interface ########################################################################### def _id_default(self): """ Trait initializer. """ return self.name.strip('&') #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2690337 envisage-4.9.0/envisage/ui/action/tests/0000755000076500000240000000000000000000000021444 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/tests/__init__.py0000644000076500000240000000000000000000000023543 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/tests/dummy_action_manager_builder.py0000644000076500000240000000444400000000000027714 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A menu builder that doesn't build real actions! """ # Enthought library imports. from envisage.ui.action.api import AbstractActionManagerBuilder from pyface.action.api import Action, Group, MenuManager from pyface.action.api import MenuBarManager from traits.testing.unittest_tools import unittest class DummyActionManagerBuilder(AbstractActionManagerBuilder): """ An action manager builder that doesn't build real actions! This makes it very easy to test! """ ########################################################################### # 'DummyActionManagerBuilder' interface. ########################################################################### def create_menu_bar_manager(self, root): """ Create a menu bar manager from the builder's action sets. """ menu_bar_manager = MenuBarManager(id='MenuBar') self.initialize_action_manager(menu_bar_manager, root) return menu_bar_manager ########################################################################### # Protected 'AbstractActionManagerBuilder' interface. ########################################################################### def _create_action(self, action_definition): """ Create an action implementation from a definition. """ return Action(name=action_definition.class_name) def _create_group(self, group_definition): """ Create a group implementation from a definition. """ return Group(id=group_definition.id) def _create_menu_manager(self, menu_definition): """ Create a menu manager implementation from a definition. """ menu_manager = MenuManager(id=menu_definition.id) for group_definition in menu_definition.groups: menu_manager.insert(-1, Group(id=group_definition.id)) return menu_manager #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/ui/action/tests/test_action_manager_builder.py0000644000076500000240000005777100000000000027553 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Tests for the action manager builder. """ # Enthought library imports. from envisage.ui.action.api import Action, ActionSet, Group, Menu from traits.testing.unittest_tools import unittest # Local imports. from .dummy_action_manager_builder import DummyActionManagerBuilder class ActionManagerBuilderTestCase(unittest.TestCase): """ Tests for the action manager builder. """ def test_action_with_nonexistent_group(self): """ action with non-existent group """ action_sets = [ ActionSet( actions = [ Action( class_name = 'Exit', path = 'MenuBar/File', group = 'Bogus' ), ] ), ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. with self.assertRaises(ValueError): builder.create_menu_bar_manager("MenuBar") def test_action_with_nonexistent_sibling(self): """ action with non-existent sibling """ action_sets = [ ActionSet( actions = [ Action( class_name = 'Exit', path = 'MenuBar/File', before = 'NonExistentAction' ), ] ), ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. with self.assertRaises(ValueError): builder.create_menu_bar_manager("MenuBar") def test_group_with_nonexistent_sibling(self): """ group with non-existent sibling """ action_sets = [ ActionSet( groups = [ Group(id='FileMenuGroup', path='MenuBar', before='Bogus') ] ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. with self.assertRaises(ValueError): builder.create_menu_bar_manager("MenuBar") def test_menu_with_nonexistent_sibling(self): """ menu with non-existent sibling """ action_sets = [ ActionSet( menus = [ Menu(name='&File', path='MenuBar', before='Bogus') ] ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. with self.assertRaises(ValueError): builder.create_menu_bar_manager("MenuBar") def test_action_with_path_component_that_is_not_a_menu(self): """ action with path component that is not a menu """ action_sets = [ ActionSet( actions = [ Action( class_name = 'Exit', path = 'MenuBar/File' ), Action( class_name = 'Broken', path = 'MenuBar/File/Exit', ), ] ), ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. with self.assertRaises(ValueError): builder.create_menu_bar_manager("MenuBar") def test_single_top_level_menu_with_no_group(self): """ single top level menu with no group """ action_sets = [ ActionSet( menus = [ Menu(name='&File', path='MenuBar'), ] ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_bar_manager = builder.create_menu_bar_manager('MenuBar') # Make sure that the 'File' menu was added to the 'additions' group # of the menubar. self.assertEqual(1, len(menu_bar_manager.groups)) group = menu_bar_manager.find_group('additions') ids = [item.id for item in group.items] self.assertEqual(['File'], ids) def test_single_top_level_group(self): """ single top level group """ action_sets = [ ActionSet( groups = [ Group(id='FileMenuGroup', path='MenuBar'), ] ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_bar_manager = builder.create_menu_bar_manager('MenuBar') # Make sure that the group was added before the 'additions' group. self.assertEqual(2, len(menu_bar_manager.groups)) ids = [group.id for group in menu_bar_manager.groups] self.assertEqual(['FileMenuGroup', 'additions'], ids) def test_top_level_menus_with_no_groups(self): """ top level menus with_no groups """ action_sets = [ ActionSet( menus = [ Menu(name='&File', path='MenuBar'), Menu(name='&Edit', path='MenuBar'), Menu(name='&Tools', path='MenuBar'), Menu(name='&Help', path='MenuBar') ], ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_bar_manager = builder.create_menu_bar_manager('MenuBar') # Make sure that all of the menus were added the the 'additions' group # of the menubar (and in the right order!). self.assertEqual(1, len(menu_bar_manager.groups)) group = menu_bar_manager.find_group('additions') ids = [item.id for item in group.items] self.assertEqual(['File', 'Edit', 'Tools', 'Help'], ids) def test_top_level_menus_no_groups_before_and_after(self): """ top level menus no groups, before and after """ action_sets = [ ActionSet( menus = [ Menu(name='&Edit', path='MenuBar', after='File'), ], ), ActionSet( menus = [ Menu(name='&File', path='MenuBar'), ], ), ActionSet( menus = [ Menu(name='&Help', path='MenuBar') ], ), ActionSet( menus = [ Menu(name='&Tools', path='MenuBar', before='Help'), ], ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure that all of the menus were added the the 'additions' group # of the menubar. self.assertEqual(1, len(menu_manager.groups)) additions = menu_manager.find_group('additions') ids = [item.id for item in additions.items] self.assertEqual(['File', 'Edit', 'Tools', 'Help'], ids) def test_top_level_menu_non_existent_group(self): """ top level menu non-existent group """ action_sets = [ ActionSet( menus = [ Menu(name='&File', path='MenuBar', group='FileMenuGroup'), ], ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. with self.assertRaises(ValueError): builder.create_menu_bar_manager("MenuBar") def test_top_level_menu_group(self): """ top level menu group """ action_sets = [ ActionSet( groups = [ Group(id='FileMenuGroup', path='MenuBar') ], menus = [ Menu(name='&File', path='MenuBar', group='FileMenuGroup'), ], ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure that the 'File' menu was added to the 'FileMenuGroup' # group of the menubar. self.assertEqual(2, len(menu_manager.groups)) ids = [group.id for group in menu_manager.groups] self.assertEqual(['FileMenuGroup', 'additions'], ids) group = menu_manager.find_group('FileMenuGroup') self.assertEqual('File', group.items[0].id) def test_sub_menus_no_groups(self): """ sub-menus no groups """ # We split the contributions into different action sets just because # that is how it might end up in an actual application... not because # you *have* to split them up this way! action_sets = [ ActionSet( menus = [ Menu(name='&File', path='MenuBar'), Menu(name='&Edit', path='MenuBar'), Menu(name='&Tools', path='MenuBar'), Menu(name='&Help', path='MenuBar') ], ), ActionSet( menus = [ Menu(name='&New', path='MenuBar/File'), ], ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure the 'New' sub-menu got added to the 'additions' group # of the 'File' menu. menu = menu_manager.find_item('File') additions = menu.find_group('additions') self.assertEqual('New', additions.items[0].id) def test_actions_no_groups(self): """ actions no groups """ # We split the contributions into different action sets just because # that is how it might end up in an actual application... not because # you *have* to split them up this way! action_sets = [ ActionSet( menus = [ Menu(name='&File', path='MenuBar'), Menu(name='&Edit', path='MenuBar'), Menu(name='&Tools', path='MenuBar'), Menu(name='&Help', path='MenuBar') ] ), ActionSet( actions = [ Action(class_name='Exit', path='MenuBar/File'), Action(class_name='About', path='MenuBar/Help') ] ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure the 'ExitAction' action got added to the 'additions' group # of the 'File' menu. menu = menu_manager.find_item('File') additions = menu.find_group('additions') self.assertEqual('Exit', additions.items[0].id) # Make sure the 'AboutAction' action got added to the 'additions' group # of the 'File' menu. menu = menu_manager.find_item('Help') additions = menu.find_group('additions') self.assertEqual('About', additions.items[0].id) def test_actions_make_submenus(self): """ actions make submenus """ action_sets = [ ActionSet( actions = [ Action(class_name='Folder', path='MenuBar/File/New'), Action(class_name='File', path='MenuBar/File/New') ] ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure the 'File' menu got added to the 'additions' group of the # menubar. self.assertEqual(1, len(menu_manager.groups)) additions = menu_manager.find_group('additions') self.assertEqual('File', additions.items[0].id) # Make sure the 'New' sub-menu got added to the 'additions' group # of the 'File' menu. menu = menu_manager.find_item('File') additions = menu.find_group('additions') self.assertEqual('New', additions.items[0].id) # Make sure the new 'Folder' and 'File' actions got added to the # 'additions' group of the 'New' menu. menu = menu_manager.find_item('File/New') additions = menu.find_group('additions') self.assertEqual('Folder', additions.items[0].id) self.assertEqual('File', additions.items[1].id) def test_actions_make_submenus_before_and_after(self): """ actions make submenus before and after """ action_sets = [ ActionSet( actions = [ Action( class_name = 'File', path = 'MenuBar/File/New', after = 'Folder' ), Action( class_name = 'Project', path = 'MenuBar/File/New', before = 'Folder' ), Action( class_name = 'Folder', path = 'MenuBar/File/New', ), ] ) ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure the 'File' menu got added to the 'additions' group of the # menubar. self.assertEqual(1, len(menu_manager.groups)) additions = menu_manager.find_group('additions') self.assertEqual('File', additions.items[0].id) # Make sure the 'New' sub-menu got added to the 'additions' group # of the 'File' menu. menu = menu_manager.find_item('File') additions = menu.find_group('additions') self.assertEqual('New', additions.items[0].id) # Make sure the new 'Folder' and 'File' actions got added to the # 'additions' group of the 'New' menu. menu = menu_manager.find_item('File/New') additions = menu.find_group('additions') ids = [item.id for item in additions.items] self.assertEqual(['Project', 'Folder', 'File'], ids) def test_explicit_groups(self): """ explicit groups """ action_sets = [ ActionSet( menus = [ Menu(name='&File', path='MenuBar'), Menu(name='&Edit', path='MenuBar'), Menu(name='&Tools', path='MenuBar'), Menu(name='&Help', path='MenuBar') ], ), ActionSet( menus = [ Menu(name='&New', path='MenuBar/File', group='NewGroup'), ], ), ActionSet( actions = [ Action( class_name = 'Exit', path = 'MenuBar/File', group = 'ExitGroup' ), ] ), ActionSet( groups = [ Group( id = 'ExitGroup', path = 'MenuBar/File' ), Group( id = 'SaveGroup', path = 'MenuBar/File', after = 'NewGroup' ), Group( id = 'NewGroup', path = 'MenuBar/File', before = 'ExitGroup' ), ] ), ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure that all of the menus were added the the 'additions' group # of the menubar. self.assertEqual(1, len(menu_manager.groups)) additions = menu_manager.find_group('additions') ids = [item.id for item in additions.items] self.assertEqual(['File', 'Edit', 'Tools', 'Help'], ids) # Make sure the 'File' menu has got 3 groups, 'NewGroup', 'ExitGroup' # and 'additions' (and in that order!). menu = menu_manager.find_item('File') self.assertEqual(4, len(menu.groups)) ids = [group.id for group in menu.groups] self.assertEqual( ['NewGroup', 'SaveGroup', 'ExitGroup', 'additions'], ids ) # Make sure the 'New' sub-menu got added to the 'NewGroup' group # of the 'File' menu. menu = menu_manager.find_item('File') group = menu.find_group('NewGroup') self.assertEqual('New', group.items[0].id) # Make sure the 'Exit' action got added to the 'ExitGroup' group # of the 'File' menu. menu = menu_manager.find_item('File') group = menu.find_group('ExitGroup') self.assertEqual('Exit', group.items[0].id) def test_actions_and_menus_in_groups(self): """ actions and menus in groups """ action_sets = [ ActionSet( menus = [ Menu( name = '&File', path = 'MenuBar', groups = ['NewGroup', 'ExitGroup'] ), Menu(name='&Edit', path='MenuBar'), Menu(name='&Tools', path='MenuBar'), Menu(name='&Help', path='MenuBar') ], ), ActionSet( menus = [ Menu(name='&New', path='MenuBar/File', group='NewGroup'), ], ), ActionSet( actions = [ Action( class_name = 'Exit', path = 'MenuBar/File', group = 'ExitGroup' ), ] ), ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure that all of the menus were added the the 'additions' group # of the menubar. self.assertEqual(1, len(menu_manager.groups)) additions = menu_manager.find_group('additions') ids = [item.id for item in additions.items] self.assertEqual(['File', 'Edit', 'Tools', 'Help'], ids) # Make sure the 'File' menu has got 3 groups, 'NewGroup', 'ExitGroup' # and 'additions' (and in that order!). menu = menu_manager.find_item('File') self.assertEqual(3, len(menu.groups)) ids = [group.id for group in menu.groups] self.assertEqual(['NewGroup', 'ExitGroup', 'additions'], ids) # Make sure the 'New' sub-menu got added to the 'NewGroup' group # of the 'File' menu. menu = menu_manager.find_item('File') group = menu.find_group('NewGroup') self.assertEqual('New', group.items[0].id) # Make sure the 'Exit' action got added to the 'ExitGroup' group # of the 'File' menu. menu = menu_manager.find_item('File') group = menu.find_group('ExitGroup') self.assertEqual('Exit', group.items[0].id) def test_duplicate_menu(self): """ duplicate menu """ action_sets = [ ActionSet( menus = [ Menu( name = '&File', path = 'MenuBar', groups = ['NewGroup', 'ExitGroup'] ), ], ), ActionSet( menus = [ Menu( name = '&File', path = 'MenuBar', groups = ['ExtraGroup'] ), ], ), ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure that all of the menus were added the the 'additions' group # of the menubar. self.assertEqual(1, len(menu_manager.groups)) # Make sure we only get *one* 'File' menu. additions = menu_manager.find_group('additions') ids = [item.id for item in additions.items] self.assertEqual(['File'], ids) # Make sure the 'File' menu has got 4 groups, 'NewGroup', 'ExitGroup', # 'ExtraGroup' and 'additions' (and in that order!). menu = menu_manager.find_item('File') self.assertEqual(4, len(menu.groups)) ids = [group.id for group in menu.groups] self.assertEqual( ['NewGroup', 'ExitGroup', 'ExtraGroup', 'additions'], ids ) def test_duplicate_group(self): """ duplicate group """ action_sets = [ ActionSet( menus = [ Menu( name = '&File', path = 'MenuBar', groups = ['NewGroup', 'ExitGroup'] ), ], ), ActionSet( menus = [ Menu( name = '&File', path = 'MenuBar', groups = ['NewGroup'] ), ], ), ] # Create a builder containing the action set. builder = DummyActionManagerBuilder(action_sets=action_sets) # Create a menu bar manager for the 'MenuBar'. menu_manager = builder.create_menu_bar_manager('MenuBar') # Make sure that all of the menus were added the the 'additions' group # of the menubar. self.assertEqual(1, len(menu_manager.groups)) # Make sure we only get *one* 'File' menu. additions = menu_manager.find_group('additions') ids = [item.id for item in additions.items] self.assertEqual(['File'], ids) # Make sure the 'File' menu has got 3 groups, 'NewGroup', 'ExitGroup' # and 'additions' (and in that order!). menu = menu_manager.find_item('File') self.assertEqual(3, len(menu.groups)) ids = [group.id for group in menu.groups] self.assertEqual( ['NewGroup', 'ExitGroup', 'additions'], ids ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/action/tool_bar.py0000644000076500000240000000611700000000000022462 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The *definition* of a tool bar. """ # Enthought library imports. from traits.api import Instance, List, Str # Local imports. from .group import Group from .location import Location from ..._compat import STRING_BASE_CLASS # fixme: Remove duplication (in menu.py too!) class CGroup(Instance): """ A trait type for a 'Group' or anything that can be cast to a 'Group'. Currently, the only cast allowed is from string -> Group using the string as the group's ID. """ ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **kw): """ Constructor. """ super(CGroup, self).__init__(klass=Group, **kw) return ########################################################################### # 'TraitType' interface. ########################################################################### def validate(self, object, name, value): """ Validate a value. """ if isinstance(value, STRING_BASE_CLASS): value = Group(id=value) return super(CGroup, self).validate(object, name, value) class ToolBar(Location): """ The *definition* of a menu in a menu bar or menu. """ # The tool bars's unique identifier (unique within the multi-toolbar # that the tool bar is to be added to). id = Str # The tool bar name (appears when the tool bar is 'undocked'). name = Str # The groups in the tool bar. groups = List(CGroup) # The optional name of a class that implements the tool bar. The class must # support the **pyface.action.ToolBarManager** protocol. class_name = Str ########################################################################### # 'object' interface ########################################################################### def __str__(self): """ Return the 'informal' string representation of the object. """ return 'ToolBar(%s)' % self.name __repr__ = __str__ ########################################################################### # 'Location' interface ########################################################################### def _path_default(self): """ Trait initializer. """ return 'ToolBar' ########################################################################### # 'ToolBar' interface ########################################################################### def _id_default(self): """ Trait initializer. """ return self.name #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/gui_application.py0000644000076500000240000000541300000000000022551 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Envisage GUI Application ------------------------ This class handles the life-cycle of a Pyface GUI. Plugins can display windows via mechinisms such as edit_traits(). This is intended to be a very simple shell for lifting an existing pure TraitsUI or Pyface (or even Qt) app into an Envisage app. More sophisticated applications should use Tasks. """ from traits.api import Event, Supports from envisage.api import Application class GUIApplication(Application): """ The entry point for an Envisage GUI application. This class handles the life-cycle of a Pyface GUI. Plugins can display windows via mechinisms such as edit_traits(). This is intended to be a very simple shell for lifting an existing pure TraitsUI or Pyface (or even Qt) app into an Envisage app. More sophisticated applications should use Tasks. """ #### 'GUIApplication' interface ######################################### #: The Pyface GUI for the application. gui = Supports('pyface.i_gui.IGUI') #: The splash screen for the application. By default, there is no splash #: screen. splash_screen = Supports('pyface.i_splash_screen.ISplashScreen') #### Application lifecycle events ######################################### #: Fired after the GUI event loop has been started. application_initialized = Event ########################################################################### # 'IApplication' interface. ########################################################################### def run(self): """ Run the application. Returns ------- bool Whether the application started successfully (i.e., without a veto). """ # Make sure the GUI has been created (so that, if required, the splash # screen is shown). gui = self.gui started = self.start() if started: gui.set_trait_later(self, 'application_initialized', self) # Start the GUI event loop. The application will block here. gui.start_event_loop() # clean up plugins once event loop stops self.stop() return started #### Trait initializers ################################################### def _gui_default(self): from pyface.api import GUI return GUI(splash_screen=self.splash_screen) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.276239 envisage-4.9.0/envisage/ui/single_project/0000755000076500000240000000000000000000000022034 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/DESIGN.txt0000644000076500000240000000464000000000000023552 0ustar00mdickinsonstaff00000000000000These are some design notes for the E3 version of the single_project plugin, including a rough background adapated from the README of the E2 single_project plugin, and plans for implementing various aspects of the new plugin. *Note: This is very much a WIP and will probably be updated/modified heavily in a short period of time. BACKGROUND: ----------- This plugin is designed to work with a single project at a time and build up 'state' within that project until the user explicitly saves the project. The plugin contributes two services to an Envisage application -- one for the model and one for the UI. Additionally, the plugin contributes a 'Project' perspective, 'Project' view, and a number of actions to the UI so that the user can create, open, save, save as, and close projects. Finally, it should be noted that the plugin manages an indicator applied to the application window(s) titlebar as a representation that the current project has unsaved modifications. The current project is visualized by a TreeEditor by default, but the user can contribute additional Views. By contributing your own Project factory, the user can define your own custom projects. E2 single_projects vs. E3 single_projects: ----------------------------------------- As described by Martin Chilvers: "domain-objects" are at the highest level of abstraction in your design and hence are the objects that are understood by the user (e.g. Book, Person, Log, LogSuite, Well etc). The original single_project plugin used resources and resource types to adapt domain-objects to add certain behaviours to them that various Envisage plugins expected. However, this approach restricted the addition of new behaviours to resource types, as well as added a lot of cruft to the process of registering new resources/resource types. The E3 single_project plugin uses Traits Adapters and takes advantage of the new ITreeNode interface. The new TreeEditor has been extended to support handling unknown domain-objects by adapting them to the ITreeNode interface. An ITreeNodeAdapter needs to be defined for any object that you want to be displayed in the TreeEditor, such as your Project class and it's sub-nodes. The Adapter exposes certain key ITreeNode interface methods, such as get_children, when_children_changed, get_lable, etc... This eliminates the extra cruft that was necessary in the E2 single_project plugin, such as the node monitor, node resource type, etc... ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/README.txt0000644000076500000240000001516600000000000023543 0ustar00mdickinsonstaff00000000000000Last updated: 2006.02.05 BACKGROUND: ----------- This plugin is designed to work with a single project at a time and build up 'state' within that project until the user explicitly saves the project. The plugin contributes two services to an Envisage application -- one for the model and one for the UI. Additionally, the plugin contributes a 'Project' perspective, 'Project' view, and a number of actions to the UI so that the user can create, open, save, save as, and close projects. Finally, it should be noted that the plugin manages an indicator applied to the application window(s) titlebar as a representation that the current project has unsaved modifications. The current project is visualized using a ResourceTree control and thus supports all the visualization and interaction customization of resources. This also makes makes it easy to build a hierarchy of nodes within the tree by implementing the apptools.naming.Context interface. Finally, this plugin contributes a single extension point which provides the capability to define your own custom projects. PRE-REQUISITES: --------------- This plugin requires the use of the envisage framework and the following plugins: - envisage.core - envisage.resource - envisage.workbench - envisage.plugins.python_shell HOW TO USE: ----------- This plugin provides a base class for projects but, as of now, that class has no capability to contain any data. So for this plugin to be useful within an Envisage application you have to complete the following: - Contribute a project_plugin_definition.FactoryDefinition to the Envisage application. This definition should point to a class derived from project_factory.ProjectFactory which will be used to create new projects as well as handle opening of persisted projects. To do that, your ProjectFactory should override the 'create' and 'open' methods as appropriate to return an instance of a class derived from project.Project. Note that multiple plugins can contribute FactoryDefinitions but only the first contributed definition with the highest priority will be used. - You will need to derive a class from project.Project which will be used to contain your project's data. This class should: - override the trait_view trait to provide the UI content that will be shown by the plugin (within a wizard dialog) to allow a user to initialize a newly created project. - override the _contains_resource() method so that the plugin can close editors for resources in your project when your project closes. - set the 'dirty' trait to True anytime modifications are made to your project data - optionally override the Project.start() and Project.stop() methods to handle any work needing to be done when a project becomes the current project or is no longer the current project. For example, the base Project uses the stop() method to close any editors associated with resources contained in the project. - optionally override the _load_hook() and _save() methods if you wish to use the base class's infrastructure for loading and saving projects but have additional work that needs to be accomplished during those events. Note that the plugin's UI service calls the save() method and the ProjectFactory calls the load() method. - optionally override the 'load()' and 'save()' methods if you want to completely replace the base class's infrastructure for loading and saving projects. Note that the plugin's UI service calls the save() method and the ProjectFactory calls the load() method. - You will likely want to register resource types -- and associated classes such as node types, monitors, and resource references -- for the resources that can be contained within your project. This will allow you to take maximum advantage of the infrastructure of the Project view's ResourceTree control. If you do this, you should note that this plugin registers a ProjectResourceType, pointing to a ProjectNodeType, that is used to visualize the root project node. You don't need to derive from these unless you want to customize the resource definition of the project node. - Another option for implementing the visualization of project contents would be to derive from ProjectView and override the _create_project_control() and _update_project_control() methods to replace the ResourceTree control with some other UI control. If you do the later, you will likely need to create your own project plugin definition but you should be able to reuse almost all of the infrastructure classes contained in this plugin. - If the resources within your project can be edited in Editors, you should derive Editor classes from ProjectEditor so that the project plugin framework can close these editors when the project closes. KNOWN ISSUES: ------------- - Due to the current capabilities of the workbench plugin, this plugin can't do the ideal closing hook whereby, if the current project is modified, the user gets prompted to save it, close it, or cancel the closing of the window. The best we can do is follow the prompt to close the workbench window with a dialog querying the user whether they want to save the current project or not. There is an Enthought Trac ticket to add the necessary capability to the workbench plugin and this plugin will be 'fixed' shortly thereafter. - The Project class doesn't support any sort of versioning framework in its saving and loading. This will cause problems as the content and capabilities of projects evolve throughout the software development cycle of an application that uses this plugin. TO-DO: ------ - It would be nice if the Project class was modified such that it could actually be used without needing to be derived from. One possibility for this would be to make it a full-fledged folder type Context in and of itself including support for binding and unbinding. If this was done, it should be easy enough to re-use resource types, node types, etc. defined for other Envisage plugins within projects. - It might be nice if editors that were open when a project was saved were restored when the project was re-loaded. One mechanism to do this would be to modify the Project._editors trait to contain ResourceReferences rather than Resources as the keys. Then when pickling, we would simple dump the editor references and persist just these keys. Upon loading, iterate through the keys/resource references and open an editor for each. One question about this mechanism is that currently we don't track order or position of editors so that info wouldn't be recovered. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/TODO.txt0000644000076500000240000000057600000000000023352 0ustar00mdickinsonstaff00000000000000To-Do List: * Update README and other documentation/examples. * Implement a way to publish the ITreeNodeAdapters so that they don't have to be in the same file as the class which they adapt. * Improve the way we are binding dynamic objects and how they are being retrieved. * Improve the handling of children in the adapters to remove redudancy. * Lots of cleanup left. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/__init__.py0000644000076500000240000000000000000000000024133 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2800746 envisage-4.9.0/envisage/ui/single_project/action/0000755000076500000240000000000000000000000023311 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/__init__.py0000644000076500000240000000000000000000000025410 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/api.py0000644000076500000240000000130700000000000024435 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .new_project_action import NewProjectAction from .open_project_action import OpenProjectAction from .save_project_action import SaveProjectAction from .save_as_project_action import SaveAsProjectAction from .close_project_action import CloseProjectAction from .switch_to_action import SwitchToAction ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/close_project_action.py0000644000076500000240000000315200000000000030054 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action to close the current project. This is only enabled when there is a current project. """ # Enthought library imports from envisage.ui.single_project.project_action import ProjectAction from pyface.api import ImageResource ############################################################################## # class 'CloseProjectAction' ############################################################################## class CloseProjectAction(ProjectAction): """ An action to close the current project. This is only enabled when there is a current project. """ # The universal object locator (UOL). uol = 'envisage.ui.single_project.ui_service.UiService' # The name of the method to invoke on the object. method_name = 'close' # A longer description of the action. description = 'Close the current project' # The action's image (displayed on tool bar tools etc). image = ImageResource('close_project') # The action's name (displayed on menus/tool bar tools etc). name = 'Close' # A short description of the action used for tooltip text etc. tooltip = 'Close this project' #### EOF ##################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/configure_action.py0000644000076500000240000000321200000000000027177 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that configures an item in the project tree. """ # Enthought library imports. from envisage.ui.single_project.project_action import ProjectAction ############################################################################## # class 'ConfigureAction' ############################################################################## class ConfigureAction(ProjectAction): """ An action that configures an item in the project tree. 'Configures' in this sense means pop up a trait sheet! """ ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Performs the action. """ # fixme: We would like to use "kind='modal'" here, but it doesn't # work! The bug we see is that when the dialog is complete the # viscosity model assigned to the trait is NOT the same as the # viscosity model in the material's '_dirty_objects' collection! ui = event.node.obj.edit_traits( parent=event.window.control, kind='livemodal' ) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.282474 envisage-4.9.0/envisage/ui/single_project/action/images/0000755000076500000240000000000000000000000024556 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/images/close_project.png0000644000076500000240000000102500000000000030115 0ustar00mdickinsonstaff00000000000000PNG  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`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/images/new_project.png0000644000076500000240000000103400000000000027601 0ustar00mdickinsonstaff00000000000000PNG  IHDRabKGDhv|ay pHYs B(xtIME  :Xa#IDAT8˕kA?{9A?« lka#BZD`kRBVp6Ebqfw޼g1qg~107H=3 ̝,RïsQ׀EcH9m7g]Q85U?Bf* _{ln/*B!'Th@ ? 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`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/images/save_as_project.png0000644000076500000240000000076200000000000030440 0ustar00mdickinsonstaff00000000000000PNG  IHDRabKGDhv|ay pHYs B(xtIME  )4nsmIDAT8ˍJAn#6QSGIaeeO#bRQ,E$ok1ͯz`s* ABr8 + 1P4~/٣yrUR Z\veorx8K;w0(6 >A7RSK_N}+ "D#,{ ,6(PO(j2g Va/0._:(MAlH(Tظh&+g-c".oܪI!IJs.oOFQX=-ZTt y6cIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/images/switch_project.png0000644000076500000240000000117600000000000030320 0ustar00mdickinsonstaff00000000000000PNG  IHDRabKGDhv|ay pHYs B(xtIME  //# IDAT8ˍKhQ{gIӦdX"tS\W* " ]&Dt.l\Q7XAp'RBAc'Jɽ]D`8\8s 9eN(oGV9['`e ୽_Qҋsf(L!A+N)lU:ǁn Q>g* €.eUpo§tVmf1?Ú. o:گz4I$SW&BNFjKlT9׈|)ZqmƒDxB 6'wQi1bT^&n ?MKϐ3MzH~U"`Y`Z)Ai}h!|ޔߣDDR E!6*4t;_NR>5KGuY`1 aԵ {BP2Ž#D="/O\ۨIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/new_project_action.py0000644000076500000240000000261100000000000027537 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that creates a new project. """ # Enthought library imports. from envisage.ui.single_project.project_action import ProjectAction from pyface.api import ImageResource ############################################################################## # class 'NewProjectAction' ############################################################################## class NewProjectAction(ProjectAction): """ An action that creates a new project. """ # The universal object locator (UOL). uol = 'envisage.ui.single_project.ui_service.UiService' # The name of the method to invoke on the object. method_name = 'create' # A longer description of the action. description = 'Create a project' # The action's image (displayed on tool bar tools etc). image = ImageResource('new_project') # The action's name (displayed on menus/tool bar tools etc). name = 'New...' # A short description of the action used for tooltip text etc. tooltip = 'Create a project' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/open_project_action.py0000644000076500000240000000260500000000000027712 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that opens a project. """ # Enthought library imports. from envisage.ui.single_project.project_action import ProjectAction from pyface.api import ImageResource ############################################################################## # class 'OpenProjectAction' ############################################################################## class OpenProjectAction(ProjectAction): """ An action that opens a project. """ # The universal object locator (UOL). uol = 'envisage.ui.single_project.ui_service.UiService' # The name of the method to invoke on the object. method_name = 'open' # A longer description of the action. description = 'Open an existing project' # The action's image (displayed on tool bar tools etc). image = ImageResource('open_project') # The action's name (displayed on menus/tool bar tools etc). name = 'Open...' # A short description of the action used for tooltip text etc. tooltip = 'Open a project' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/rename_action.py0000644000076500000240000000521400000000000026471 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that renames an item in the project tree. """ # Enthought library imports. from envisage.ui.single_project.project_action import ProjectAction class RenameAction(ProjectAction): """ Renames an item in the project tree. """ #### 'Action' interface ################################################### # The action's name (displayed on menus/tool bar tools etc). name = 'Rename' ########################################################################### # 'Action' interface. ########################################################################### def perform (self, event): """ Performs the action. """ event.widget.edit_label(event.node) return # fixme: This should be a selection listener action that probably is only # enabled if there is exactly ONE item in the selection and it is editable. ## ########################################################################### ## # 'Action' interface. ## ########################################################################### ## def refresh(self): ## """ Refresh the enabled/disabled state of the action etc. ## This is called whenever the workbench window that the action is in ## and/or the selection in that window have been changed. ## """ ## resource_manager = self.window.application.get_service( ## 'envisage.resource.IResourceManager' ## ) ## # fixme: It seems there is a glitch in the tree selection handling. ## # When the selection changes we get an empty selection first, then ## # the new selection. ## if len(self.window.selection) > 0: ## for node in self.window.selection: ## resource_type = resource_manager.get_type_of(node.obj) ## if resource_type is None or resource_type.node_type is None: ## self.enabled = False ## break ## if not resource_type.node_type.is_editable(node.obj): ## self.enabled = False ## break ## else: ## self.enabled = True ## return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/save_as_project_action.py0000644000076500000240000000752200000000000030375 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that saves the current project to a different location. """ # Enthought library imports. from envisage.ui.single_project.project_action import ProjectAction from pyface.api import ImageResource ############################################################################## # class 'SaveAsProjectAction' ############################################################################## class SaveAsProjectAction(ProjectAction): """ An action that saves the current project to a different location. """ # The universal object locator (UOL). uol = 'envisage.ui.single_project.ui_service.UiService' # The name of the method to invoke on the object. method_name = 'save_as' # A longer description of the action. description = 'Save the current project to a different location' # The action's image (displayed on tool bar tools etc). image = ImageResource('save_as_project') # The action's name (displayed on menus/tool bar tools etc). name = 'Save As...' # A short description of the action used for tooltip text etc. tooltip = 'Save this project to a different location' #### public interface #################################################### def refresh(self): """ Refresh the enabled state of this action. This implementation enables the action when there is a current project which is marked as being allowed to do a 'save as' operation. """ self.enabled = self._refresh_project_exists() and \ self._refresh_is_save_as_allowed() return #### trait handlers ###################################################### def _on_project_changed(self, obj, trait_name, old, new): """ Handle changes to the value of the current project. Extended to ensure that we listen for changes to the is_save_as_allowed flag on the current project. """ if old is not None: self._update_project_listeners(old, remove=True) if new is not None: self._update_project_listeners(new, remove=False) super(SaveAsAction, self)._on_project_changed(obj, trait_name, old, new) ########################################################################## # 'SaveAsAction' interface ########################################################################## #### protected interface ################################################# def _refresh_is_save_as_allowed(self): """ Return the refresh state according to whether the current project is marked as being capable of doing a 'save as'. Returns True if the action should be enabled and False otherwise. """ return self.model_service.project.is_save_as_allowed def _update_project_listeners(self, project, remove): """ Update listeners on the specified project. """ logger.debug( (remove and 'Removing ' or 'Adding ') + \ 'listeners on project [%s] for SaveAsAction [%s]', project, self) project.on_trait_change(self._on_is_save_as_allowed, 'is_save_as_allowed', remove=remove) return #### trait handlers ###################################################### def _on_is_save_as_allowed(self, obj, trait_name, old, new): """ Handle changes to the value of the current project's is_save_as_allowed. """ self.refresh() return ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/save_project_action.py0000644000076500000240000000717200000000000027713 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that saves the current project. """ # Enthought library imports. from envisage.ui.single_project.project_action import ProjectAction from pyface.api import ImageResource ############################################################################## # class 'SaveProjectAction' ############################################################################## class SaveProjectAction(ProjectAction): """ An action that saves the current project. """ # The universal object locator (UOL). uol = 'envisage.ui.single_project.ui_service.UiService' # The name of the method to invoke on the object. method_name = 'save' # A longer description of the action. description = 'Save the current project' # The action's image (displayed on tool bar tools etc). image = ImageResource('save_project') # The action's name (displayed on menus/tool bar tools etc). name = 'Save' # A short description of the action used for tooltip text etc. tooltip = 'Save this project' #### public interface #################################################### def refresh(self): """ Refresh the enabled state of this action. This implementation enables the action when there is a current project which is marked as saveable. """ self.enabled = self._refresh_project_exists() and \ self._refresh_is_save_allowed() return #### trait handlers ###################################################### def _on_project_changed(self, obj, trait_name, old, new): """ Handle changes to the value of the current project. Extended to ensure that we listen for changes to the saveable flag on the current project. """ if old is not None: self._update_project_listeners(old, remove=True) if new is not None: self._update_project_listeners(new, remove=False) super(SaveAction, self)._on_project_changed(obj, trait_name, old, new) ########################################################################## # 'SaveAction' interface ########################################################################## #### protected interface ################################################# def _refresh_is_save_allowed(self): """ Return the refresh state according to whether the current project is marked as saveable. Returns True if the action should be enabled and False otherwise. """ return self.model_service.project.is_save_allowed def _update_project_listeners(self, project, remove): """ Update listeners on the specified project. """ logger.debug( (remove and 'Removing ' or 'Adding ') + \ 'listeners on project [%s] for SaveAction [%s]', project, self) project.on_trait_change(self._on_is_save_allowed, 'is_save_allowed', remove=remove) return #### trait handlers ###################################################### def _on_is_save_allowed(self, obj, trait_name, old, new): """ Handle changes to the value of the current project's is_save_allowed. """ self.refresh() return ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/action/switch_to_action.py0000644000076500000240000000234500000000000027227 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that switches the project perspective. """ # Enthought library imports. from envisage.ui.single_project.project_action import ProjectAction from pyface.api import ImageResource class SwitchToAction(ProjectAction): """ An action that switches the project perspective. """ # A longer description of the action. description = 'View the current project in the Project perspective' # The action's image (displayed on tool bar tools etc). image = ImageResource('switch_project') # The action's name (displayed on menus/tool bar tools etc). name = 'Switch To Project' # A short description of the action used for tooltip text etc. tooltip = 'Go to the Project perspective' def perform(self, event): """ Perform the action. """ self.window.application.about() return ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/api.py0000644000076500000240000000172700000000000023166 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # IDs of services provided by this plugin from .services import IPROJECT_MODEL, IPROJECT_UI # Commonly referred to classes within this plugin from .factory_definition import FactoryDefinition from .model_service import ModelService from .project import Project from .project_action import ProjectAction from .project_factory import ProjectFactory from .view.project_view import ProjectView # FIXME: Add back this import when it actually works :) #from .editor.project_editor import ProjectEditor #### EOF ##################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/default_path_preference_page.py0000644000076500000240000000314300000000000030241 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Preference page for default path for a project """ # Enthought library imports from apptools.preferences.ui.api import PreferencesPage from traits.api import Directory, Str from traitsui.api import View, Item # Global assignment of ID ID = 'envisage.ui.single_project' #------------------------------------------------------------------------------- # DefaultPathPreferencePage Class #------------------------------------------------------------------------------- class DefaultPathPreferencePage(PreferencesPage): """ Preference page for default path for a plugin. """ # The page name (this is what is shown in the preferences dialog. name = 'Single Project' # The path to the preferences node that contains the preferences. preferences_path = 'envisage.ui.single_project' #### Preferences ########################################################## # Choose the unit system that needs to be used for the project preferred_path = Directory('') # Set the traits view traits_view = View(Item('preferred_path', style='custom', tooltip='Path that will be used for storing projects')) ### EOF ------------------------------------------------------------------------ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2831037 envisage-4.9.0/envisage/ui/single_project/editor/0000755000076500000240000000000000000000000023322 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/editor/__init__.py0000644000076500000240000000000000000000000025421 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/editor/project_editor.py0000644000076500000240000000573600000000000026723 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A base class for editors that can be tracked by single project plugin projects. """ # Standard library imports. import logging # Enthought library imports from envisage.workbench import DecoratedEditor from traits.api import Instance # Application specific imports. from envisage.single_project.services import IPROJECT_MODEL # Setup a logger for this module. logger=logging.getLogger(__name__) class ProjectEditor(DecoratedEditor): """ A base class for editors that can be tracked by single project plugin projects. """ ######################################################################### # Attributes ######################################################################### ### public 'ProjectEditor' interface #################################### # The project containing the resource we're editing project = Instance('envisage.single_project.project.Project') ######################################################################### # `object` interface ######################################################################### #### operator methods ################################################### def __init__(self, **traits): """ Constructor. Extended to associate ourself with the current project. """ super(ProjectEditor, self).__init__(**traits) # Make sure the current project knows this editor is associated with # it's resources model_service = self.window.application.get_service(IPROJECT_MODEL) self.project = model_service.project self.project.register_editor(self.resource, self) return ######################################################################### # 'Editor' interface. ######################################################################### ### public 'Editor' interface ########################################### def destroy_control(self): """ Destroys the toolkit-specific control that represents the editor. Extended to ensure that the current project stops associating us with its resources. """ # Only do something if the editor is still open if self.control: logger.debug('Destroying control in ProjectEditor [%s]', self) # Unregister from the associated project immediately. self.project.register_editor(self.resource, self, remove=True) super(ProjectEditor, self).destroy_control() return #### EOF #################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/factory_definition.py0000644000076500000240000000156200000000000026271 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! #Enthought imports from traits.api import HasTraits, Int, Str class FactoryDefinition(HasTraits): """ A project factory definition. An instance of the specified class is used to open and/or create new projects. The extension with the highest priority wins! In the event of a tie, the first instance wins. """ # The name of the class that implements the factory. class_name = Str # The priority of this factory priority = Int ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/model_service.py0000644000076500000240000001063000000000000025226 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The Envisage service providing the model state for the single project plugin. """ # Standard imports import logging import os import shutil # Enthought library imports from envisage.api import IApplication from apptools.preferences.api import IPreferences from traits.api import Any, HasTraits, Instance, List # Setup a logger for this module. logger = logging.getLogger(__name__) class ModelService(HasTraits): """ The Envisage service providing the model state for the single project plugin. """ ########################################################################## # Attributes (Traits) ########################################################################## ### public 'ModelService' interface ###################################### # The Envisage application that this service is part of. application = Instance(IApplication) # The factory to use for creating new projects factory = Instance('envisage.ui.single_project.project_factory.' 'ProjectFactory') # The preferences to be exposed through this service. preferences = Instance(IPreferences) # The currently open project project = Instance('envisage.ui.single_project.project.Project') # The current selection within the current project. selection = List(Any) ########################################################################## # 'object' interface. ########################################################################## ### operator methods ##################################################### def __init__(self, application, factory, **traits): """ Constructor. We require a reference to an Envisage application and a project factory to create an instance. """ super(ModelService, self).__init__(application = application, factory = factory, **traits) return ########################################################################## # 'ModelService' interface. ########################################################################## ### public interface ##################################################### def are_projects_files(self): """ Returns True if project instances are saved as files and False if they are saved as directories. """ return self.factory.PROJECT_CLASS.PROJECTS_ARE_FILES def clean_location(self, location): """ Ensures that there are no existing files or directories at the specified location by removing them. Exceptions are raised if there are any errors cleaning out existing files or directories. """ logger.debug('Trying to clean location [%s]', location) if os.path.isfile(location): os.path.remove(location) else: shutil.rmtree(location) return def get_default_path(self): """ Return the default location for projects. """ return self.factory.PROJECT_CLASS.get_default_path(self.application) ### trait handlers ####################################################### def _project_changed(self, old, new): """ Called whenever the current project is changed. We hook this to make sure the new project knows it's current and the old project knows it's not. """ logger.debug('Detected project change from [%s] to [%s] in ' 'ModelService [%s]', old, new, self) if old is not None: old.stop() self.selection = [] if new is not None: new.start() return def _selection_changed(self, old, new): """ Called whenever the selection within the project is changed. Implemented simply to log the change. """ logger.debug('ModelService [%s] selection changed from [%s] to [%s] ', self, old, new) return ### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/preferences.ini0000644000076500000240000000007300000000000025036 0ustar00mdickinsonstaff00000000000000[enthought.envisage.ui.single_project] preferred_path = ''././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/project.py0000644000076500000240000005637400000000000024073 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A base class for projects that can be displayed by the single_project plugin. """ # Standard library imports. import logging import os import sys # Enthought library imports from traits.etsconfig.api import ETSConfig import apptools.sweet_pickle from apptools.io.api import File from traits.api import Any, Bool, Dict, Directory, HasTraits, \ Instance, Property, Str from traitsui.api import Group, View from traits.util.clean_strings import clean_filename # Local imports. #from envisage.ui.single_project.editor.project_editor import \ # ProjectEditor from envisage.api import Application # Setup a logger for this module. logger = logging.getLogger(__name__) class Project(HasTraits): """ A base class for projects that can be displayed by the single_project plugin. """ ########################################################################## # CLASS Attributes ########################################################################## #### public 'Project' class interface #################################### # Indicates whether instances of this project class are stored as files or # directories. The rest of the single_project plugin will follow this # setting when using this project class. # # This is meant to be a constant for the lifetime of this class! PROJECTS_ARE_FILES = True # Current envisage application. application = Instance(Application, transient=True) #### protected 'Project' class interface ################################# # Format used to create a unique name from a location and a counter. _unique_name_format = '%s_%s' ########################################################################## # Attributes ########################################################################## #### public 'Project' interface ########################################## # True if this project contains un-saved state (has been modified.) dirty = Bool(False, transient=True) # True if we allow save requests on the project. is_save_allowed = Bool(True, transient=True) # True if we allow save_as requests on the project. is_save_as_allowed = Bool(True, transient=True) # The location of this project on the filesystem. The default value # depends on the runtime environment so we use a traits default method to # set it. location = Directory # The name of this project. This is calculated from the project's current # location or, if there is no location, from a default value. See the # property's getter method. name = Property(Str) # The UI view to use when creating a new project traits_view = View( Group('location'), title = 'New Project', id = 'envisage.single_project.project.Project', buttons = [ 'OK', 'Cancel' ], width = 0.33, # Ensure closing via the dialog close button is the same # as clicking cancel. close_result = False, # Ensure the user can resize the dialog. resizable = True, ) #### protected 'Project' interface ####################################### # A list of editors currently open to visualize our resources # FIXME: Re-add the ProjectEditor's(if we need them) once they are fixed. #_editors = Dict(Any, ProjectEditor, transient=True) # The cache of this project's name. We can't just initialize it to a # default value since the location may be cleared at any time. _name = Str(transient=True) ########################################################################## # 'Object' interface. ########################################################################## #### operator methods #################################################### def __getstate__(self): """ Get the state of this object for pickling. Extended to limit which attributes get pickled and also to add version numbers to our pickle. It is STRONGLY recommended that derived classes not leverage the pickling mechanism if they wish to store additional data in OTHER files as part of their project. Instead, they should override the *_save* method. Extending this method in derived classes IS appropriate if you want to store additional data in the SAME pickle file as the project. """ # Determine the starting point for our state. state = super(Project, self).__getstate__().copy() # Remove any transient traits. for trait_name in self.trait_names(transient=True): state.pop(trait_name, None) # Add in our current version number. Note use a different attribute # name from any base or derived class so that our numbers don't # override theirs. state['_project_version_major'] = 1 state['_project_version_minor'] = 0 return state def __setstate__(self, state): """ Restore the state of this object during unpickling. Extended to upgrade pickles to the current Project version. It is STRONGLY recommended that derived classes not leverage the unpickling mechanism if they wish to load data from additional pickle files. Instead, they should override the *_load* method. Extending this method in derived classes IS appropriate if you want to load additional data from the SAME pickle file as the project. """ # Get the version info out of the state dictionary. major = state.pop('_project_version_major', 0) minor = state.pop('_project_version_minor', 0) # Upgrade to version 1. if major < 1: # Remove any old attributes. # - name is now a calculated property instead of stored value. for key in ['name']: state.pop(key, None) # Restore our state. return super(Project, self).__setstate__(state) def __str__(self): """ Return the unofficial string representation of this object. """ result ='%s(name=%s)' % (super(Project, self).__str__(), self.name) return result ########################################################################## # 'Project' interface. ########################################################################## #### public interface #################################################### def get_default_project_location(self, application): """ Return the default location for a new project. """ path = self.get_default_path(application) name = clean_filename(self.get_default_name()) location = os.path.join(path, name) location = self._make_location_unique(location) return location def get_default_path(cls, application): """ Return the default path to the parent directory for a new project. """ # When unpickling, we don't have a reference to the current application, so we # fallback on application_home. if application is None: return ETSConfig.application_home app_preferences = application.preferences path_id = 'envisage.ui.' \ 'single_project.preferred_path' path = app_preferences.get(path_id) # If the 'preferred_path' variable isn't set in the user's preferences, # then we set to the the application home by default. if len(path) == 0: app_home = ETSConfig.application_home app_preferences.set(path_id, app_home) return app_home return path get_default_path = classmethod(get_default_path) def get_default_name(self): """ Return the default name for a new project. """ return 'New Project' def get_pickle_filename(cls, location): """ Generate the project's pickle filename given a source location. By default, the filename IS the specified location or, when saving projects as directories, a file called 'project' within a directory that is the specified location, Derived classes may wish to use the location as the basis for identifying the real pickle file. """ if cls.PROJECTS_ARE_FILES: result = location else: result = os.path.join(location, 'project') return result get_pickle_filename = classmethod(get_pickle_filename) def get_pickle_package(cls): """ Returns the pickle package to use for pickling and unpickling projects. Implementors can override this to customize the way in which projects are pickled and unpickled. This implementation returns the apptools.sweet_pickle package which supports versioning and refactoring of classes. """ return apptools.sweet_pickle get_pickle_package = classmethod(get_pickle_package) def load(cls, location, application): """ Load a project from a specified location. The loaded project's location is always set to the location the project was actually loaded from. Additionally, the dirty flag is cleared on the loaded project. An exception will be raised to indicate a failure. """ # Load the project in a manner that derived classes can modify. project = cls._load(location) # Ensure the project's location reflects where the project was loaded # from and that the dirty flag is not set. project.location = location project.dirty = False # Set the project's 'application' to the running application passed in. project.application = application return project load = classmethod(load) def register_editor(self, resource, editor, remove=False): """ Inform us that an editor has been opened for what is believed to be a resource in this project. Note that if this project can be represented as a hierarchy, the resource may not be a top level object in that hierarchy! """ # Warn if the resource is not part of this project if not self._contains_resource(resource): logger.warning( 'This Project [%s] does not contain resource [%s]', self, resource) # Add or remove from our set of editors as requested if not remove: self._editors[resource] = editor else: del self._editors[resource] def save(self, location=None, overwrite=False): """ Save this project. The project is saved to its current location, identified by the value of the *location* trait, unless a new location is explicitly provided. The specification of a new location is used to do a 'save as' operation. If a new location is provided, and a file or directory already exists at that location, then an *AssertionError* exception is raised unless the overwrite flag is True. This ensures that users won't accidentally overwrite existing data. This method requires the overwrite flag because prompting the user to confirm the overwrite requires GUI interaction and thus should not be done at the model level. Note that, because we can rely on the location of a loaded project always being set to the location it was loaded from, there is no reason to try to force the *location* trait within a saved project to the location we are trying to save to. Instead, we update the value in the in-memory version of the project only if the save completed successfully. The dirty flag is always cleared upon a succesfull save of the project. An exception will be raised to indicate a failure. """ # Ensure saving (or save as) is allowed at this time. if location is None or location == self.location: if not self.is_save_allowed: raise AssertionError('Saving is currently not allowed.') elif location != self.location: if not self.is_save_as_allowed: raise AssertionError('Save as is currently not allowed.') # Use the internally-specified location unless a new location was # explicitly provided. The new location can not contain any starting # or trailing whitespace and it cannot overwrite an existing file or # directory unless that was explicitly allowed. loc = self.location if location is not None: location = location.strip() if len(location) > 0 and location != self.location: # Ensure we never overwrite existing files / directories just # because someone specified a new location. (Confirmation or # correction of overwriting requires prompting of the user and # is thus not part of the project model.) if os.path.exists(location) and overwrite is False: raise AssertionError('Can not overwrite existing ' + \ 'location [%s]' % location) # The requested location is valid so let's use it. loc = location # Ensure all necessary directories exist. If we're saving a file, then # these are the path upto the file name. If we're saving to a directory # then the path is the complete location. if self.PROJECTS_ARE_FILES: path, filename = os.path.split(loc) else: path = loc if len(path) > 0: f = File(path) if f.is_file: f.delete() if not f.exists: f.create_folders() # Save this project in a manner that derived classes can modify. self._save(loc) # If the save succeeds (no exceptions were raised), then update the # location of the project and clear the dirty flag. self.location = loc self.dirty = False return def start(self): """ Notify this project that it is now the 'current' project. This call should only be made by the project plugin framework! This call *could* happen multiple times to a project, but only if interwoven with paired calls to the 'stop' method. Derived classes should override this, and chain the base implementation, if they need to do anything when a project becomes current. """ logger.debug('Project [%s] started', self) # Ensure we start with an empty set of editors self._editors = {} def stop(self): """ Called only by the project plugin framework to notify this project it is no longer the current project. This call *could* happen multiple times to a project, but only if interwoven with paired calls to the 'start' method. Derived classes should override this, and chain the base implementation, if they need to do anything when a project stops being current. """ # Close all of the editors displaying our resources self._close_all_editors() logger.debug('Project [%s] stopped', self) #### protected interface ################################################# def _close_all_editors(self): """ Called to close all editors associated with this project. """ # NOTE: The close() method on the editor will call back to remove # itself from our set of registered editors. (This assumes the # editor is derived from ProjectEditor.) for editor in self._editors.values(): logger.debug('Project requesting close of ProjectEditor [%s]', editor) editor.close() def _close_resource_editors(self, resource): """ Close any editors associated with the specified resource(s). The passed value may be a single resource or a list of resources. The resources should be parts of this project but no error is generated if they are not, nor if they are not currently associated with an editor. """ # Ensure we're dealing with a list of resources. if not isinstance(resource, list): resource = [resource] # Close any editors associated with the resources for r in resource: editor = self._editors.get(r, None) if editor is not None: logger.debug('Requesting close of ProjectEditor [%s] from ' + \ 'Project [%s]', editor, self) editor.close() return def _contains_resource(self, resource): """ Called to determine if this project contains the specified resource. Note that if this project can be represented as a hierarchy, the resource may not be a top level object in that hierarchy! Derived classes must implement this! """ return False def _get_name(self): """ Returns the current name for this project. The name is always the last part of the path that is the project's location. If we have no location, then a default name is returned. """ # Prefer to use the cached version of the name if self._name is not None and len(self._name) > 0: result = self._name # Use (and cache) a name from our current location else: # Strip any trailing path separator off the current location so # that we use the last directory name if our location is a # directory location = self.location.rstrip(os.path.sep) # The project name is then the basename of the location self._name = os.path.basename(location) result = self._name return result def _load(cls, location): """ Load a project from the specified location. This method exists purely to allow derived classes to have an easy way to override or extend the loading of a project. The default behavior is to load the project from a file using the unpickle mechanism. The caller is notified of loading errors by raised exceptions. """ # Allow derived classes to determine the actual pickle file given the # requested source location. filename = cls.get_pickle_filename(location) logger.debug('Loading Project of class [%s] from [%s]', cls, filename) # Try to unpickle the project while making sure to close any file we # opened. fh = None try: fh = open(filename, 'rb') pickle_package = cls.get_pickle_package() project = pickle_package.load(fh) # Allow derived classes to customize behavior after unpickling # is complete. project._load_hook(location) logger.debug('Loaded Project [%s] from location [%s]', project, filename) # Ensure any opened file is closed finally: if fh: try: fh.close() except: logger.exception('Unable to close project file [%s]', filename) return project _load = classmethod(_load) def _load_hook(self, location): """ Finish loading of a project. This method exists purely to allow derived classes to customize the steps that finish the loading of a project. Note that the project's internal location value does not yet reflect the location the project was loaded from, nor do we guarantee the dirty flag isn't set. (Doing the right thing to both of these is done by the framework after this method!) """ pass def _location_default(self): """ Generates the default value for our location trait. """ return self.get_default_project_location(self.application) def _make_location_unique(cls, location): """ Return a location, based off the specified location, that does not already exist on the filesystem. """ result = location counter = 1 while os.path.exists(result): result = cls._unique_name_format % (location, counter) counter+=1 return result _make_location_unique = classmethod(_make_location_unique) def _save(self, location): """ Save this project to the specified location. This method exists purely to allow derived classes to have an easy way to override or extend the saving of a project. The default behavior is to save this project to a file using the pickle mechanism. The caller is notified of saving errors by raised exceptions. """ # Allow derived classes to determine the actual pickle file from the # specified location. filename = self.get_pickle_filename(location) logger.debug('Saving Project [%s] to [%s]', self, filename) # Pickle the object to a file while making sure to close any file we # open. Note that we can't just log or ignore errors here as the # caller needs to know whether we succeeded or not, and could possibly # handle the exception if they knew what it was. fh = None try: # Allow derived classes to customize behavior before pickling is # applied. self._save_hook(location) fh = open(filename, 'wb') pickle_package = self.get_pickle_package() pickle_package.dump(self, fh, 1) logger.debug('Saved Project [%s] to [%s]', self, filename) finally: try: if fh is not None: fh.close() except: logger.exception('Unable to close project pickle file [%s]', filename) return def _save_hook(self, location): """ Start saving a project. This method exists purely to allow derived classes to customize the steps that initiate the saving of a project. Note that the project's internal location value does not reflect the target location for the save. """ pass #### trait handlers ###################################################### def _location_changed(self, old, new): """ Called whenever the project's location changes. """ logger.debug('Location changed from [%s] to [%s] for Project [%s]', old, new, self) # Invalidate any cached project name old_name = self._name self._name = '' self.trait_property_changed('name', old_name, self.name) # Indicate this project is now dirty self.dirty = True #### EOF ##################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/project_action.py0000644000076500000240000001667500000000000025430 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A base class for actions that determine their enabled status based on the state of the current project. """ # Standard library imports import logging import inspect # Enthought library imports from envisage.api import Application from envisage.ui.workbench.api import WorkbenchWindow from pyface.action.api import Action from traits.api import Instance, Str # Local imports. from envisage.ui.single_project.services import IPROJECT_MODEL # Setup a logger for this module. logger = logging.getLogger(__name__) class ProjectAction(Action): """ A base class for actions that determine their enabled status based on the state of the current project. This class is architected such that implementors can override the 'refresh' method without losing the default logic that determines the enabled state as a function of the current project. """ #### 'ProjectAction' interface #################################### # The application that the action is part of. This is a convenience # property and is equivalent to 'self.window.application'. application = Instance(Application) # The project model service we refresh our state against. model_service = Instance(IPROJECT_MODEL) # The universal object locator (UOL). This string is used to locate an # instance to invoke a method on. # # UOLs can currently have one of the following forms: # # * ``'service://a_service_identifier'`` # * ``'name://a/path/through/the/naming/service'`` # * ``'file://the/pathname/of/a/file/containing/a/UOL'`` # * ``'http://a/URL/pointing/to/a/text/document/containing/a/UOL'`` uol = Str # The name of the method to invoke on the object. method_name = Str ########################################################################## # 'object' interface ########################################################################## def __init__(self, *args, **kws): """ Constructor. Extended to setup a listener on the project model service so that we can refresh whenever the project changes. """ # Retrieve the project model service and register ourself to listen # for project state changes. This is done first to avoid errors # during any refresh calls triggered by further initialization. # FIXME: I don't think my implementation of the ProjectAction class is correct # because I can't see to get a reference to the current application. Because of # this, I'm not able to setup the listeners yet but this needs to be done eventually! """ if 'model_service' in kws: self.model_service = kws['model_service'] del kws['model_service'] else: self.model_service = self.window.application.get_service(IPROJECT_MODEL) self._update_model_service_listeners(remove=False) """ super(ProjectAction, self).__init__(*args, **kws) return ########################################################################## # 'Action' interface ########################################################################## #### public interface #################################################### def destroy(self): """ Destroy this action. Overridden here to remove our project model service listeners. """ self._update_model_service_listeners(remove=True) return ########################################################################## # 'ProjectAction' interface ########################################################################## #### public interface #################################################### def refresh(self): """ Refresh the enabled state of this action. This default implementation enables the action only when there is a current project. """ self.enabled = self._refresh_project_exists() return def perform(self, event): """ Performs the action. This implementation simply performs the action specified by **uol** and **method_name**. Override this method to add additional work to the performance of this action. """ self._perform_uol_action(event) return ########################################################################### # Private interface. ########################################################################### def _perform_uol_action(self, event): """ Called to perform the configured UOL action. """ # Find the object. object = event.window.application.get_service(self.uol) if object is not None: method = getattr(object, self.method_name) # If the only argument is 'self' then don't pass the event. This # makes it easier to hook up actions that invoke NON-UI methods. # # fixme: Should we check the argument spec of the method more # closely and only pass the event iff there is exactly one argument # called 'event'? args, varargs, varkw, dflts = inspect.getargspec(method) if len(args) == 1: method() else: method(event) else: logger.error("Cannot resolve UOL: %s" % self.uol) return #### protected interface ################################################# def _refresh_project_exists(self): """ Return the refresh state according to whether the model service has a current project. Returns True if this action should be enabled. False otherwise. """ enabled = False if self.model_service is not None \ and self.model_service.project is not None: enabled = True return enabled def _update_model_service_listeners(self, remove=False): """ Update our listeners on the project model service. These are done as individual listener methods so that derived classes can change the behavior when a single event occurs. """ logger.debug( (remove and 'Removing ' or 'Adding ') + \ 'listeners on project model service for ProjectAction [%s]', self) self.model_service.on_trait_change(self._on_project_changed, 'project', remove=remove) self.model_service.on_trait_change(self._on_project_selection_changed, 'selection', remove=remove) return #### trait handlers ###################################################### def _on_project_changed(self, obj, trait_name, old, new): """ Handle changes to the value of the current project. """ self.refresh() return def _on_project_selection_changed(self, obj, trait_name, old, new): """ Handle changes to the selection value within the current project. """ self.refresh() return #### EOF ##################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/project_action_set.py0000644000076500000240000000747600000000000026302 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Single project action set. """ # Enthought library imports. from envisage.ui.action.api import Action, Group, Menu, ToolBar from envisage.ui.workbench.api import WorkbenchActionSet # This module's package. PKG = '.'.join(__name__.split('.')[:-1]) class ProjectActionSet(WorkbenchActionSet): """ Action set of a default Project. """ # The action set's globally unique identifier. id = 'envisage.ui.single_project.action_set' # List of menus we provide. menus = [ Menu( id='ProjectMenu', name='&Project', path='MenuBar/File', group='ProjectGroup', ), ] # List of groups we provide. groups = [ Group( id='OpenGroup', path='MenuBar/File/ProjectMenu' ), Group( id='SaveGroup', path='MenuBar/File/ProjectMenu' ), Group( id='CloseGroup', path='MenuBar/File/ProjectMenu' ), Group( id='ProjectGroup', path='MenuBar/File', before='ExitGroup' ), ] # List of toolbars we provide. tool_bars = [ ToolBar( name='Project', groups=['PerspectiveGroup', 'ProjectGroup'] ), ] # List of actions we provide. actions = [ # File menu actions. Action( class_name=PKG + '.action.api:NewProjectAction', group='OpenGroup', path='MenuBar/File/ProjectMenu', ), Action( class_name=PKG + '.action.api:OpenProjectAction', group='OpenGroup', path='MenuBar/File/ProjectMenu', ), Action( class_name=PKG + '.action.api:SaveProjectAction', group='SaveGroup', path='MenuBar/File/ProjectMenu', ), Action( class_name=PKG + '.action.api:SaveAsProjectAction', group='SaveGroup', path='MenuBar/File/ProjectMenu', ), Action( class_name=PKG + '.action.api:CloseProjectAction', group='CloseGroup', path='MenuBar/File/ProjectMenu', ), # Toolbar actions. Action( class_name=PKG + '.action.api:SwitchToAction', group='PerspectiveGroup', path='ToolBar/Project', ), Action( class_name=PKG + '.action.api:NewProjectAction', group='ProjectGroup', path='ToolBar/Project', ), Action( class_name=PKG + '.action.api:OpenProjectAction', group='ProjectGroup', path='ToolBar/Project', ), Action( class_name=PKG + '.action.api:SaveProjectAction', group='ProjectGroup', path='ToolBar/Project', ), Action( class_name=PKG + '.action.api:SaveAsProjectAction', group='ProjectGroup', path='ToolBar/Project', ), Action( class_name=PKG + '.action.api:CloseProjectAction', group='ProjectGroup', path='ToolBar/Project', ), ] #### 'WorkbenchActionSet' interface ####################################### # The Ids of the perspectives that the action set is enabled in. enabled_for_perspectives = ['Project'] # The Ids of the perspectives that the action set is visible in. visible_for_perspectives = ['Project'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/project_factory.py0000644000076500000240000000524000000000000025604 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A base class for project factories. """ # Standard library imports import logging # Enthought library imports from envisage.api import IApplication from traits.api import HasTraits, Instance # Local imports. from .project import Project # Setup a logger for this module. logger = logging.getLogger(__name__) class ProjectFactory(HasTraits): """ A base class for project factories. """ ########################################################################## # Attributes ########################################################################## #### public 'ProjectFactory' interface ################################### # The class of the project created by this factory. # # This is provided so that the single_project services can call class # methods. # # This value is meant to be constant for the lifetime of this class! PROJECT_CLASS = Project # Current envisage application. application = Instance(IApplication) ########################################################################## # 'ProjectFactory' interface. ########################################################################## #### public method ####################################################### def create(self): """ Create a new project from scratch. This must return an instance of a Project or 'None'. A return value of 'None' indicates that no project could be created. The plugin will display the default traits view to the user so that they can configure this new project. """ return self.PROJECT_CLASS(application=self.application) def open(self, location): """ Open a project from the specified location. This must return an instance of a Project or 'None'. A return value of 'None' indicates that no project could be opened from the specified location. """ try: project = self.PROJECT_CLASS.load(location, self.application) except: logger.exception('Unable to load Project from location %s', location) project = None return project #### EOF ##################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/project_plugin.py0000644000076500000240000003567300000000000025450 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The Envisage single project plugin. """ # Standard library imports import logging # Enthought library imports. from envisage.api import ExtensionPoint, Plugin, ServiceOffer from envisage.ui.single_project.api import FactoryDefinition from pyface.action.api import MenuManager from pyface.workbench.api import Perspective from traits.api import Callable, List # Local imports. from .model_service import ModelService from .project_action_set import ProjectActionSet from .services import IPROJECT_MODEL, IPROJECT_UI from .ui_service_factory import UIServiceFactory # This module's package. PKG = '.'.join(__name__.split('.')[:-1]) # Setup a logger for this module. logger = logging.getLogger(__name__) ############################################################################### # `ProjectPerspective` class. ############################################################################### class ProjectPerspective(Perspective): """ A default perspective for the single_project plugin. """ # The perspective's name. name = 'Project' # Should this perspective be enabled or not? enabled = True # Should the editor area be shown in this perspective? show_editor_area = True # The contents of the perspective. # TODO: Setup the PerspectiveItems based on the areas in our perspective. #contents = [] ############################################################################## # 'ProjectPlugin' class. ############################################################################## class ProjectPlugin(Plugin): """ The single-project plugin. """ # The Ids of the extension points that this plugin offers. ACTION_SETS = 'envisage.ui.workbench.action_sets' FACTORY_DEFINITIONS = 'envisage.ui.single_project.factory_definitions' UI_SERVICE_FACTORY = 'envisage.ui.single_project.ui_service_factory' # The Ids of the extension points that this plugin contributes to. PERSPECTIVES = 'envisage.ui.workbench.perspectives' PREFERENCES = 'envisage.preferences' PREFERENCES_PAGES = 'envisage.ui.workbench.preferences_pages' SERVICE_OFFERS = 'envisage.service_offers' VIEWS = 'envisage.ui.workbench.views' #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = 'envisage.ui.single_project' # The plugin's name (suitable for displaying to the user). name = 'Single Project' #### Extension points offered by this plugin ############################## # Factory definitions. factory_definitions = ExtensionPoint( List(Callable), id=FACTORY_DEFINITIONS, desc=""" A project factory definition. An instance of the specified class is used to open and/or create new projects. The extension with the highest priority wins! In the event of a tie, the first instance wins. """ ) # Ui service factories. ui_service_factory = ExtensionPoint( List(Callable), id=UI_SERVICE_FACTORY, desc=""" A ui service factory definition. """ ) #### Contributions to extension points made by this plugin ################ # Action sets. action_sets = List(contributes_to=ACTION_SETS) def _action_sets_default(self): """ Default project actions. """ return [ProjectActionSet] # Factory definitions. my_factory_definitions = List(contributes_to=FACTORY_DEFINITIONS) def _my_factory_definitions_default(self): """ Default factory definition. """ factory_definition = FactoryDefinition( class_name = PKG + '.project_factory.ProjectFactory', priority = 0, ) return [factory_definition] # Perspectives. perspectives = List(contributes_to=PERSPECTIVES) def _perspectives_default(self): """ Default project perspective. """ return [ProjectPerspective] # Service offers. service_offers = List(contributes_to=SERVICE_OFFERS) def _service_offers_default(self): """ Our service contributions. """ model_service = ServiceOffer( protocol = IPROJECT_MODEL, factory = self._create_model_service ) ui_service = ServiceOffer( protocol = IPROJECT_UI, factory = self._create_ui_service ) # FIXME: Eventually we will register the services here intead # of in the plugin's start() method. #return [model_service, ui_service] return [] # Ui service factories. my_ui_service_factory = List(contributes_to=UI_SERVICE_FACTORY) def _my_ui_service_factory_default(self): """ Default ui service factory. """ ui_service_factory = UIServiceFactory( class_name = PKG + '.ui_service_factory.UIServiceFactory', priority = 0, ) return [ui_service_factory] # Preferences. my_preferences = List(contributes_to=PREFERENCES) def _my_preferences_default(self): """ Default preferences. """ return ['pkgfile://%s/preferences.ini' % PKG] # Preference pages. my_preferences_pages = List(contributes_to=PREFERENCES_PAGES) def _my_preferences_pages_default(self): """ Default preference page. """ from .default_path_preference_page import DefaultPathPreferencePage return [DefaultPathPreferencePage] # Views. views = List(contributes_to=VIEWS) def _views_default(self): """ Add our project views. """ return [self._project_view_factory] ### protected interface ################################################## def start(self): """ Starts the plugin. Overridden here to start up our services and load the project that was open when we were last shut-down. """ super(ProjectPlugin, self).start() # FIXME: We eventually won't have to explicitly register the # services ourselves, since we contribute them as service offers # so they are instantiated when they are invoked, but since they are # not used anywhere else yet, I had to use this same old approach # just to test and make sure they were working correctly. # Create and register the model service we offer model_service = self._create_model_service() self.application.register_service(IPROJECT_MODEL, model_service) # Create and register the ui service we offer ui_service = self._create_ui_service(model_service) self.application.register_service(IPROJECT_UI, ui_service) # Set up any listeners interested in the current project selection # FIXME: Register the selection listeners for the current project selection. #self._register_selection_listeners(model_service) return ###################################################################### # Private methods. def _project_view_factory(self, window, **traits): """ Factory method for project views. """ from pyface.workbench.traits_ui_view import \ TraitsUIView from envisage.ui.single_project.api import \ ProjectView project_view = ProjectView(application=window.application) tui_project_view = TraitsUIView(obj=project_view, id='envisage.ui.single_project.view.project_view.ProjectView', name='Project View', window=window, position='left', **traits ) return tui_project_view def _create_model_service(self): """ Creates a model service for this plugin. """ # Determine which contributed project factory to use. factory = self._get_contributed_project_factory() # Make sure the factory has a reference to our Envisage application. factory.application = self.application # Create the project service instance. result = ModelService(self.application, factory) return result def _create_ui_service(self, model_service): """ Creates a UI service for this plugin. """ # Create the menu manager representing the context menu we show when # nothing is selected in the project view. menu_manager = self._get_no_selection_context_menu_manager() # Get the UI service factory. ui_service_factory = self._get_contributed_ui_service_factory() # Create the ui service instance ui_service = ui_service_factory.create_ui_service(model_service, menu_manager) return ui_service def _get_contributed_project_factory(self): """ Retrieves the instance of the project factory to use with this plugin. The instance is generated from the contributed factory definition that was the first one with the highest priority. """ # Retrieve all the factory definition contributions extensions = self.application.get_extensions('envisage.ui.single_project.factory_definitions') # Find the winning contribution definition = None for extension in extensions: if not definition or extension.priority > definition.priority: definition = extension # Create an instance of the winning project factory logger.info("Using ProjectFactory [%s]", definition.class_name) klass = self.application.import_symbol(definition.class_name) factory = klass() return factory def _get_contributed_ui_service_factory(self): """ Retrieves the instance of the UiService factory to use with this plugin. The instance is generated from the contributed factory definition that was the first one with the highest priority. """ # Retrieve all the factory definition contributions extensions = self.get_extensions('envisage.ui.single_project.ui_service_factory') # Find the winning contribution definition = None for extension in extensions: if not definition or extension.priority > definition.priority: definition = extension # Create an instance of the winning factory logger.info("Using UiService Factory [%s]", definition.class_name) class_name = definition.class_name klass = self.application.import_symbol(class_name) factory = klass() return factory def _get_no_selection_context_menu_manager(self): """ Generates a menu manager representing the context menu shown when nothing is selected within the project view. That is, when the user right clicks on any empty space within our associated UI. """ # Retrieve all contributions for the no-selection context menu. extensions = self.get_extensions(ProjectActionSet) # Populate a menu manager from the extensions. menu_manager = MenuManager() if len(extensions) > 0: action_set_manager = ActionSetManager(action_sets=extensions) menu_builder = DefaultMenuBuilder(application=self.application) menu_builder.initialize_menu_manager(menu_manager, action_set_manager, NO_SELECTION_MENU_ID) return menu_manager def _register_selection_listeners(self, model_service): """ Registers any extension-requested listeners on the project selection. """ for sps in self.get_extensions(SyncProjectSelection): object = self.application.lookup_application_object(sps.uol) if object is not None: name = sps.name self._register_selection_handler(object, name, model_service) else: logger.error('Could not resolve the SyncProjectSelection ' + \ 'UOL: "%s"', sps.uol ) return def _register_selection_handler(self, object, name, model_service): """ Creates a handler and registers it. """ def handler(): # The key to this method is to realize that our goal is to # make it as easy as possible to create recipients for # notification. Using traits as the recipients makes # creation very simple because we can rely on the type # knowledge within that trait to ensure only valid values # get assigned to the recipient. That is the recipient # doesn't need to do anything complex to validate the # values they get assigned. This method also works if the # recipient isn't a trait, but in that case, they will # have to handle multiple selection of the project # bindings. # # First, try to provide the recipient with a multiple # selection type value i.e. a list of bindings. try: setattr(object, name, model_service.selection) return except: pass # If that didn't work, remove the binding wrappers and try # notification of the resulting list. selection = [s.obj for s in model_service.selection] try: setattr(object, name, selection) return except: pass # If that didn't work, and only a single item is selected, # then try to provide that item to the recipient. if len(selection) == 1: try: setattr(object, name, selection[0]) return except: pass # The recipient must not be accepting the type of the # current selection, so let's clear its current selection # instead. If this fails, then something has gone wrong # with the declaration of the recipient. try: setattr(object, name, None) except: logger.debug('Error informing object [%s] of project ' 'selection change via attribute [%s]', object, name) model_service.on_trait_change(handler, 'selection') model_service.on_trait_change(handler, 'selection_items') return ### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/services.py0000644000076500000240000000123200000000000024227 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # IDs of services provided by this plugin IPROJECT_MODEL = 'envisage.ui.single_project.model_service.ModelService' IPROJECT_UI = 'envisage.ui.single_project.ui_service.UiService' #### EOF ##################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2837288 envisage-4.9.0/envisage/ui/single_project/tests/0000755000076500000240000000000000000000000023176 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/ui/single_project/tests/__init__.py0000644000076500000240000000000000000000000025275 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/ui/single_project/tests/test_project_view.py0000644000076500000240000000242000000000000027305 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! import unittest from traits.api import HasTraits, Supports from traits.interface_checker import check_implements from traitsui.api import ITreeNode from envisage.ui.single_project.api import Project from envisage.ui.single_project.view.project_view import EmptyProject class MyProject(Project): pass class MyView(HasTraits): tree_node = Supports(ITreeNode) class TestProjectView(unittest.TestCase): def test_empty_project_adapts_to_i_tree_node(self): empty_project = EmptyProject() view = MyView() view.tree_node = empty_project adapted = view.tree_node check_implements(type(adapted), ITreeNode) def test_project_adapts_to_i_tree_node(self): my_project = MyProject() view = MyView() view.tree_node = my_project adapted = view.tree_node check_implements(type(adapted), ITreeNode) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/ui_service.py0000644000076500000240000007062000000000000024550 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ A service to enable UI interactions with the single project plugin. """ # Standard library imports. import logging import os import shutil # Enthought library imports from apptools.preferences.api import bind_preference from apptools.io.api import File from apptools.naming.api import Context from pyface.api import CANCEL, confirm, ConfirmationDialog, \ DirectoryDialog, error, FileDialog, information, NO, OK, YES from pyface.action.api import MenuManager from pyface.timer.api import do_later, Timer from traits.api import Any, Event, HasTraits, Instance, Int # Local imports. from .model_service import ModelService # Setup a logger for this module. logger = logging.getLogger(__name__) class UiService(HasTraits): """ A service to enable UI interactions with the single project plugin. """ ########################################################################## # Attributes ########################################################################## #### public 'UiService' interface ######################################## # The manager of the default context menu default_context_menu_manager = Instance(MenuManager) # A reference to our plugin's model service. model_service = Instance(ModelService) # The project control (in our case a tree). This is created by the # project view. Provided here so that sub-classes may access it. project_control = Any # Fired when a new project has been created. The value should be the # project instance that was created. project_created = Event # A timer to implement automatic project saving. timer = Instance(Timer) # The interval (minutes)at which automatic saving should occur. autosave_interval = Int(5) ########################################################################## # 'object' interface. ########################################################################## #### operator methods #################################################### def __init__(self, model_service, menu_manager, **traits): """ Constructor. Extended to require a reference to the plugin's model service to create an instance. """ super(UiService, self).__init__( model_service = model_service, default_context_menu_manager = menu_manager, **traits ) try: # Bind the autosave interval to the value specified in the # single project preferences p = self.model_service.preferences bind_preference(self, 'autosave_interval', 5, p) except: logger.exception('Failed to bind autosave_interval in [%s] to ' 'preferences.' % self) return ########################################################################## # 'UiService' interface. ########################################################################## #### public interface #################################################### def close(self, event): """ Close the current project. """ # Ensure any current project is ready for this change. if self.is_current_project_saved(event.window.control): # If we have a current project, close it. current = self.model_service.project if current is not None: logger.debug("Closing Project [%s]", current.name) self.model_service.project = None return def create(self, event): """ Create a new project. """ # Ensure any current project is ready for this change. if self.is_current_project_saved(event.window.control): # Use the registered factory to create a new project project = self.model_service.factory.create() if project is not None: # Allow the user to customize the new project dialog = project.edit_traits( parent = event.window.control, # FIXME: Due to a bug in traits, using a wizard dialog # causes all of the Instance traits on the object being # edited to be replaced with new instances without any # listeners on those traits being called. Since we can't # guarantee that our project's don't have Instance traits, # we can't use the wizard dialog type. #kind = 'wizard' kind = 'livemodal' ) # If the user closed the dialog with an ok, make it the # current project. if dialog.result: logger.debug("Created Project [%s]", project.name) self.model_service.project = project self.project_created = project return def display_default_context_menu(self, parent, event): """ Display the default context menu for the plugin's ui. This is the context menu used when neither a project nor the project's contents are right-clicked. """ # Determine the current workbench window. This should be safe since # we're only building a context menu when the user clicked on a # control that is contained in a window. workbench = self.model_service.application.get_service('envisage.ui.workbench.workbench.Workbench') window = workbench.active_window # Build our menu from envisage.workbench.action.action_controller import \ ActionController menu = self.default_context_menu_manager.create_menu(parent, controller = ActionController(window=window)) # Popup the menu (if an action is selected it will be performed # before before 'PopupMenu' returns). if menu.GetMenuItemCount() > 0: menu.show(event.x, event.y) return def delete_selection(self): """ Delete the current selection within the current project. """ # Only do something if we have a current project and a non-empty # selection current = self.model_service.project selection = self.model_service.selection[:] if current is not None and len(selection) > 0: logger.debug('Deleting selection from Project [%s]', current) # Determine the context for the current project. Raise an error # if we can't treat it as a context as then we don't know how # to delete anything. context = self._get_context_for_object(current) if context is None: raise Exception('Could not treat Project ' + \ '[%s] as a context' % current) # Filter out any objects in the selection that can NOT be deleted. deletables = [] for item in selection: rt = self._get_resource_type_for_object(item.obj) nt = rt.node_type if nt.can_delete(item): deletables.append(item) else: logger.debug('Node type reports selection item [%s] is ' 'not deletable.', nt) if deletables != []: # Confirm the delete operation with the user names = '\n\t'.join([b.name for b in deletables]) message = ('You are about to delete the following selected ' 'items:\n\t%s\n\n' 'Are you sure?') % names title = 'Delete Selected Items?' action = confirm(None, message, title) if action == YES: # Unbind all the deletable nodes if len(deletables) > 0: self._unbind_nodes(context, deletables) return def is_current_project_saved(self, parent_window): """ Give the user the option to save any modifications to the current project prior to closing it. If the user wanted to cancel the closing of the current project, this method returns False. Otherwise, it returns True. """ # The default is the user okay'd the closing of the project result = True # If the current project is dirty, handle that now by challenging the # user for how they want to handle them. current = self.model_service.project if not(self._get_project_state(current)): dialog = ConfirmationDialog( parent = parent_window, cancel = True, title = 'Unsaved Changes', message = 'Do you want to save the changes to project "%s"?' \ % (current.name), ) action = dialog.open() if action == CANCEL: result = False elif action == YES: result = self._save(current, parent_window) elif action == NO: # Delete the autosaved file as the user does not wish to # retain the unsaved changes. self._clean_autosave_location(current.location.strip()) return result def listen_for_application_exit(self): """ Ensure that we get notified of any attempts to, and thus have a chance to veto, the closing of the application. FIXME: Normally this should be called during startup of this plugin, however, Envisage won't let us find the workbench service then because we've made a contribution to its extension points and it insists on starting us first. """ workbench = self.model_service.application.get_service('envisage.ui.workbench.workbench.Workbench') workbench.on_trait_change(self._workbench_exiting, 'exiting') return def open(self, event): """ Open a project. """ # Ensure any current project is ready for this change. if self.is_current_project_saved(event.window.control): # Query the user for the location of the project to be opened. path = self._show_open_dialog(event.window.control) if path is not None: logger.debug("Opening project from location [%s]", path) project = self.model_service.factory.open(path) if project is not None: logger.debug("Opened Project [%s]", project.name) self.model_service.project = project else: msg = 'Unable to open %s as a project.' % path error(event.window.control, msg, title='Project Open Error') return def save(self, event): """ Save a project. """ current = self.model_service.project if current is not None: self._save(current, event.window.control) return def save_as(self, event): """ Save the current project to a different location. """ current = self.model_service.project if current is not None: self._save(current, event.window.control, prompt_for_location=True) return #### protected interface ################################################# def _auto_save(self, project): """ Called periodically by the timer's Notify function to automatically save the current project. The auto-saved project has the extension '.autosave'. """ # Save the project only if it has been modified. if project.dirty and project.is_save_as_allowed: location = project.location.strip() if not(location is None or len(location) < 1): autosave_loc = self._get_autosave_location(location) try: # We do not want the project's location and name to be # updated. project.save(autosave_loc, overwrite=True, autosave=True) msg = '[%s] auto-saved to [%s]' % (project, autosave_loc) logger.debug(msg) except: logger.exception('Error auto-saving project [%s]'% project) else: logger.exception('Error auto-saving project [%s] in ' 'location %s' % (project, location)) return def _clean_autosave_location(self, location): """ Removes any existing autosaved files or directories for the project at the specified location. """ autosave_loc = self._get_autosave_location(location) if os.path.exists(autosave_loc): self.model_service.clean_location(autosave_loc) return def _get_autosave_location(self, location): """ Returns the path for auto-saving the project in location. """ return os.path.join(os.path.dirname(location), os.path.basename(location) + '.autosave') def _get_context_for_object(self, obj): """ Return the context for the specified object. """ if isinstance(obj, Context): context = obj else: context = None resource_type = self._get_resource_type_for_object(obj) if resource_type is not None: factory = resource_type.context_adapter_factory if factory is not None: # FIXME: We probably should use a real environment and # context (parent context?) context = factory.adapt(obj, Context, {}, None) return context def _get_resource_type_for_object(self, obj): """ Return the resource type for the specified object. If no type could be found, returns None. """ resource_manager = self.model_service.resource_manager return resource_manager.get_type_of(obj) def _get_project_state(self, project): """ Returns True if the project is clean: i.e., the dirty flag is False and all autosaved versions have been deleted from the filesystem. """ result = True if project is not None: autosave_loc = self._get_autosave_location( project.location.strip()) if project.dirty or os.path.exists(autosave_loc): result = False return result def _get_user_location(self, project, parent_window): """ Prompt the user for a new location for the specified project. Returns the chosen location or, if the user cancelled, an empty string. """ # The dialog to use depends on whether we're prompting for a file or # a directory. if self.model_service.are_projects_files(): dialog = FileDialog(parent = parent_window, title = 'Save Project As', default_path = project.location, action = 'save as', ) title_type = 'File' else: dialog = DirectoryDialog(parent = parent_window, message = 'Choose a Directory for the Project', default_path = project.location, action = 'open' ) title_type = 'Directory' # Prompt the user for a new location and then validate we're not # overwriting something without getting confirmation from the user. result = "" while(dialog.open() == OK): location = dialog.path.strip() # If the chosen location doesn't exist yet, we're set. if not os.path.exists(location): logger.debug('Location [%s] does not exist yet.', location) result = location break # Otherwise, confirm with the user that they want to overwrite the # existing files or directories. If they don't want to, then loop # back and prompt them for a new location. else: logger.debug('Location [%s] exists. Prompting for overwrite ' 'permission.', location) message = 'Overwrite %s?' % location title = 'Project %s Exists' % title_type action = confirm(parent_window, message, title) if action == YES: # Only use the location if we successfully remove the # existing files or directories at that location. try: self.model_service.clean_location(location) result = location break # Otherwise, display the remove error to the user and give # them another chance to pick another location except Exception as e: msg = str(e) title = 'Unable To Overwrite %s' % location information(parent_window, msg, title) logger.debug('Returning user location [%s]', result) return result def _restore_from_autosave(self, project, autosave_loc): """ Restores the project from the version saved in autosave_loc. """ workbench = self.model_service.application.get_service( 'envisage.ui.workbench.workbench.Workbench') window = workbench.active_window app_name = workbench.branding.application_name message = ('The app quit unexpectedly when [%s] was being modified.\n' 'An autosaved version of this project exists.\n' 'Do you want to restore the project from the ' 'autosaved version ?' % project.name) title = '%s-%s' % (app_name, project.name) action = confirm(window.control, message, title, cancel=True, default=YES) if action == YES: try: saved_project = self.model_service.factory.open(autosave_loc) if saved_project is not None: # Copy over the autosaved version to the current project's # location, switch the model service's project, and delete # the autosaved version. loc = project.location.strip() saved_project.save(loc, overwrite=True) self.model_service.clean_location(autosave_loc) self.model_service.project = saved_project else: logger.debug('No usable project found in [%s].' % autosave_loc) except: logger.exception( 'Unable to restore project from [%s]' % autosave_loc) self._start_timer(self.model_service.project) return def _save(self, project, parent_window, prompt_for_location=False): """ Save the specified project. If *prompt_for_location* is True, or the project has no known location, then the user is prompted to provide a location to save to. Returns True if the project was saved successfully, False if not. """ location = project.location.strip() # If the project's existing location is valid, check if there are any # autosaved versions. autosave_loc = '' if location is not None and os.path.exists(location): autosave_loc = self._get_autosave_location(location) # Ask the user to provide a location if we were told to do so or # if the project has no existing location. if prompt_for_location or location is None or len(location) < 1: location = self._get_user_location(project, parent_window) # Rename any existing autosaved versions to the new project # location. if location is not None and len(location) > 0: self._clean_autosave_location(location) new_autosave_loc = self._get_autosave_location(location) if os.path.exists(autosave_loc): shutil.move(autosave_loc, new_autosave_loc) # If we have a location to save to, try saving the project. if location is not None and len(location) > 0: try: project.save(location) saved = True msg = '"%s" saved to %s' % (project.name, project.location) information(parent_window, msg, 'Project Saved') logger.debug(msg) except Exception as e: saved = False logger.exception('Error saving project [%s]', project) error(parent_window, str(e), title='Save Error') else: saved = False # If the save operation was successful, delete any autosaved files that # exist. if saved: self._clean_autosave_location(location) return saved def _show_open_dialog(self, parent): """ Show the dialog to open a project. """ # Determine the starting point for browsing. It is likely most # projects will be stored in the default path used when creating new # projects. default_path = self.model_service.get_default_path() project_class = self.model_service.factory.PROJECT_CLASS if self.model_service.are_projects_files(): dialog = FileDialog(parent=parent, default_directory=default_path, title='Open Project') if dialog.open() == OK: path = dialog.path else: path = None else: dialog = DirectoryDialog(parent=parent, default_path=default_path, message='Open Project') if dialog.open() == OK: path = project_class.get_pickle_filename(dialog.path) if File(path).exists: path = dialog.path else: error(parent, 'Directory does not contain a recognized ' 'project') path = None else: path = None return path def _start_timer(self, project): """ Resets the timer to work on auto-saving the current project. """ if self.timer is None: if self.autosave_interval > 0: # Timer needs the interval in millisecs self.timer = Timer(self.autosave_interval*60000, self._auto_save, project) return def _unbind_nodes(self, context, nodes): """ Unbinds all of the specified nodes that can be found within this context or any of its sub-contexts. This uses a breadth first algorithm on the assumption that the user will have likely selected peer nodes within a sub-context that isn't the deepest context. """ logger.debug('Unbinding nodes [%s] from context [%s] within ' 'UiService [%s]', nodes, context, self) # Iterate through all of the selected nodes looking for ones who's # name is within our context. context_names = context.list_names() for node in nodes[:]: if node.name in context_names: # Ensure we've found a matching node by matching the objects # as well. binding = context.lookup_binding(node.name) if id(node.obj) == id(binding.obj): # Remove the node from the context -AND- from the list of # nodes that are still being searched for. context.unbind(node.name) nodes.remove(node) # Stop if we've unbound the last node if len(nodes) < 1: break # If we haven't unbound the last node, then search any sub-contexts # for more nodes to unbind. else: # Build a list of all current sub-contexts of this context. subs = [] for name in context.list_names(): if context.is_context(name): obj = context.lookup_binding(name).obj sub_context = self._get_context_for_object(obj) if sub_context is not None: subs.append(sub_context) # Iterate through each sub-context, stopping as soon as possible # if we've run out of nodes. for sub in subs: self._unbind_nodes(sub, nodes) if len(nodes) < 1: break def _workbench_exiting(self, event): """ Handle the workbench polling to see if it can exit and shutdown the application. """ logger.debug('Detected workbench closing event in [%s]', self) # Determine if the current project is dirty, or if an autosaved file # exists for this project (i.e., the project has changes which were # captured in the autosave operation but were not saved explicitly by # the user). If so, let the user # decide whether to veto the closing event, save the project, or # ignore the dirty state. current = self.model_service.project if not(self._get_project_state(current)): # Find the active workbench window to be our dialog parent and # the application name to use in our dialog title. workbench = self.model_service.application.get_service('envisage.ui.workbench.workbench.Workbench') window = workbench.active_window app_name = workbench.branding.application_name # Show a confirmation dialog to the user. message = 'Do you want to save changes before exiting?' title = '%s - %s' % (current.name, app_name) action = confirm(window.control, message, title, cancel=True, default=YES) if action == YES: # If the save is successful, the autosaved file is deleted. if not self._save(current, window.control): event.veto = True elif action == NO: # Delete the autosaved file as the user does not wish to # retain the unsaved changes. self._clean_autosave_location(current.location.strip()) elif action == CANCEL: event.veto = True #### Trait change handlers ############################################### def _autosave_interval_changed(self, old, new): """ Restarts the timer when the autosave interval changes. """ self.timer = None if new > 0 and self.model_service.project is not None: self._start_timer(self.model_service.project) return def _project_changed_for_model_service(self, object, name, old, new): """ Detects if an autosaved version exists for the project, and displays a dialog to confirm restoring the project from the autosaved version. """ if old is not None: self.timer = None if new is not None: # Check if an autosaved version exists and if so, display a dialog # asking if the user wishes to restore the project from the # autosaved version. # Note: An autosaved version should exist only if the app crashed # unexpectedly. Regular exiting of the workbench should cause the # autosaved version to be deleted. autosave_loc = self._get_autosave_location(new.location.strip()) if (os.path.exists(autosave_loc)): # Issue a do_later command here so as to allow time for the # project view to be updated first to reflect the current # project's state. do_later(self._restore_from_autosave, new, autosave_loc) else: self._start_timer(new) return #### EOF ##################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/ui_service_factory.py0000644000076500000240000000226100000000000026273 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The default UI service factory. """ # Enthought library imports. from traits.api import HasTraits, Int, Str # Local imports. from .ui_service import UiService class UIServiceFactory(HasTraits): """ The default UI service factory. """ # The name of the class that implements the factory. class_name = Str # The priority of this factory priority = Int ########################################################################### # 'UIServiceFactory' interface. ########################################################################### def create_ui_service(self, *args, **kw): """ Create the UI service. """ return UiService(*args, **kw) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2844844 envisage-4.9.0/envisage/ui/single_project/view/0000755000076500000240000000000000000000000023006 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/single_project/view/__init__.py0000644000076500000240000000000000000000000025105 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157387.0 envisage-4.9.0/envisage/ui/single_project/view/project_view.py0000644000076500000240000002720000000000000026061 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The single project plugin's project view """ # Standard library imports. import logging # Enthought library imports from apptools.naming.api import Binding from traits.api import register_factory, Any, HasTraits, Instance, Str from traitsui.api import Item, Group, TreeEditor, ITreeNode, \ ITreeNodeAdapter, View # Application specific imports. from envisage.api import IApplication from envisage.ui.single_project.project import Project from envisage.ui.single_project.services import IPROJECT_MODEL, \ IPROJECT_UI # Setup a logger for this module. logger=logging.getLogger(__name__) # Dummy EmptyProject class for when the ProjectView doesn't have a reference # to a Project. class EmptyProject(Project): pass class EmptyProjectAdapter(ITreeNodeAdapter): """ Adapter for our EmptyProject. """ #-- ITreeNodeAdapter Method Overrides -------------------------------------- def get_label(self): """ Gets the label to display for a specified object. """ return 'No project loaded.' register_factory(EmptyProjectAdapter, EmptyProject, ITreeNode) class ProjectAdapter(ITreeNodeAdapter): """ Base ProjectAdapter for the root of the tree. """ #-- ITreeNodeAdapter Method Overrides -------------------------------------- def allows_children(self): """ Returns whether this object can have children. """ return False def has_children(self): """ Returns whether the object has children. """ return False def get_children(self): """ Gets the object's children. """ return [] def get_label(self): """ Gets the label to display for a specified object. """ return self.adaptee.name def get_tooltip(self): """ Gets the tooltip to display for a specified object. """ return "Project" def get_icon(self, is_expanded): """ Returns the icon for a specified object. """ return '' def can_auto_close(self): """ Returns whether the object's children should be automatically closed. """ return True register_factory(ProjectAdapter, Project, ITreeNode) class ProjectView(HasTraits): """ The single project plugin's project view """ ########################################################################## # Traits ########################################################################## #### public 'ProjectView' interface ###################################### # The Envisage application that this service is part of. application = Instance(IApplication) # The suffix currently applied to our name name_suffix = Str('') # The suffix applied to titles when the current project is dirty. title_suffix = Str('*') # Root node for the project. root = Instance(Project) # The traits view to display: view = View( Item('root', editor = TreeEditor(editable=False, auto_open=1), show_label = False, ), resizable = True ) ########################################################################## # 'View' interface. ########################################################################## def __init__(self, **traits): super(ProjectView, self).__init__(**traits) # Make sure our view stays in sync with the current project model_service = self._get_model_service() model_service.on_trait_change(self._on_project_changed, 'project') model_service.on_trait_change(self._on_project_selection_changed, 'selection') # Make sure our control is initialized to the current project. self._switch_projects(EmptyProject(), model_service.project) ########################################################################## # 'ProjectView' interface. ########################################################################## #### protected 'ProjectView' interface ################################# def _are_list_contents_different(self, list1, list2): """ Convenience method to determine if two lists contain different items. Returns True if the lists are different and False otherwise. """ set1 = set(list1) set2 = set(list2) return set1 != set2 def _clear_state(self): """ Clears out all indications of any project state from this view. """ #self.name_suffix = '' # Set the root to an EmptyProject. self.root = EmptyProject() return def _get_model_service(self): """ Return a reference to the single project plugin's model service. """ return self.application.get_service(IPROJECT_MODEL) def _get_ui_service(self): """ Return a reference to the single project plugin's UI service. """ return self.window.application.get_service(IPROJECT_UI) def _switch_projects(self, old, new): """ Switches this view to the specified new project from the old project. Either value may be None. """ # Remove listeners on the old project, if any. if old is not None and not isinstance(old, EmptyProject): self._update_project_listeners(old, remove=True) # Update our state according to what the new project is, making sure # to add listeners to any new project. if new is not None: logger.debug("Changing ProjectView to Project [%s]", new.name) self._sync_state_to_project(new) self._update_project_listeners(new) else: logger.debug("Changing ProjectView to no project") self._clear_state() return def _sync_state_to_project(self, project): """ Sync the state of this view to the specified project's state. """ logger.debug('Syncing ProjectView [%s] to project state [%s]', self, project) # Update our Project reference. self.root = project # Update our name suffix based on the dirty state of the project. #self.name_suffix = (project.dirty and self.title_suffix) or '' return def _update_project_listeners(self, project, remove=False): """ Update listeners on the specified project instance. If remove is False then listeners are added, else listeners are removed. """ project.on_trait_change(self._on_project_name_changed, 'name', remove) project.on_trait_change(self._on_project_dirty_changed, 'dirty', remove) return #### trait handlers ###################################################### def _name_suffix_changed(self, old, new): """ Handle changes to our name suffix. """ logger.debug('Detected change in name suffix to [%s] within ' + \ 'ProjectView [%s]', new, self) # Update our own name by removing the old suffix, if any, and adding # on the new suffix, if any. name = self.name if old is not None and len(old) > 0: index = (" " + old).rfind(name) if index > -1: name = name[0:index] if new is not None and len(new) > 0: name = name + ' ' + new self.name = name # Update the containing window's suffix as well self.window.title_suffix = new return def _on_control_right_clicked (self, event): """ Handle events when the tree control itself is right-clicked. """ logger.debug('ProjectView control right-clicked') self._get_ui_service().display_default_context_menu(self.control, event) return def _on_key_pressed_changed(self, event): """ Handle key presses while our control has focus. """ logger.debug("ProjectView key pressed [%s]", event) # If the delete key was pressed, then delete the current selected # object. if event.key_code == 127: self._get_ui_service().delete_selection() return def _on_node_activated_changed(self, node): """ Handle nodes being activated (i.e. double-clicked.) """ logger.debug("ProjectView node activated [%s]", node) return def _on_project_changed(self, obj, trait_name, old, new): """ Handle when the current project changes. """ logger.debug('\n\n ***************** \n\n') logger.debug('Detected project changed from [%s] to [%s] in ' 'ProjectView [%s]', old, new, self) self._switch_projects(old, new) return def _on_project_dirty_changed(self, obj, trait_name, old, new): """ Handle the open project's dirty flag changing. """ logger.debug('Detected change in project dirty to [%s] within ' + \ 'ProjectView [%s]', new, self) suffix = (new and self.title_suffix) or '' #self.name_suffix = suffix return def _on_project_name_changed(self, obj, trait_name, old, new): """ Handle the open project's name changing. """ self._project_control.root.name = new return def _on_project_selection_changed(self, obj, trait_name, old, new): """ Handle the current project's selection changing. """ logger.debug('Detected project selection changed from [%s] ' + \ 'to [%s] within ProjectView [%s]', old, new, self) # Ensure that the Tree control's selection matches the selection in # the project. control_selection = self._project_control.selection if self._are_list_contents_different(new, control_selection): logger.debug(' Updating selection on tree control for ' + \ 'ProjectView [%s]', self) self._project_control.set_selection(new) # Ensure that this view's selection contains whatever was selected # within the project. if self._are_list_contents_different(new, self.selection): logger.debug(' Updating selection on ProjectView [%s]', self) self.selection = new return def _on_selection_changed(self, obj, trait_name, old, new): """ Handle selection changes in our tree control. """ logger.debug('Detected tree control selection change from [%s] ' + \ 'to [%s] within ProjectView [%s]', old, new, self) # Ensure that the project model service's record of selection contains # the same elements as the tree control. model_service = self._get_model_service() if self._are_list_contents_different(new, model_service.selection): logger.debug(' Updating selection on project model service') model_service.selection = new # Note that we don't have to update this view's selection record as # we do that through our listener on the project selection. return def _on_closing_changed(self, old, new): """ Handle when this view closes. """ logger.debug("ProjectView [%s] closing!", self) return #### EOF ##################################################################### ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.288751 envisage-4.9.0/envisage/ui/tasks/0000755000076500000240000000000000000000000020152 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/__init__.py0000644000076500000240000000000000000000000022251 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2906647 envisage-4.9.0/envisage/ui/tasks/action/0000755000076500000240000000000000000000000021427 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/action/__init__.py0000644000076500000240000000000000000000000023526 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/action/api.py0000644000076500000240000000105500000000000022553 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .task_window_launch_group import TaskWindowLaunchAction, \ TaskWindowLaunchGroup from .task_window_toggle_group import TaskWindowToggleGroup ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/action/exit_action.py0000644000076500000240000000227100000000000024311 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from pyface.api import ImageResource from pyface.action.api import Action class ExitAction(Action): """ An action that exits the application. """ #### 'Action' interface ################################################### # A longer description of the action. description = 'Exit the application' # The action's name (displayed on menus/tool bar tools etc). name = 'E&xit' # A short description of the action used for tooltip text etc. tooltip = 'Exit the application' ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): event.task.window.application.exit() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/action/preferences_action.py0000644000076500000240000000403700000000000025643 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from pyface.action.api import Action, Group class PreferencesAction(Action): """ An action that displays the preferences dialog. """ #### 'Action' interface ################################################### # A longer description of the action. description = 'Open the preferences dialog' # The action's name (displayed on menus/tool bar tools etc). name = 'Prefere&nces...' # A short description of the action used for tooltip text etc. tooltip = 'Open the preferences dialog' ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): from envisage.ui.tasks.preferences_dialog import \ PreferencesDialog window = event.task.window dialog = window.application.get_service(PreferencesDialog) ui = dialog.edit_traits(parent=window.control, kind='livemodal') if ui.result: window.application.preferences.save() class PreferencesGroup(Group): """ A group that contains the preferences action. """ #### 'Action' interface ################################################### # The group's identifier (unique within action manager). id = 'PreferencesGroup' ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): super(PreferencesGroup, self).__init__(PreferencesAction(), **traits) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/action/task_window_launch_group.py0000644000076500000240000000527300000000000027107 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from pyface.action.api import ActionItem, Group from pyface.tasks.api import TaskWindowLayout from pyface.tasks.action.api import TaskAction from traits.api import List, Str # Local imports from envisage._compat import unicode_str class TaskWindowLaunchAction(TaskAction): """ An Action that creates a task window with a single task. """ #### 'TaskWindowLaunchAction' interface ################################### task_id = Str ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): application = event.task.window.application window = application.create_window(TaskWindowLayout(self.task_id)) window.open() ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ def _task_changed(self, task): """ Name the action (unless a name has already been assigned). """ if task and not self.name: name = unicode_str() for factory in task.window.application.task_factories: if factory.id == self.task_id: name = factory.name break self.name = name class TaskWindowLaunchGroup(Group): """ A Group for creating task windows with a single task. """ #### 'Group' interface #################################################### id = 'TaskWindowLaunchGroup' items = List ########################################################################### # Private interface. ########################################################################### def _items_default(self): manager = self while isinstance(manager, Group): manager = manager.parent application = manager.controller.task.window.application items = [] for factory in application.task_factories: action = TaskWindowLaunchAction(task_id=factory.id) items.append(ActionItem(action=action)) return items ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/action/task_window_toggle_group.py0000644000076500000240000001025500000000000027112 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from pyface.action.api import Action, ActionItem, Group from traits.api import Any, Instance, List, Property, Unicode, \ on_trait_change # Local imports. from envisage._compat import unicode_str class TaskWindowToggleAction(Action): """ An action for activating an application window. """ #### 'Action' interface ################################################### name = Property(Unicode, depends_on='window.active_task.name') style = 'toggle' #### 'TaskWindowToggleAction' interface ################################### # The window to use for this action. window = Instance('envisage.ui.tasks.task_window.TaskWindow') ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event=None): if self.window: self.window.activate() ########################################################################### # Private interface. ########################################################################### def _get_name(self): if self.window.active_task: return self.window.active_task.name return unicode_str() @on_trait_change('window:activated') def _window_activated(self): self.checked = True @on_trait_change('window:deactivated') def _window_deactivated(self): self.checked = False class TaskWindowToggleGroup(Group): """ A Group for toggling the activation state of an application's windows. """ #### 'Group' interface #################################################### id = 'TaskWindowToggleGroup' items = List #### 'TaskWindowToggleGroup' interface #################################### # The application that contains the group. application = Instance('envisage.ui.tasks.tasks_application.' 'TasksApplication') # The ActionManager to which the group belongs. manager = Any ########################################################################### # 'Group' interface. ########################################################################### def destroy(self): """ Called when the group is no longer required. """ super(TaskWindowToggleGroup, self).destroy() if self.application: self.application.on_trait_change( self._rebuild, 'window_opened, window_closed', remove=True) ########################################################################### # Private interface. ########################################################################### def _get_items(self): items = [] for window in self.application.windows: active = window == self.application.active_window action = TaskWindowToggleAction(window=window, checked=active) items.append(ActionItem(action=action)) return items def _rebuild(self): # Clear out the old group, then build the new one. for item in self.items: item.destroy() self.items = self._get_items() # Inform our manager that it needs to be rebuilt. self.manager.changed = True #### Trait initializers ################################################### def _application_default(self): return self.manager.controller.task.window.application def _items_default(self): self.application.on_trait_change(self._rebuild, 'window_opened, window_closed') return self._get_items() def _manager_default(self): manager = self while isinstance(manager, Group): manager = manager.parent return manager ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/api.py0000644000076500000240000000123600000000000021277 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .preferences_category import PreferencesCategory from .preferences_pane import PreferencesPane from .task_extension import TaskExtension from .task_factory import TaskFactory from .task_window import TaskWindow from .tasks_application import TasksApplication ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/preferences_category.py0000644000076500000240000000223600000000000024725 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from traits.api import HasTraits, Str, Unicode class PreferencesCategory(HasTraits): """ The description for a container of PreferencesPanes. """ # The globally unique identifier for the category. id = Str # The user-visible name of the category. name = Unicode # The category appears after the category with this ID. before = Str # The category appears after the category with this ID. after = Str ########################################################################### # Protected interface. ########################################################################### def _name_default(self): """ By default, use the ID for the name. """ return self.id ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/preferences_dialog.py0000644000076500000240000001215100000000000024344 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from traits.api import Bool, HasTraits, Instance, List, Unicode, \ on_trait_change from traitsui.api import Item, Handler, ListEditor, View from pyface.tasks.topological_sort import before_after_sort # Local imports. from .preferences_category import PreferencesCategory from .preferences_pane import PreferencesPane class PreferencesTab(HasTraits): """ An object used internally by PreferencesDialog. """ name = Unicode panes = List(PreferencesPane) view = View(Item('panes', editor = ListEditor(style = 'custom'), show_label = False, style = 'readonly'), resizable = True) class PreferencesDialog(Handler): """ A dialog for editing preferences. """ #### 'PreferencesDialog' interface ######################################## # The application that created and is managing this dialog. application = Instance('envisage.ui.tasks.api.TasksApplication') # The list of categories to use when building the dialog. categories = List(PreferencesCategory) # The list of panes to use when building the dialog. panes = List(PreferencesPane) # Should the Apply button be shown? show_apply = Bool(False) #### Private interface #################################################### _tabs = List(PreferencesTab) _selected = Instance(PreferencesTab) ########################################################################### # Public interface ########################################################################### def select_pane(self, pane_id): """ Find and activate the notebook tab that contains the given pane id. """ for tab in self._tabs: for pane in tab.panes: if pane.id == pane_id: self._selected = tab return ########################################################################### # 'HasTraits' interface. ########################################################################### def trait_context ( self ): """ Returns the default context to use for editing or configuring traits. """ return { 'object': self, 'handler': self } def traits_view(self): """ Build the dynamic dialog view. """ buttons = ['OK', 'Cancel'] if self.show_apply: buttons = ['Apply'] + buttons # Only show the tab bar if there is more than one category. tabs_style = 'custom' if len(self._tabs) > 1 else 'readonly' return View(Item('_tabs', editor = ListEditor(page_name = '.name', style ='custom', use_notebook = True, selected = '_selected'), show_label = False, style = tabs_style), buttons = buttons, kind = 'livemodal', resizable = True, title = 'Preferences') ########################################################################### # 'Handler' interface. ########################################################################### def apply(self, info=None): """ Handles the Apply button being clicked. """ for tab in self._tabs: for pane in tab.panes: pane.apply() def close(self, info, is_ok): """ Handles the user attempting to close a dialog-based user interface. """ if is_ok: self.apply() return super(PreferencesDialog, self).close(info, is_ok) ########################################################################### # Protected interface. ########################################################################### @on_trait_change('categories, panes') def _update_tabs(self): # Build a { category id -> [ PreferencePane ] } map. categories = self.categories[:] category_map = dict((category.id, []) for category in categories) for pane in self.panes: if pane.category in category_map: category_map[pane.category].append(pane) else: categories.append(PreferencesCategory(id=pane.category)) category_map[pane.category] = [ pane ] # Construct the appropriately sorted list of preference tabs. tabs = [] for category in before_after_sort(categories): panes = before_after_sort(category_map[category.id]) tabs.append(PreferencesTab(name = category.name, panes=panes)) self._tabs = tabs ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/preferences_pane.py0000644000076500000240000000637500000000000024043 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from apptools.preferences.api import IPreferences, PreferencesHelper from traits.api import Callable, Dict, HasTraits, Instance, Str from traitsui.api import Controller class PreferencesPane(Controller): """ A panel for configuring application preferences. """ #### 'Controller' interface ############################################### # The preferences helper for which this pane is a view. model = Instance(PreferencesHelper) #### 'PreferencesPane' interface ########################################## # An identifier for the pane (unique within a category). id = Str # The ID of the category in which to place the pane. category = Str('General') # The pane appears after the pane with this ID. before = Str # The pane appears after the pane with this ID. after = Str # The preferences dialog to which the pane belongs. Set by the framework. dialog = Instance( 'envisage.ui.tasks.preferences_dialog.PreferencesDialog') # # The factory to use for creating the preferences model object, of form: # callable(**traits) -> PreferencesHelper # If not specified, the preferences helper must be supplied manually. model_factory = Callable #### Private interface #################################################### _model = Instance(PreferencesHelper) ########################################################################### # 'HasTraits' interface. ########################################################################### def trait_context ( self ): """ Re-implemented to use a copy of the model that is not connected to the preferences node. """ if self.model is None: if self.model_factory is not None: preferences = self.dialog.application.preferences self.model = self.model_factory(preferences = preferences) else: raise ValueError('A preferences pane must have a model!') self._model = self.model.clone_traits() self._model.preferences = None return { 'object': self._model, 'controller': self, 'handler': self } ########################################################################### # 'Handler' interface. ########################################################################### def apply(self, info=None): """ Handles the Apply button being clicked. """ trait_names = list(filter(self._model._is_preference_trait, self._model.trait_names())) self.model.copy_traits(self._model, trait_names) def close(self, info, is_ok): """ Handles the user attempting to close a dialog-based user interface. """ if is_ok: self.apply() return super(PreferencesPane, self).close(info, is_ok) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/task_extension.py0000644000076500000240000000200100000000000023553 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from pyface.tasks.action.api import SchemaAddition from traits.api import HasStrictTraits, Callable, List, Str class TaskExtension(HasStrictTraits): """ A bundle of items for extending a Task. """ # The ID of the task to extend. If the ID is omitted, the extension applies # to all tasks. task_id = Str # A list of menu bar and tool bar items to add to the set provided # by the task. actions = List(SchemaAddition) # A list of dock pane factories that will extend the dock panes provided by # the task. dock_pane_factories = List(Callable) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/task_factory.py0000644000076500000240000000276200000000000023224 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from traits.api import Callable, HasTraits, Str, Unicode class TaskFactory(HasTraits): """ A factory for creating a Task with some additional metadata. """ # The task factory's unique identifier. This ID is assigned to all tasks # created by the factory. id = Str # The task factory's user-visible name. name = Unicode # A callable with the following signature: # # callable(**traits) -> Task # # Often this attribute will simply be a Task subclass. factory = Callable def create(self, **traits): """ Creates the Task. The default implementation simply calls the 'factory' attribute. """ return self.factory(**traits) def create_with_extensions(self, extensions, **traits): """ Creates the Task using the specified TaskExtensions. """ task = self.create(**traits) for extension in extensions: task.extra_actions.extend(extension.actions) task.extra_dock_pane_factories.extend(extension.dock_pane_factories) return task ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/task_window.py0000644000076500000240000000434100000000000023057 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from pyface.image_resource import ImageResource from pyface.tasks.api import TaskWindow as PyfaceTaskWindow from traits.api import Instance, Property # Local imports. from envisage._compat import unicode_str class TaskWindow(PyfaceTaskWindow): """ A TaskWindow for use with the Envisage Tasks plugin. """ # The application that created and is managing this window. application = Instance('envisage.ui.tasks.api.TasksApplication') # The window's icon. We override it so it can delegate to the application # icon if the window's icon is not set. icon = Property(Instance(ImageResource), depends_on='_icon') #### Protected interface ################################################## _icon = Instance(ImageResource, allow_none=True) ########################################################################### # Protected 'TaskWindow' interface. ########################################################################### def _get_title(self): """ If the application has a name, add it to the title. Otherwise, behave like the base class. """ if self._title or self.active_task is None: return self._title title = self.active_task.name if self.application.name: form = unicode_str('%s - %s') title = form % (title, self.application.name) return title def _get_icon(self): """If we have an icon return it, else delegate to the application. """ if self._icon is not None: return self._icon elif self.application is not None: return self.application.icon else: return None def _set_icon(self, icon): """Explicitly set the icon to use. None is allowed. """ self._icon = icon ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/task_window_event.py0000644000076500000240000000147700000000000024267 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from traits.api import HasTraits, Instance, Vetoable # Local imports. from .task_window import TaskWindow class TaskWindowEvent(HasTraits): """ A task window lifecycle event. """ # The window that the event occurred on. window = Instance(TaskWindow) class VetoableTaskWindowEvent(TaskWindowEvent, Vetoable): """ A vetoable task window lifecycle event. """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/tasks_application.py0000644000076500000240000004752700000000000024253 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Standard library imports. import logging import os.path # Enthought library imports. from envisage.api import Application, ExtensionPoint from traits.api import ( Bool, Callable, Directory, Event, HasStrictTraits, Instance, Int, List, Unicode, Vetoable) from traits.etsconfig.api import ETSConfig # Local imports from envisage._compat import pickle, STRING_BASE_CLASS # Logging. logger = logging.getLogger(__name__) #: Default filename for saving layout information DEFAULT_STATE_FILENAME = "application_memento" class TasksApplication(Application): """The entry point for an Envisage Tasks application. This class handles the common case for Tasks applications and is intended to be subclassed to modify its start/stop behavior, etc. """ # Extension point IDs. TASK_FACTORIES = 'envisage.ui.tasks.tasks' TASK_EXTENSIONS = 'envisage.ui.tasks.task_extensions' # Pickle protocol to use for persisting layout information. Subclasses may # want to increase this, depending on their compatibility needs. Protocol # version 2 is safe for Python >= 2.3. Protocol version 4 is safe for # Python >= 3.4. layout_save_protocol = Int(2) #### 'TasksApplication' interface ######################################### # The active task window (the last one to get focus). active_window = Instance('envisage.ui.tasks.task_window.TaskWindow') # The Pyface GUI for the application. gui = Instance('pyface.gui.GUI') # Icon for the whole application. Will be used to override all taskWindows # icons to have the same. icon = Instance('pyface.image_resource.ImageResource', allow_none=True) # The name of the application (also used on window title bars). name = Unicode # The splash screen for the application. By default, there is no splash # screen. splash_screen = Instance('pyface.splash_screen.SplashScreen') # The directory on the local file system used to persist window layout # information. state_location = Directory # The filename that the application uses to persist window layout # information. state_filename = Unicode(DEFAULT_STATE_FILENAME) # Contributed task factories. This attribute is primarily for run-time # inspection; to instantiate a task, use the 'create_task' method. task_factories = ExtensionPoint(id=TASK_FACTORIES) # Contributed task extensions. task_extensions = ExtensionPoint(id=TASK_EXTENSIONS) # The list of task windows created by the application. windows = List(Instance('envisage.ui.tasks.task_window.TaskWindow')) # The factory for creating task windows. window_factory = Callable #### Application layout ################################################### # The default layout for the application. If not specified, a single window # will be created with the first available task factory. default_layout = List( Instance('pyface.tasks.task_window_layout.TaskWindowLayout')) # Whether to always apply the default *application level* layout when the # application is started. Even if this is True, the layout state of # individual tasks will be restored. always_use_default_layout = Bool(False) #### Application lifecycle events ######################################### # Fired after the initial windows have been created and the GUI event loop # has been started. application_initialized = Event # Fired immediately before the extant windows are destroyed and the GUI # event loop is terminated. application_exiting = Event # Fired when a task window has been created. window_created = Event( Instance('envisage.ui.tasks.task_window_event.TaskWindowEvent')) # Fired when a task window is opening. window_opening = Event( Instance( 'envisage.ui.tasks.task_window_event.VetoableTaskWindowEvent')) # Fired when a task window has been opened. window_opened = Event( Instance('envisage.ui.tasks.task_window_event.TaskWindowEvent')) # Fired when a task window is closing. window_closing = Event( Instance( 'envisage.ui.tasks.task_window_event.VetoableTaskWindowEvent')) # Fired when a task window has been closed. window_closed = Event( Instance('envisage.ui.tasks.task_window_event.TaskWindowEvent')) #### Protected interface ################################################## # An 'explicit' exit is when the the 'exit' method is called. # An 'implicit' exit is when the user closes the last open window. _explicit_exit = Bool(False) # Application state. _state = Instance( 'envisage.ui.tasks.tasks_application.TasksApplicationState') ########################################################################### # 'IApplication' interface. ########################################################################### def run(self): """ Run the application. Returns ------- bool Whether the application started successfully (i.e., without a veto). """ # Make sure the GUI has been created (so that, if required, the splash # screen is shown). gui = self.gui started = self.start() if started: # Create windows from the default or saved application layout. self._create_windows() # Start the GUI event loop. gui.set_trait_later(self, 'application_initialized', self) gui.start_event_loop() return started ########################################################################### # 'TasksApplication' interface. ########################################################################### def create_task(self, id): """ Creates the Task with the specified ID. Returns ------- pyface.tasks.task.Task The new Task, or None if there is not a suitable TaskFactory. """ # Get the factory for the task. factory = self._get_task_factory(id) if factory is None: return None # Create the task using suitable task extensions. extensions = [ext for ext in self.task_extensions if ext.task_id == id or not ext.task_id] task = factory.create_with_extensions(extensions) task.id = factory.id return task def create_window(self, layout=None, restore=True, **traits): """Creates a new TaskWindow, possibly with some Tasks. Parameters ---------- layout : TaskWindowLayout, optional The layout to use for the window. The tasks described in the layout will be created and added to the window automatically. If not specified, the window will contain no tasks. restore : bool, optional (default True) If set, the application will restore old size and positions for the window and its panes, if possible. If a layout is not provided, this parameter has no effect. **traits : dict, optional Additional parameters to pass to ``window_factory()`` when creating the TaskWindow. Returns ------- envisage.ui.tasks.task_window.TaskWindow The new TaskWindow. """ from .task_window_event import TaskWindowEvent from pyface.tasks.task_window_layout import TaskWindowLayout window = self.window_factory(application=self, **traits) # Listen for the window events. window.on_trait_change(self._on_window_activated, 'activated') window.on_trait_change(self._on_window_opening, 'opening') window.on_trait_change(self._on_window_opened, 'opened') window.on_trait_change(self._on_window_closing, 'closing') window.on_trait_change(self._on_window_closed, 'closed') # Event notification. self.window_created = TaskWindowEvent(window=window) if layout: # Create and add tasks. for task_id in layout.get_tasks(): task = self.create_task(task_id) if task: window.add_task(task) else: logger.error( 'Missing factory for task with ID %r', task_id) # Apply a suitable layout. if restore: layout = self._restore_layout_from_state(layout) else: # Create an empty layout to set default size and position only layout = TaskWindowLayout() window.set_window_layout(layout) return window def exit(self, force=False): """Exits the application, closing all open task windows. Each window is sent a veto-able closing event. If any window vetoes the close request, no window will be closed. Otherwise, all windows will be closed and the GUI event loop will terminate. This method is not called when the user clicks the close button on a window or otherwise closes a window through his or her window manager. It is only called via the File->Exit menu item. It can also, of course, be called programatically. Parameters ---------- force : bool, optional (default False) If set, windows will receive no closing events and will be destroyed unconditionally. This can be useful for reliably tearing down regression tests, but should be used with caution. Returns ------- bool A boolean indicating whether the application exited. """ self._explicit_exit = True try: if not force: for window in reversed(self.windows): window.closing = event = Vetoable() if event.veto: return False self._prepare_exit() for window in reversed(self.windows): window.destroy() window.closed = True finally: self._explicit_exit = False return True ########################################################################### # Protected interface. ########################################################################### def _create_windows(self): """ Called at startup to create TaskWindows from the default or saved application layout. """ # Build a list of TaskWindowLayouts. self._load_state() if (self.always_use_default_layout or not self._state.previous_window_layouts): window_layouts = self.default_layout else: # Choose the stored TaskWindowLayouts, but only if all the task IDs # are still valid. window_layouts = self._state.previous_window_layouts for layout in window_layouts: for task_id in layout.get_tasks(): if not self._get_task_factory(task_id): logger.warning('Saved application layout references ' 'non-existent task %r. Falling back to ' 'default application layout.' % task_id) window_layouts = self.default_layout break else: continue break # Create a TaskWindow for each TaskWindowLayout. for window_layout in window_layouts: if self.always_use_default_layout: window = self.create_window(window_layout, restore=False) else: window = self.create_window(window_layout, restore=True) window.open() def _get_task_factory(self, id): """ Returns the TaskFactory with the specified ID, or None. """ for factory in self.task_factories: if factory.id == id: return factory return None def _prepare_exit(self): """ Called immediately before the extant windows are destroyed and the GUI event loop is terminated. """ self.application_exiting = self self._save_state() def _load_state(self): """ Loads saved application state, if possible. """ state = TasksApplicationState() filename = os.path.join(self.state_location, self.state_filename) if os.path.exists(filename): # Attempt to unpickle the saved application state. logger.debug('Loading application state from %s', filename) try: with open(filename, 'rb') as f: restored_state = pickle.load(f) except Exception: # If anything goes wrong, log the error and continue. logger.exception('Error while restoring application state') else: if state.version == restored_state.version: state = restored_state logger.debug('Application state successfully restored') else: logger.warning( 'Discarding outdated application state: ' 'expected version %s, got version %s', state.version, restored_state.version) else: logger.debug("No saved application state found at %s", filename) self._state = state def _restore_layout_from_state(self, layout): """ Restores an equivalent layout from saved application state. """ # First, see if a window layout matches exactly. match = self._state.get_equivalent_window_layout(layout) if match: # The active task is not part of the equivalency relation, so we # ensure that it is correct. match.active_task = layout.get_active_task() layout = match # If that fails, at least try to restore the layout of # individual tasks. else: layout = layout.clone_traits() for i, item in enumerate(layout.items): id = item if isinstance(item, STRING_BASE_CLASS) else item.id match = self._state.get_task_layout(id) if match: layout.items[i] = match return layout def _save_state(self): """ Saves the application state. """ # Grab the current window layouts. window_layouts = [w.get_window_layout() for w in self.windows] self._state.previous_window_layouts = window_layouts # Attempt to pickle the application state. filename = os.path.join(self.state_location, self.state_filename) logger.debug('Saving application state to %s', filename) try: with open(filename, 'wb') as f: pickle.dump(self._state, f, protocol=self.layout_save_protocol) except Exception: # If anything goes wrong, log the error and continue. logger.exception('Error while saving application state') else: logger.debug('Application state successfully saved') #### Trait initializers ################################################### def _window_factory_default(self): from envisage.ui.tasks.task_window import TaskWindow return TaskWindow def _default_layout_default(self): from pyface.tasks.task_window_layout import TaskWindowLayout window_layout = TaskWindowLayout() if self.task_factories: window_layout.items = [self.task_factories[0].id] return [window_layout] def _gui_default(self): from pyface.gui import GUI return GUI(splash_screen=self.splash_screen) def _state_location_default(self): state_location = os.path.join(ETSConfig.application_home, 'tasks', ETSConfig.toolkit) if not os.path.exists(state_location): os.makedirs(state_location) logger.debug('Tasks state location is %s', state_location) return state_location #### Trait change handlers ################################################ def _on_window_activated(self, window, trait_name, event): self.active_window = window def _on_window_opening(self, window, trait_name, event): from .task_window_event import VetoableTaskWindowEvent # Event notification. self.window_opening = window_event = VetoableTaskWindowEvent( window=window) if window_event.veto: event.veto = True def _on_window_opened(self, window, trait_name, event): from .task_window_event import TaskWindowEvent self.windows.append(window) # Event notification. self.window_opened = TaskWindowEvent(window=window) def _on_window_closing(self, window, trait_name, event): from .task_window_event import VetoableTaskWindowEvent # Event notification. self.window_closing = window_event = VetoableTaskWindowEvent( window=window) if window_event.veto: event.veto = True else: # Store the layout of the window. window_layout = window.get_window_layout() self._state.push_window_layout(window_layout) # If we're exiting implicitly and this is the last window, save # state, because we won't get another chance. if len(self.windows) == 1 and not self._explicit_exit: self._prepare_exit() def _on_window_closed(self, window, trait_name, event): from .task_window_event import TaskWindowEvent self.windows.remove(window) # Event notification. self.window_closed = TaskWindowEvent(window=window) # Was this the last window? if len(self.windows) == 0: self.stop() class TasksApplicationState(HasStrictTraits): """ A class used internally by TasksApplication for saving and restoring application state. """ # TaskWindowLayouts for the windows extant at application # exit. Only used if 'always_use_default_layout' is disabled. previous_window_layouts = List( Instance('pyface.tasks.task_window_layout.TaskWindowLayout')) # A list of TaskWindowLayouts accumulated throughout the application's # lifecycle. window_layouts = List( Instance('pyface.tasks.task_window_layout.TaskWindowLayout')) # The "version" for the state data. This should be incremented whenever a # backwards incompatible change is made to this class or any of the layout # classes. This ensures that loading application state is always safe. version = Int(1) def get_equivalent_window_layout(self, window_layout): """ Gets an equivalent TaskWindowLayout, if there is one. """ for layout in self.window_layouts: if layout.is_equivalent_to(window_layout): return layout return None def get_task_layout(self, task_id): """ Gets a TaskLayout with the specified ID, there is one. """ for window_layout in self.window_layouts: for layout in window_layout.items: if layout.id == task_id: return layout return None def push_window_layout(self, window_layout): """ Merge a TaskWindowLayout into the accumulated list. """ self.window_layouts = [layout for layout in self.window_layouts if not layout.is_equivalent_to(window_layout)] self.window_layouts.insert(0, window_layout) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/tasks_plugin.py0000644000076500000240000001367200000000000023240 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! # Enthought library imports. from envisage.api import ExtensionPoint, Plugin, ServiceOffer from traits.api import Callable, Instance, List # Local imports. from .preferences_category import PreferencesCategory # Constants. PKG = '.'.join(__name__.split('.')[:-1]) class TasksPlugin(Plugin): """ The Envisage Tasks plugin. The Tasks plugin uses Pyface Tasks to provide an extensible framework for building user interfaces. For more information, see the Tasks User Manual. """ # The IDs of the extension point that this plugin offers. PREFERENCES_CATEGORIES = PKG + '.preferences_categories' PREFERENCES_PANES = PKG + '.preferences_panes' TASKS = PKG + '.tasks' TASK_EXTENSIONS = PKG + '.task_extensions' # The IDs of the extension points that this plugin contributes to. SERVICE_OFFERS = 'envisage.service_offers' #### 'IPlugin' interface ################################################## #: The plugin's unique identifier. id = 'envisage.ui.tasks' #: The plugin's name (suitable for displaying to the user). name = 'Tasks' #### Extension points offered by this plugin ############################## #: Contributed preference categories. Contributions to this extension #: point must have type ``PreferencesCategory``. Preference categories #: will be created automatically if necessary; this extension point is #: useful when ensuring that a category is inserted at a specific location. preferences_categories = ExtensionPoint( List(PreferencesCategory), id=PREFERENCES_CATEGORIES, desc=""" This extension point makes preference categories available to the application. Note that preference categories will be created automatically if necessary; this extension point is useful when one wants to ensure that a category is inserted at a specific location. """) #: Contributed preference pane factories. Each contribution to this #: extension point must be a callable with the signature #: ``callable(**traits) -> PreferencePane``. preferences_panes = ExtensionPoint( List(Callable), id=PREFERENCES_PANES, desc=""" A preferences pane appears in the preferences dialog to allow the user manipulate certain preference values. Each contribution to this extension point must be a factory that creates a preferences pane, where 'factory' means any callable with the following signature:: callable(**traits) -> PreferencesPane The easiest way to contribute such a factory is to create a class that derives from 'envisage.ui.tasks.api.PreferencesPane'. """) #: Contributed task factories. Contributions to this extension point #: must have type ``TaskFactory``. tasks = ExtensionPoint( List(Instance('envisage.ui.tasks.task_factory.TaskFactory')), id=TASKS, desc=""" This extension point makes tasks avaiable to the application. Each contribution to the extension point must be an instance of 'envisage.tasks.api.TaskFactory. """) #: Contributed task extensions. Contributions to this extension point #: must have type ``TaskExtension``. task_extensions = ExtensionPoint( List(Instance('envisage.ui.tasks.task_extension.TaskExtension')), id=TASK_EXTENSIONS, desc=""" This extension point permits the contribution of new actions and panes to existing tasks (without creating a new task). Each contribution to the extension point must be an instance of 'envisage.tasks.api.TaskExtension'. """) #### Contributions to extension points made by this plugin ################ my_service_offers = List(contributes_to=SERVICE_OFFERS) def _my_service_offers_default(self): preferences_dialog_service_offer = ServiceOffer( protocol='envisage.ui.tasks.preferences_dialog.PreferencesDialog', factory=self._create_preferences_dialog_service) return [preferences_dialog_service_offer] my_task_extensions = List(contributes_to=TASK_EXTENSIONS) def _my_task_extensions_default(self): from .action.exit_action import ExitAction from .action.preferences_action import PreferencesGroup from .task_extension import TaskExtension from pyface.tasks.action.api import DockPaneToggleGroup, SchemaAddition actions = [SchemaAddition(id='Exit', factory=ExitAction, path='MenuBar/File'), SchemaAddition(id='Preferences', factory=PreferencesGroup, path='MenuBar/Edit'), SchemaAddition(id='DockPaneToggleGroup', factory=DockPaneToggleGroup, path='MenuBar/View')] return [TaskExtension(actions=actions)] ########################################################################### # Private interface. ########################################################################### def _create_preferences_dialog_service(self): """ Factory method for preferences dialog service. """ from .preferences_dialog import PreferencesDialog dialog = PreferencesDialog(application=self.application) dialog.trait_set(categories=self.preferences_categories, panes=[factory(dialog=dialog) for factory in self.preferences_panes]) return dialog ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.291224 envisage-4.9.0/envisage/ui/tasks/tests/0000755000076500000240000000000000000000000021314 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/tests/__init__.py0000644000076500000240000000000000000000000023413 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.291911 envisage-4.9.0/envisage/ui/tasks/tests/data/0000755000076500000240000000000000000000000022225 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/tests/data/application_memento_v2.pkl0000644000076500000240000000105600000000000027375 0ustar00mdickinsonstaff00000000000000cenvisage.ui.tasks.tasks_application TasksApplicationState q)q}q(Xprevious_window_layoutsqctraits.trait_handlers TraitListObject q)qcpyface.tasks.task_window_layout TaskWindowLayout q)q}q(X active_taskq Xq Xitemsq h)q }q (Xnameqh X name_itemsqX items_itemsqubXpositionqJJqXsizeqMMqX size_stateqXnormalqX__traits_version__qX5.1.2quba}q(hhhXprevious_window_layouts_itemsqubXwindow_layoutsqh)q}q(hhhXwindow_layouts_itemsqubXversionqKhhub.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/tests/data/application_memento_v3.pkl0000644000076500000240000000105600000000000027376 0ustar00mdickinsonstaff00000000000000cenvisage.ui.tasks.tasks_application TasksApplicationState q)q}q(Xprevious_window_layoutsqctraits.trait_handlers TraitListObject q)qcpyface.tasks.task_window_layout TaskWindowLayout q)q}q(X active_taskq Xq Xitemsq h)q }q (Xnameqh X name_itemsqX items_itemsqubXpositionqJJqXsizeqMMqX size_stateqXnormalqX__traits_version__qX5.1.2quba}q(hhhXprevious_window_layouts_itemsqubXwindow_layoutsqh)q}q(hhhXwindow_layouts_itemsqubXversionqKhhub.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/tasks/tests/test_tasks_application.py0000644000076500000240000001052200000000000026435 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! import os import shutil import tempfile import unittest import pkg_resources from six.moves import cPickle as pickle from envisage.ui.tasks.api import TasksApplication from envisage.ui.tasks.tasks_application import DEFAULT_STATE_FILENAME requires_gui = unittest.skipIf( os.environ.get("ETS_TOOLKIT", "none") in {"null", "none"}, "Test requires a non-null GUI backend", ) @requires_gui class TestTasksApplication(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.tmpdir) def test_layout_save_uses_protocol_2(self): # We use pickle protocol 2 by default, to allow compatibility # across Python versions. state_location = self.tmpdir # Create application, and set it up to exit as soon as it's launched. app = TasksApplication(state_location=state_location) app.on_trait_change(app.exit, "application_initialized") memento_file = os.path.join(state_location, app.state_filename) self.assertFalse(os.path.exists(memento_file)) app.run() self.assertTrue(os.path.exists(memento_file)) # Check that the generated file has protocol 2. with open(memento_file, "rb") as f: protocol_bytes = f.read(2) self.assertEqual(protocol_bytes, b"\x80\x02") @unittest.skipUnless( 3 <= pickle.HIGHEST_PROTOCOL, "Test uses pickle protocol 3") def test_layout_save_with_protocol_3(self): # Test that the protocol can be overridden on a per-application basis. state_location = self.tmpdir # Create application, and set it up to exit as soon as it's launched. app = TasksApplication( state_location=state_location, layout_save_protocol=3, ) app.on_trait_change(app.exit, "application_initialized") memento_file = os.path.join(state_location, app.state_filename) self.assertFalse(os.path.exists(memento_file)) app.run() self.assertTrue(os.path.exists(memento_file)) # Check that the generated file uses protocol 3. with open(memento_file, "rb") as f: protocol_bytes = f.read(2) self.assertEqual(protocol_bytes, b"\x80\x03") def test_layout_load(self): # Check we can load a previously-created state. That previous state # has an main window size of (492, 743) (to allow us to check that # we're actually using the file). stored_state_location = pkg_resources.resource_filename( "envisage.ui.tasks.tests", "data") state_location = self.tmpdir shutil.copyfile( os.path.join(stored_state_location, "application_memento_v2.pkl"), os.path.join(state_location, DEFAULT_STATE_FILENAME), ) app = TasksApplication(state_location=state_location) app.on_trait_change(app.exit, "application_initialized") app.run() state = app._state self.assertEqual(state.previous_window_layouts[0].size, (492, 743)) @unittest.skipUnless( 3 <= pickle.HIGHEST_PROTOCOL, "Test uses pickle protocol 3") def test_layout_load_pickle_protocol_3(self): # Same as the above test, but using a state stored with pickle # protocol 3. stored_state_location = pkg_resources.resource_filename( "envisage.ui.tasks.tests", "data") state_location = self.tmpdir shutil.copyfile( os.path.join(stored_state_location, "application_memento_v3.pkl"), os.path.join(state_location, "fancy_state.pkl"), ) # Use a non-standard filename, to exercise that machinery. app = TasksApplication( state_location=state_location, state_filename="fancy_state.pkl", ) app.on_trait_change(app.exit, "application_initialized") app.run() state = app._state self.assertEqual(state.previous_window_layouts[0].size, (492, 743)) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2965484 envisage-4.9.0/envisage/ui/workbench/0000755000076500000240000000000000000000000021007 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/__init__.py0000644000076500000240000000000000000000000023106 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.298293 envisage-4.9.0/envisage/ui/workbench/action/0000755000076500000240000000000000000000000022264 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/action/__init__.py0000644000076500000240000000000000000000000024363 0ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/action/about_action.py0000644000076500000240000000256200000000000025312 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that shows the 'About' dialog. """ # Enthought library imports. from pyface.action.api import Action class AboutAction(Action): """ An action that shows the 'About' dialog. """ #### 'Action' interface ################################################### # A longer description of the action. description = 'Display information about the application' # The action's name (displayed on menus/tool bar tools etc). name = 'About' # A short description of the action used for tooltip text etc. tooltip = 'Display information about the application' ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Perform the action. """ self.window.application.about() return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/action/api.py0000644000076500000240000000103300000000000023404 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! from .about_action import AboutAction from .edit_preferences_action import EditPreferencesAction from .exit_action import ExitAction ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/action/edit_preferences_action.py0000644000076500000240000000363500000000000027510 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that displays the preferences dialog. """ # Enthought library imports. from pyface.api import ImageResource from pyface.action.api import Action class EditPreferencesAction(Action): """ An action that displays the preferences dialog. """ #### 'Action' interface ################################################### # A longer description of the action. description = 'Manage Preferences' # The action's image (displayed on tool bar tools etc). image = ImageResource('preferences') # The action's name (displayed on menus/tool bar tools etc). name = 'Preferences' # A short description of the action used for tooltip text etc. tooltip = 'Manage Preferences' ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Performs the action. """ from apptools.preferences.ui.api import PreferencesManager # Lookup the preferences manager service. manager = event.window.application.get_service(PreferencesManager) ui = manager.edit_traits(parent=event.window.control, kind='modal') # If the user hit the "Ok" button, then save the preferences in case # application crashes before it exits! if ui.result: self.window.application.preferences.save() return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/action/exit_action.py0000644000076500000240000000267700000000000025160 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action that exits the workbench. """ # Enthought library imports. from pyface.api import ImageResource from pyface.action.api import Action class ExitAction(Action): """ An action that exits the workbench. """ #### 'Action' interface ################################################### # A longer description of the action. description = 'Exit the application' # The action's image (displayed on tool bar tools etc). image = ImageResource('exit') # The action's name (displayed on menus/tool bar tools etc). name = 'Exit' # A short description of the action used for tooltip text etc. tooltip = 'Exit the application' ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Perform the action. """ self.window.application.exit() return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2994506 envisage-4.9.0/envisage/ui/workbench/action/images/0000755000076500000240000000000000000000000023531 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/action/images/exit.png0000644000076500000240000000155400000000000025215 0ustar00mdickinsonstaff00000000000000PNG  IHDRa3IDAT8m_L[`RBKmˈs,721LpaJF4Abq^M6$ȦƘm:-0aȟB7HKmm)9NDxJkygy!ycfw*=k@zÝO_}gv~# 'o7ffy_c?إl+c-"7f.[?]_0G=3pB{ $B<^xIRs1,'N9e,'H M À@]Ef;EOkg_=<ټ̐zSs +%Ȥ!fU Zi1b*JKzS-"  b#Ɲ T._|^~ ri6ҜrlLv %2ʛA D= CdsTC*"5ɄbE hkFx)I9Pwo誂MQ m;@^/1sф6fn.l١AW(} ܭ54Qx%:53eADx_1*eI_1r9I!Cۥ璻NU4g5 ŪcӺ 柯cuTƠtHwWF-aC YN(q51n- 8>?D&_;fn;4`55fIf˯m=|`/7(9q*{.{p4ߍf3?Z`p]IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/action/images/image_LICENSE.txt0000644000076500000240000000067200000000000026523 0ustar00mdickinsonstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: CP (Crystal Project Icons): LGPL license as described in image_LICENSE_CP.txt Unless stated in this file, icons are work of enthought, and are released under BSD-like license. Files and orginal authors: -------------------------------------------------------------------- exit.png CP preferences.png CP ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/action/images/preferences.png0000644000076500000240000000150600000000000026542 0ustar00mdickinsonstaff00000000000000PNG  IHDRa IDAT8uSMle}M/^gel)RDOR/(M`HЖ8Pz(BJF*@8Eij5v&'GF{yf@D;FE- nb~~޿~Xt;N/!˲0=="#"@,,x_j'GF"BW^?5ð^=r/J}:595 I+vFNTUU*W*T3 2- Z^^ζ$Z:7Ǿ'ǁ (O(R4m`- VΙ3#Qw" ^N?666P7ûgϾ}sd*lmӃkT(黥%Ep xsU?P,Q:iUOԉ/\;Cö۷nc\^?hDi?ᢟ;NLطprDSFNz@T 8F ib1>Q|$<|D"qc^cM+|zʴ(_(/FJ=ޤͿOwe_6|pve0 K%(yBZSvݿgY$IAપ cvTU8>>dY,Nd>U(eIY? q!ݗ}sZ2S.{pmp'E"H$q\o;? /q IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/api.py0000644000076500000240000000135200000000000022133 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Envisage package Copyright 2003, 2004, 2005 Enthought, Inc. """ from .workbench import Workbench from .workbench_action_set import WorkbenchActionSet from .workbench_application import WorkbenchApplication from .workbench_window import WorkbenchWindow #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/default_action_set.py0000644000076500000240000000325600000000000025223 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The default workbench action set. """ # Enthought library imports. from envisage.ui.action.api import Action, ActionSet, Menu # This module's package. PKG = '.'.join(__name__.split('.')[:-1]) class DefaultActionSet(ActionSet): """ The default workbench action set. """ menus = [ Menu( name='&File', path='MenuBar', groups=['OpenGroup', 'SaveGroup', 'ImportGroup', 'ExitGroup'] ), Menu( path='MenuBar', class_name='pyface.workbench.action.api:ViewMenuManager' ), Menu( name='&Tools', path='MenuBar', groups=['PreferencesGroup'] ), Menu( name='&Help', path='MenuBar', groups=['AboutGroup'] ) ] actions = [ Action( path='MenuBar/File', group='ExitGroup', class_name=PKG + '.action.api:ExitAction' ), Action( path='MenuBar/Tools', group='PreferencesGroup', class_name=PKG + '.action.api:EditPreferencesAction' ), Action( path='MenuBar/Help', group='AboutGroup', class_name=PKG + '.action.api:AboutAction' ), ] #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1574158504.300682 envisage-4.9.0/envisage/ui/workbench/images/0000755000076500000240000000000000000000000022254 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/images/about.png0000644000076500000240000002106600000000000024101 0ustar00mdickinsonstaff00000000000000PNG  IHDRP\m:sBIT|dtEXtSoftwarewww.inkscape.org< IDATxyxU'OIMtoi)dw6·~:#^|μ.̫36ਠ((PZR( M,mf_n.K4<虿f*++vܙUSS#p\dg2rYa^je'.coW c?򫟗: ΄ HUs'5AwrXR l:Z ǒqyvM[SǸaP1XԜ@HP?.ϯifˍcܰLu@}r:$VKtӉfffb;r9C&9d1_8A (M^줫jzbb355}|ZMS4CxL;ZP~3֏UrxY@5Qav<_kOZ,ɓJQOO 5srr7nh!̽{&^tI۶mx=V*,M6X>A EjkkO?l[ְ!aRVV&lllj4^1|߮l\SS*a}L&III+ ao>;~|8h4T*,RɪmٲeHNcĩT*r֮]\~}O{8N:t(`0P%$C,;v;YV3V+nᇔQ"D<[LP+mZҦ8;vH|@,;֭[*,, ^R̎oꫯ6Df GiiTs)] {ymhh544!Ν7v;zYAyy?H6lؠx?8*@ y䑶DǃpvޝfZ)Lf}:nٲe mp[|tGiiiJAǏ'I֮]a2xNj/ܩ) Q\\l۷ou:ŋ555BFH)L&ׯq8M---\EJ E,`|l޼yAyyxB5B233ͯJ@sM70RGGqسgOo&##QZZ޻wo2;hA *jjL.\\J8L %l/dNVTl6۳hѢpZdL&̋/FAf^6h#٘P.찻GWw]M8peJ%##Á(_wuu1rssݮ"ɬ׮];;;ֻ$pfj6MKKp|MѴFyVkT xsTyl5isd21B0 nO4PԐZ:g \.7p^ww7=ܹŘ\mjPs;zBכԀ}?輠؝c#l&lj v`Ͱ`&bh4NK V Hhth-&U(}|Ȏy!^; .,|nWVzh%o`Xn( #B 'u@64DHsF.|p5J!Avpmc>XMHj Ezq# IkfsdP89IoQ ȑ+ {ʼ8qc F0^ ZH"r/v/$E"шѮT˵׿ߨF(LP|<3FC V)1].״3ϧ|']]],3x^$xݒ2ތ:ۋOhtI=nɓ'^w 0d-Dp0PXXhH$v˗/s3D"q̙3bDIe_ hXܨ/ ZsD%%%ځѺ)))ȉ'M:@X r^vm 6СCVuTߊn:dSO&}k.[4BJAyy``DnGmۖe2٣tPٳ纺:]C)5557|3WRdʢVnV.))1Zt:?L8p8~iJcc# ##RRRb EĦD(S Ƴ1"I'[*u {aj>DT&z=/L߿T*;Z-].sL&۴iSH$dZj^./\ 8uT¥KV@6B2 bp8+%oߞ3gCAA:#6D^xyǎi׮]kjj36Jhnnj&/󆌀]kٜش}j p; V03GlrܲeKÇ~EBйyT:}N@.++`FlXTj+..mll455ʨU^9sL`%%F-4 ߼ys7|TQQ`0hUUU(_*]fz\$]]"d(SbpGhƌF@VBAohh`i4j%d2[AA599yX3&L&6lؠ^b8L&swyZ18<E~Xvڞ&VOOrt:= P͛77p8aC֭[׽xb=@RRRD"WmذAhnnfuwwӓ9996L7-\WRCqvM צX@)]:pע.6#:Pz]a#QÑD! =7|s6bBгhѢz>|%ԱP8SRR"|(YYYeǻ#Fr`"V4[5i[2gnxx((>]A{(pހ/g^j(-+feηS3s5S>PQ2Db_ V tq˵ r|_ `3O+Ϗ*"Q J$ $-Cs(Bz3_0%IqYib;.5:&j'_?p' ^' P,dj4_u*ϼ`K1"G0& 9 3$Y8 Ȏv+oȀ&O`Bǃ=*1Tᄏ+33sRbn"%t uzcV^0O z&HpƠ*/[ɾܘ">}:T*jvXiin |""Vtʨ?_"_7?÷.0Z(:AUC8 E4?EQB(:zd‘db߾})pbؑg^~z}6F" Βu3Qg*XS201$L&[nmiiaVVq`(rEQ"==ݖI94w@*d9[_ਟ>#)wy礇bD(Jjʽ 2i+EbL]6* v"AU".ꙗ`#T;'ⰋS/q.HxZMN@Lc2jdȷX@RE7N[&H eZGƀ=3bM3a7(:8/!*lN5\IE0n7$ ȺQm_FFgӯG?9ަ!~ O `~&3&>Ѥ̱+٧9EG>>ͷYɯ߫& 1ËU #^=+TqM;zU1b▌z5\iᨴn*sgY4J4fvVM00+67/& rZ.W) BW̌tj෷.9qk[+M]̻e1{MVZuMήoS3R)tE`^+W*j'߶а~yum]^uM`^ Cjj؎|m+`TZPnft)VzȎi5izּs|T` HKwyc8B{xM@q3&\B\5>]񽯳  ܻzE*ɲ\sp|wL$!^z5>vB=Yї?KZY[~{26o:AvmC 6ֳ۾̲]dW.3oT¦SO㲆]IvVZ-̿vUCgEۿ: p™sMyDXNyv~7s_O5"ý+;W̝a¼^:O%}ii]% ɼifBìma^Vc9WC H^4687CFkĻevx%/WJ^y劧67u™&Whz_tN}:sSeBFz0+91RzғDΗmfյVe^`X ߸y"W^ma/O5'JG(r'yKK+eaᄣMEڢ^?-(0;_' ؝/۲j`<}j%;:NQ8`F2}uCY#tTGfخ\mb\.5vз-*4VjC@B()4ү@tY/HYޝ(9_w+<噓+=RyUX9oFA0?# Х1} x 1"Qy3=0/t:sE+t9_;9 t"'L 7'bš j:#=Y II kUhE9vf&̴kr%ïKp]I%E9o.8ށH`uH5- pZGCgN\j \_hmeAW.W'AA@|G:#y_iTc3TCj(L3Tlf[+IԦF8ΐ'(Q N!<{}fߢ\#@BRi{)~iⱙ4laaz`k+| 7''lJk,.U"t8ORӷPSgwLHG@/d[E:Ne:ypgoR@c4 I[Jf?K Sp[V^M4ڿW;e霜~2.wVR#]';z 8\گv)Ll6 Eh3V("Wб O5jqh93W楙m) 6Ξ'8|Fpy=C1D|66#-D뷶[\ zAAs2!|W~qD(E:rɬf}ť7)ZxHʐc8L2|NTfG*&W+/_>> g:w.U؅AUQv|[1il& {!7,+jdD mUdm" kOBFcB=jKJsW.E\˚W.kS &=x r߻zIzd,S^%0tF ec_|l .?R+/ n.J;ntR# M]<ѭW|]yv5u06[NbhUHg=|(-%ƏH(N5 +k|~E!dHO߻J#_JlaTess5rNUm+OoRH3}m_ntă߯&W:?+]R)tF+ͼVۋ8y4ghٲc}st wCq\MŞh}[}Ǒ0R0Ѝ!qX3 GǙ3>AlugCyNs">Ԇ{V tϊDoK);/{AzKff_A GW8iw LZSR`ۮCDX\hhw,Ѳ4ʥy!+I"nx~ u#Iue3h'~/K˫{=;F!9'~{Fz pne}k ,O4榻ΎzC h/`ICWI"Y  n2;UZH{s\?[jr3xZ;*Whh^3 C@6w1Xvjaog*ǹ0_cj19t2 )/D/| ^^1H| DwL dQ erCMxˌ<07b3 q Xmmrs:?s:e6b3mE3xU5tT%cDS,NJLKAt ~Cs: o=yH"Y3nYTGv>Dz>҇;΃6~l_ Z W)j'Y"[NJ~HyDs+]9o^ٚ[֗WѓR̎BÁd W4rQRZÏdÓc`Y$Yf۠e؞bԚ[ϔUɎMÇ1rNz? OO:sr͟}̤yƟs?mNlڢk֠dљ]˓TŋS.`s:*RC{s͟ϪɤeSCyl֟fқ]̔]Ƒ0cy=uP Oy!\7l:n(Z t@y>iG'[!V }At:2 L I' F6 {Bs:(, R*N(5J&>E# s:)s:dx=w<s:_s:#^15[0 Z2d?b@ O,E#I$Vx=M;oQN8kHv<s:g6i=ChGk7[4zVDa?|\u>@v_Ә;ǀ$p&p;{^Ƒ8jw<s: n;_{LNuNo;h7b3[/rn:g6h6)]Zؘifda _ \ Y XZQs:D {B6h,d H E zA t=m:vh٠lifda _ \ Y W Ubv< HU_ ] ZR F y@IiޢKԏKҍKό:ǀ#oa \ Y WT] w? JXE|CxAu?p;k(Z y@oܥYؘWՕUҒSΐOʌEń!j Y WTbu; LLXWURO|KwHgКbڝ_ך]ԗZЕV̐QȌLć:y_]Wv< M{6p^ǒiƗh”fb^0_qfX{6^!yLi<a3e<&mI?|]wANe͗jؠ'udb` _ ]]Fr̞=ot;s:a m:rBSzKt~I q<m9h7d4_2[/f4 yAnաTԓkfdb` _ ] [ Y'kjə:kt:s: s=L pq<l9h6c4c3s:Ne۟jhfdb` _ ] [ Y W,npțIs: x@ v?*[XN F ~C y@ u>p;l9g6p9$Ywߪ&xihfdb` _ ] [ Y W VQYs:s:= {BJ yAX.e J H E }C y@ t>p;k8t;OSؕkihfdb` _ ] [ Y W V]rȝ|Gs:t }C|N^S M J G E }B x@ t=p;s:eΙ=Ӈkihfdb` _ ] [ Y W VThŖ)Zs: E*^UQ O L J G E |B x?s<u=xݪ4с"u$t&u%shb` _ ] [ Y W VTT>nt: F:oN(f)e)c)a&\Q F |Bu=IyUؖQ֒PԐOяMύK͋:%oa ] [ Y W VTE}Lzs: H9pT>w=uLlޥaڜ`ך^ՙ]ӗZѕWΑTˎQȌNƉIÅD)l Yarȝ{Cs:n LE JX]ƑdŔdÒcb`^ZWNQxުg۠fٞdםbԚ_Ҙ[ДY͑UʏRNjMćJF>z`ÑPs:s:2N L)caʔk˛qʝqǜnęmjgdQ{u LF~cʖ|Ц~Υ|̣yȠwŝsok:gv=gЛqܥl٢kנg՝dҚ`ϖ]̔XɐTƌPÈYōq̞0ct:s: MSScʖ~ѧӮϫ̧ȣ{ğvqLFZĎuۧjנk֠hӜdЙ`͖[ʒVǍi˙oɚ/as:s:LO MM p< s:s:as:t;s:t;s:s:|s:C K I6 Gl F ~DV |B!s:(0` $s:s:js:s:s:s:s:s:ws:1V-T,cS+Q*O(M'K&I%G$E#\C"s:s:ls:s:s:s:s:s:s:s:s:s:s:s:s:\07Z/X.V-T,R+P)O(M'K&I%G$E#C"A ,s:Rs:s:s:v<']=rI}UL?s/c {Cs:s:s:s:vs:b3`2^1\0[0iB6|YDdMkRoLhC}_1nOW6G$E#D"e3s:s:FNrգwاwצvץvեuԤuӣuңsϡXPs:s:s:s:e5-c4a3_2$vMTuZzZyZxVtQoVsXsXrXqNg[:f3s:t:,ioעwۨpףN͍3z&ph!l-sC‚f̘tСrϟq<n:k8h7e5b3_2\0`1s:s:NĈx߫UՔlgfedca` _ ^ ] [ Z Y X9xs̞ax>s:s: r= p<tAT~[ U }B zA w? t=q<n:k8h6e5b3_1n8s:(eyiۢligfedca` _ ^ ] [ Z Y X WMs˞>ss:s:s: u? s> r==m]7h F D |B y@ w? s=p;m:j8g6d5g5s: {Alڢy+{jigfedca` _ ^ ] [ Z Y X W[jȘqɜPs:s: w@ u? S]RK G E D |B y@ v? s=p;m:j8g6o8s:G~yO֑kjigfedca` _ ^ ] [ Z Y X W U-msʝ]t;s:s: {B yA w@R^ [ J I G E D |B y@ v> s=p;m9j8s:u r<o;n9s:#XyUٖlkjigfedca` _ ^ ] [ Z Y X W UT3qrɝ=ms:s: ~D |C4g^-h O M L J H F E ~C {A x@ u> r<q:s:Bxy7҃lkjigfedca` _ ^ ] [ Z Y X W UT[rɝYs:s: E ~DGy^Y Q O M K J H F E ~C {A x@ u>s;s:Ny&xmp!s rnkfdca` _ ^ ] [ Z Y X W UTSkƘes:s: F EP]WWXYVSPK F D }C zA w?s;s:ZŏyMבM֐MԐMӏLҎKЌJϋF̈7~(td` _ ^ ] [ Z Y X W UTSaÑoĘs:s: G FYZ1m0l0j/i.f.e,c+a']TH }B zAt;s:\ȒzSؕRדPՒPԑPӐOюMύLΌJ̊IʉFȆ/vc ^ ] [ Z Y X W UTS_qŚt;s: H GXY:u9t9r9q7n7m5j4i3f1d/b$XGv=s:RyRؔUוT֔TԓRӒRґQЏPώN͍LˋJʉJȇDŃ%n ^ [ Z Y X W UTShŖis:s: I HP_Ðr=pl%Vs:/ey_ۜ\ٚ[ؙ[֘Z՗YԕWҔVВUϑS͏Q̍OʋMȉKƇHąD*o\ X W UT(irɝGws:s: K J2j`ȓOTTSRQPON}L{KyHvFt=ks: xAxުq`ڜ_ٛ_ך^֙]՗[ӖZѕWГVΑT͏RˍOɋNljKćH„F@}$j X UTHrɝ'Xs:s:^ L9 KT`ɔ\Ɛ]Đ]Ž[[[YXVUSR~O{My{Es:ZŎye۟cٝb؜`כ`֚^Ԙ]җ[ѕYϓW͑UˏRɍPNjNʼnKÆHƒFD:ydapɛkÖ v>s:s: M L JN`ɔ_ǓdǕdŔdÒdba`^\ZXU.]s:Pvvߩfڟfٟd؝c֛b՚aә^ї\ЕZΓX̑UʏSȍQƋMćKÅGFB~A|iȘsʝ-bs:s: M KY_˔aȔlʚnʛmȚlŘk×jifdba_W x@s:>|ynܤhڠh٠fםd֜cԛaҙ_З\ϔ[͓XˑUɎSnjPʼnMÇJGDYs˞Ut:s:s:8 N L K9v`ʕdʖv΢v͡u˟tȝsƜqĚo˜nkifd8fs:z@dԛx߫jڢkڡjؠg֞f՝dӛbљ_Ж]Δ[̒XʐTȍRƋOĈL†ILr˞nɚIs:s: M{ LMSċ`ʔl̛ѧ~Ϧ|ͥ{ˣyɠxǟvĝsšqnkfLs:Rvܩyݪmڣn٢kנi֞gԝeқbј_ϖ]͔ZʑWȏTƌQĉM‡^ǒr͟s̞3fs:s:s:N M L \^ɓ`ʔdʗҩҬЩͧ˥~ɣ|ƠxÞvroczCs:!Xlՠxݪtۦmءl֠i՞gӜdљbϗ_͕\˒YɐVǍWǍpΞuΠo˜2es:s:s:BN M LYQŠ`ʔaȔyϣ԰ҮЫͩ˧Ȥ}ơzÞviR| x@s: DONJxܩxڨj֟[ѕWΒcљdЙaΖ[˒\ʓ^ʔpϟtϡ\ÎMs:s:s:cN- M LN9v_ɔ`ȓ^ő^ÑfĔwɠΩzǡoj[][*[r:s:s:+cmҟw٧wاwצnԡkҝnӟsӢuңtѢrΟuFzK~Fx r= p< o;pm:s:4s:s:s:s:s:s:t;s:s:s:s:s:s:s:W M L J I H G F E ~D |C zA x@ v? t> r=s:s:ls:s:s:s:s:s:s:s:s:- K JA I H G F E }D |B zA9 x@s: s:??././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/images/image_LICENSE.txt0000644000076500000240000000047400000000000025246 0ustar00mdickinsonstaff00000000000000These are my best guesses at where each icon came from, this file should be updated whenever icons change. filename source -------------------------------------------------------------- about.png Python Software Foundation application.ico Gael Varoquaux ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/preferences.ini0000644000076500000240000000007000000000000024006 0ustar00mdickinsonstaff00000000000000[enthought.envisage.ui.workbench] prompt_on_exit = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/workbench.py0000644000076500000240000000420600000000000023345 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The Envisage workbench. """ # Enthought library imports. import pyface.workbench.api as pyface from envisage.api import IApplication from pyface.api import YES from traits.api import Delegate, Instance # Local imports. from .workbench_preferences import WorkbenchPreferences from .workbench_window import WorkbenchWindow class Workbench(pyface.Workbench): """ The Envisage workbench. There is (usually) exactly *one* workbench per application. The workbench can create any number of workbench windows. """ #### 'pyface.Workbench' interface ######################################### # The factory that is used to create workbench windows. window_factory = WorkbenchWindow #### 'Workbench' interface ################################################ # The application that the workbench is part of. application = Instance(IApplication) # Should the user be prompted before exiting the workbench? prompt_on_exit = Delegate('_preferences') #### Private interface #################################################### # The workbench preferences. _preferences = Instance(WorkbenchPreferences, ()) ########################################################################### # Private interface. ########################################################################### def _exiting_changed(self, event): """ Called when the workbench is exiting. """ if self.prompt_on_exit: answer = self.active_window.confirm( "Exit %s?" % self.active_window.title, "Confirm Exit" ) if answer != YES: event.veto = True return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/workbench_action_manager_builder.py0000644000076500000240000001507700000000000030112 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The action manager builder used to build the workbench menu/tool bars. """ # Standard library imports. import weakref # Enthought library imports. from envisage.ui.action.api import AbstractActionManagerBuilder from pyface.action.api import Action, Group, MenuManager from pyface.workbench.action.api import MenuBarManager from pyface.workbench.action.api import ToolBarManager from traits.api import Any, Instance class WorkbenchActionManagerBuilder(AbstractActionManagerBuilder): """ The action manager builder used to build the workbench menu/tool bars. """ #### 'WorkbenchActionManagerBuilder' interface ############################ # The workbench window that we build the menu and tool bars for. window = Instance('envisage.ui.workbench.api.WorkbenchWindow') #### Private interface #################################################### # All action implementations. _actions = Any ########################################################################### # Protected 'AbstractActionManagerBuilder' interface. ########################################################################### def _create_action(self, definition): """ Create an action implementation from an action definition. """ traits = {'window' : self.window} # Override any traits that can be set in the definition. if len(definition.name) > 0: traits['name'] = definition.name if len(definition.class_name) > 0: action = self._actions.get(definition.class_name) if action is None: klass = self._import_symbol(definition.class_name) action = klass(**traits) self._actions[definition.class_name] = action # fixme: Do we ever actually do this? It seems that in Envisage 3.x # we always specify an action class!?! else: action = Action(**traits) # fixme: We need to associate the action set with the action to # allow for dynamic enabling/disabling etc. This is a *very* hacky # way to do it! action._action_set_ = definition._action_set_ return action def _create_group(self, definition): """ Create a group implementation from a group definition. """ traits = {} # Override any traits that can be set in the definition. if len(definition.id) > 0: traits['id'] = definition.id if len(definition.class_name) > 0: klass = self._import_symbol(definition.class_name) else: klass = Group group = klass(**traits) # fixme: We need to associate the action set with the action to # allow for dynamic enabling/disabling etc. This is a *very* hacky # way to do it! group._action_set_ = definition._action_set_ return group def _create_menu_manager(self, definition): """ Create a menu manager implementation from a menu definition. """ # fixme: 'window' is not actually a trait on 'MenuManager'! We set # it here to allow the 'View' menu to be created. However, it seems # that menus and actions etc should *always* have a reference to # the window that they are in?!? traits = {'window' : self.window} # Override any traits that can be set in the definition. if len(definition.id) > 0: traits['id'] = definition.id if len(definition.name) > 0: traits['name'] = definition.name if len(definition.class_name) > 0: klass = self._import_symbol(definition.class_name) else: klass = MenuManager menu_manager = klass(**traits) # Add any groups to the menu. for group in definition.groups: group._action_set_ = definition._action_set_ menu_manager.insert(-1, self._create_group(group)) # fixme: We need to associate the action set with the action to # allow for dynamic enabling/disabling etc. This is a *very* hacky # way to do it! menu_manager._action_set_ = definition._action_set_ return menu_manager def _create_menu_bar_manager(self): """ Create a menu bar manager from the builder's action sets. """ return MenuBarManager(window=self.window) def _create_tool_bar_manager(self, definition): """ Create a tool bar manager implementation from a definition. """ traits = { 'window' : self.window, 'show_tool_names' : False } # Override any traits that can be set in the definition. if len(definition.id) > 0: traits['id'] = definition.id if len(definition.name) > 0: traits['name'] = definition.name if len(definition.class_name) > 0: klass = self._import_symbol(definition.class_name) else: klass = ToolBarManager # fixme: 'window' is not actually a trait on 'ToolBarManager'! We # set it here because it is set on the 'MenuManager'! However, it # seems that menus and actions etc should *always* have a reference # to the window that they are in?!? tool_bar_manager = klass(**traits) # Add any groups to the tool bar. for group in definition.groups: group._action_set_ = definition._action_set_ tool_bar_manager.insert(-1, self._create_group(group)) # fixme: We need to associate the action set with the action to # allow for dynamic enabling/disabling etc. This is a *very* hacky # way to do it! tool_bar_manager._action_set_ = definition._action_set_ return tool_bar_manager ########################################################################### # Private interface. ########################################################################### def __actions_default(self): """ Trait initializer. """ return weakref.WeakValueDictionary() def _import_symbol(self, symbol_path): """ Import a symbol. """ return self.window.application.import_symbol(symbol_path) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/workbench_action_set.py0000644000076500000240000001636400000000000025565 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An action set in a workbench window. """ # Enthought library imports. from envisage.ui.action.api import ActionSet from traits.api import Instance, List, Str class WorkbenchActionSet(ActionSet): """ An action set in a workbench window. This class adds a 'window' trait which is the workbench window that the action set is in. The trait is set by the framework when the action set is added to the window. It also adds a simple way for the action set to be enabled and/or visible in specific perspectives. """ ########################################################################### # 'WorkbenchActionSet' interface. ########################################################################### # It is common for an action set to be enabled and/or visible only in a # particular perspective (or group of perspectives). The following traits # allow you to say which by specifiying a list of the appropriate # perspective *Ids*. # # For finer control over the enablement/visibility simply override the # 'initialize' method. enabled_for_perspectives = List(Str) visible_for_perspectives = List(Str) # It is common for an action set to be enabled and/or visible only when # particular view (or group of views) is visible. The following traits # allow you to say which by specifiying a list of the appropriate view # *Ids*. # # For finer control over the enablement/visibility simply override the # 'initialize' method. enabled_for_views = List(Str) visible_for_views = List(Str) # The workbench window that the action set is in. # # The framework sets this trait when the action set is first added to a # window. window = Instance('envisage.ui.workbench.api.WorkbenchWindow') ########################################################################### # 'ActionSet' interface. ########################################################################### def _enabled_changed(self, trait_name, old, new): """ Static trait change handler. """ if self.window is not None: self._update_tool_bars(self.window, 'enabled', new) self._update_actions(self.window, 'enabled', new) return def _visible_changed(self, trait_name, old, new): """ Static trait change handler. """ if self.window is not None: self._update_tool_bars(self.window, 'visible', new) self._update_actions(self.window, 'visible', new) return ########################################################################### # 'WorkbenchActionSet' interface. ########################################################################### def initialize(self): """ Called when the action set has been added to a window. Use this method to hook up any listeners that you need to control the enabled and/or visible state of the action set. By default, we listen to the window being opened and the active perspective and active view being changed. """ # We use dynamic trait handlers here instead of static handlers (or # @on_trait_change) because sub-classes might have a completely # different way to determine the anabled and/or visible state, hence # we might want to hook up completely different events. self.window.on_trait_change(self._refresh, 'opened') self.window.on_trait_change(self._refresh, 'active_part') self.window.on_trait_change(self._refresh, 'active_perspective') return ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ def _window_changed(self): """ Static trait change handler. """ # fixme: We put the code into an 'initialize' method because it seems # easier to explain that we expect it to be overridden. It seems a bit # smelly to say that a trait change handfler needs to be overridden. self.initialize() return #### Methods ############################################################## def _refresh(self): """ Refresh the enabled/visible state of the action set. """ window = self.window if len(self.enabled_for_perspectives) > 0: self.enabled = window is not None \ and window.active_perspective is not None \ and window.active_perspective.id in \ self.enabled_for_perspectives if len(self.visible_for_perspectives) > 0: self.visible = window is not None \ and window.active_perspective is not None \ and window.active_perspective.id in \ self.visible_for_perspectives if len(self.enabled_for_views) > 0: self.enabled = window is not None \ and window.active_part is not None \ and window.active_part.id in \ self.enabled_for_views if len(self.visible_for_views) > 0: self.visible = window is not None \ and window.active_part is not None \ and window.active_part.id in \ self.visible_for_views return def _update_actions(self, window, trait_name, value): """ Update the state of the tool bars in the action set. """ def visitor(item): """ Called when we visit each item in an action manager. """ # fixme: The 'additions' group gets created by default and hence # has no '_action_set_' attribute. This smells because of the fact # that we 'tag' the '_action_set_' attribute onto all items to be # ble to find them later. This link should be maintained externally # (maybe in the action set itself?). if hasattr(item, '_action_set_'): if item._action_set_ is self: setattr(item, trait_name, value) return # Update actions on the menu bar. window.menu_bar_manager.walk(visitor) # Update actions on the tool bars. for tool_bar_manager in window.tool_bar_managers: tool_bar_manager.walk(visitor) return def _update_tool_bars(self, window, trait_name, value): """ Update the state of the tool bars in the action set. """ for tool_bar_manager in window.tool_bar_managers: if tool_bar_manager._action_set_ is self: setattr(tool_bar_manager, trait_name, value) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/workbench_application.py0000644000076500000240000001467300000000000025741 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The entry point for an Envisage Workbench application. """ # Standard library imports. import logging # Enthought library imports. # # fixme: The ordering of these imports is critical. We don't use traits UI in # this module, but it must be imported *before* any 'HasTraits' class whose # instances might want to have 'edit_traits' called on them. # # fixme: Just importing the package is enought (see above). import traitsui # Enthought library imports. from envisage.api import Application from pyface.api import AboutDialog, Dialog, GUI, ImageResource from pyface.api import SplashScreen from pyface.workbench.api import IWorkbench from traits.api import Callable, Instance, Str, Tuple # Local imports. from .workbench import Workbench # Logging. logger = logging.getLogger(__name__) class WorkbenchApplication(Application): """ The entry point for an Envisage Workbench application. i.e. a GUI application whose user interface is provided by the workbench plugin. This class handles the common case for Workbench applications, and it is intended to be subclassed to change start/stop behaviour etc. In fact, I generally create a subclass for every Workbench application I write since it is a good place to put branding information etc. """ #### 'WorkbenchApplication' interface ##################################### # The Pyface GUI for the application (this is here to make it easy for # parts of the application to get a reference to the GUI so they can get # system metrics, etc. gui = Instance(GUI) # The workbench. workbench = Instance(IWorkbench) # The factory for creating the workbench (used *instead* of providing a # workbench explicitly). workbench_factory = Callable(Workbench) # Branding information. # # The 'About' dialog. about_dialog = Instance(Dialog) # The icon used on window title bars etc. icon = Instance(ImageResource, ImageResource('application.ico')) # The name of the application (also used on window title bars etc). name = Str('Workbench') # The splash screen (None, the default, if no splash screen is required). splash_screen = Instance(SplashScreen) # The default position of the main window. window_position = Tuple((200, 200)) # The default size of the main window. window_size = Tuple((800, 600)) ########################################################################### # 'IApplication' interface. ########################################################################### def run(self): """ Run the application. This does the following (so you don't have to ;^):- 1) Starts the application 2) Creates and opens a workbench window 3) Starts the GUI event loop 4) When the event loop terminates, stops the application """ logger.debug('---------- workbench application ----------') # Make sure the GUI has been created (so that, if required, the splash # screen is shown). gui = self.gui # Start the application. if self.start(): # Create and open the first workbench window. window = self.workbench.create_window( position=self.window_position, size=self.window_size ) window.open() # We stop the application when the workbench has exited. self.workbench.on_trait_change(self._on_workbench_exited, 'exited') # Start the GUI event loop. # # THIS CALL DOES NOT RETURN UNTIL THE GUI IS CLOSED. gui.start_event_loop() return ########################################################################### # 'WorkbenchApplication' interface. ########################################################################### #### Initializers ######################################################### def _about_dialog_default(self): """ Trait initializer. """ return AboutDialog(image=ImageResource('about')) def _gui_default(self): """ Trait initializer. """ return GUI(splash_screen=self.splash_screen) def _workbench_default(self): """ Trait initializer. """ return self.create_workbench() #### Methods ############################################################## def about(self): """ Display the about dialog. """ # fixme: We really need to create a new 'about dialog' every time so # that it can have the active window as its parent. self.about_dialog.open() return # fixme: Is this needed on the public API? Why can't we just do this in # the default initializer (_workbench_default)? def create_workbench(self): """ Create the workbench. """ logger.debug('workbench factory %s', self.workbench_factory) return self.workbench_factory(application=self) def exit(self): """ Exit the application. This closes all open windows and hence exits the GUI event loop. """ self.workbench.exit() return ########################################################################### # Private interface. ########################################################################### def _on_workbench_exited(self): """ Dynamic trait change handler. """ # We don't invoke 'stop' directly because:- # # The workbench is often exited via a user action (either by closing # the last open window, or by choosing 'File/Exit'). If this happens # then the workbench 'exit' method is called from within an event # handler which would cause the 'stop' method to get called *before* # the handling of the window 'closed' event is complete. Hance, this # might mean that somebody listening for the window being closed would # get the event *after* the application had already stopped! self.gui.invoke_later(self.stop) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/workbench_editor_manager.py0000644000076500000240000000350500000000000026406 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An editor manager that uses contributed editors. """ # Enthought library imports. from pyface.workbench.api import EditorManager, TraitsUIEditor class WorkbenchEditorManager(EditorManager): """ An editor manager that uses contributed editors. """ ########################################################################### # 'IEditorManager' interface. ########################################################################### def create_editor(self, window, obj, kind): """ Create an editor for an object. For now, the 'kind' is actually a factory that produces editors. It should be a callable with the following signature:: callable(window=window, obj=obj) -> IEditor """ if kind is None: kind = TraitsUIEditor editor = kind(window=window, obj=obj) self.add_editor(editor, kind) return editor ########################################################################### # 'Protected' 'EditorManager' interface. ########################################################################### def _is_editing(self, editor, obj, kind): """ Return True if the editor is editing the object. """ if kind is None: kind = TraitsUIEditor return self.get_editor_kind(editor) is kind and editor.obj == obj #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/workbench_plugin.py0000644000076500000240000002041000000000000024716 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The Envisage workbench plugin. """ # Enthought library imports. from envisage.api import ExtensionPoint, Plugin, ServiceOffer from traits.api import Callable, List # This module's package. PKG = '.'.join(__name__.split('.')[:-1]) class WorkbenchPlugin(Plugin): """ The Envisage workbench plugin. The workbench plugin uses the Pyface workbench to provide the basis of an IDE-like user interface. The interface is made up of perspectives, views and editors. Note that this is not intended to be a 'general-purpose' plugin for user interfaces - it provides an IDE-like style and that is all. If your application requires another style of interface then write another plugin (you can still re-use all the menu, group and action contribution stuff!). """ # The Ids of the extension points that this plugin offers. ACTION_SETS = PKG + '.action_sets' PERSPECTIVES = PKG + '.perspectives' PREFERENCES_PAGES = PKG + '.preferences_pages' WORKBENCH_SERVICE_OFFERS = PKG + '.service_offers' VIEWS = PKG + '.views' # The Ids of the extension points that this plugin contributes to. PREFERENCES = 'envisage.preferences' SERVICE_OFFERS = 'envisage.service_offers' #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = 'envisage.ui.workbench' # The plugin's name (suitable for displaying to the user). name = 'Workbench' #### Extension points offered by this plugin ############################## action_sets = ExtensionPoint( List(Callable), id=ACTION_SETS, desc=""" An action set contains the toobars, menus, groups and actions that you would like to add to top-level workbench windows (i.e. the main application window). You can create new toolbars, menus and groups and/or add to existing ones. Each contribution to this extension point must be a factory that creates an action set, where 'factory' means any callable with the following signature:: callable(**traits) -> IActionSet The easiest way to contribute such a factory is to create a class that derives from 'envisage.ui.action.api.ActionSet'. """ ) perspectives = ExtensionPoint( List(Callable), id=PERSPECTIVES, desc=""" A perspective is simply an arrangment of views around the (optionally hidden) editor area. Each contribution to this extension point must be a factory that creates a perspective, where 'factory' means any callable with the following signature:: callable(**traits) -> IPerspective The easiest way to contribute such a factory is to create a class that derives from 'pyface.workbench.api.IPerspective'. """ ) preferences_pages = ExtensionPoint( List(Callable), id=PREFERENCES_PAGES, desc=""" A preferences page appears in the preferences dialog to allow the user to manipulate some preference values. Each contribution to this extension point must be a factory that creates a preferences page, where 'factory' means any callable with the following signature:: callable(**traits) -> IPreferencesPage The easiest way to contribute such a factory is to create a class that derives from 'apptools.preferences.ui.api.IPreferencesPage'. """ ) service_offers = ExtensionPoint( List(ServiceOffer), id = WORKBENCH_SERVICE_OFFERS, desc = """ Services are simply objects that a plugin wants to make available to other plugins. This extension point allows you to offer 'per window' services that are created 'on-demand' (where 'on demand' means the first time somebody looks up a service of the appropriate protocol). . e.g. my_service_offer = ServiceOffer( protocol = 'acme.IMyService', factory = an_object_or_a_callable_that_creates_one, properties = {'a dictionary' : 'that is passed to the factory'} ) Any properties specified are passed as keywrod arguments to the factory, i.e. the factory signature is:: callable(**properties) """ ) views = ExtensionPoint( List(Callable), id=VIEWS, desc=""" A view provides information to the user to support their current task. Views can contain anything you like(!) and are arranged around the (optionally hidden) editor area. The user can re-arrange views as he/she sees fit. Each contribution to this extension point must be a factory that creates a view, where 'factory' means any callable with the following signature:: callable(**traits) -> IView The easiest way to contribute such a factory is to create a class that derives from 'pyface.workbench.api.View'. It is also common to use a simple function (especially when a view is a representation of a service) e.g:: def foo_view_factory(**traits): ' Create a view that is a representation of a service. ' foo = self.application.get_service('IFoo') return FooView(foo=foo, **traits) """ ) #### Contributions to extension points made by this plugin ################ my_action_sets = List(contributes_to=ACTION_SETS) def _my_action_sets_default(self): """ Trait initializer. """ from .default_action_set import DefaultActionSet return [DefaultActionSet] my_preferences = List(contributes_to=PREFERENCES) def _my_preferences_default(self): """ Trait initializer. """ return ['pkgfile://envisage.ui.workbench/preferences.ini'] my_preferences_pages = List(contributes_to=PREFERENCES_PAGES) def _my_preferences_pages_default(self): """ Trait initializer. """ from .workbench_preferences_page import WorkbenchPreferencesPage return [WorkbenchPreferencesPage] my_service_offers = List(contributes_to=SERVICE_OFFERS) def _my_service_offers_default(self): """ Trait initializer. """ preferences_manager_service_offer = ServiceOffer( protocol = 'apptools.preferences.ui.preferences_manager' '.PreferencesManager', factory = self._create_preferences_manager_service ) workbench_service_offer = ServiceOffer( protocol = 'envisage.ui.workbench.workbench.Workbench', factory = self._create_workbench_service ) return [preferences_manager_service_offer, workbench_service_offer] ########################################################################### # Private interface. ########################################################################### def _create_preferences_manager_service(self, **properties): """ Factory method for the preferences manager service. """ from apptools.preferences.ui.api import PreferencesManager preferences_manager = PreferencesManager( pages=[factory() for factory in self.preferences_pages] ) return preferences_manager def _create_workbench_service(self, **properties): """ Factory method for the workbench service. """ # We don't actually create the workbench here, we just return a # reference to it. # # fixme: This guard is really just for testing when we have the # workbench plugin as a source egg (i.e. if the egg is on our path # then we get the plugin for any egg-based application, even if it is # not a workbench application!). return getattr(self.application, 'workbench', None) ### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/workbench_preferences.py0000644000076500000240000000213000000000000025720 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The workbench preferences. """ # Enthought library imports. from apptools.preferences.api import PreferencesHelper from traits.api import Bool class WorkbenchPreferences(PreferencesHelper): """ Helper for the workbench preferences. """ #### 'PreferencesHelper' interface ######################################## # The path to the preferences node that contains the preferences. preferences_path = 'envisage.ui.workbench' #### Preferences ########################################################## # Should the user be prompted before exiting the workbench? prompt_on_exit = Bool(True) #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/workbench_preferences_page.py0000644000076500000240000000321700000000000026723 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The main preferences page for the workbench. """ # Enthought library imports. from apptools.preferences.ui.api import PreferencesPage from traits.api import Bool from traitsui.api import View class WorkbenchPreferencesPage(PreferencesPage): """ The main preferences page for the workbench. """ #### '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 = 'General' # The path to the preferences node that contains the preferences. preferences_path = 'envisage.ui.workbench' #### Preferences ########################################################## # Should the user be prompted before exiting the workbench? prompt_on_exit = Bool(True) #### Traits UI views ###################################################### trait_view = View('prompt_on_exit') #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/ui/workbench/workbench_window.py0000644000076500000240000002161200000000000024734 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ An extensible workbench window. """ # Standard library imports. import logging # Enthought library imports. import pyface.workbench.api as pyface from envisage.api import IExtensionPointUser, IExtensionRegistry from envisage.api import IServiceRegistry from envisage.api import ExtensionPoint, ServiceRegistry from envisage.ui.action.api import ActionSet from pyface.action.api import StatusBarManager from traits.api import Delegate, Instance, List, Property, provides # Local imports. from .workbench_action_manager_builder import WorkbenchActionManagerBuilder from .workbench_editor_manager import WorkbenchEditorManager # Logging. logger = logging.getLogger(__name__) @provides(IServiceRegistry, IExtensionPointUser) class WorkbenchWindow(pyface.WorkbenchWindow): """ An extensible workbench window. """ # Extension point Ids. ACTION_SETS = 'envisage.ui.workbench.action_sets' VIEWS = 'envisage.ui.workbench.views' PERSPECTIVES = 'envisage.ui.workbench.perspectives' SERVICE_OFFERS = 'envisage.ui.workbench.service_offers' #### 'WorkbenchWindow' interface ########################################## # The application that the window is part of. # # This is equivalent to 'self.workbench.application', and is provided just # as a convenience since windows often want access to the application. application = Delegate('workbench', modify=True) # The action sets that provide the toolbars, menus groups and actions # used in the window. action_sets = List(Instance(ActionSet)) # The service registry for 'per window' services. service_registry = Instance(IServiceRegistry, factory=ServiceRegistry) #### 'IExtensionPointUser' interface ###################################### # The extension registry that the object's extension points are stored in. extension_registry = Property(Instance(IExtensionRegistry)) #### Private interface #################################################### # The workbench menu and tool bar builder. # # The builder is used to create the window's tool bar and menu bar by # combining all of the contributed action sets. _action_manager_builder = Instance(WorkbenchActionManagerBuilder) # Contributed action sets (each contribution is actually a factory). _action_sets = ExtensionPoint(id=ACTION_SETS) # Contributed views (each contribution is actually a factory). _views = ExtensionPoint(id=VIEWS) # Contributed perspectives (each contribution is actually a factory). _perspectives = ExtensionPoint(id=PERSPECTIVES) # Contributed service offers. _service_offers = ExtensionPoint(id=SERVICE_OFFERS) # The Ids of the services that were automatically registered. _service_ids = List ########################################################################### # 'IExtensionPointUser' interface. ########################################################################### def _get_extension_registry(self): """ Trait property getter. """ return self.application ########################################################################### # 'pyface.Window' interface. ########################################################################### #### Trait initializers ################################################### def _menu_bar_manager_default(self): """ Trait initializer. """ return self._action_manager_builder.create_menu_bar_manager('MenuBar') def _status_bar_manager_default(self): """ Trait initializer. """ return StatusBarManager() def _tool_bar_managers_default(self): """ Trait initializer. """ return self._action_manager_builder.create_tool_bar_managers('ToolBar') #### Trait change handlers ################################################ def _opening_changed(self): """ Static trait change handler. """ self._service_ids = self._register_service_offers(self._service_offers) return def _closed_changed(self): """ Static trait change handler. """ self._unregister_service_offers(self._service_ids) return ########################################################################### # 'pyface.WorkbenchWindow' interface. ########################################################################### #### Trait initializers ################################################### def _editor_manager_default(self): """ Trait initializer. """ return WorkbenchEditorManager(window=self) def _icon_default(self): """ Trait initializer. """ return self.workbench.application.icon def _perspectives_default(self): """ Trait initializer. """ return [factory() for factory in self._perspectives] def _title_default(self): """ Trait initializer. """ return self.workbench.application.name def _views_default(self): """ Trait initializer. """ return [factory(window=self) for factory in self._views] ########################################################################### # 'WorkbenchWindow' interface. ########################################################################### def _action_sets_default(self): """ Trait initializer. """ return [factory(window=self) for factory in self._action_sets] ########################################################################### # 'IServiceRegistry' interface. ########################################################################### def get_service(self, protocol, query='', minimize='', maximize=''): """ Return at most one service that matches the specified query. """ service = self.service_registry.get_service( protocol, query, minimize, maximize ) return service def get_service_properties(self, service_id): """ Return the dictionary of properties associated with a service. """ return self.service_registry.get_service_properties(service_id) def get_services(self, protocol, query='', minimize='', maximize=''): """ Return all services that match the specified query. """ services = self.service_registry.get_services( protocol, query, minimize, maximize ) return services def register_service(self, protocol, obj, properties=None): """ Register a service. """ service_id = self.service_registry.register_service( protocol, obj, properties ) return service_id def set_service_properties(self, service_id, properties): """ Set the dictionary of properties associated with a service. """ self.service_registry.set_service_properties(service_id, properties) return def unregister_service(self, service_id): """ Unregister a service. """ self.service_registry.unregister_service(service_id) return ########################################################################### # Private interface. ########################################################################### def __action_manager_builder_default(self): """ Trait initializer. """ action_manager_builder = WorkbenchActionManagerBuilder( window=self, action_sets=self.action_sets ) return action_manager_builder def _register_service_offers(self, service_offers): """ Register all service offers. """ return list(map(self._register_service_offer, service_offers)) def _register_service_offer(self, service_offer): """ Register a service offer. """ # Add the window to the service offer properties (this is so that it # is available to the factory when it is called to create the actual # service). service_offer.properties['window'] = self service_id = self.register_service( protocol = service_offer.protocol, obj = service_offer.factory, properties = service_offer.properties ) return service_id def _unregister_service_offers(self, service_ids): """ Unregister all service offers. """ # Unregister the services in the reverse order that we registered # them. service_ids_copy = service_ids[:] service_ids_copy.reverse() for service_id in service_ids_copy: self.unregister_service(service_id) return #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/unknown_extension.py0000644000076500000240000000123500000000000022556 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The exception raised when an unknown extension is referenced. """ class UnknownExtension(Exception): """ The exception raised when an unknown extension is referenced. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/envisage/unknown_extension_point.py0000644000076500000240000000125600000000000023772 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ The exception raised when an unknown extension point is referenced. """ class UnknownExtensionPoint(Exception): """ The exception raised when an unknown extension point is referenced. """ #### EOF ###################################################################### ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574158504.0 envisage-4.9.0/envisage/version.py0000644000076500000240000000145200000000000020451 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Version information for this Envisage distribution. This file is autogenerated by the Envisage setup.py script. """ from __future__ import unicode_literals #: The full version of the package, including a development suffix #: for unreleased versions of the package. version = "4.9.0" #: The Git revision from which this release was made. git_revision = "ef7ff4c44efa0f3318e8ecfdfcb8ac6c54c63739" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.2300034 envisage-4.9.0/envisage.egg-info/0000755000076500000240000000000000000000000020102 5ustar00mdickinsonstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574158504.0 envisage-4.9.0/envisage.egg-info/PKG-INFO0000644000076500000240000000672100000000000021205 0ustar00mdickinsonstaff00000000000000Metadata-Version: 2.1 Name: envisage Version: 4.9.0 Summary: Extensible application framework Home-page: http://docs.enthought.com/envisage Author: Enthought Author-email: info@enthought.com Maintainer: ETS Developers Maintainer-email: enthought-dev@enthought.com License: BSD Download-URL: https://github.com/enthought/envisage Description: ========================================== envisage: extensible application framework ========================================== .. image:: https://travis-ci.org/enthought/envisage.svg?branch=master :alt: Build Status :target: https://travis-ci.org/enthought/envisage .. image:: https://codecov.io/github/enthought/envisage/coverage.svg?branch=master :alt: Code Coverage :target: https://codecov.io/github/enthought/envisage?branch=master Documentation: https://docs.enthought.com/envisage Envisage is a Python-based framework for building extensible applications, that is, applications whose functionality can be extended by adding "plug-ins". Envisage provides a standard mechanism for features to be added to an application, whether by the original developer or by someone else. In fact, when you build an application using Envisage, the entire application consists primarily of plug-ins. In this respect, it is similar to the Eclipse and Netbeans frameworks for Java applications. Each plug-in is able to: - Advertise where and how it can be extended (its "extension points"). - Contribute extensions to the extension points offered by other plug-ins. - Create and share the objects that perform the real work of the application ("services"). The Envisage project provides the basic machinery of the Envisage framework. This project contains no plug-ins. You are free to use: - plug-ins from the EnvisagePlugins project - plug-ins from other ETS projects that expose their functionality as plug-ins - plug-ins that you create yourself Prerequisites ------------- The supported versions of Python are Python 2.7.x and Python >= 3.5. * `apptools `_ * `traits `_ Platform: UNKNOWN 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 :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: User Interfaces Requires-Python: >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.* Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574158504.0 envisage-4.9.0/envisage.egg-info/SOURCES.txt0000644000076500000240000002417300000000000021775 0ustar00mdickinsonstaff00000000000000CHANGES.rst LICENSE.txt MANIFEST.in README.rst image_LICENSE.txt image_LICENSE_CP.txt setup.py envisage/__init__.py envisage/_compat.py envisage/api.py envisage/application.py envisage/application_event.py envisage/category.py envisage/class_load_hook.py envisage/composite_plugin_manager.py envisage/core_plugin.py envisage/egg_basket_plugin_manager.py envisage/egg_plugin_manager.py envisage/egg_utils.py envisage/extension_point.py envisage/extension_point_binding.py envisage/extension_point_changed_event.py envisage/extension_provider.py envisage/extension_registry.py envisage/i_application.py envisage/i_extension_point.py envisage/i_extension_point_user.py envisage/i_extension_provider.py envisage/i_extension_registry.py envisage/i_import_manager.py envisage/i_plugin.py envisage/i_plugin_activator.py envisage/i_plugin_manager.py envisage/i_provider_extension_registry.py envisage/i_service_registry.py envisage/i_service_user.py envisage/import_manager.py envisage/package_plugin_manager.py envisage/plugin.py envisage/plugin_activator.py envisage/plugin_event.py envisage/plugin_extension_registry.py envisage/plugin_manager.py envisage/provider_extension_registry.py envisage/safeweakref.py envisage/service.py envisage/service_offer.py envisage/service_registry.py envisage/twisted_application.py envisage/unknown_extension.py envisage/unknown_extension_point.py envisage/version.py envisage.egg-info/PKG-INFO envisage.egg-info/SOURCES.txt envisage.egg-info/dependency_links.txt envisage.egg-info/entry_points.txt envisage.egg-info/not-zip-safe envisage.egg-info/requires.txt envisage.egg-info/top_level.txt envisage/plugins/__init__.py envisage/plugins/event_manager/__init__.py envisage/plugins/event_manager/plugin.py envisage/plugins/ipython_kernel/__init__.py envisage/plugins/ipython_kernel/actions.py envisage/plugins/ipython_kernel/api.py envisage/plugins/ipython_kernel/heartbeat.py envisage/plugins/ipython_kernel/internal_ipkernel.py envisage/plugins/ipython_kernel/ipython_kernel_plugin.py envisage/plugins/ipython_kernel/ipython_kernel_ui_plugin.py envisage/plugins/ipython_kernel/kernelapp.py envisage/plugins/ipython_kernel/tests/__init__.py envisage/plugins/ipython_kernel/tests/test_internal_ipkernel.py envisage/plugins/ipython_kernel/tests/test_ipython_kernel_plugin.py envisage/plugins/python_shell/__init__.py envisage/plugins/python_shell/api.py envisage/plugins/python_shell/i_python_shell.py envisage/plugins/python_shell/python_shell_plugin.py envisage/plugins/python_shell/view/__init__.py envisage/plugins/python_shell/view/api.py envisage/plugins/python_shell/view/namespace_view.py envisage/plugins/python_shell/view/python_shell_view.py envisage/plugins/tasks/__init__.py envisage/plugins/tasks/python_shell_plugin.py envisage/plugins/text_editor/__init__.py envisage/plugins/text_editor/actions.py envisage/plugins/text_editor/api.py envisage/plugins/text_editor/text_editor_action_set.py envisage/plugins/text_editor/text_editor_plugin.py envisage/plugins/text_editor/editor/__init__.py envisage/plugins/text_editor/editor/text_editor.py envisage/plugins/text_editor/editor/text_editor_handler.py envisage/resource/__init__.py envisage/resource/api.py envisage/resource/file_resource_protocol.py envisage/resource/http_resource_protocol.py envisage/resource/i_resource_manager.py envisage/resource/i_resource_protocol.py envisage/resource/no_such_resource_error.py envisage/resource/package_resource_protocol.py envisage/resource/resource_manager.py envisage/resource/tests/__init__.py envisage/resource/tests/test_resource_manager.py envisage/tests/__init__.py envisage/tests/bar_category.py envisage/tests/ets_config_patcher.py envisage/tests/event_tracker.py envisage/tests/foo.py envisage/tests/i_foo.py envisage/tests/mutable_extension_registry.py envisage/tests/preferences.ini envisage/tests/test_application.py envisage/tests/test_class_load_hook.py envisage/tests/test_composite_plugin_manager.py envisage/tests/test_core_plugin.py envisage/tests/test_egg_based.py envisage/tests/test_egg_basket_plugin_manager.py envisage/tests/test_egg_plugin_manager.py envisage/tests/test_extension_point.py envisage/tests/test_extension_point_binding.py envisage/tests/test_extension_point_changed.py envisage/tests/test_extension_registry.py envisage/tests/test_import_manager.py envisage/tests/test_package_plugin_manager.py envisage/tests/test_plugin.py envisage/tests/test_plugin_manager.py envisage/tests/test_provider_extension_registry.py envisage/tests/test_safeweakref.py envisage/tests/test_service.py envisage/tests/test_service_registry.py envisage/tests/test_slice.py envisage/tests/test_version.py envisage/tests/bad_eggs/acme.bad-0.1a1-py2.7.egg envisage/tests/bad_eggs/acme.bad-0.1a1-py3.5.egg envisage/tests/bad_eggs/acme.bad-0.1a1-py3.6.egg envisage/tests/bad_eggs/acme.bad-0.1a1-py3.7.egg envisage/tests/bad_eggs/acme.bad-0.1a1-py3.8.egg envisage/tests/eggs/acme.bar-0.1a1-py2.7.egg envisage/tests/eggs/acme.bar-0.1a1-py3.5.egg envisage/tests/eggs/acme.bar-0.1a1-py3.6.egg envisage/tests/eggs/acme.bar-0.1a1-py3.7.egg envisage/tests/eggs/acme.bar-0.1a1-py3.8.egg envisage/tests/eggs/acme.baz-0.1a1-py2.7.egg envisage/tests/eggs/acme.baz-0.1a1-py3.5.egg envisage/tests/eggs/acme.baz-0.1a1-py3.6.egg envisage/tests/eggs/acme.baz-0.1a1-py3.7.egg envisage/tests/eggs/acme.baz-0.1a1-py3.8.egg envisage/tests/eggs/acme.foo-0.1a1-py2.7.egg envisage/tests/eggs/acme.foo-0.1a1-py3.5.egg envisage/tests/eggs/acme.foo-0.1a1-py3.6.egg envisage/tests/eggs/acme.foo-0.1a1-py3.7.egg envisage/tests/eggs/acme.foo-0.1a1-py3.8.egg envisage/tests/plugins/banana/__init__.py envisage/tests/plugins/banana/banana_plugin.py envisage/tests/plugins/banana/plugins.py envisage/tests/plugins/orange/__init__.py envisage/tests/plugins/orange/orange_plugin.py envisage/tests/plugins/orange/plugins.py envisage/tests/plugins/pear/__init__.py envisage/tests/plugins/pear/pear_plugin.py envisage/ui/__init__.py envisage/ui/gui_application.py envisage/ui/action/__init__.py envisage/ui/action/abstract_action_manager_builder.py envisage/ui/action/action.py envisage/ui/action/action_set.py envisage/ui/action/action_set_manager.py envisage/ui/action/api.py envisage/ui/action/group.py envisage/ui/action/i_action_manager_builder.py envisage/ui/action/i_action_set.py envisage/ui/action/location.py envisage/ui/action/menu.py envisage/ui/action/tool_bar.py envisage/ui/action/tests/__init__.py envisage/ui/action/tests/dummy_action_manager_builder.py envisage/ui/action/tests/test_action_manager_builder.py envisage/ui/single_project/DESIGN.txt envisage/ui/single_project/README.txt envisage/ui/single_project/TODO.txt envisage/ui/single_project/__init__.py envisage/ui/single_project/api.py envisage/ui/single_project/default_path_preference_page.py envisage/ui/single_project/factory_definition.py envisage/ui/single_project/model_service.py envisage/ui/single_project/preferences.ini envisage/ui/single_project/project.py envisage/ui/single_project/project_action.py envisage/ui/single_project/project_action_set.py envisage/ui/single_project/project_factory.py envisage/ui/single_project/project_plugin.py envisage/ui/single_project/services.py envisage/ui/single_project/ui_service.py envisage/ui/single_project/ui_service_factory.py envisage/ui/single_project/action/__init__.py envisage/ui/single_project/action/api.py envisage/ui/single_project/action/close_project_action.py envisage/ui/single_project/action/configure_action.py envisage/ui/single_project/action/new_project_action.py envisage/ui/single_project/action/open_project_action.py envisage/ui/single_project/action/rename_action.py envisage/ui/single_project/action/save_as_project_action.py envisage/ui/single_project/action/save_project_action.py envisage/ui/single_project/action/switch_to_action.py envisage/ui/single_project/action/images/close_project.png envisage/ui/single_project/action/images/new_project.png envisage/ui/single_project/action/images/open_project.png envisage/ui/single_project/action/images/save_as_project.png envisage/ui/single_project/action/images/save_project.png envisage/ui/single_project/action/images/switch_project.png envisage/ui/single_project/editor/__init__.py envisage/ui/single_project/editor/project_editor.py envisage/ui/single_project/tests/__init__.py envisage/ui/single_project/tests/test_project_view.py envisage/ui/single_project/view/__init__.py envisage/ui/single_project/view/project_view.py envisage/ui/tasks/__init__.py envisage/ui/tasks/api.py envisage/ui/tasks/preferences_category.py envisage/ui/tasks/preferences_dialog.py envisage/ui/tasks/preferences_pane.py envisage/ui/tasks/task_extension.py envisage/ui/tasks/task_factory.py envisage/ui/tasks/task_window.py envisage/ui/tasks/task_window_event.py envisage/ui/tasks/tasks_application.py envisage/ui/tasks/tasks_plugin.py envisage/ui/tasks/action/__init__.py envisage/ui/tasks/action/api.py envisage/ui/tasks/action/exit_action.py envisage/ui/tasks/action/preferences_action.py envisage/ui/tasks/action/task_window_launch_group.py envisage/ui/tasks/action/task_window_toggle_group.py envisage/ui/tasks/tests/__init__.py envisage/ui/tasks/tests/test_tasks_application.py envisage/ui/tasks/tests/data/application_memento_v2.pkl envisage/ui/tasks/tests/data/application_memento_v3.pkl envisage/ui/workbench/__init__.py envisage/ui/workbench/api.py envisage/ui/workbench/default_action_set.py envisage/ui/workbench/preferences.ini envisage/ui/workbench/workbench.py envisage/ui/workbench/workbench_action_manager_builder.py envisage/ui/workbench/workbench_action_set.py envisage/ui/workbench/workbench_application.py envisage/ui/workbench/workbench_editor_manager.py envisage/ui/workbench/workbench_plugin.py envisage/ui/workbench/workbench_preferences.py envisage/ui/workbench/workbench_preferences_page.py envisage/ui/workbench/workbench_window.py envisage/ui/workbench/action/__init__.py envisage/ui/workbench/action/about_action.py envisage/ui/workbench/action/api.py envisage/ui/workbench/action/edit_preferences_action.py envisage/ui/workbench/action/exit_action.py envisage/ui/workbench/action/images/exit.png envisage/ui/workbench/action/images/image_LICENSE.txt envisage/ui/workbench/action/images/preferences.png envisage/ui/workbench/images/about.png envisage/ui/workbench/images/application.ico envisage/ui/workbench/images/image_LICENSE.txt././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574158504.0 envisage-4.9.0/envisage.egg-info/dependency_links.txt0000644000076500000240000000000100000000000024150 0ustar00mdickinsonstaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574158504.0 envisage-4.9.0/envisage.egg-info/entry_points.txt0000644000076500000240000000010400000000000023373 0ustar00mdickinsonstaff00000000000000[envisage.plugins] envisage.core = envisage.core_plugin:CorePlugin ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574158504.0 envisage-4.9.0/envisage.egg-info/not-zip-safe0000644000076500000240000000000100000000000022330 0ustar00mdickinsonstaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574158504.0 envisage-4.9.0/envisage.egg-info/requires.txt0000644000076500000240000000003700000000000022502 0ustar00mdickinsonstaff00000000000000apptools setuptools six traits ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574158504.0 envisage-4.9.0/envisage.egg-info/top_level.txt0000644000076500000240000000001100000000000022624 0ustar00mdickinsonstaff00000000000000envisage ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/image_LICENSE.txt0000644000076500000240000000302000000000000017567 0ustar00mdickinsonstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: Project License File ---------------------------------------------------------------------------- CP (Crystal Project) LGPL image_LICENSE_CP.txt GV (Gael Varoquaux) BSD-like LICENSE.txt PSF Python Software Fo. see 1) CCL Creative Commons see 2) Unless stated in this file, icons are the work of Enthought, and are released under a 3 clause BSD license. 1) The Python logo is a trademark of the Python Software Foundation. Please refer to: http://www.python.org/psf/trademarks/ 2) Creative Commons Attribution ShareAlike license versions 3.0, 2.5, 2.0, and 1.0 Files and orginal authors: ---------------------------------------------------------------------------- envisage/ui/workbench/images: about.png | PSF application.ico | GV envisage/ui/workbench/action/images: exit.png | CP preferences.png | CP examples/workbench/AcmeLab/acme/acmelab/images: about.png | Public domain acmelab.ico | GV splash.jpg | CCL examples/workbench/AcmeLabUsingEggs/src/acme.acmelab/acme/acmelab/images: about.png | Public domain acmelab.ico | GV splash.jpg | CCL ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1568617854.0 envisage-4.9.0/image_LICENSE_CP.txt0000644000076500000240000004671600000000000020174 0ustar00mdickinsonstaff00000000000000License The Crystal Project are released under LGPL. GNU General Public License. 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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1574158504.3015711 envisage-4.9.0/setup.cfg0000644000076500000240000000004600000000000016430 0ustar00mdickinsonstaff00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574157818.0 envisage-4.9.0/setup.py0000644000076500000240000002360700000000000016331 0ustar00mdickinsonstaff00000000000000# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! import io import os import runpy import subprocess from setuptools import setup, find_packages # Version information; update this by hand when making a new bugfix or feature # release. The actual package version is autogenerated from this information # together with information from the version control system, and then injected # into the package source. MAJOR = 4 MINOR = 9 MICRO = 0 IS_RELEASED = True # If this file is part of a Git export (for example created with "git archive", # or downloaded from GitHub), ARCHIVE_COMMIT_HASH gives the full hash of the # commit that was exported. ARCHIVE_COMMIT_HASH = "$Format:%H$" # Templates for version strings. RELEASED_VERSION = u"{major}.{minor}.{micro}" UNRELEASED_VERSION = u"{major}.{minor}.{micro}.dev{dev}" # Paths to the autogenerated version file and the Git directory. HERE = os.path.abspath(os.path.dirname(__file__)) VERSION_FILE = os.path.join(HERE, "envisage", "version.py") GIT_DIRECTORY = os.path.join(HERE, ".git") # Template for the autogenerated version file. VERSION_FILE_TEMPLATE = u'''\ # (C) Copyright 2007-2019 Enthought, Inc., Austin, TX # 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! """ Version information for this Envisage distribution. This file is autogenerated by the Envisage setup.py script. """ from __future__ import unicode_literals #: The full version of the package, including a development suffix #: for unreleased versions of the package. version = "{version}" #: The Git revision from which this release was made. git_revision = "{git_revision}" ''' # Git executable to use to get revision information. GIT = "git" def _git_output(args): """ Call Git with the given arguments and return the output as (Unicode) text. """ return subprocess.check_output([GIT] + args).decode("utf-8") def _git_info(commit="HEAD"): """ Get information about the given commit from Git. Parameters ---------- commit : str, optional Commit to provide information for. Defaults to "HEAD". Returns ------- git_count : int Number of revisions from this commit to the initial commit. git_revision : unicode Commit hash for HEAD. Raises ------ EnvironmentError If Git is not available. subprocess.CalledProcessError If Git is available, but the version command fails (most likely because there's no Git repository here). """ count_args = ["rev-list", "--count", "--first-parent", commit] git_count = int(_git_output(count_args)) revision_args = ["rev-list", "--max-count", "1", commit] git_revision = _git_output(revision_args).rstrip() return git_count, git_revision def write_version_file(version, git_revision): """ Write version information to the version file. Overwrites any existing version file. Parameters ---------- version : unicode Package version. git_revision : unicode The full commit hash for the current Git revision. """ with io.open(VERSION_FILE, "w", encoding="ascii") as version_file: version_file.write( VERSION_FILE_TEMPLATE.format( version=version, git_revision=git_revision ) ) def read_version_file(): """ Read version information from the version file, if it exists. Returns ------- version : unicode The full version, including any development suffix. git_revision : unicode The full commit hash for the current Git revision. Raises ------ EnvironmentError If the version file does not exist. """ version_info = runpy.run_path(VERSION_FILE) return (version_info["version"], version_info["git_revision"]) def git_version(): """ Construct version information from local variables and Git. Returns ------- version : unicode Package version. git_revision : unicode The full commit hash for the current Git revision. Raises ------ EnvironmentError If Git is not available. subprocess.CalledProcessError If Git is available, but the version command fails (most likely because there's no Git repository here). """ git_count, git_revision = _git_info() version_template = RELEASED_VERSION if IS_RELEASED else UNRELEASED_VERSION version = version_template.format( major=MAJOR, minor=MINOR, micro=MICRO, dev=git_count ) return version, git_revision def archive_version(): """ Construct version information for an archive. Returns ------- version : unicode Package version. git_revision : unicode The full commit hash for the current Git revision. Raises ------ ValueError If this does not appear to be an archive. """ if "$" in ARCHIVE_COMMIT_HASH: raise ValueError("This does not appear to be an archive.") version_template = RELEASED_VERSION if IS_RELEASED else UNRELEASED_VERSION version = version_template.format( major=MAJOR, minor=MINOR, micro=MICRO, dev="-unknown" ) return version, ARCHIVE_COMMIT_HASH def resolve_version(): """ Process version information and write a version file if necessary. Returns the current version information. Returns ------- version : unicode Package version. git_revision : unicode The full commit hash for the current Git revision. """ if os.path.isdir(GIT_DIRECTORY): # This is a local clone; compute version information and write # it to the version file, overwriting any existing information. version = git_version() print(u"Computed package version: {}".format(version)) print(u"Writing version to version file {}.".format(VERSION_FILE)) write_version_file(*version) elif "$" not in ARCHIVE_COMMIT_HASH: # This is a source archive. version = archive_version() print(u"Archive package version: {}".format(version)) print(u"Writing version to version file {}.".format(VERSION_FILE)) write_version_file(*version) elif os.path.isfile(VERSION_FILE): # This is a source distribution. Read the version information. print(u"Reading version file {}".format(VERSION_FILE)) version = read_version_file() print(u"Package version from version file: {}".format(version)) else: # This is a source archive for an unreleased version. raise RuntimeError( u"Unable to determine package version. No local Git clone " u"detected, and no version file found at {}." u"Please use a source dist or a git clone.".format(VERSION_FILE) ) return version def get_long_description(): """ Read long description from README.txt. """ with io.open("README.rst", "r", encoding="utf-8") as readme: return readme.read() if __name__ == "__main__": version, git_revision = resolve_version() setup( name="envisage", version=version, url="http://docs.enthought.com/envisage", author="Enthought", author_email="info@enthought.com", maintainer="ETS Developers", maintainer_email="enthought-dev@enthought.com", download_url="https://github.com/enthought/envisage", 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 :: MacOS X Operating System :: Microsoft :: Windows Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: Implementation :: CPython Topic :: Scientific/Engineering Topic :: Software Development Topic :: Software Development :: Libraries Topic :: Software Development :: User Interfaces """.splitlines() if len(c.strip()) > 0 ], description="Extensible application framework", long_description=get_long_description(), long_description_content_type="text/x-rst", entry_points={ "envisage.plugins": [ "envisage.core = envisage.core_plugin:CorePlugin" ] }, install_requires=["apptools", "setuptools", "six", "traits"], license="BSD", packages=find_packages(), package_data={ "": ["images/*", "*.ini"], "envisage.tests": [ "bad_eggs/*.egg", "eggs/*.egg", "plugins/pear/*.py", "plugins/banana/*.py", "plugins/orange/*.py", ], "envisage.ui.single_project": ["*.txt"], "envisage.ui.tasks.tests": ["data/*.pkl"], }, python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", zip_safe=False, )