pax_global_header00006660000000000000000000000064144125737240014523gustar00rootroot0000000000000052 comment=896dfe14015dbad3622d66bcca7020ed8663caa5 envisage-7.0.3/000077500000000000000000000000001441257372400133335ustar00rootroot00000000000000envisage-7.0.3/.coveragerc000066400000000000000000000006721441257372400154610ustar00rootroot00000000000000[run] branch = True source = envisage omit = */tests/* [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError if __name__ == .__main__.: ignore_errors = True envisage-7.0.3/.flake8000066400000000000000000000001511441257372400145030ustar00rootroot00000000000000[flake8] max-line-length = 79 exclude = build ignore = E203, E266, W503 per-file-ignores = */api.py:F401 envisage-7.0.3/.github/000077500000000000000000000000001441257372400146735ustar00rootroot00000000000000envisage-7.0.3/.github/actions/000077500000000000000000000000001441257372400163335ustar00rootroot00000000000000envisage-7.0.3/.github/actions/install-qt-support/000077500000000000000000000000001441257372400221355ustar00rootroot00000000000000envisage-7.0.3/.github/actions/install-qt-support/README.md000066400000000000000000000010021441257372400234050ustar00rootroot00000000000000# Install Qt dependencies This action uses `apt-get` to install necessary packages for Qt support on Linux. It does nothing on non-Linux runners. ## Inputs There are no inputs. ## Outputs There are no outputs. ## Example usage ```yml jobs: # Test against EDM packages test-with-edm: strategy: matrix: os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - uses: ./.github/actions/install-qt-support ``` envisage-7.0.3/.github/actions/install-qt-support/action.yml000066400000000000000000000012641441257372400241400ustar00rootroot00000000000000name: install-qt-support description: 'Install supporting OS packages for Qt-using code' runs: using: composite steps: - name: Install Linux packages for Qt if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install libegl1 sudo apt-get install libxkbcommon-x11-0 sudo apt-get install libxcb-icccm4 sudo apt-get install libxcb-image0 sudo apt-get install libxcb-keysyms1 sudo apt-get install libxcb-randr0 sudo apt-get install libxcb-render-util0 sudo apt-get install libxcb-shape0 # Needed to work around https://bugreports.qt.io/browse/PYSIDE-1547 sudo apt-get install libopengl0 shell: bash envisage-7.0.3/.github/workflows/000077500000000000000000000000001441257372400167305ustar00rootroot00000000000000envisage-7.0.3/.github/workflows/bootstrap-requirements.txt000066400000000000000000000000061441257372400242230ustar00rootroot00000000000000click envisage-7.0.3/.github/workflows/check-style.yml000066400000000000000000000010141441257372400216620ustar00rootroot00000000000000name: Run style checks on: [pull_request, workflow_dispatch] jobs: style: runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' cache: 'pip' cache-dependency-path: '.github/workflows/style-requirements.txt' - run: python -m pip install -r .github/workflows/style-requirements.txt - run: | python -m black --check --diff . python -m isort --check --diff . python -m flake8 . envisage-7.0.3/.github/workflows/ets-from-source.yml000066400000000000000000000052431441257372400225110ustar00rootroot00000000000000name: Test with ETS packages from source on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: schedule: # Every Friday at 00:00 UTC - cron: '0 0 * * 5' env: INSTALL_EDM_VERSION: 3.4.0 QT_MAC_WANTS_LAYER: 1 jobs: # Test against EDM packages test-with-edm: strategy: matrix: os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] runtime: ['3.8'] toolkit: ['null', 'pyside6'] runs-on: ${{ matrix.os }} steps: - name: Clone the Envisage source uses: actions/checkout@v3 - name: Install packages for Qt support uses: ./.github/actions/install-qt-support - name: Cache EDM packages uses: actions/cache@v3 with: path: ~/.cache key: ${{ runner.os }}-${{ matrix.runtime }}-${{ matrix.toolkit }}-${{ hashFiles('etstool.py') }} - name: Set up EDM uses: enthought/setup-edm-action@v2 with: edm-version: ${{ env.INSTALL_EDM_VERSION }} - name: Set up bootstrap Python uses: actions/setup-python@v4 with: python-version: '3.10' cache: 'pip' cache-dependency-path: '.github/workflows/bootstrap-requirements.txt' - name: Install click to the bootstrap environment run: python -m pip install -r .github/workflows/bootstrap-requirements.txt - name: Install test environment run: python etstool.py install --runtime=${{ matrix.runtime }} --toolkit=${{ matrix.toolkit }} --source - name: Run tests (Ubuntu) if: matrix.os == 'ubuntu-latest' run: xvfb-run -a python etstool.py test --runtime=${{ matrix.runtime }} --toolkit=${{ matrix.toolkit }} - name: Run tests (not Ubuntu) if: matrix.os != 'ubuntu-latest' run: python etstool.py test --runtime=${{ matrix.runtime }} --toolkit=${{ matrix.toolkit }} notify-on-failure: needs: test-with-edm if: failure() runs-on: ubuntu-latest steps: - name: Notify Slack channel on failure uses: voxmedia/github-action-slack-notify-build@v1 with: channel_id: ${{ secrets.ETS_SLACK_CHANNEL_ID }} status: FAILED color: danger env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_ACTION_SECRET }} notify-on-success: needs: test-with-edm if: success() runs-on: ubuntu-latest steps: - name: Notify Slack channel on success uses: voxmedia/github-action-slack-notify-build@v1 with: channel_id: ${{ secrets.ETS_BOTS_SLACK_CHANNEL_ID }} status: SUCCESS color: good env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_ACTION_SECRET }} envisage-7.0.3/.github/workflows/publish-on-pypi.yml000066400000000000000000000013761441257372400225210ustar00rootroot00000000000000name: Publish release to PyPI on: workflow_dispatch: release: types: [published] jobs: build-and-upload: runs-on: ubuntu-latest steps: - name: Check out the release commit uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install Python packages needed for build and upload run: | python -m pip install build twine - name: Build sdist and wheel run: | python -m build - name: Publish to PyPI env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python -m twine check --strict dist/* python -m twine upload dist/* envisage-7.0.3/.github/workflows/style-requirements.txt000066400000000000000000000000461441257372400233520ustar00rootroot00000000000000black ~= 23.0 flake8 flake8-ets isort envisage-7.0.3/.github/workflows/test-doc-build.yml000066400000000000000000000012251441257372400222720ustar00rootroot00000000000000name: Test the documentation build on: [pull_request, workflow_dispatch] jobs: docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' cache: 'pip' cache-dependency-path: 'docs/requirements.txt' - run: | python -m pip install -r docs/requirements.txt python -m pip install . - run: | python -m sphinx -b html -d docs/build/doctrees docs/source docs/build/html - uses: actions/upload-artifact@v3 with: name: documentation path: | docs/build/ !docs/build/doctrees/ envisage-7.0.3/.github/workflows/test-with-edm.yml000066400000000000000000000036211441257372400221500ustar00rootroot00000000000000# This workflow targets stable released dependencies from EDM. # Note that some packages may not actually be installed from EDM but from # PyPI, see etstool.py implementations. name: Test with EDM on: [pull_request, workflow_dispatch] env: INSTALL_EDM_VERSION: 3.4.0 QT_MAC_WANTS_LAYER: 1 jobs: # Test against EDM packages test-with-edm: strategy: matrix: os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] runtime: ['3.8'] toolkit: ['null', 'pyside6'] runs-on: ${{ matrix.os }} steps: - name: Clone the Envisage source uses: actions/checkout@v3 - name: Install packages for Qt support uses: ./.github/actions/install-qt-support - name: Cache EDM packages uses: actions/cache@v3 with: path: ~/.cache key: ${{ runner.os }}-${{ matrix.runtime }}-${{ matrix.toolkit }}-${{ hashFiles('etstool.py') }} - name: Set up EDM uses: enthought/setup-edm-action@v2 with: edm-version: ${{ env.INSTALL_EDM_VERSION }} - name: Set up bootstrap Python uses: actions/setup-python@v4 with: python-version: '3.10' cache: 'pip' cache-dependency-path: '.github/workflows/bootstrap-requirements.txt' - name: Install click to the bootstrap environment run: python -m pip install -r .github/workflows/bootstrap-requirements.txt - name: Install test environment run: python etstool.py install --runtime=${{ matrix.runtime }} --toolkit=${{ matrix.toolkit }} - name: Run tests (Ubuntu) if: matrix.os == 'ubuntu-latest' run: xvfb-run -a python etstool.py test --runtime=${{ matrix.runtime }} --toolkit=${{ matrix.toolkit }} - name: Run tests (not Ubuntu) if: matrix.os != 'ubuntu-latest' run: python etstool.py test --runtime=${{ matrix.runtime }} --toolkit=${{ matrix.toolkit }} envisage-7.0.3/.github/workflows/test-with-pypi.yml000066400000000000000000000024441441257372400223660ustar00rootroot00000000000000name: Test with PyPI on: [pull_request, workflow_dispatch] env: PYTHONUNBUFFERED: 1 jobs: # Test against PyPI packages, using pytest test-with-pypi: strategy: matrix: os: ['ubuntu-latest'] python-version: ['3.7', '3.8', '3.10', '3.11'] runs-on: ${{ matrix.os }} steps: - name: Clone the Envisage source uses: actions/checkout@v3 - name: Install packages for Qt support uses: ./.github/actions/install-qt-support - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install packages needed for testing run: python -m pip install pytest - name: Install toolkit # See https://github.com/enthought/envisage/issues/528 for restrictions run: python -m pip install 'pyside6<6.4' if: matrix.python-version != '3.11' - name: Install package under test run: python -m pip install . - name: List installed packages run: python -m pip list - name: Run tests (Ubuntu) if: matrix.os == 'ubuntu-latest' run: xvfb-run -a python -m pytest - name: Run tests (not Ubuntu) if: matrix.os != 'ubuntu-latest' run: python -m pytest envisage-7.0.3/.github/workflows/update-gh-pages-on-release.yml000066400000000000000000000024311441257372400244560ustar00rootroot00000000000000name: Update gh-pages documentation on release on: workflow_dispatch: release: types: [published] jobs: update-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' cache: 'pip' cache-dependency-path: 'docs/requirements.txt' - run: | python -m pip install -r docs/requirements.txt python -m pip install . - run: | python -m sphinx -b html -d docs/build/doctrees docs/source docs/build/html - name: Check out gh-pages branch run: | git fetch --depth=1 origin gh-pages git worktree add ../docs gh-pages - name: Update the docs on the gh-pages branch run: | python docs/update_gh_pages.py docs/build/html ../docs --tag ${{ github.ref_name }} - name: Configure Git run: | git config user.name "Documentation Bot" git config user.email "info@enthought.com" - name: Commit the changes run: | cd ../docs git add . # Only commit if there are changes git diff-index --quiet --cached HEAD || git commit -m "Automated update of release branch documentation" - name: Push the changes upstream run: | git push origin gh-pages envisage-7.0.3/.github/workflows/update-gh-pages.yml000066400000000000000000000023511441257372400224270ustar00rootroot00000000000000name: Update gh-pages documentation on: workflow_dispatch: push: branches: [main] jobs: update-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' cache: 'pip' cache-dependency-path: 'docs/requirements.txt' - run: | python -m pip install -r docs/requirements.txt python -m pip install . - run: | python -m sphinx -b html -d docs/build/doctrees docs/source docs/build/html - name: Check out gh-pages branch run: | git fetch --depth=1 origin gh-pages git worktree add ../docs gh-pages - name: Update the docs on the gh-pages branch run: | python docs/update_gh_pages.py docs/build/html ../docs - name: Configure Git run: | git config user.name "Documentation Bot" git config user.email "info@enthought.com" - name: Commit the changes run: | cd ../docs git add . # Only commit if there are changes git diff-index --quiet --cached HEAD || git commit -m "Automated update of main branch documentation" - name: Push the changes upstream run: | git push origin gh-pages envisage-7.0.3/.gitignore000066400000000000000000000002361441257372400153240ustar00rootroot00000000000000# file types to ignore *.pyc # ignore the build directories *.egg-info/ build/ /dist/ /docs/build/ /docs/source/api/*.rst !/docs/source/api/envisage.api.rst envisage-7.0.3/.readthedocs.yaml000066400000000000000000000004701441257372400165630ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: configuration: docs/source/conf.py python: install: - requirements: docs/requirements.txt - method: pip path: . envisage-7.0.3/CHANGES.rst000066400000000000000000000550041441257372400151410ustar00rootroot00000000000000================= Release history ================= Version 7.0.3 ============= Released: 2023-04-03 This is a bugfix release that fixes the automated documentation build for releases. Fixes ----- * Fix the GitHub Actions workflow for automated documentation updates on releases. (#568) Version 7.0.2 ============= Released: 2023-04-03 This is a bugfix release that fixes some test interaction issues and automates the documentation build. Fixes ----- * Fix test-ordering issues with egg-based tests that modify ``pkg_resources`` global state. (#564) Documentation ------------- * Reorganise the gh-pages documentation. The main gh-pages page (https://docs.enthought.com/envisage) now matches the contents of the GitHub ``main`` branch, while subdirectories (e.g., https://docs.enthought.com/envisage/7.0) provide documentation for released versions. A 'latest' symlink is in place, so https://docs.enthought.com/envisage/latest will bring up the documentation for the latest release. (#565) Build ----- * Add GitHub Actions workflows to automate documentation updates. The main documentation is updated on every push to ``main``, while the release documentation is updated on each GitHub release creation. (#565) Version 7.0.1 ============= Released: 2023-03-24 This is a bugfix release whose main purpose is to fix the Read the Docs build for the Envisage documentation. Fixes ----- * Fix the documentation build for Read the Docs. (#560) * Fix example code that was importing ``Font`` and ``Color`` from Traits instead of TraitsUI. (#558) Version 7.0.0 ============= Released: 2023-03-24 This is a major release aimed at modernization and cleanup of some out-of-date code. In particular, this release removes the IPython-related plugins, and drops support for Python 3.6. Thanks to: * Mark Dickinson * Chengyu Liu * Corran Webster Features -------- * There's a new ``unbind_extension_point`` function that reverses the effects of ``bind_extension_point``. This can be useful for clean teardown. (#546) * All id string constants that were previously available from ``envisage.ids`` are now also exported in ``envisage.api``. Users are encouraged to import everything they need from ``envisage.api``, and to open an issue if anything they need is not exported in ``envisage.api``. (#508) Changes ------- * When exiting a ``TasksApplication``, the plugins are now stopped after exiting the event loop. Previously they were stopped while the event loop was still running, causing some lifecycle issues. (#524) * The ``extension_registry`` argument to ``bind_extension_point`` is no longer optional, and ``ExtensionPointBinding`` will no longer look for an extension registry on the ``ExtensionPoint`` class. (#545) * Operations that used to raise ``SystemError`` now raise something more appropriate. In particular, the ``PluginManager.start_plugin`` and ``PluginManager.stop_plugin`` methods now raise ``ValueError`` rather than ``SystemError`` when given an invalid plugin id. (#529) * ``envisage.__version__`` is no longer defined. If you need the Envisage version at runtime, use ``importlib.metadata`` to retrieve it. (#513) * Python 3.6 is no longer supported. All current versions of Python (3.7 through 3.11) are supported. (#513) Fixes ----- * The ``repr`` of a ``Plugin`` instance now correctly uses the name of the plugin class. (#535) Deprecations ------------ * The ``EggPluginManager``, ``EggBasketPluginManager`` and ``PackagePluginManager`` are deprecated, and will be removed in Envisage 8.0. (#540) * The ``include`` and ``exclude`` traits on the ``PluginManager`` are deprecated, and will be removed in Envisage 8.0. (#544) Removals -------- * Plugins and machinery related to IPython have been removed. Specifically, the ``IPythonKernelPlugin`` and ``IPythonKernelUIPlugin`` plugins have been removed, along with supporting classes ``InternalIPKernel`` and ``IPKernelApp``. (#496) * The ``ExtensionPoint.bind`` method has been removed. (#545) * The previously deprecated ``safeweakref.ref`` class has been removed. (#522) * Some legacy unmaintained examples have been removed. (#557) Documentation ------------- * The changelog is now included in the built documentation. (#550) Tests ----- * Old-style namespace packages used for testing have been replaced with normal non-namespace packages. This fixes some warnings from the latest ``setuptools``. (#543) * The test suite now runs cleanly under ``pytest``. (#539) * Tests that are skipped due to a PySide6 problem should now be run if the version of PySide6 is recent enough. (#554) Build ----- * Package configuration now uses ``pyproject.toml`` in place of the old ``setup.py``-based configuration. (#513) * Optional dependencies are no longer declared. (#513) * A new style-checking workflow has been added that runs ``black``, ``isort`` and ``flake8`` over the codebase, and new code is expected to comply with ``black`` and ``isort`` configurations. (#549) * A new documentation-build workflow has been added. The built documentation is uploaded as an artifact. (#551) Version 6.1.1 ============= Released: 2023-02-07 This is a bugfix release that fixes an incompatibility between the workbench code and the latest Traits version, among other minor fixes. Thanks to: * Mark Dickinson * Prabhu Ramachandran * Scott Talbert Fixes ----- * A trait validation error in the Workbench ``DefaultActionSet`` has been fixed. This fixes a compatibility issue with Traits 6.4. (#485) * Initialization of application directories now correctly respects the value of ``self.state_location``. (#490) * In the test suite, egg generation now uses ``sys.executable`` to ensure it picks up the correct Python executable. (#499) Other changes ------------- * The version of PySide6 used in test workflows has been restricted. (#487) * The EDM version used in test workflow has been updated. (#484) * Various fixes have been made to the GitHub Actions workflows, for compatibility with the newest runners. (#491, #494) * Copyright headers have been updated for 2023. (#493) Version 6.1.0 ============= Released: 2022-08-15 This is a minor feature release whose main focus is on compatibility with Python 3.8 and PySide 6. It includes a collection of other cleanups and minor fixes. In this release, there are some changes in the way that the Envisage ``Application`` interacts with ``ETSConfig``. You should double check that the locations of user data, preferences and application home directories are the ones that you expect after upgrading. Please note that the IPython-related portions of Envisage are currently not compatible with the latest versions of ipykernel and IPython available from PyPI. This has been made explicit in this release in the form of version restrictions on those packages in the ``envisage[ipython]`` install target. Thanks to: * Aaron Ayres * Mark Dickinson * Sai Rahul Poruri * Corran Webster Changes ------- * The ``Application.user_data`` directory no longer includes the id of the application, but instead matches the ``ETSConfig.user_data``. (#467) * The Envisage ``Application`` will no longer try to change the ``ETSConfig.application_home`` attribute. (#467) * The ``PackageResourceProtocol`` now uses ``importlib.resources`` instead of ``pkg_resources``. (#466) * The IPython-related features of Envisage require ipykernel version < 6 and IPython version < 8. (#449) Fixes ----- * Fix EggPluginManager to use current pkg_resources.working_set. (#444) Refactoring and maintenance --------------------------- * Simplify ImportManager by using importlib. (#465) * Update end year in copyright headers. (#458) Tests ----- * The tests no longer rely on pre-built eggs for test packages. (#459, #436) * Skip tests for recent ipykernel, and add check for IPython version. (#457) * Work around a Python 3.6 issue with ``isinstance`` and lru caches in tests. (#470) * Fix an ``EggBasketPluginManager`` test that only passed due to test interactions. (#443) * Some pickles used in testing have been regenerated in order to work correctly with more recent versions of Traits. (#472) * Fix poorly specified action and menu groups in tests. (#468) * Fix test hangs with PySide2 / macOS 11. (#454) * Skip an ipykernel-using test if ipykernel is not available in the test environment. (#423) Examples -------- * Fixes for the Attractors example. (#408, #416) Documentation -------------- * Code samples in the documentation now have a "copy" button. (#474) * Stylistic changes and updates to documentation. (#406) * Fix documentation links to examples on main branch. (#447) * Add a Read the Docs config file. (#434) * Miscellaneous minor fixes. (#435) Build and CI ------------ * Update build machinery to support Python 3.8 and PySide 6. (#477) * Add workflow to automatically upload releases to PyPI. (#478) * Set up Slack notification for cron jobs. (#433) * Cron job failures are now reported to the main Slack channel, not to the bots channel. (#473) * Add GitHub Actions workflow to test PyPI install. (#450) * The default branch has been renamed from master to main. (#446) * Update classifiers for Python 3.10. (#437) * Port CI from Appveyor to GitHub Actions. (#432, #426) * Simplify flake8 command in ``etstool.py``. (#431) * Traits version 6.2 or later is now required. (#410) Version 6.0.1 ============= Released: 2021-06-18 This bugfix release fixes the issue where Extension Point resolution was happening too eagerly, which caused issues during application startup time in certain cases. We recommend all users of Envisage to upgrade to this bugfix version. Fixes ----- - Revert PR #354, which caused the issue #417. (#422) Tests ----- - Ensure that the testsuite passes with minimal dependencies. (#423) - Add a regression test for issue #417. (#421) Version 6.0.0 ============= Released: 2021-05-14 This major release focuses on speeding up Envisage applications. We achieved this speedup by removing unused functionality in the package. Specifically, we removed the ``@contributes_to`` decorator and the code needed to handle methods decorated with the above decorator. Additionally, with this release, parts of envisage start using the new traits observation framework instead of the old traits ``on_trait_change``. So, Envisage now depends on Traits version >= 6.2. Features -------- - Support ``observe(name:items)`` for Extension Points. (#354) Changes ------- - Replace ``Either`` trait type with ``Union``. (#405) - Rewrite ``*_changed`` static trait handlers to use ``observe``. (#401) - Replace ``depends_on`` in ``Property`` traits with ``observe``. (#400) - Change default pickle protocol to be compatible with Python >= 3.4. (#390) Removals -------- - Remove ``contributes_to`` decorator and supporting code. (#402) - Remove unnecessary return statements throughout the codebase. (#393) Build ----- - Ensure that the cron job installs all necessary dependencies. (#383) Version 5.0.0 ============= Released: 2020-12-03 This is a major release mainly relating to code modernization. In this release, support for Python versions <3.6 have been dropped. The class_load_hooks and single_project modules have been removed. Additionally, there were various fixes to bugs, examples, tests, and documentation. Demo examples are also distributed as package data such that they are visible via the "etsdemo" GUI application (to be installed separately). Features -------- - Re-export CorePlugin in envisage.api (#332) - Create and fill plugin subpackage api modules (#323) - Add relevant classes to envisage.ui.tasks.api (#322) Fixes ----- - Fix index slice in ExtensionPointChangedEvent when plugin changes (#357) - Fix ValueError from unregistering services when application stops (#345) - Fix the MOTD example (#319) - Fix the Hello_World example (#318) - Fix the attractors tasks application example (#317) - Make TasksApplication.gui expect an IGUI interface, not a GUI instance (#301) Documentation ------------- - Contribute examples to etsdemo (#380) - Refactor documentation links to source on GitHub (#379) - Make example run from any directory (#377) - Setup intersphinx in docs (#343) - Add documentation for envisage APIs (#340) - Use jinja templates for API documentation (#339) - Improve API docs : document traits (#334) - Rebuild documentation, mostly to fix search functionality (#290) Deprecations ------------ - Deprecate safeweakref and replace its uses (#275) Removals -------- - Drop support for Python 3 versions older than Python 3.6. (#341) - Remove single_project (#331) - Remove class_load_hooks and ClassLoadHook (#321) Tests ----- - Add tests for ExtensionRegistry getters (#349) - Add tests to demonstrate behaviour when mutating extension point directly (#346) - Use mixin instead of having ProviderExtensionRegistryTestCase inherit from ExtensionRegistryTestCase (#335) - Switch on default warning flag for CI test command (#326) - Add test eggs for Python 3.9 and remove eggs for Python 2.7 (#289) Build ----- - Turn off macOS builds on Travis CI (#375) - Fix CI cron job setup to install apptools (#348) - Update setup.py to allow prerelease version (#344) - Add wx as being supported in etstool, add it back to CI, and test against wxPython v4.x (#336) - Update EDM version to 3.0.1 in Travis CI and Appveyor. (#297) - Stop reporting code coverage in CI (#288) - Fix CI setup on Linux, Windows (#287) - Remove support for PySide and PyQt4 from CI (#285) - Add Slack notification for Travis CI runs (#283) - Add flake8 check to etstool and CI (#268) Version 4.9.2 ============= Released: 2020-02-17 This is a bugfix release that fixes tests that assumed the existence of categories machinery (which is removed in Traits 6.0.0). Fixes ----- - Conditionally skip tests that fail against Traits 6.0.0 due to the removal of Categories. (#263) Version 4.9.1 ============= Released: 2020-02-13 This is a bugfix release aimed at ensuring compatibility with the upcoming Traits 6.0.0 release. Fixes ----- - Fix tests that fail against Traits 6.0.0 due to the removal of double nesting in list events. (#255) - Replace a comment mention of ``AdaptedTo`` with ``Supports``. (#253) - Remove dependence on ``clean_filename`` from Traits. (#252) - Replace a use of the deprecated ``DictStrAny`` trait with ``Dict(Str, Any)``. (#250) 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) envisage-7.0.3/LICENSE.txt000066400000000000000000000031201441257372400151520ustar00rootroot00000000000000This 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. envisage-7.0.3/MANIFEST.in000066400000000000000000000004201441257372400150650ustar00rootroot00000000000000include 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 envisage-7.0.3/README.rst000066400000000000000000000036731441257372400150330ustar00rootroot00000000000000========================================== envisage: extensible application framework ========================================== 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 or its own. - 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. You are free to use: - the envisage ``CorePlugin`` available through the `envisage.api `__ module - plug-ins from the envisage `plugins `__ module - 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 >= 3.7. Envisage requires: * `apptools `_ * `traits `_ Envisage has the following optional dependencies: * `Pyface `_ * `TraitsUI `_ To build the full documentation one needs: * `Sphinx `_ version 2.1 or later. * The `Enthought Sphinx Theme `_. envisage-7.0.3/docs/000077500000000000000000000000001441257372400142635ustar00rootroot00000000000000envisage-7.0.3/docs/Makefile000066400000000000000000000060761441257372400157340ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/envisage.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/envisage.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." envisage-7.0.3/docs/requirements.txt000066400000000000000000000001571441257372400175520ustar00rootroot00000000000000# Requirements for building documentation Sphinx==6.1.3 enthought-sphinx-theme==0.7.2 sphinx-copybutton==0.5.1 envisage-7.0.3/docs/source/000077500000000000000000000000001441257372400155635ustar00rootroot00000000000000envisage-7.0.3/docs/source/api.rst000066400000000000000000000002311441257372400170620ustar00rootroot00000000000000API documentation ================= This section contains auto-generated API documentation for Envisage. .. toctree:: :maxdepth: 3 api/envisage envisage-7.0.3/docs/source/api/000077500000000000000000000000001441257372400163345ustar00rootroot00000000000000envisage-7.0.3/docs/source/api/envisage.api.rst000066400000000000000000000007461441257372400214460ustar00rootroot00000000000000.. (C) Copyright 2007-2023 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.api module =================== .. automodule:: envisage.api envisage-7.0.3/docs/source/api/templates/000077500000000000000000000000001441257372400203325ustar00rootroot00000000000000envisage-7.0.3/docs/source/api/templates/modules.rst_t000066400000000000000000000012341441257372400230570ustar00rootroot00000000000000.. (C) Copyright 2007-2023 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! {% macro automodule(modname, options) -%} .. automodule:: {{ modname }} {%- for option in options %} :{{ option }}: {%- endfor %} {%- endmacro -%} {{ [basename, "module"] | join(' ') | e | heading }} {{ automodule(qualname, automodule_options) }} envisage-7.0.3/docs/source/api/templates/package.rst_t000066400000000000000000000016231441257372400230040ustar00rootroot00000000000000.. (C) Copyright 2007-2023 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! {% macro automodule(modname, options) -%} .. automodule:: {{ modname }} {%- for option in options %} :{{ option }}: {%- endfor %} {%- endmacro -%} {%- macro toctree(docnames) -%} .. toctree:: {% for docname in docnames %} {{ docname }} {%- endfor %} {%- endmacro -%} {{ [pkgname, "package"] | join(" ") | e | heading }} {{ automodule(pkgname, automodule_options) }} {%- if submodules %} {{ toctree(submodules) }} {% endif %} {%- if subpackages %} {{ toctree(subpackages) }} {% endif %} envisage-7.0.3/docs/source/changelog.rst000066400000000000000000000000371441257372400202440ustar00rootroot00000000000000.. include:: ../../CHANGES.rst envisage-7.0.3/docs/source/conf.py000066400000000000000000000207641441257372400170730ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 file is execfile()d with the current directory set to its containing # dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed # automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import importlib.metadata import enthought_sphinx_theme # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. # sys.path.append(os.path.abspath('some/directory')) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.githubpages", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.viewcode", "sphinx_copybutton", "traits.util.trait_documenter", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The master toctree document. master_doc = "index" # General substitutions. project = "envisage" copyright = "2007-2023, Enthought" # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. version = release = importlib.metadata.version("envisage") # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = "%B %d, %Y" # List of documents that shouldn't be included in the build. # unused_docs = [] # List of directories, relative to source directories, that shouldn't be # searched for source files. # exclude_dirs = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # Substitutions reusable for all files. rst_epilog = """ .. # substitutions for API objects .. |Application| replace:: :class:`~envisage.application.Application` .. |IApplication| replace:: :class:`~envisage.i_application.IApplication` .. |ExtensionPoint| replace:: :class:`~envisage.extension_point.ExtensionPoint` .. |Plugin| replace:: :class:`~envisage.plugin.Plugin` .. |IPlugin| replace:: :class:`~envisage.i_plugin.IPlugin` .. |envisage.api| replace:: :mod:`envisage.api` .. |envisage.ui.workbench| replace:: :mod:`envisage.ui.workbench.api` .. # substitutions for the Hello World example .. |Hello World| replace:: :github-demo:`Hello World ` .. # substitutions for MOTD examples .. |acme.motd| replace:: :github-demo:`acme.motd ` .. |acme.motd.software_quotes| replace:: :github-demo:`acme.motd.software_quotes ` .. |MOTD| replace:: :github-demo:`MOTD ` .. |IMOTD| replace:: :github-demo:`IMOTD ` .. |MOTDPlugin| replace:: :github-demo:`MOTDPlugin ` .. |MOTD run| replace:: :github-demo:`run.py ` .. |IMessage| replace:: :github-demo:`IMessage ` .. |Message| replace:: :github-demo:`Message ` .. |messages.py| replace:: :github-demo:`message.py ` .. |Message of the Day| replace:: :github-demo:`Message of the Day ` """ # noqa: E501 # Options for HTML output # ----------------------- # Use the Enthought Sphinx Theme (see # https://github.com/enthought/enthought-sphinx-theme) html_theme_path = [enthought_sphinx_theme.theme_path] html_theme = "enthought" # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = "Envisage Documentation" # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = "%b %d, %Y" # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. html_use_modindex = False # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. # html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = "Envisagedoc" # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class # [howto/manual]). latex_documents = [ ( "index", "Envisage.tex", "Envisage Documentation", "Enthought, Inc.", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = "enthought_logo.jpg" latex_logo = "e-logo-rev.png" # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_use_modindex = True # -- Options for intersphinx extension --------------------------------------- intersphinx_mapping = { "pyface": ("https://docs.enthought.com/pyface", None), "python": ("https://docs.python.org/3", None), "traits": ("https://docs.enthought.com/traits", None), "traitsui": ("https://docs.enthought.com/traitsui", None), } # -- Options for extlinks extension ------------------------------------------- extlinks = { "github-demo": ( f"https://github.com/enthought/envisage/tree/{version}/envisage/examples/demo/%s", # noqa: E501 "", ) } # -- Automated apidoc building def run_apidoc(app, config): """ Hook to autogenerate API documentation via sphinx-apidoc. """ import pathlib import sphinx.ext.apidoc root = pathlib.Path(__file__).parent.parent.parent docs = root / "docs" envisage = root / "envisage" apidoc_args = [ "--separate", "--no-toc", "--output-dir", docs / "source" / "api", "--templatedir", docs / "source" / "api" / "templates", envisage, "*/tests", # exclusions ] sphinx.ext.apidoc.main(list(map(str, apidoc_args))) def setup(app): app.connect("config-inited", run_apidoc) envisage-7.0.3/docs/source/e-logo-rev.png000066400000000000000000000075111441257372400202510ustar00rootroot00000000000000PNG  IHDRoi*sRGB pHYs  tIMELIDATx]ktT>z\$m ȥ!TD jV|_CWph PT\"B$$6aIf9qfB%= By}; vo`!4ChVv[12 !HGiyg):QVd8SmsQAj2~~AH3qU:?!4[a6SRu ǎ7H'1"_Qq!JbBwx$I-S^QO>AO~( HAPU)=Ojjm+ v$~ئ"33zviJn[*.\v(/E1U`Ycֿ&y3g>=x$;GS@]d1YÓo"۾X6n8o2 ,c_܊U?y" "cdL5HfFj~}Q]H錩/Oxcq'~lӕ_ ţeW\| &cLMhdȶ9-՗ $ Θڳ9i˗>xa6>#E _h2$}앿"a\l߰0/"ޑҦ.*:UQyٕ~`:oYfxu? b)<̜>җ'rYgԾ6ngeSMkm>uv" Snhj ̌ry_ݚLM01@$(]vƏ{_{#&>4l|c.8~rK05bjԈm;14*:Ο3yK|ީT\> 8nd٤B]j맻]8#&[5TEUlu#u\/kk^6t=Zo`Ӌ-,R'*EP1#EQ DfsnlOYYYҨ!${G2yZ~\pN|olӋnϯBu-\$5˘TYgNR^\8gF{@|4Ņ0ov2֊^:j)D"zM En1]WfN@wǛ뿨k B|c!>8T'JԉaZxubOW~;c%dLynظedNSt~WX\f-pO',9UI21`xĥd  ,{ER"Z G 4PLq@$#15! G}\.-2kEfV=G15Q&ph!9Ce Cvj(# 5#GX:InHJZmڞU__(h݆' H7cHκ})"Db-&`i\eU?*YJ05 D S[GabDěrqEʪ9կm"4LwtGTدr{OPۿhj?:}"i b:/7yA@eK#$t13mj51K &^w !%PSSSֆlr{s^#w4DmQI S#3a@57Q; S#:į v4yR+A&P0j/))-&Z4S.[Z2d^!j8J01-j(T!05Q)"jԌ+@vpd"'4LuyC͉cv,@A1i_qLq|s4bvGz!U !KIQD1E3[1vI $00h6FL̙dnu˞?SScw\LGaʃcf-N]y/4u: c c PM18_h>4~h޽f l%&N^>?2=iC)9v!˜j>hN'N~(aİ}Wx+' u0?1sL _/>_nH ! x9zq@bzlLؘO_6Ac6~t=F&מc2\汋rh3.婓Jx`x^_>_mqKkj+-++Y.zw3TU+qܹ~M\_:pBI" D5 JcTubd!P%+~fz*EP]6R2;/uz] g,'Nd=C^n188D,dZ}W/)~ǎ/z~*0P]g*ݐ[{s]b76 $?`[퍘JTDDKŽ t "((}qqwZΦO11fZ XSXk71E~;{GbN#"k" r@4˗mrN"srLڀ?Vh?݁nw'?0l۶`bF4]2UU ;llgL bkx'ۄ&%QU#c*B{awE|DǶBhZ-f/wIENDB`envisage-7.0.3/docs/source/envisage_core_documentation/000077500000000000000000000000001441257372400233255ustar00rootroot00000000000000envisage-7.0.3/docs/source/envisage_core_documentation/core.rst000066400000000000000000000007541441257372400250150ustar00rootroot00000000000000Core ==== The goal of the *Core* project is to provide a set of general-purpose tools for building extensible, pluggable applications. Hopefully, these tools are useful whether you are building an airline reservation system, a web server or a game of tetris! There are just 3 simple concepts at the heart of every Envisage application: 1) `Extension Points`_ 2) Services_ 3) Plugins_ .. _`Extension Points`: extension_points.html .. _`Plugins`: plugins.html .. _`Services`: services.html envisage-7.0.3/docs/source/envisage_core_documentation/extension_points.rst000066400000000000000000000176211441257372400274760ustar00rootroot00000000000000Extension Points ================ Whether or not we as software developers like to admit it, most (if not all) of the applications we write need to change over time. We fix bugs, and we add, modify, and remove features. In other words, we spend most of our time either fixing or extending applications. Sometimes we extend our applications by changing the actual code, sometimes we have other ad hoc extension mechanisms in place -- a text file here, a directory of scripts there. As applications grow, they often end up with numerous places where they can be extended, but with a different extension mechanism at each one. This makes it hard for developers who want to extend the application to know a) *where* they can add extensions, and b) *how* to add them. Envisage attempts to address this problem by admitting up front that applications need to be extensible, and by providing a standard way for developers to advertise the places where extension can occur (known as *extension points*), and for other developers to contribute *extensions* to them. In Envisage, extension points and the extensions contributed to them are stored in the *extension registry*. To see how extension points actually work, let's take a look at the |Message of the Day| example included in the Envisage distribution. This example shows how to build a very simple application that prints a (hopefully witty, educational, or inspiring) "Message of the Day" chosen at random from a list of contributed messages. Declaring an Extension Point ---------------------------- Plugins declare the extension points declaratively - using the |ExtensionPoint| trait type. Note that extension points can also be defined programmatically - by overriding the 'get_extension_points' method. In the MOTD example, the |acme.motd| plugin needs to advertise an extension point that allows other plugins to contribute new messages. Using the |ExtensionPoint| trait type, the plugin would look like this:: class MOTDPlugin(Plugin): """ The MOTD Plugin. """" ... #: The messages extension point. messages = ExtensionPoint( List(IMessage), id='acme.motd.messages', desc = """ This extension point allows you to contribute messages to the 'Message Of The Day'. """ ) ... This tells us three things about the extension point: 1) That the extension point is called "acme.motd.messages" 2) That every item in a list of contributions to the extension point must implement the |IMessage| interface. 3) That the extension point allows you to contribute messages! Making contributions to an Extension Point ------------------------------------------ The |Message of the Day| example has a second plugin, |acme.motd.software_quotes| that contributes some pithy quotes about software development to the application. First of all, we have to create the messages that we want to add. Remember that when the |acme.motd| plugin advertised the extension point, it told us that every contribution had to implement the |IMessage| interface. Happily, there is a class that does just that already defined for us (|Message|) and so we create a simple module (|messages.py|) and add our |Message| instances to it:: messages = [ ... Message( author="Martin Fowler", text=( "Any fool can write code that a computer can understand. Good" " programmers write code that humans can understand." ), ), Message( author="Chet Hendrickson", text=( "The rule is, 'Do the simplest thing that could possibly" " work', not the most stupid." ), ), ... ] Now we create a plugin for the |acme.motd.software_quotes| package and tell Envisage about the messages that we have just created. This can be done declaratively - using the 'contributes_to' trait metadata. Note that this can also be achieved programmatically - by overriding the 'get_extensions' method, similar to how we could define extension points programmatically by overriding the 'get_extension_points' method. The declarative version looks like this:: class SoftwareQuotesPlugin(Plugin): """ The software quotes plugin. """ ... # The 'contributes_to' trait metadata tells Envisage the ID of the # extension point that this trait contributes to. messages = List(contributes_to='acme.motd.messages') def _messages_default(self): """ Returns the default value for the ``messages`` trait. """ # It is good practise to only import your extensions when they # are actually required. from .messages import messages return messages ... The messages are contributed simply by creating a list trait and setting its "contributes_to" metadata to the ID of the extension point that we want to contribute to. All we have to do then is to intialize the trait with our messages and "Job done"! Note that if a plugin changes a list of contributions then the extension registry will be updated automatically, and anybody that is consuming the extensions will be notified accordingly. The difference between the programmatic and the declarative version is that the application is not automatically notified if the plugin wants to change its contributions to an extension point. To do this manually fire an 'extension_point_changed' event. Retrieving the contributions to an Extension Point -------------------------------------------------- OK, here's where we are so far: One plugin (|acme.motd|) has advertised the fact that it has an extension point called "acme.motd.messages", and that the contributions to the extension point must implement the |IMessage| interface. Another plugin (|acme.motd.software_quotes|) has kindly offered to contribute some messages about software development. Now we need to know how to retrieve the contributed messages at runtime. In the MOTD example, the messages are retrieved by the |acme.motd| plugin:: class MOTDPlugin(Plugin): """ The MOTD Plugin. """" ... # The messages extension point. messages = ExtensionPoint( List(IMessage), id='acme.motd.messages', desc = """ This extension point allows you to contribute messages to the 'Message Of The Day'. """ ) ... def _motd_default(self): """ Returns the default value for the motd trait. """ # Only do imports when you need to! from .motd import MOTD return MOTD(messages=self.messages) ... As you can see, all we have to do is to access the **messages** extension point trait when we create our instance of the |MOTD| class. This example demonstrates a common pattern in Envisage application development, in that contributions to extension points are most often used by plugin implementations to create and initialize services (in this case, an instance of the |MOTD| class). The extension registry can also be accessed through the following method on the |IApplication| interface:: def get_extensions(self, extension_point): """ Return a list containing all contributions to an extension point. Return an empty list if the extension point does not exist. """ For example, to get the messages contributed to the "acme.motd.messages" extension point you would use:: messages = application.get_extensions('acme.motd.messages') Note however, that using the |ExtensionPoint| trait type, adds the ability to validate the contributions -- in this case, to make sure that they are all objects that implement (or can be adapted to) the |IMessage| interface. It also automatically connects the trait so that the plugin will receive trait change events if extensions are added/removed to/from the extension point at runtime. .. _`Python Eggs`: http://peak.telecommunity.com/DevCenter/PythonEggs envisage-7.0.3/docs/source/envisage_core_documentation/front.rst000066400000000000000000000033171441257372400252130ustar00rootroot00000000000000==================== Front Matter ==================== :Authors: Martin Chilvers :Version: Document Version 1 :Copyright: 2008 Martin Chilvers. All Rights Reserved. Redistribution and use of this document in source and derived forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source or derived format (for example, Portable Document Format or Hypertext Markup Language) must retain the above copyright notice, this list of conditions and the following disclaimer. * Neither the name of Enthought, Inc., nor the names of contributors may be used to endorse or promote products derived from this document without specific prior written permission. THIS DOCUMENT 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 HOLDERS 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 DOCUMENT, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. All trademarks and registered trademarks are the property of their respective owners. | Enthought, Inc. | 515 Congress Avenue | Suite 2100 | Austin TX 78701 | 1.512.536.1057 (voice) | 1.512.536.1059 (fax) | http://www.enthought.com | info@enthought.com envisage-7.0.3/docs/source/envisage_core_documentation/glossary.rst000066400000000000000000000020771441257372400257300ustar00rootroot00000000000000Glossary ======== Application The thing that plugins plugin to! Extension Point A well defined place where new functionality and/or data can be contributed to an application. Extension (aka Contribution) An *actual* piece of functionality or data that is contributed (this is why extensions are often known as contributions). Think of 'extension points' as the 'where' and 'extensions' as the 'what'. Extension Registry The place where (by default) all of the extension points and their associated extensions are stored. Service In Envisage, a service is *any* object that a developer thinks is sufficiently useful to want to share it. Service Registry The 'yellow pages' style mechanism that is used to publish and look up services. Plugin The mechanism for delivering new functionality to an application. A plugin can do 3 simple things: 1) offer extension points 2) make contributions to extension points (including its own) 3) create and publish services Plugin Manager Finds, starts, and manages the plugins that make up an application. envisage-7.0.3/docs/source/envisage_core_documentation/gui_application.rst000066400000000000000000000033261441257372400272320ustar00rootroot00000000000000GUIApplication ============== The :py:class:`GUIApplication` subclass of the standard Envisage :py:class:`Application` class uses Pyface's cross-platform GUI tools to ensure that an appropriate back-end application object is instantiated before plugins run their :py:meth:`start` methods, and then ensures that the GUI mainloop is started after the :py:meth:`start` methods have successfully completed. This allows developers to have a single place to start the application, but have plugins to listen for the :py:attr:`application_initialized` event to create UI components. In particular, Traits UIs can be created in an event listener method by calling :py:meth:`edit_traits`, something like this:: class MyViewPlugin(Plugin): model = Instance ui = Instance('traitsui.ui.UI') @on_trait_change('application:application_initialized') def create_ui(self): # we need to keep a reference to the ui object self.ui = self.model.edit_traits() This is intended to be a cheap and simple way to take an existing application and start "lifting" it to Envisage, without having to use a more heavyweight UI framework such as Tasks immediately. Developers can work on adding extensibility to the application's model without having to at the same time add extensibility to their UI. The :py:class:`GUIApplication` exposes the following public API: .. py:attribute:: gui The Pyface :py:class:`GUI` instance the :py:class:`GUIApplication` creates. .. py:attribute:: splash_screen An optional Pyface :py:class:`ISplashScreen` that gets shown while the plugins are started. .. py:attribute:: application_initialized An event which is fired after the UI mainloop has started. envisage-7.0.3/docs/source/envisage_core_documentation/howto_create_a_plugin.rst000066400000000000000000000252231441257372400304240ustar00rootroot00000000000000How To Create a Plugin for an Envisage Application ================================================== This document describes the process of creating an Envisage plugin that works with a Workbench-based application. There are several questions to consider when designing a plugin: * What does it do? * What functionality does it offer to other plugins? * What does it add to what other plugins already do? * What does it need from other plugins? What Does Your Plugin Do? ------------------------- "What your plugin does" refers to the basic functionality that your plugin adds to the application. Very often, you want to take some pre-existing chunk of functionality (module, library, etc.) and make it available within the Envisage application. Assuming the library has a well-defined API, you do not need to alter it in any way. You only create the plugin code to wrap it for Envisage. Sometimes, however, you are designing a new chunk of functionality just for the Envisage application. In this case, step back and think about how you would design the core functionality of the plugin, independent of Envisage. It is important to keep this separate from the plugin machinery that make that functionality work within Envisage. In either case, we will refer to the code that does this core functionality as "the library". Example Library ~~~~~~~~~~~~~~~ Suppose you have a library that implements a game of Tetris. You want to add this game to an Envisage application that lets users pick a game to play from a catalog of available games. You have a Tetris class that looks something like this:: class Tetris(HasTraits): # Basic colors background_color = Color foreground_color = Color # Shapes to use in the game shapes = List(IShape) ... In the following sections, we'll look at ways that this library can be integrated into the application by a plugin. What Does Your Plugin Offer to Other Plugins? --------------------------------------------- There are two ways that a plugin can provide functionality to other plugins. * **Offering services:** Your plugin may provide an API, the core functionality of your library, as a service. Yours might be the only provider of that type of service in an application, or it might be one of many. * **Defining extension points:** Your plugin may offer one or more extension points, which means that your plugin will do something specific with contributions to that extension point. Contributions to an extension point must be instances of a type or interface that you specify. An extension point is a trait attribute whose type is ExtensionPoint, which consists of a List of some other type, plus an ID. In other words, an extension point is a list, used by your plugin, that is populated by plugins. Examples of Offering Functionality to Other Plugins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Suppose that the games application defines an "IGame" interface that it uses to control games, with methods for starting, stopping, displaying scores, and so on. In this case, you offer the Tetris library as a service that implements the "IGame" interface used by the application. (You might need to create an adapter for your Tetris class to the IGame interface, but we'll ignore that.) You can manually register a service, but a simple way to do it is to contribute to the 'envisage.services_offers' extension point of the Core plugin. This ensures that the plugin is registered, and is created when it is needed. You also want to allow users to contribute their own Tetris shapes, so you define an extension point for shapes. We'll leave aside the question of how users actually define their shapes. The point is that the catalog of shapes is extensible. (You would probably also contribute some basic shapes from your plugin, so that users don't *need* to contribute any.) What Does Your Plugin Add to Other Plugins? ------------------------------------------- Other plugins may provide extension points that are useful to your plugin. If so, you can contribute to those extension points. Essentially, your plugin passes one or more objects to the plugin whose extension point you are contributing to, and that plugin "does the right thing" with those items. Examples of Contributing to Extension Points ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Continuing the example of the Tetris game, the application might keep a list of available games, and present the list for the user to select from. Thus, it might have a 'games' extension point, to which you can contribute an object containing information about your Tetris game, such as name, description, icon, and entry point. More concretely, you want to specify default colors for the foreground and background colors of the game, but allow users to change them and save their changes across sessions. For specifying default preference values and saving changed values, you can contribute to the 'envisage.preferences' extension point offered by the Envisage core plugin; for a UI to change preferences, you can contribute to the 'envisage.ui.workbench.preferences_pages' extension point offered by the Workbench plugin. (A game application probably wouldn't use the Workbench plugin, but we'll assume it does to avoid using a fictional plugin.) A contribution to the preferences extension point must be a URL of a preferences file (readable by ConfigObj). A plugin typically has only one preferences file, even if it has many categories of preferences. A contribution to the preference_pages extension point must be a callable that returns an object that implements the `apptools.preferences.ui.api.IPreferencesPage` interface. Such an object typically has a Traits UI view that can be used in the Preferences dialog box to set the values of the preference attributes. A plugin may have multiple preferences pages, depending on how it groups the items to be configured. There are two strategies for defining a callable that returns an object: * Subclass from a class that implements the interface, in this case `apptools.preferences.ui.api.PreferencesPage`. * Define a factory function that returns an appropriate object. This strategy is not needed for preferences pages, but can be helpful when the object being returned contains a reference to a service. In either case, the contribution is a trait attribute on the plugin object. What Does Your Plugin Need from Other Plugins? ---------------------------------------------- Your plugin may need to use the API of some other plugin. In Envisage, you use the other plugin's API via a service, rather than directly. This allows for the plugin offering the service to be replaced with another one, transparently to your plugin. There may be multiple plugins offering a particular type of service, but a client plugin uses only one instance of a service at any given time. Example of Using a Service ~~~~~~~~~~~~~~~~~~~~~~~~~~ The service you use may be your own. For the Tetris game, the object that you contribute to the application's 'games' extension point needs to be able to start the game. However, to reduce memory overhead, you don't want the Tetris library to be imported until the user actually chooses to play Tetris. Using the service offered by the Tetris plugin is a way to accomplish that. Complete Example ---------------- The complete plugin for the Tetris game might look like this:: class TetrisPlugin(Plugin): """ Plugin to make the Tetris library available in Envisage. """ ### Extension points offered by the plugin #: Shapes to be used in the game shape = ExtensionPoint(List(IShape), id='acme.tetris.shapes') ### Contributions to extension points my_shapes = List(contributes_to='acme.tetris.shapes') def _my_shapes_default(self): """ Trait initializer for 'my_shapes' contribution to this plugin's own 'shapes' extension point. """ return [Shape1(), Shape2(), Shape3()] games = List(contributes_to='acme.game_player.game_infos') def _games_default(self): """ Trait initializer for 'games' contribution to the application plugin's 'games' extension point. """ return [ GameInfo( name='Tetris', icon='tetris.png', description='Classic shape-fitting puzzle game', entry_point=self._start_game, ), ] preferences = List(contributes_to='envisage.preferences') def _preferences_default(self): """ Trait initializer for 'preferences' contribution. """ return ['pkgfile://acme.tetris.plugin/preferences.ini'] preferences_pages = List( contributes_to='envisage.ui.workbench.preferences_pages' ) def _preferences_pages_default(self): """ Trait initializer for 'preferences_pages' contribution. """ from acme.tetris.plugin.preferences_pages import ( TetrisPreferencesPages, ) return [TetrisPreferencesPages] services_offers = List(contributes_to='envisages.service_offers') def _service_offers_default(self): """ Trait initializer for 'service_offers' contribution. """ return [ ServiceOffer( protocol=IGame, factory=self._create_tetris_service, properties={'name':'tetris'}, ), ] #### Private interface ################################################# def _create_tetris_service(self, **properties): """ Factory method for the Tetris service. """ tetris = Tetris() # This creates the non-Envisage library object. # Hook up the extension point contributions to the library object trait. bind_extension_point(tetris, 'shapes', 'acme.tetris.shapes') # Hook up the preferences to the library object traits. bind_preference( tetris, 'background_color', 'acme.tetris.background_color' ) bind_preference( tetris, 'foreground_color', 'acme.tetris.foreground_color' ) return tetris def _start_game(self): """ Starts a Tetris game. """ game = self.application.get_service(IGame, "name == 'tetris'") game.start() envisage-7.0.3/docs/source/envisage_core_documentation/images/000077500000000000000000000000001441257372400245725ustar00rootroot00000000000000envisage-7.0.3/docs/source/envisage_core_documentation/images/application.png000066400000000000000000001000741441257372400276050ustar00rootroot00000000000000PNG  IHDRcsRGBgAMA a cHRMz&u0`:pQ< pHYsttfxIDATx^ݽu3hȀ)q= Ό GGPn ʜ!޽wo6EUUgﵚ?@@@T@@@|+j6&      @iZiw|@@@U5{@@(ZyČ@@@{T-  GwO@@'@պ   @yZyČ@@@{T-}?~{c+.cj?B z;AEMMd @vT-;RD  )Ƈ,z:ڟ~mW{zTLZ!@Zց矟t5U wCC٪cTi$U[; 򺖿 J=.;n?lQ:": NTm'8!@>k(?h5T"[ Tmˍr, -`'0]"U+o? PV03O_H>Amzx*sgL|nat')!b xH2=l}6Gwi/仳mK_ FvGN%bWBi$g8&BUKJ埧Ʉc O|4)l}RͶg?<4g/$"Qx'=vMN PɶG9?'w/iNi,?[ůٍ[I _NzN4o{kmܑHSUm^=sgOs?/O7Yor P ]*!0c GR1jS0ZmE|fN'}^oi7Fy|-Ë8%Mh-ޝ9-]I1M@  @fc%>ViC&GЙO*\Umwq힜Ɗ 5ękU؉8%If>{ro>AR擒|eYs@ Po "@&k_{Vr>-[vwMci`iSe*QnbGzQʶ y'pF3O}O/vVvjvS=JdRHtDut- eS)̙PUz5cj6Yr<=e;ncn\v{Ь?|ܺYUo}=U\] +f .2"\`\əP^j#WB^>IdߧdK0/ #!D+e%kG -" P0] J% ĴrU{Lޤha-i6# Wj[izk~X4ԎS6ڤo=JMwaJiSJ{`- @Zͤ@`4P?'9ʀڗ<~qGpiӪkкa3jGז9 S3D;NIR!-$wܣ u'=`@Ti 0O 13s'gBjO?4m1Aoo __wc'9O`?ʃt8pOqJ}q&p=׻ 8\O.ƪ  1ͷt:!}60w?~ =4'W{r_I7y1()i%ʩ?8%Q/LccދǏԭtnC[e 9|'1n"9tuRa}k4UkZu|VNݾt㧟?5Q׼d~_M+$O*= =-ӕɅ2rdV]j]vF#2IW˙Pҳa-GjW/{y@JA^l鑒Hݗy|? kmvAզw{tD̏0;9_Y4KEP"x֛cb;N)h EE7. 5Dd4޵U@T`  @}rnGCiw8 @( @֞b8\A#@SV~"-qJʌ PV 1@3@V;k) 3$ @iZiw|@@@T&@@@#@Պ%&  P5{@@(U+   @@@@8T[bB   U@@@Pn !  T@@@#@n7@@@@T-` ^_E@@ 5 i~HU"$$  U pߩZv&! TD8UR'  @=Z5j}d  T-_4@`j  @EZ3hrE1T@@PkAUN ".  p/ S3"k" D8 5@" CXCȨv=.  p/ S3"k" D8 )\@UQ;TUd  T-_4@`j  @EZ3hR$  @=Z5j}d  T-_4@`j  @EZ3hrE1T@@PkAUN ".  p/ [3^B  Z_~5@@N"@ըZ/FN ".  pFը3^Z  TQ5  @qUjŽ,|\@@6P5Fը  jTls @@8BQ5F@@(UjTO_  FըUC@@8TQ^m|d  GP5Fը  jT}q.  @UjT @@P5FՊ{Y9U  !@ըUj  GQ5Vȧ/E@hUjT!  PFըZq/6> @@#UjT @@P5FՊ{YŹ  mjTQ5@@#@ըU+e@V nYϩc8!UjT @ %0۷ߥcCl"pUjT7m# P)EYjXLCTQ5 Eԩtw8\FըZoNF@ #$m0q .@vTQ5 a"$2Uj 4p6쫣jTS  p)$]zLjTP  pwO8Ϻ8ҶԵ:FըuoNuEE@ /RiOx5–ֻ>TQ5 (ZS^ۧKQ5vS! P UjJQP#jT]]Ujj>SBMxV_HըR*jPv^se@ P5vsDFըZժ@V P5.m]T.*EB&CJ | pUjGs P5FN'@C#@`OIsҟMWv0 UUZ~o/|D_~}?o~T5Rk™QQ5Dͫ'xHNT툿_(e7=<+,q𲝐\U˵\gUjIV>Y2E{01j1 CPm FzWQ/tx߿gcʔq U2 m!gDk=Fo%DQˎP5jw= kTmSDD^H'"iØ9;7TcEzQjTmEnߏjXUKíxijW5f?y!\@QNJLJUB̷=4U!bbj2C P*٪rM`cjT-ZfEF:G@Z4mձ[i9{aۇQ5jvUϲHϒB il1euT!@ըȔ]s#̹NL_Gըy˕U;Aզ)f_aT!@4Jju?'@ըZ6Ua֞~$6ڧSe lVjsc?1!G&Vgk[jEWcTTB{~/)wo^U` P3TM(ڧ4v Frsam\_ʞ~ȯmmR^~z{qm`BOY~ [{5vljT-@ "Oҩ8|o^{=7VT@1nqiF+KպV#L8ܯoFx}Aʠf,!Лi,g]c.'0$Э zWnUxډR Uja-5YKTh3.Z,֨U;Ujv7Ou5 !IA"й*jㅏ۟s-%|K0FU^K9U+!ؙjyf+m~lFޛ{iۯP5FN' 7=- IUjTtTMEvCNըp,qifP'@ը[2[ójTN5C- !Fռ"5Z/%@ը颒Xzj0&wA<5~=56gkJj kmMc: 3FըZjs9=y[KT  @.TQ+Ku#Пyz`kj:A(TQ+i tiom;!.CZP5Fը; jǨ&5mxEP5FvJcOѳ Fըz% J TKEQ5FL{OSX[3}xZu@Fըz%X 0"@|c-EѡQ@ UjT!O{4Y6OK̽$jT[/'uBQޕQH@ UjT!O4mӉ񴔬1 &TQ|kKjTmAx]x@ UjT!Uj)oc5cW Oy,tUjTmC^kT Ӟ=MѰfeP7) PFըچd׶AQv7ik<-}K k-d'@ըUKJӳ\:Ti 6z+_9[[@TQ5@F.x.DHZʽ,eAXTQ4=jP5v>IKU{A#uDjTQ5P5v[Foo#$@ըUKJ_.UjQ[la#il36w#BQ5FH"@ըZcлP|gim?:\شZ6|eP5VȮMz]tN%@պS0O O %=\Q2y'neοY5ilMhj!-djKjT-rP"/T-;Uk#fQgX{Z3:sZ3|NS5q86ck~K74w+DD4v3.T- 懤j P5v|Bu46 7O@U-TbjTGz45/"@b!dUU+*T:6j>9@/ݡ)"T-_4@`jb.*oaF՚%@Պ&@'Z3hMTYSyU4+S2c !v>hUjY6O P5ήY Uj4bxjΠ2H O[lA]V xZUkZ"@BEdvRh-ﵵH[_֯T:oT\Z3hRdzkkB<ګy͸\ MZh fShp5.w6T]@   ckY0 i$T$ЎAT-BF|PUkbQ]dޑY9P5Ն PLoH`l3YR~C`MTּ{z H8 n U bn$jT-jzj12j\f T?E #/ Så@ Pylx[ZZ3hR$ @FժHMT-BF|P ѬB nT-_4@`;L8Ujy-U p 1}:Sf2+@bTo9!0IXCȨ]GDH'йyL*D<T-_4@`s^qe@==wUJ8 5@"l!ЭQJznUQ;O{ˊ@ thk.T@:/ {I DjvjΠ2-m?Q @Q5kLc !v>@`~l{,N"@BEdvo?'E'Љyl~'[`uZ3hR$ ^O.7PkA Ѭ8U; "/ S5@m[䑍\#@Ad t {~ @EI[iumBUQ;[QWb@4fkO'.@T-_4@`n%d @3fFpT-4@@@ [TstCFXCȨ=ICHPyO̸\ Pyqĕ@> TjkܮV]#  ckSc3g@<}peKXCȨ)op5@*l@uZh -d @EJ~ЈbZETjΠ2  '(MH PkAUժU&TDa#imSE`U  ة8 p%(a#iWec!p*  ckXIw$AahuGmfZ5C#P=r/}wY p[x΂өZh .k/~Y2z=e}=s,g=XT-4@@uWl={bwɒ:D]I\N9ܙ!is烪li<^lZ+E @l݅-K  j1;U>L\h@luQkAĠప AazO<-1-tĨZh .]P KIO @ rV꺨Z3hըu;l2Vu!p=x>)aDc !v>WB1Ԙyl6/6+#zceT-_4@`Ļ ԛy!P |}uޔZ3h( h Ht69y +#6Iu:c !v> b4jc[,y 4*e5yGWj!2Vl2#K>R>TZ>a@ a8 m!XO䃭U=HSJM" JV>7Uc !v>p/F8d \ζ|߾:Icac;v/ emDzW|֊z.fG>]صX]zқRg]ȶ,ؚdѳiyM1i;sI}Nb!dd.Ca}w}}K@au*NJrڜ鳥j!2(Y>R9a`H vb IK  c{oɈWaoG~ ^ZI[,}v)0H Msc !v>dsQ kowO+G҆3Q^{ w@ARKW{u(] 5B qMr  c{wAl+B&kkRICak{:^-m7uC=`^y"26/팯J<6A)$f %\8Z tUVjJ%9t'c/Eb!dbcsP{/1& 6c'W U  ؅pc E]P㫶yH"'<㽷9Wp@T-4@@h % -jYaC}l_7G?d%J>泃\6m[}l}< g.>ߥ=ǣe Wj!2UB1U;mQT8FW(8e>E}jΠ2H P}o:4&6RөZ5>[*6 X:UK-GvjЗT`l,t/ SbÐPzdOF&~/|BΥjΠ2 ӠjTmz5Q`?XCȨ곥HV7I}BiM`*΢j!2;U"$2dk˒%r}Z3hRdd$2EǞֺ5Q`k9XCȨVKT22ɎU)Ɗ֩Rm/DFu[vi kƅ'[J~ҎjΠ2J GFBն:GA6nk#U+:XCȨꃥ׭FնZu6}T-_4@`jq%&%^ n.**\XZFAd t  )joJjMkښHңbuGRkA}^]x2IҠjE֦ě;TdTm?yukLkjTĨabcU p 15@6@=[#d^Q5H Cb!dRAʴ_ P5jS6`.$jL ;, nU.HBW[9YV=(!U먰&tyse@`7ø#U+!ݻÙ@ (UjA߰jD3D@!@ըZbVN3@@|TKR" jT?g Z1U+'   P>bTDŽoiJyOJWԸǟJ"TcE@@'PݥHC]RqZhVUϮ@(@6(ew+Pr  (Lnms*r~|ypXt\]A$U+_~4C@@lMKUبUj  JnNxkTa5@ɓ# -hT6l|-Ľ{!sj8:lX|\jVqVtDUu~Wm }~I^\@h@U6+UmUmŲa)j[+Ti-   SzԲ~W}l^:I^\@hU{DնJPB  gj3p=T5 +vv8s}@@%]cB>VwrM{נj-NkA@&кM~~֞fXU;;>  FU־3Ꮴx8գqo?yH{Ti-  UGg''m*ZWcTp  @KUaamhk7%{6gO`k!6]:@8@Ӫve&iÂثS:$͓85U\QÙ# -(F\\\]s?}]:@8U{['GQ:oFّ@@jU'IQ\Q5  P<jO>\=R5V|`:@@PO=ddknkTQ5@@ PB'_Q5V|`jc0K@@jM4nRGFը  @U ,9@Uj9SЊZWb@@U j@@P5֟3u[-@8HQ qoXv0Z9@Uj9SЊZWb@@U j@@P5֟3u[-@8HQ qoXv0Z9@Uj9SЊZWb@@U j@@P5֟3u[-@8HQ qoXv0Z9@Uj9SЊZWb@@U j@@P5֟3u[-@8HQ qoXv0Z9@Uj9SЊZWb@@U j@@P5֟3u[-@8HQ qoXv0Z9@Uj9SЊZWb@@U j@@P5֟3u[-@8HQ qoXv0Z9@Uj9SЊZWb@@U j@@P5֟3u[-@8HQ qoXv0Z9@Uj9SЊZWb@@U j@@P5֟3u[-@8HQ qoXv0Z9@ޟ)ݖ&ISb" 踠/J37g@Vf tE] _RTg  @Z-|ISz# ]j@"+b@@*%@"!pnVU4T6  , Sb" j2^i2m@@+T-vhU" j΢2>U7Z9  `  #k  Pi cw;ZWEY,  P/, Sz# ]j2]EaE@& PHkۡUՄB@@*P@gUL@@؂H 5@" @EZC*@@*P@gUL@@؂H k(   T-vhU5Q@@ T-Y4@jU&D@j2H   5j8vC @@ T-Y4@jU&D@j2B3  UZUMC@U t Z$@@Z-hR$  @ Z5Ъj>B@U t Z$@@Z-hrЌ  @"!pnVU@@ @Ed|VE`2I@@`  #k  Pi cw;@@ @Ed|VE`2I@@`  #k/4# PHkۡUD=@@*P@gUL@@؂H 5@" @]: Un@@*"pK_=` P" PgPUr  p'@b!dUU@@*"@BEdvVQ`2U@@  ck  Pc !v>@@*"@BEdvVQ`2U@@  ck/4# PkAU;@@PUL@@8 5@" CXCȨ)@@PUL@@8 )   T-BF|PU5@@"T-_4@`j&SE@jΠ2H   j12j烪h @@"T-_4@`j&SE@jΠ2zB3  UQ;TUMC@U  ةZET@@Z3hR$  @=Z5j>B@U  ةZET@@Z3hrЌ  @b!dUU@@*"@BEdvVQ`2U@@  ck  Pc !v>@@*"@BEdvVQ`2U@@  ck/4# PkAU;@@PUL@@8 5@" CXCȨ)@@PUL@@8 )   T-BF|PU5@@"T-_4@`j&SE@jΠ2H   j12j烪h @@"T-_4@`j&SE@jΠ2zB3  UQ;TUMC@U  ةZET@@Z3h~   PAU6%;@ }/ :4w?$['gDə@; CRTMl%@դ @ T- H FT-*E3. @¬!jUUi@&YDP(s@I:@ZTf\@0 76>mMjE@ U {WUm_>nB T-*E3. @բEdyʱ#qA/?@ShU,"QZ0h rcӨZ U UJьP0kq&jUck[9>d cd"@բEdyyᦜoijk98m}ݫ2U1T-*E3. @Adt bF4,U,"QZ5D >OL R}\gu&@P(s@29 9V+} UXsGdA6.lf~GBcnpAW|p&j 2 z% $ C$y\: :6yݿN J8V*gj&p,.֨Uۛ_~U~]o(Yl-}8vqNp鯰sbotU8N/3}k.德wLx8[{.X['ԳxNܳO w`=F"!pn.M!v^73̫ v1ڟ>=i9Mvf$='.٭vËW0쁱hg-AÑwky9)v2 ~_nz~ :gxw|HTzFb3b~ܽ81wl3, CR;iQ9+B v۱p$O=#5m5BR5r&[i9+E'$YWk Ϲ[' S\1,M>ƻ{ 91|A4Uۙ8堸M7B+{v _8|)Eby|ɻ[#i<+*v2:U;8=SjTmc^T-&b5yD Y,ЉȔ,{ڹna؝f O>Wa32OENB SjT- ?8$U;p%XMO[,a5P32%v?6ȃΰ#_'pWqpGWR۱o[=pi=Mt?9T-- ϙwJ×Lo!Sst}kh2,U33΢j7-A>s\U&e8 ;,3dY kYfSڡZ-|IZƷ̅_\`)Y!qYaGz0lFM=U+㔌Uj !Z.h2s=y<uX  Q;Vq;Fsj KQ[r[heT-˛%[L#@)99wM>r݅6֔Ԏ4@&j^1D6E8!$3(4I P{vFKw3-ne|&snctUI`.jMn gfιp7CVڭg>pYU j>$U _x>1s)g[Q?=;L#v¥%;j},&6]͖?t9 )֤ H-LU4@T->U&5 ,XKAʲ5[l@`-NxW7$U eTM%HHA 5VBjtH !T=Sqቩի%Ub5;wrJ~d$7FM \'ö́*Kj lAd$DDʋ'k{D=j}Y 4ŹDv$3G=]rj{һ=* [AJYB2?cnWZ`Ъj=3+mԱi~U˓͟%s=:l!v 'ӏ/zxwFa- {?ǹB2WW:~d;9Qk-؂Z$tUc}zpv\5IiA<ʯ*lce~d5uK< i/ +4˿Te>yZ^rpvM T-0uhU;v0KӀomk 9bWOq H閼2LU3ZGImAVX[HS}6a6񓈗èZ#?o1ǟPj3t 86P5nv6ܣooU=k`{iW?b$JNL U̿[ IW=Qj{ k4@؂H c #B>)v4U?.bQ>>l.yy>\'SЩ.g8yӚKՒ'ڝֱ/n~?~8FW yx%v kWO'm>)+xÆ?>9[̟Z o` '?:UmdF+w`N\&y#f{-ƎAS8 @{dW1x*ͨڿ~ד*TmΔ7(n)*sP CRT-%\8@F;2Հj7|B[Y)zCx1Y8h=z.m'^O˪=I~,lkocgǻ9N9=O2fNYl-"ohóCV|bkφQ/26TWr%vO 2.mJ?b o?6Iզ颌5ߒPajaU4@/c1(dB _sUW~rJqhAQ$sU߼|!}{ /G>qb%{cAwM>Oo򱐏~_'':꺪M}+svAWf=k!{1ݡְQ3SQqa?1xIQ OV ޤ؅Yf*Es< ?EQ^;XX+aωw6OSY(]m_lm̊bM?drbiSqUfkXyqiYTAdt y NSnS )6 Z뿎=#ѯnNr 6I}z_5U%H~ʳCni&(E9U+9ϝ{23U c2NPG]pAK澫6<Ñۉgx$6"0GTtR P(s@Hl^Sȯ_1T{m:xG,h%lwcEg ?R.s;2<}g1[yVM*j䗻Dh!O$Uo~|ۤ/<~\}sU=An&aչ=jHݏW+\j 2 cZGTm6(rV3 M}4`П9SK bWUՆ?~{g O5.ڣQ9.lT[{~:DIj?bY qO׫^anUoJ7~{fmurjT-y\ULU x\+m2mg FyfˏyzG_zKI]aR^_Uۈ}8k 7hW׭KөZyC'K.{bSU)&o魩"{VϫжO 2͓Y\ǺoO>ͽTX>;?VoP(s@LwO'ytT0eb>RwKsX9L/X->qד_ܵ0ɥOr:9(MAN$3_]߾_fEՖ#aP/|z{VՆf8m*\|jeg?0IėcTU-@}%xrϠjO?r09q ϯ7?5U:t#tPR:ya:  àk }_ a$_hA/ןN-T՞glɄ_ax_K[HAj?vSOja;.>VKE=ƶ~',0Mf1oaYTFyL?>ϗA`6k;vSrja5p޶{!P?pAEY]CP\) 0 -vbVZh O w&VQ~˵GJl-=13,gB%}\~$öJs2æ^j 2 ڱ2H '0S]h,)s_Ҙ'@:-8A⸷sʸy|ۮxzk~[KrEa }U {WU-<7 M29;x^"[UՙjIV8eWʐ|﷫ =e. S̑X|fTBU3TZEYE`Slf(F毛ޝKl&Oj 2 -Yjԏ(j𩚤J] N+Qܻ!Zj[S4#TM8V؂Z$tUI* @.NR Tl%@BT"@&UTlAU-OӷtGP53DakT-0xhU^iE p6'@T-O&FGwUt%jUO J#@,*%@cE ! PJ37(Uk ۳U2 ZI"@iZi#@Z0h rcSi) Zٞ% @Z5D j'yFD PJ33"NU64@ET@zTl@RT-@4@AHU64@YC=vFD P=K@J P(s@՞2?\OU64@0 76>]J#@,*%@¬!jUU+-4(U43mhU2 ZI"@iZٞ% @Z0h R$ U43mhU {WUO'@,*%@բEdyV~Rh PVig   àkTZh> p=@g  P)f Q>> from acme.motd.api import Message, MOTD >>> motd = MOTD(messages=[Message(author='Anon', text='Hello World!')]) >>> message = motd.motd() >>> print(f'"{message.text}" - {message.author}') "Hello World!" - Anon Well, we had to get "Hello World" in there somewhere! The important point here is that this code is written without any knowledge of Envisage or extensibility whatsoever. It is just a small, reusable piece of code (albeit a very simple one). Not So Plain Ol' MOTD --------------------- Now lets look at the steps that we have to go through to use this code and turn it into an extensible, pluggable Envisage application. Create the main Application class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ First of all, we need to create an object that represents the application itself. In Envisage, this can be any object that implements the |IApplication| interface, but is usually either an instance of the default |Application| class, or one derived from it. In the |MOTD| example, we create the class in the |MOTD run| module as follows:: application = Application( id='acme.motd', plugins=[ CorePlugin(), MOTDPlugin(), SoftwareQuotesPlugin(), ], ) application.run() In this case, we use the simplest way to tell Envisage which plugins make up the application by passing them in explicitly. Envisage applications allow you to completely configure how plugins are located by setting the plugin manager. Envisage ships with two plugin managers by default, one that simply takes a list of plugins as in the above example, and one that finds all plugins offered via the 'envisage.plugins' entry point in Python Eggs. The 'acme.motd' plugin ~~~~~~~~~~~~~~~~~~~~~~ As shown above, the corresponding plugin implementation is in the |MOTDPlugin| class:: class MOTDPlugin(Plugin): """ The 'Message of the Day' plugin. This plugin simply prints the 'Message of the Day' to stdout. """ # The IDs of the extension points that this plugin offers. MESSAGES = 'acme.motd.messages' # The IDs of the extension points that this plugin contributes to. SERVICE_OFFERS = 'envisage.service_offers' #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = 'acme.motd' # The plugin's name (suitable for displaying to the user). name = 'MOTD' #### Extension points offered by this plugin ############################## # The messages extension point. # # Notice that we use the string name of the 'IMessage' interface rather # than actually importing it. This makes sure that the import only happens # when somebody actually gets the contributions to the extension point. messages = ExtensionPoint( List(Instance('acme.motd.api.IMessage')), id=MESSAGES, desc=""" This extension point allows you to contribute messages to the 'Message Of The Day'. """ ) #### Contributions to extension points made by this plugin ################ service_offers = List(contributes_to=SERVICE_OFFERS) def _service_offers_default(self): """ Trait initializer. """ # Register the protocol as a string containing the actual module path # (do not use a module path that goes via an 'api.py' file as this does # not match what Python thinks the module is!). This allows the service # to be looked up by passing either the exact same string, or the # actual protocol object itself. motd_service_offer = ServiceOffer( protocol='acme.motd.i_motd.IMOTD', factory=self._create_motd_service, ) return [motd_service_offer] ########################################################################### # Private interface. ########################################################################### def _create_motd_service(self): """ Factory method for the 'MOTD' service. """ # Only do imports when you need to! This makes sure that the import # only happens when somebody needs an 'IMOTD' service. from .motd import MOTD return MOTD(messages=self.messages) # This plugin does all of its work in this method which gets called when # the application has started all of its plugins. @on_trait_change('application:started') def _print_motd(self): """ Print the 'Message of the Day' to stdout! """ # Note that we always offer the service via its name, but look it up # via the actual protocol. from acme.motd.api import IMOTD # Lookup the MOTD service. motd = self.application.get_service(IMOTD) # Get the message of the day... message = motd.motd() # ... and print it. print(f'\n"{message.text}"\n\n- {message.author}') return Although it is obviously a bit of overkill, the example shows how we would take a |MOTD| object and register it a service for other parts of the application to use. Sadly, in this example, there are no other parts of the application, so we just lookup and use the service ourselves! The 'acme.motd.software_quotes' plugin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ First of all, we have to create the messages that we want to add. Remember that when the |acme.motd| plugin advertised the extension point, it told us that every contribution had to implement the |IMessage| interface. Happily, there is a class that does just that already defined for us (|Message|) and so we create a simple module ('messages.py'_) and add our |Message| instances to it:: messages = [ ... Message( author="Martin Fowler", text=( "Any fool can write code that a computer can understand. Good" " programmers write code that humans can understand." ), ), Message( author="Chet Hendrickson", text=( "The rule is, 'Do the simplest thing that could possibly" " work', not the most stupid." ), ), ... ] Now we create a plugin for the |acme.motd.software_quotes| package and tell Envisage about the messages that we have just created:: class SoftwareQuotesPlugin(Plugin): """ The 'Software Quotes' plugin. """ #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = 'acme.motd.software_quotes' # The plugin's name (suitable for displaying to the user). name = 'Software Quotes' #### Contributions to extension points made by this plugin ################ # Messages for the 'Message Of The Day'. messages = List(contributes_to='acme.motd.messages') ########################################################################### # 'SoftwareQuotesPlugin' interface. ########################################################################### def _messages_default(self): """ Trait initializer. """ # Only do imports when you need to! from .messages import messages return messages envisage-7.0.3/docs/source/envisage_core_documentation/plugins.rst000066400000000000000000000061661441257372400255510ustar00rootroot00000000000000Plugins ======= *Plugins* are used to "deliver" `Extension Points`_, contributions to extension points, and Services_ into an Envisage application. In fact, plugins can be thought of as simply being "delivery trucks" -- they rarely (if ever) do any *real* work themselves. In other words, plugins can do 3 things: 1) Declare extension points 2) Make contributions to extension points (including its own) 3) Create and publish services Plugins are located and managed by the *plugin manager*, and, whilst Envisage is designed such that you can write your own plugin manager, by default, it uses an implementation based on `Python Eggs`_. Creating a Plugin ----------------- All plugins must implement the |IPlugin| interface (an easy way to achieve this is to subclass the base |Plugin| class), and should provide the following "housekeeping" information: - a globally unique identifier (e.g., "acme.motd") This obviously allows Envisage to identify a particular plugin in a sea of other plugins. Technically, this identifier only need be unique within your application, but since the intent is for applications to be able to include plugins from (potentially) all over the world, using the common reverse domain name notation is probably a good idea! - a name This name should be suitable for display to the user. - a doc string! A nice description of the intent and features of your plugin. Here is a snippet from the |acme.motd| plugin that is part of the |Message of the Day| example:: class MOTDPlugin(Plugin): """ The 'Message of the Day' plugin. When this plugin is started it prints the 'Message of the Day' to stdout. """ #### 'IPlugin' interface ############################################## # The plugin's unique identifier. id = 'acme.motd' # The plugin's name (suitable for displaying to the user). name = 'MOTD' Plugin Lifecycle ---------------- Plugins have a lifecycle in the context of an application. There are currently two key lifecycle methods on the |IPlugin| interface:: 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) """ When Envisage starts up it calls the start() method of every plugin in the the same order that its iterator returns them in. This depends on the plugin manager being used, but by default this will be either a) the order of the list of plugins that was passed into the application or b) the order of the plugins based on egg dependencies. When the application stops, it calls the stop() method of each plugin in the reverse order that they were started in. .. _`Extension Points`: extension_points.html .. _`Python Eggs`: http://peak.telecommunity.com/DevCenter/PythonEggs .. _Services: services.html envisage-7.0.3/docs/source/envisage_core_documentation/preferences.rst000066400000000000000000000027171441257372400263670ustar00rootroot00000000000000 Preferences with Envisage ========================= Envisage presents preferences with two different extension points:: PREFERENCES = 'envisage.preferences' PREFERENCES_PAGES = 'envisage.ui.workbench.preferences_pages' The first one is only model-related and is for programmatic access to preferences, whereas the second one is for displaying UIs to the user in the workbench plugin. Preferences ------------ The contribution point is simply a list of URLs to the preference file, e.g.:: preferences_pages = List( ['pkgfile://acme.acmelab/preferences.ini'], contributes_to=PREFERENCES_PAGES, ) where acme.acmelab is the python-module-like path to the package in which the default preferences.ini is stored. A plugin usually needs only one preferences file, regardless of how many preference pages or settings it has. Preferences pages ------------------ The preference pages are a Traits UI view to wrap the preferences and allow the user to modify them. A preference page is defined as in the preference_manager example in the AppTools examples. It can than be contributed to the workbench, as in:: preferences_pages = List(contributes_to=PREFERENCES_PAGES) def _preferences_pages_default(self): """ Trait initializer. """ from acme.preference_pages import ACMEPreferencePages return [ACMEPreferencePages] A plugin needs to contribute a preferences pages class for each category of preferences it contributes. envisage-7.0.3/docs/source/envisage_core_documentation/services.rst000066400000000000000000000263231441257372400257100ustar00rootroot00000000000000Services ======== Services are the real "guts" of an Envisage application -- they are the objects that do the actual work! To get the job done, services obviously need to interact, and so Envisage provides a way for them to find each other. This is where the *service registry* comes in. Don't be fazed by the term *service*. In Envisage it just means any objects in your application that you want to share between plugins. Services can be any Python object and do not have to inherit from any Envisage class or even implement any particular interface! Service Registry ================ The service registry provides a "Yellow Pages" style mechanism, in that services are published and looked up by *protocol* meaning *interface*, or *type*. 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)! In an Envisage application, the service registry is accessed through the following methods on the |IApplication| interface:: def get_service(self, protocol, query='', minimize='', maximize=''): """ Return at most one service that matches the specified query. """ def get_service_properties(self, service_id): """ Return the dictionary of properties associated with a service. """ def get_services(self, protocol, query='', minimize='', maximize=''): """ Return all services that match the specified query. """ def register_service(self, protocol, obj, properties=None): """ Register a service. Returns a service ID that can be used to retrieve any service properties, and to unregister the service. """ def unregister_service(self, service_id): """ Unregister a service. """ The easiest way to explain the workings of these methods is to take a look at some examples, and so to continue our plumber theme, let's assume we have the following interface and implementation :: class IPlumber(Interface): """ What plumbers do! """ #: The plumber's name. name = Str #: The plumber's location. location = Str #: The price per hour (in say, Estonian Krooni ;^) price = Int def fix_leaking_pipe(self, pipe): """ Fix a leaking pipe! """ @provides(IPlumber) class Plumber(HasTraits): """ An actual plumber implementation! """ #: The plumber's name. name = Str #: The plumber's location. location = Str #: The price per hour (in say, Estonian Krooni ;^) price = Int def fix_leaking_pipe(self, pipe): """ Fix a leaking pipe! """ # code that actually fixes it! Registering a service --------------------- To register a service, create an object and call the register_service() method, passing it the protocol (interface or type) to publish the object under (for "protocol" think "Yellow Pages" section), and the object to publish. Note that the object to publish does *not* have to inherit from any particular base class or implement any special interface -- any arbitrary Python object will do:: fred = Plumber(name='fred', location='BH1', price=90) fred_id = application.register_service(IPlumber, fred) Note that each registered service gets assigned an ID that is unique within the current process. This can be used later to access its properties, or to unregister it, etc. You can also associate an arbitrary dictionary of properties with an object when you register it. These properties, along with the actual attributes of the service itself, can be used later to lookup the service using the query mechanism as shown in Section 4. :: wilma = Plumber(name='wilma', location='BH6') wilma_id = application.register_service(IPlumber, wilma, {'price' : 125}) Note that the symbol name of the protocol can be specified instead of the actual type or class. Say, for example, that the *IPlumber* interface can be imported via 'from acme.plumber.api import IPlumber', then the registration can be written as:: wilma_id = application.register_service( 'acme.plumber.api.IPlumber', wilma, {'price' : 125} ) This comes in handy when using service factories (see later) to make sure that implementation classes are imported only when necessary. Looking up a service -------------------- Looking up a service is just as easy -- call get_service() specifiying the protocol of the service required:: plumber = application.get_service(IPlumber) Assuming that we have registered both *fred* and *wilma* as in Section 1, then there is no way of knowing which of those objects would be returned. The choice of the object returned does *not* necessarily reflect the order in which they were added, so don't depend on it. Note that the symbol name of the protocol can be specified instead of the actual type or class. Say, for example, that the *IPlumber* interface can be imported via 'from acme.plumber.api import IPlumber', then the service lookup can be written as:: plumber = application.get_service('acme.plumber.api.IPlumber') This comes in handy when using service factories (see later) to make sure that implementation classes are imported only when necessary. Looking up a list of services ----------------------------- You can also look up *all* services of a particular protocol:: plumbers = application.get_services(IPlumber) Assuming the registrations in Section 1, this returns a list containing both *fred* and *wilma*, again in arbitrary order. Using queries ------------- The get_service() and get_services() methods both take optional arguments that allow more control over the selection of an appropriate service. The first of these is the *query* argument, which is a string containing an arbitrary Python expression that is evaluated for each service, with the service only being returned if the expression evaluates to True. The namespace that the expression is evaluated in is created by first adding each of the service's attributes, followed by any additional properties that were specified when the service was registered (i.e., properties take precedence over attributes). Once again, assuming that we have registered *fred* and *wilma* as in Section 1, let's look at how to use the query mechanism to be more selective about the plumber(s) we look up. Find all plumbers whose price is less than 100 Krooni/Hour:: plumbers = application.get_services(IPlumber, "price < 100") This query would return a list containing one plumber, *fred*. Find plumbers named *fred*:: plumbers = application.get_services(IPlumber, "name == 'fred'") This query, again (and unsurprisingly), would return a list containing just *fred*. Queries can be used with the singular form of the get_service() method too, in which case only one of the services that matches the query is returned:: plumber = application.get_service(IPlumber, "price < 200") This query would return *either* *fred* or *wilma*. Using *minimize* and *maximize* ------------------------------- The *minimize* and *maximize* (optional) arguments to the get_service() and get_services() methods allow the services returned to be sorted by an attribute or property in either ascending or descending order respectively. To find the cheapest plumber:: cheapest = application.get_service(IPlumber, minimize='price') Or, if you believe that you get what you pay for, the most expensive:: most_expensive = application.get_service(IPlumber, maximize='price') The *minimize* and *maximize* arguments can also be used in conjunction with a query. For example to find the cheapest plumber in my area:: cheap_and_local = application.get_service( IPlumber, location='BH6', minimize='price', ) This query would definitely give the job to *wilma*! Unregistering a service ----------------------- When you register a service, Envisage returns a value that uniquely identifies the service within the current process (i.e., it is not suitable for persisting to use next time the application is run). To unregister a service, call the unregister_service() method, passing in the appropriate identifier:: fred = Plumber(name='fred', location='BH1', price=90) fred_id = application.register_service(IPlumber, fred) ... application.unregister_service(fred_id) Getting any additional service properties ----------------------------------------- If you associate an arbitrary dictionary of properties with an object when you register it, you can retrieve those properties by calling the get_service_properties() method with the appropriate service identifier:: wilma = Plumber(name='wilma', location='BH6') wilma_id = application.register_service( IPlumber, wilma, {'price':125}, ) ... properties = application.get_service_properties(wilma_id) This call would return a dictionary containing the following:: {'price' : 125} To set the properties for a service that has already been registered, use:: wilma = Plumber(name='wilma', location='BH6') wilma_id = application.register_service( IPlumber, wilma, {'price':125}, ) ... application.set_service_properties(wilma_id, {'price' : 150}) Note however, that in practise, it is more common to use the actual attributes of a service object for the purposes of querying, but this is useful if you want additional properties that aren't part of the object's type. Service Factories ----------------- Last, but not least, we will look at an important feature of the service registry, namely, service factories. Service factories allow a Python callable to be registered in place of an actual service object. The callable is invoked the first time anybody asks for a service with the same type that the factory was registered against, and the object returned by the callable replaces the factory in the registry (so that the next time it is asked for it is simply returned as normal). To register a service factory, just register any callable that takes two arguments. The first is the protocol (type) of the service being requested, and the second is the (possibly empty) dictionary of properties that were registered along with the factory, e.g.:: def wilma_factory(protocol, properties): """ A service factory that creates wilma the plumber! """ return Plumber(name='wilma', location='BH6') To register the factory, we just use 'application.register_service' as usual:: wilma_id = application.register_service( IPlumber, wilma_factory, {'price':125}, ) Now, the first time somebody tries to get any 'IPlumber' service, the factory is called and the returned plumber object replaces the factory in the registry. envisage-7.0.3/docs/source/envisage_core_documentation/workbench.rst000066400000000000000000000040561441257372400260460ustar00rootroot00000000000000Workbench ========= The workbench plugin, found in the :mod:`envisage.ui.workbench.api` package, provides a style of user interface that is often (but not exclusively) found in integrated development environments (IDEs). Note that this does not mean that all of your user interfaces must fit this pattern -- just that if they do, then we have done a lot of the work for you. Workbench user interfaces are based on 3 simple concepts: 1) Views Views are primarily used to present information to the user to help them perform their current task. In an IDE application, views might be: - file tree - class outlines 2) Editors Editors allow the user to manipulate data and objects to perform their current task. Editors are really the focus of users attention with the views used to provide supporting information. Editors are grouped together geographically in what is known as the *editor area*. In an IDE application, editors would contain the source code that a developer is currently working on. 3) Perspectives A perspective is a particular grouping of views (usually around the editor area) that correspond to a user task. For example, in an IDE, I might have the following perspectives: - Coding perspective This is the perspective that the user (in this case a developer) would be in when they are actually writing the code. It might contain views that show the files in the current project, the outline of the current class etc, and the editors would contain the source code that they are actually working on. - Debugging perspective In this perspective, the user would still see the source code in the editors, but the views might show things like breakpoints, variable watches, stack information etc. In keeping with the Envisage philosophy of making code as reuseable as possible, the workbench plugin is just a thin layer over the Pyface Workbench widget to allow views, editors and perspectives to be contributed via plugins. .. toctree:: :maxdepth: 2 preferences.rst envisage-7.0.3/docs/source/index.rst000066400000000000000000000003751441257372400174310ustar00rootroot00000000000000Envisage Documentation ====================== .. toctree:: :maxdepth: 2 :glob: envisage_core_documentation/index tasks_user_manual/index api changelog Indices and tables ================== * :ref:`genindex` * :ref:`search` envisage-7.0.3/docs/source/tasks_user_manual/000077500000000000000000000000001441257372400213035ustar00rootroot00000000000000envisage-7.0.3/docs/source/tasks_user_manual/extensibility.rst000066400000000000000000000427301441257372400247370ustar00rootroot00000000000000.. _extensibility: =============== Extensibility =============== .. index:: Envisage The foregoing sections have described those elements of the Tasks framework that belong to the Pyface project; as such, our imports have been from the ``pyface.tasks`` package. We now discuss how Tasks can be used in conjunction with Envisage to build extensible applications. Accordingly, our imports in this section will be from the ``envisage.ui.tasks`` package. As remarked in the :ref:`introduction`, some familiarity with the Envisage plugin framework is assumed. For more information about Envisage, the reader is referred to the :doc:`../envisage_core_documentation/index`. .. _tasks-plugin: The Tasks Plugin ================ In creating an extensible Tasks application, we imagine two primary extensibility use cases: 1. **Contributing to an existing task:** In this case, the user wishes to add some combination of dock panes, menu items, and tool bar buttons to an existing task. In other words, the user would like to extend a task's functionality without changing its fundamental purpose. 2. **Contributing a new task:** Alternatively, the user may find that the none of the existing tasks are suitable for the functonality that he or she wishes to implement. Thus the user decides to contribute an entirely new task. The Tasks plugin has an extension point corresponding to each of these two use cases. These extensions points are: .. index:: TaskFactory 1. ``envisage.ui.tasks.tasks``: A list of ``TaskFactory`` instances. ``TaskFactory`` is a lightweight class for associating a task factory with a name and an ID. We shall see an example of its use in the following subsection. .. index:: TaskExtension 2. ``envisage.ui.tasks.task_extensions``: A list of ``TaskExtension`` instances. A ``TaskExtension`` is a bundle of menu bar, tool bar, and dock pane additions to an existing task. This class is discussed in detail in the subsection on :ref:`extending-a-task`. The Tasks plugin also provides two extensions points that permit the creation of extensible preferences dialogs. We defer discussion of this functionality to the subsection on :ref:`creating-a-preferences-dialog`. .. _tasks-applications: .. index:: application, TasksApplication Creating a Tasks Application ============================ Let us imagine that we are building a (slightly whimsical) application for visualizing `strange attractors `_, in two and three dimensions [1]_. We take for granted the existence of tasks for performing each of these two kinds of visualization. Like any Envisage application, our application will contain a ``Plugin`` instance to expose functionality to other plugins. In this case, we will contribute our two tasks to the Tasks plugin. Because the Tasks plugin is responsible for creating tasks and the windows that contain them, it expects to receive *factories* for creating ``Task`` instances rather than the instances themselves. The ``TaskFactory`` class fulfills this role. With this in mind, we can define a ``Plugin`` for our application:: class AttractorsPlugin(Plugin): #### 'IPlugin' interface ############################################## # The plugin's unique identifier. id = 'example.attractors' # The plugin's name (suitable for displaying to the user). name = 'Attractors' #### Contributions to extension points made by this plugin ############ tasks = List(contributes_to='envisage.ui.tasks.tasks') def _tasks_default(self): return [ TaskFactory( id='example.attractors.task_2d', name='2D Visualization', factory=Visualize2dTask, ), TaskFactory( id='example.attractors.task_3d', name='3D Visualization', factory=Visualize3dTask, ), ] .. index:: application; layout, TaskWindowLayout Having contributed tasks to the Tasks plugin, we must now specify how the tasks shall be added to windows to constitute our application. We call this specification the *application-level layout* to distinguish it from the lower-level layout attached to a task. Concretely, an application-level layout consists of a set of ``TaskWindowLayout`` objects, each of which indicates which tasks are attached to the window, which task is active in the window, and, optionally, the size and position of the window. The default application-level layout is defined inside our application class, which must inherit ``TasksApplication``:: class AttractorsApplication(TasksApplication): #### 'IApplication' interface ######################################### # The application's globally unique identifier. id = 'example.attractors' # The application's user-visible name. name = 'Attractors' #### 'TasksApplication' interface ##################################### # The default application-level layout for the application. default_layout = [ TaskWindowLayout( 'example.attractors.task_2d', 'example.attractors.task_3d', size=(800, 600), ), ] Observe that each of the IDs specified in the layout must correspond to the ID of a ``TaskFactory`` that has been contributed to the Tasks plugin. Also note that the ``TaskWindowLayout`` class has an ``active_task`` attribute; by omitting it, we indicate that the first task in the task list is to be active by default. .. index:: application; state restoration By default, the Tasks framework will restore application-level layout when the application is restarted. That is, the set of windows and tasks attached to those windows that is extant when the application exits will be restored when the application is started again. If, however, the ``always_use_default_layout`` attribute of the application is set, the default application layout will be applied when the application is restarted. Tasks will still attempt to restore as much user interface state as possible, including window positions and task layouts. This setting is particularly useful for multi-window applications. Apart from this functionality, the Tasks plugin provides no additional *default* behavior for managing tasks and their windows, permitting users to switch tasks within a window, etc. This is to be expected, as these behaviors are fundamentally application-specific. That said, we shall see in :ref:`global-task-extensions` that the Tasks plugins provides a few built-in extensions for implementing common behaviors. .. _creating-a-preferences-dialog: Creating a Preferences Dialog ============================= .. index:: preferences There are three extensions points associated with preferences. One of these extension points is built into the Envisage core plugin, while the other two belong to the Tasks plugin. Let us survey each of them in turn. 1. ``envisage.preferences``: A list of locators for default preferences files (INI files). This extension point is at the model level in the preferences system. .. index:: preferences; category 2. ``envisage.ui.tasks.preferences_categories``: A list of ``PreferencesCategory`` instances. Preference categories have name and ID attributes. To each category with a given name corresponds a tab with that name in the preferences dialog, unless there is only a single category, in which the case the tab bar will not be shown. .. index:: preferences; pane 3. ``envisage.ui.tasks.preferences_panes``: A list of ``PreferencesPane`` instances. A preferences pane defines a set of user interface elements for changing application preferences via a model object called a ``PreferencesHelper``. A preferences pane has a name and an ID, as well as a ``category`` attribute for specifying the ID of the category to which it belongs. Preferences panes are stacked vertically among the other panes in their category. By default, the category of a pane is "General". As a convenience, if a category with the specified ID does not exist, it will be created automatically. Note that both preference panes and categories have ``before`` and ``after`` attributes for specifying their order, if this is necessary. See the next subsection for more information about this idiom. We shall now expand the example from the previous subsection by adding a preferences dialog for changing the default task and the application-level state restoration behavior. By doing so, we shall see concretely how to use the preferences system in Tasks, as well as reinforce our knowledge about application-level layout. We begin by defining "preferences.ini", our default preferences file:: [example.attractors] default_task = example.attractors.task_2d always_use_default_layout = False and contributing it to the Envisage core plugin:: class AttractorsPlugin(Plugin): [ ... ] preferences = List(contributes_to='envisage.preferences') def _preferences_default(self): return ['pkgfile://example.attractors/preferences.ini'] This construction assumes that the attractors example is in Python's path (in the ``example.attractors`` package). Alternatively, we could have used the "file://" prefix in conjunction with an absolute path on the local filesystem. We can now define two classes: a preferences helper and preferences pane. The preferences helper is a model-level class that makes accessing the keys in the preferences file convenient and type safe. The preferences pane, introduced above, exposes a Traits UI view for this helper object:: from envisage.ui.tasks.api import PreferencesPane, TaskFactory from apptools.preferences.api import PreferencesHelper class AttractorsPreferences(PreferencesHelper): #### 'PreferencesHelper' interface #################################### # The path to the preference node that contains the preferences. # Notice that this corresponds to the section header in our preferences # file above. preferences_path = 'example.attractors' #### Preferences ###################################################### default_task = Str always_use_default_layout = Bool class AttractorsPreferencesPane(PreferencesPane): #### 'PreferencesPane' interface ###################################### # The factory to use for creating the preferences model object. model_factory = AttractorsPreferences #### 'AttractorsPreferencesPane' interface ############################ task_map = Dict(Str, Str) # Notice that the default context for trait names is that of the model # object, and that we must prefix names for this object with 'handler.'. view = View( Group( Item('always_use_default_layout'), Item( 'default_task', editor=EnumEditor(name='handler.task_map'), enabled_when='always_use_default_layout', ), label='Application startup', ), resizable=True, ) def _task_map_default(self): return dict( (factory.id, factory.name) for factory in self.dialog.application.task_factories ) Finally, we modify our application to make use of this new functionality:: class AttractorsApplication(TasksApplication): [ ... ] #### 'TasksApplication' interface ##################################### default_layout = List(TaskWindowLayout) always_use_default_layout = Property(Bool) #### 'AttractorsApplication' interface ################################ preferences_helper = Instance(AttractorsPreferences) def _default_layout_default(self): active_task = self.preferences_helper.default_task tasks = [factory.id for factory in self.task_factories] return [ TaskWindowLayout( *tasks, active_task=active_task, size=(800, 600), ), ] def _get_always_use_default_layout(self): return self.preferences_helper.always_use_default_layout def _preferences_helper_default(self): return AttractorsPreferences(preferences=self.preferences) and contribute the preferences pane to the Tasks plugin:: class AttractorsPlugin(Plugin): [ ... ] preferences_panes = List( contributes_to='envisage.ui.tasks.preferences_panes' ) def _preferences_panes_default(self): return [AttractorsPreferencesPane] .. _extending-a-task: Extending an Existing Task ========================== Contributions are made to an existing task via the ``TaskExtension`` class, which was briefly introduced above. ``TaskExtension`` is a simple class with three attributes: 1. ``task_id``: The ID of the task to extend. 2. ``actions``: A list of ``SchemaAddition`` objects. 3. ``dock_pane_factories``: A list of callables for creating dock panes. .. index:: SchemaAddition The second attributes requires further discussion. In the previous section, we remarked that a task's menu and tool bars are defined using schemas; the ``SchemaAddition`` class provides a mechanism for inserting new items into these schemas. .. index:: path A schema implicitly defines a *path* for each of its elements. For example, in the schema:: SMenuBar( SMenu( SGroup( [ ... ], id='SaveGroup', ), [ ... ], id='File', name='&File, ), SMenu( [ ... ], id='Edit', name='&Edit', ), ) the edit menu has the path "MenuBar/Edit". Likewise, the save group in the file menu has the path "MenuBar/File/SaveGroup". We might define an addition for this menu as follows:: SchemaAddition( factory=MyContributedGroup, path='MenuBar/File', ) where ``factory`` is a callable that produces either a schema or an object from the Pyface action API [2]_. A schema addition that produces a schema can in turn be extended by another schema addition. If it produces a Pyface object, it cannot be further extended. In this case we have opted for latter, using a custom subclass of ``Group``. .. index:: before, after The group created from the schema addition above would be inserted at the bottom of the file menu. The ``SchemaAddition`` class provides two further attributes for specifying with greater precision the location of the insertion: ``before`` and ``after``. Setting one of these attributes to the ID of a schema with the same path ensures that the insertion will be made before or after, respectively, that schema. For example, in the expanded addition:: SchemaAddition( factory=MyContributedGroup, before='SaveGroup', path='MenuBar/File', ) the created group would be inserted before the save group. If both ``before`` and ``after`` are set, Tasks will attempt to honor both of them [3]_. In the event that Tasks cannot, the menu order is undefined (although the insertions are guaranteed to made) and an error is logged. .. _global-task-extensions: Global Task Extensions ======================= .. index:: TaskExtension; global When creating an application with several tasks it is frequently the case that certain menu bar or tool bar actions should be present in all tasks. Such actions might include an "Exit" item in the "File" menu or an "About" item in the "Help" menu. One can, of course, include these items in the schemas of each task; indeed, if the actions require task-specific behavior, this is the only reasonable approach to take. But for actions that are truly global in nature Tasks provides an alternative that may be more convenient. To create a ``TaskExtension`` that applies to all tasks, simply omit the ``task_id`` attribute. Tasks itself contributes a global task extension with the following menu items: - A group of actions in the menu with ID "View" for toggling the visibility of dock panes (see ``pyface.tasks.action.api.DockPaneToggleGroup``) - A "Preferences" action in the menu with ID "Edit", if the application has any preferences panes - An "Exit" action in the menu with ID "File" The user is free to supplement these items by contributing additional global task extensions. For example, to provide a simple mechanism for changing tasks, one might add include the built-in task switching group in the "View" menu, either at the toplevel or as a sub-menu (see ``pyface.tasks.action.api.TaskToggleGroup``). For switching between windows, Tasks includes the ``TaskWindowToggleGroup``. This class, as well as several other menu-related conveniences, can be found in ``envisage.ui.tasks.action.api``. .. rubric:: Footnotes .. [1] In this section, we shall be referencing--often with considerable simplification--the Attractors example code in the EnvisagePlugins package, available :github-demo:`online ` and in the ETS distribution. .. [2] Although they are expanded into Pyface action items, schemas belong to a distinct API. It is beyond the scope of this document to describe the Pyface action API. For more complete documentation, the reader is referred to the `Pyface documentation `_. .. [3] Tasks differs from the Workbench in this regard. envisage-7.0.3/docs/source/tasks_user_manual/front.rst000066400000000000000000000033151441257372400231670ustar00rootroot00000000000000==================== Front Matter ==================== :Authors: Evan Patterson :Version: Document Version 1 :Copyright: 2011 Enthought, Inc. All Rights Reserved. Redistribution and use of this document in source and derived forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source or derived format (for example, Portable Document Format or Hypertext Markup Language) must retain the above copyright notice, this list of conditions and the following disclaimer. * Neither the name of Enthought, Inc., nor the names of contributors may be used to endorse or promote products derived from this document without specific prior written permission. THIS DOCUMENT 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 HOLDERS 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 DOCUMENT, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. All trademarks and registered trademarks are the property of their respective owners. | Enthought, Inc. | 515 Congress Avenue | Suite 2100 | Austin TX 78701 | 1.512.536.1057 (voice) | 1.512.536.1059 (fax) | http://www.enthought.com | info@enthought.com envisage-7.0.3/docs/source/tasks_user_manual/index.rst000066400000000000000000000002231441257372400231410ustar00rootroot00000000000000Tasks User Manual ================= .. toctree:: :maxdepth: 3 front.rst intro.rst layout.rst menus.rst extensibility.rst envisage-7.0.3/docs/source/tasks_user_manual/intro.rst000066400000000000000000000102361441257372400231720ustar00rootroot00000000000000.. _introduction: ============== Introduction ============== Tasks is an extensible, toolkit-independent framework for building scriptable, task-oriented user interfaces. This document describes its concepts and design principles, surveys the classes in its API, and provides a brief tutorial illustrating its use. We assume that the reader has a basic familiarity with the concepts of Traits and Traits UI. These packages are well documented in their respective user manuals. In the :ref:`extensibility` section of this document, some additional knowledge of the Envisage plugin framework is assumed. For more detailed information concerning the Tasks API, the reader is referred to the Tasks API Reference. What is a Task? --------------- .. index:: task, pane .. _what-is-a-task: For the purposes of this document, a *task* is a collection of user interface elements, called *panes*, which are present in a single window, unified by a specific purpose, and possessed of a certain structure. In addition, a task may provide menus, toolbars, and configuration options appropriate for these elements. .. index:: central pane At the heart of every task is its *central pane*, which exposes its core functionality. The central pane is always visible and occupies the center of the window. For example, in an IDE there will be at least one task concerned with writing code. A code editing widget would constitute the central pane for this task. If the IDE also provided a GUI construction task (i.e., a WYSIWYG user interface builder ala Glade or Qt Designer), the central pane would consist of the "canvas" upon which the user arranges UI elements. .. index:: dock pane In addition to the central pane, a task may include any number of subsidiary panes. These panes are arranged around the central pane in various *dock areas*, for which reason they are called *dock panes*. Dock panes provide functionality that is relevant but unessential to the task at hand. For example, in the code editing task described above, the list of dock panes might include a file browser, a context-sensitive documentation window, a compilation log, and a debugger. In general, dock panes can be moved from one dock area to another, can be made visible or hidden, and can be detached from the main window. Historical Background --------------------- .. index:: background, Workbench .. _historical-background: Many of the ideas behind the Tasks plugin originate in another Envisage plugin, called the Workbench (which, in turn, took considerable inspiration from Eclipse). While the Workbench is useful for creating IDE-style applications---it was designed for this purpose---there is a large class of applications for which it is too inflexible. Significant issues include: - **A lack of distinction between the semantics of UI elements and the layout of those elements.** A perspective (the Workbench analogue of a task) has very little responsibility besides saving the state the user interface. Views (the analogues of dock panes) are added directly to the Workbench, which means that they cannot be restricted to certain perspectives, nor can a perspective exercise meaningful control over the layout of its views. Furthermore, since there is no structure imposed on views by the active perspective, there is no mechanism for coupling UI components, if this becomes necessary. Finally, since the application menus are not connected to perspectives, it is very difficult to maintain multiple sets of menus over the application lifecycle. - **A non-customizable central pane.** The notion of editors is inextricably connected to the Workbench; the central pane must be a notebook-style collection of editors. The above are design considerations that could not be remedied without a vast degree of backwards-incompatible change. Less systemic deficiencies of the Workbench include: - **A lack of robust support for multi-window applications.** The Workbench does not correctly persist layout state for multiple windows. - **An inflexible API for exposing user-configurable preferences.** The preferences dialog does not permit fine-grained layout of UI elements. Tasks has been designed specifically to address these issues. envisage-7.0.3/docs/source/tasks_user_manual/layout.rst000066400000000000000000000226251441257372400233610ustar00rootroot00000000000000.. _layout: ======================= Window Layout ======================= First and foremost, the Tasks plugin is designed to facilitate the layout of modern, customizable user interfaces. Accordingly, we shall begin by learning how to use Tasks to perform basic window layout. .. index:: task; lifecycle Lifecycle of a Task ------------------- .. index:: model-view-controller pattern, MVC The Tasks plugin adheres to the model-view-controller (MVC) design pattern [1]_, for which the ``Task`` class functions as the controller and the ``TaskWindow`` class as the view, with the model being application-specific [2]_. Insofar as a ``Task`` is a controller, it does not itself instantiate or contain any GUI controls, although it does provide factories and a default layout for the controls that it manages. Controls are instantiated when a ``Task`` is assigned to a ``TaskWindow``. Specifically, the following actions are taken at this time: 1. The task's central pane is constructed and attached to the window 2. All of the task's dock panes are constructed, even if they are not by default visible 3. The visible dock panes are laid out around the central pane according to the task's default layout or persisted UI state information While the ``Task`` remains connected to the ``TaskWindow``, neither its central pane nor its dock panes will be destroyed. If the user "closes" a dock pane, the pane will be merely hidden, and can be re-activated via the application's View menu. Only when the ``Task`` itself is closed---that is, when it is removed from the ``TaskWindow``---will its panes be destroyed. This marks the end of the ``Task``'s lifecycle. If the task is to be re-used, a new instance should be constructed. .. index:: task; defining Defining a Task --------------- Minimally, a task is defined by subclassing ``Task`` and providing a central pane. For example, we define a task for editing Python scripts:: from pyface.tasks.api import Task class ExampleTask(Task): id = 'example.example_task' name = 'Python Script Editor' def create_central_pane(self): return PythonEditorPane() .. index:: ID, name The ``id`` attribute is a unique internal identifier for the task, whereas ``name`` is a user-visible descriptor for the task. We shall see that this is a common pattern in the Tasks framework. .. index:: central pane; defining All tasks must implement ``create_central_pane()`` and return a TaskPane instance. We might define the ``PythonEditorPane`` pane above as follows, making use of the ``PythonEditor`` available in Pyface:: from pyface.api import PythonEditor from pyface.tasks.api import TaskPane from traits.api import Instance class PythonEditorPane(TaskPane): id = 'example.python_editor_pane' name = 'Python Editor' editor = Instance(PythonEditor) def create(self, parent): self.editor = PythonEditor(parent) self.control = self.editor.control def destroy(self): self.editor.destroy() self.control = self.editor = None Besides providing an ID and a name for the pane, we implement the two basic methods of ``TaskPane``, namely ``create(parent)`` and ``destroy()``. The former constructs a toolkit-specific control for the pane and assigns its to the pane's ``control`` attribute. The latter performs the inverse operation, destroying the control and clearing ``control`` attribute. These methods give one full control over how a pane is constructed, but as we shall see below there are other, more convenient methods for defining a pane. .. index:: dock pane; defining Defining a Dock Pane -------------------- Now we imagine that we are building a very primitive Python IDE and that we would like to add a dock pane for browsing the local filesystem. We could create a ``DockPane`` subclass similarly to the ``TaskPane`` above, implementing the ``create_contents(parent)`` method of ``DockPane`` to provide the toolkit-specific control for the file browser. But if we are familiar with Traits UI we see that it would be more convenient to use the Traits UI ``FileEditor`` for this purpose. The Tasks framework provides the ``TraitsDockPane`` class to facilitate this. We define the pane as follows:: from pyface.tasks.api import TraitsDockPane from traits.api import Event, File, List, Str from traitsui.api import View, Item, FileEditor class FileBrowserPane(TraitsDockPane): #### TaskPane interface ############################################### id = 'example.file_browser_pane' name = 'File Browser' #### FileBrowserPane interface ######################################## # Fired when a file is double-clicked. activated = Event # The list of wildcard filters for filenames. filters = List(Str) # The currently selected file. selected_file = File # The view used to construct the dock pane's widget. view = View( Item( 'selected_file', editor=FileEditor( dclick_name='activated', filter_name='filters', ), style='custom', show_label=False ), resizable=True, ) When a control is needed for the pane, it will be constructed using the standard Traits UI mechanisms. There exist additional options, not described here, for specifying a model object, which is often important when building a complex application. There is also a ``TraitsTaskPane`` class that provides similar functionality for defining Traits-based central panes. As always, the reader is referred to the Tasks API documentation for more information. Now let us amend the example task defined above with a ``create_dock_panes()`` method. This method returns the list of dock pane instances associated with the task. We also define a method on our task for opening a file in the editor, which we connect to the dock pane's ``activated`` event:: class ExampleTask(Task): [ ... ] def create_dock_panes(self): """ Create the file browser and connect to its double click event. """ browser = PythonScriptBrowserPane() handler = lambda: self.open_file(browser.selected_file) browser.on_trait_change(handler, 'activated') return [browser] def open_file(self, filename): """ Open the file with the specified path in the central pane. """ self.window.central_pane.editor.path = filename .. index:: task; layout, PaneItem, TaskLayout, Tabbed, Splitter Providing a Default Layout -------------------------- Although dock panes are designed to be moved around and otherwise manipulated by the user, we often have a particular default layout in mind when designing an application. The Tasks framework provides the ``TaskLayout`` class to make the specification of this layout possible. Usually, we are only concerned with four attributes of this class, namely ``left``, ``right``, ``bottom``, and ``top``. Each of these attributes may be assigned a layout item, which is either a ``PaneItem``, for specifying a particular dock pane; a ``Tabbed`` item, containing other ``PaneItem`` instances; or a ``Splitter``, containing arbitrary subitems. A few examples should suffice to make this clear. To stack the dock pane with ID 'dock_pane_1' on top of that with ID 'dock_pane_2', with both to the left of the central pane, one specifies:: left = Splitter( PaneItem('dock_pane_1'), PaneItem('dock_pane_2'), orientation='vertical', ) .. index:: HSplitter, VSplitter We could also have used ``VSplitter``, which is a convenient abbreviation for a splitter with vertical orientation. Similarly, ``HSplitter`` is an abbrevation for a splitter with horizontal orientation. To put these dock panes in tab group below the central pane, we might write:: bottom_panes = Tabbed( PaneItem('dock_pane_1', height=400), PaneItem('dock_pane_2'), ) Observe that we have explicitly provided a height for the first dock pane. Provided that the dock pane's underlying control does not have a conflicting minimum or maximum size constraint, Tasks guarantees that it will honor this height exactly. Of course, if ``width`` or ``height`` are not provided, Tasks will use the dock pane's toolkit-specific size hint to determine its size. Now we will provide our example task with a simple layout using the ``default_layout`` attribute of ``Task``:: class ExampleTask(Task): [ ... ] default_layout = TaskLayout( left=PaneItem('example.python_script_browser_pane') ) Note that dock panes that do not appear in the layout will not be visible by default. A task without a default layout is equivalent to a task with an empty layout; in both cases, only the central pane will be visible by default. Finally, note that the layout behavior is undefined if a dock pane appears multiple times in a layout. .. rubric:: Footnotes .. [1] For more information about the MVC pattern as used in ETS, the reader is referred to the `Introduction `_ of the Traits UI User Guide. .. [2] Throughout this document, "``Task``" will refer to the class of that name in the Tasks API, while "task" will be reserved for the general UI concept, and similarly for other terms. envisage-7.0.3/docs/source/tasks_user_manual/menus.rst000066400000000000000000000117451441257372400231740ustar00rootroot00000000000000.. _menus: ==================== Menus and Tool Bars ==================== In addition to specifying a layout of panes, a task can include a menu bar, a status bar, and any number of tool bars. In this section, we shall see how the Tasks framework supports these important user interface elements. It is important to recall that a task is a kind of *blueprint* for a user interface. As such, a task can provide blueprints, or *schemas* as we shall henceforth call them, for menus and tool bars, but does not itself contain menu or tool bar controls. This distinction, though perhaps not very useful in the present section, will take on considerable importance in the subsequent section on :ref:`extensibility`. .. index:: menu bar Defining a Menu Bar ------------------- .. index:: Action Resuming our example of the script editing task from the previous section, we shall define some menu items for opening and saving files. As in Traits UI and Pyface, individual menu items are instances of the ``Action`` class. We might define the 'Open' action as follows:: from pyface.action.api import Action class OpenAction(Action): name = 'Open' accelerator = 'Ctrl+O' def perform(self, event): event.task.open() Of course, we must also implement the ``open()`` method on our task:: from pyface.api import FileDialog, OK class ExampleTask(Task): [ ... ] def open(self): """ Shows a dialog to open a file. """ dialog = FileDialog( parent=self.window.control, wildcard='*.py', ) if dialog.open() == OK: # Recall that 'open_file' was defined in the previous section. self.open_file(dialog.path) .. index:: MenuBarSchema, MenuSchema, GroupSchema, SMenuBar, SMenu, SGroup Now let us suppose that we have similarly implemented a ``SaveAction`` and a ``save()`` method on our task. We would like to add these actions to a menu bar. Tasks provides several ``Schema`` classes for this purpose: :``MenuBarSchema``: The root of a menu hierarchy. Contains some number of menu schemas. :``MenuSchema``: A menu or sub-menu in the menu hierarchy. :``GroupSchema``: A group of menu items that are logically related and that may or may not require separators from other groups. For convenience, these classes also have the abbreviated names ``SMenuBar``, ``SMenu``, and ``SGroup``, respectively. We can implement the menu bar for our task using these abbreviations:: from pyface.tasks.action.api import SMenu, SMenuBar class ExampleTask(Task): [ ... ] menu_bar = SMenuBar( SMenu( OpenAction(), SaveAction(), id='File', name='&File', ), ) .. index:: TaskAction The pattern of having an ``Action`` instance call back to its task is so common that there is a predefined class for this purpose. We can use this ``TaskAction`` class to implement the above menu more simply, without having to define separately the ``OpenAction`` and ``SaveAction`` classes:: from pyface.tasks.action.api import SMenu, SMenuBar, TaskAction class ExampleTask(Task): [ ... ] menu_bar = SMenuBar( SMenu( TaskAction( name='Open...', method='open', accelerator='Ctrl+O', ), TaskAction( name='Save', method='save', accelerator='Ctrl+S', ), id='File', name='&File', ), ) .. index:: tool bar Defining a Tool Bar ------------------- .. index:: ToolBarSchema, SToolBar Like a menu bar, a tool bar uses the ``Action`` class to represent individual items. A tool bar, however, is defined with a different set of schemas: :``ToolBarSchema``: The root of a tool bar hierarchy. Contains some number of group schemas and actions. :``GroupSchema``: A group of tool bar buttons that are logically related and that may or may not require separators from other groups. As above, these classes are often abbreviated as ``SToolBar`` and ``SGroup``, respectively. Let us now add a tool bar with buttons for opening and saving files to our script editing task:: from pyface.api import ImageResource from pyface.tasks.action.api import SToolBar, TaskAction class ExampleTask(Task): [ ... ] tool_bars = [ SToolBar( TaskAction( method='open', tooltip='Open a file', image=ImageResource('document_open'), ), TaskAction( method='save', tooltip='Save the current file', image=ImageResource('document_save'), ), ), ] envisage-7.0.3/docs/update_gh_pages.py000066400000000000000000000151511441257372400177570ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ Helper script for gh-pages documentation updates. This script helps maintain the documentation in the gh-pages branch of the repository; that documentation is automatically served by GitHub and made available via docs.enthought.com. The intended structure of the gh-pages branch is: - the root directory contains documentation matching the contents of the 'main' branch of the codebase. - named subdirectories with names of the form . contain documentation for released versions of the package. - the root directory also contains a 'latest' symlink pointing to the docs matching the latest release (i.e., the release with highest version number). Example usage ------------- In the examples below we assume that: - the current working directory is the root of the repository - the gh-pages branch has been checked out into ../docs (for example using `git worktree add ../docs gh-pages`) - documentation has been built locally via Sphinx and is in docs/build/html Then to update the docs in the root directory of the gh-pages branch (for example after a push to the main branch), do: python docs/update_gh_pages.py docs/build/html ../docs After releasing version 7.3.2 (for example) of the package, to update the docs in the 7.3/ subdirectory of the gh-pages branch, do: python docs/update_gh_pages.py docs/build/html ../docs --tag 7.3.2 Note that for a bugfix release, the intention is that the docs for the bugfix release (e.g., 7.3.2) overwrite the docs for the previous release with the same . version (e.g., 7.3.1). The docs end up in the 7.3/ subdirectory of the gh-pages tree in both cases. """ import argparse import pathlib import re import shutil #: Matcher for names of directories containing release docs. RELEASE_DOCS_DIR_MATCHER = re.compile(r"\d+\.\d+").fullmatch #: Name of the symlink that points to the latest docs LATEST = "latest" def release_version(dir_name: str) -> list[int]: """ Mapping from release docs directory names to orderable values. E.g., '7.13' -> (7, 13). """ return [int(piece) for piece in dir_name.split(".")] def subdir_from_tagname(version: str) -> str: """ Map a version tag (e.g., '7.2.1') to the gh-pages subdirectory containing docs for that tag (e.g., '7.2'). """ subdir = ".".join(version.split(".")[:2]) if not RELEASE_DOCS_DIR_MATCHER(subdir): raise RuntimeError( f"tagname {version} does not have the expected form" ) return subdir def update_latest_symlink(docs_dir: pathlib.Path) -> None: """ Update the 'latest' symlink to point to documentation for the most recent release. docs_dir should point to the root gh-pages directory. """ all_release_docs = [ child.name for child in docs_dir.iterdir() if child.is_dir() and RELEASE_DOCS_DIR_MATCHER(child.name) ] latest_docs = max(all_release_docs, key=release_version) # Remove existing symlink if present. latest_symlink = docs_dir / LATEST if latest_symlink.is_symlink(): print(f"Removing symlink {latest_symlink}") latest_symlink.unlink() # Create new symlink print(f"Updating symlink {latest_symlink} to point to {latest_docs}") latest_symlink.symlink_to(latest_docs, target_is_directory=True) def remove_existing_docs(docs_dir: pathlib.Path) -> None: """ Remove existing documentation files and directories. Skips hidden files and directories (like .nojekyll and .git), and ignores directories whose name matches . - these are directories that contain previous documentation versions. """ print(f"Removing existing documentation from {docs_dir} ...") for child in docs_dir.iterdir(): if child.name.startswith("."): print(f" Not removing hidden file or directory {child}") elif child.is_file(): print(f" Removing file {child}") child.unlink() elif child.is_dir(): if RELEASE_DOCS_DIR_MATCHER(child.name): print(f" Not removing release docs directory {child}") elif child.is_symlink() and child.name == LATEST: print(f" Not removing symlink {child}") else: print(f" Removing directory {child}") shutil.rmtree(child) else: raise RuntimeError("Not a file or directory: {child}: aborting") def copy_new_docs(source_docs: pathlib.Path, target_dir: pathlib.Path) -> None: """ Copy new documentation into place. Copies newly-built docs from their build location (e.g., docs/build/html) to the target directory in the gh-pages branch. Hidden files and directories (for example .buildinfo, .nojekyll, .doctrees) are ignored. """ print(f"Copying docs from {source_docs} to {target_dir} ...") for child in source_docs.iterdir(): if child.name.startswith("."): print(f" Not copying hidden file or directory {child.name}") elif child.is_file(): print(f" Copying file {child} to {target_dir}") shutil.copyfile(child, target_dir / child.name) elif child.is_dir(): print(f" Copying directory {child} to {target_dir}") shutil.copytree(child, target_dir / child.name) else: raise RuntimeError("Not a file or directory: {child}: aborting") def main() -> None: parser = argparse.ArgumentParser() parser.add_argument( "source", help="Directory containing newly-built documentation", type=pathlib.Path, ) parser.add_argument( "target", help="Directory containing the gh-pages checkout", type=pathlib.Path, ) parser.add_argument( "--tag", help="Release tag name (when updating for a release)", ) args = parser.parse_args() if args.tag is None: target = args.target else: target = args.target / subdir_from_tagname(args.tag) if not target.exists(): print(f"Creating target directory {target}") target.mkdir() remove_existing_docs(target) copy_new_docs(args.source, target) if args.tag is not None: update_latest_symlink(args.target) if __name__ == "__main__": main() envisage-7.0.3/edm.yaml000066400000000000000000000002431441257372400147630ustar00rootroot00000000000000# Change this if you are using on-premise brood store_url: "https://packages.enthought.com" repositories: - enthought/free - enthought/lgpl - enthought/gpl envisage-7.0.3/envisage/000077500000000000000000000000001441257372400151345ustar00rootroot00000000000000envisage-7.0.3/envisage/__init__.py000066400000000000000000000011041441257372400172410ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Per logging best practices, add a NullHandler to the root 'envisage' # logger. import logging logging.getLogger(__name__).addHandler(logging.NullHandler()) del logging envisage-7.0.3/envisage/api.py000066400000000000000000000064161441257372400162660ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ Primary API for envisage Interfaces ---------- - :class:`~.IApplication` - :class:`~.IExtensionPoint` - :class:`~.IExtensionPointUser` - :class:`~.IExtensionProvider` - :class:`~.IExtensionRegistry` - :class:`~.IImportManager` - :class:`~.IPlugin` - :class:`~.IPluginActivator` - :class:`~.IPluginManager` - :class:`~.IServiceRegistry` Constants --------- - :data:`~.BINDINGS` - :data:`~.COMMANDS` - :data:`~.PREFERENCES` - :data:`~.PREFERENCES_CATEGORIES` - :data:`~.PREFERENCES_PANES` - :data:`~.SERVICE_OFFERS` - :data:`~.TASKS` - :data:`~.TASK_EXTENSIONS` Application, plugin and related classes --------------------------------------- - :class:`~.Application` - :class:`~.CorePlugin` - :class:`~.EggPluginManager` - :class:`~.ExtensionPoint` - :class:`~.ExtensionPointBinding` - :func:`~.bind_extension_point` - :func:`~.unbind_extension_point` - :class:`~.ExtensionProvider` - :class:`~.ExtensionPointChangedEvent` - :class:`~.ImportManager` - :class:`~.Plugin` - :class:`~.PluginActivator` - :class:`~.PluginExtensionRegistry` - :class:`~.PluginManager` - :class:`~.ProviderExtensionRegistry` - :class:`~.Service` - :class:`~.ServiceOffer` - :class:`~.ServiceRegistry` Exceptions ---------- - :class:`~.NoSuchServiceError` - :class:`~.UnknownExtension` - :class:`~.UnknownExtensionPoint` """ from .application import Application from .core_plugin import CorePlugin from .egg_plugin_manager import EggPluginManager from .extension_point import ExtensionPoint from .extension_point_binding import ( bind_extension_point, ExtensionPointBinding, unbind_extension_point, ) from .extension_point_changed_event import ExtensionPointChangedEvent from .extension_provider import ExtensionProvider from .extension_registry import ExtensionRegistry 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 .ids import ( BINDINGS, COMMANDS, PREFERENCES, PREFERENCES_CATEGORIES, PREFERENCES_PANES, SERVICE_OFFERS, TASK_EXTENSIONS, TASKS, ) 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 .unknown_extension import UnknownExtension from .unknown_extension_point import UnknownExtensionPoint envisage-7.0.3/envisage/application.py000066400000000000000000000405311441257372400200140ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import os # Enthought library imports. from apptools.preferences.api import ( IPreferences, ScopedPreferences, set_default_preferences, ) from traits.api import ( Delegate, Event, HasTraits, Instance, observe, provides, Str, VetoableEvent, ) from traits.etsconfig.api import ETSConfig from .application_event import ApplicationEvent # 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 .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().__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) ########################################################################### # 'IApplication' interface. ########################################################################### #### Trait initializers ################################################### def _home_default(self): """Trait initializer.""" return ETSConfig.application_home def _user_data_default(self): """Trait initializer.""" return ETSConfig.user_data def _preferences_default(self): """Trait initializer.""" return ScopedPreferences( application_preferences_filename=os.path.join( self.home, "preferences.ini" ) ) #### Methods ############################################################## def run(self): """Run the application.""" if self.start(): self.stop() ########################################################################### # '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 ) def add_extension_point(self, extension_point): """Add an extension point.""" self.extension_registry.add_extension_point(extension_point) 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 ) def remove_extension_point(self, extension_point_id): """Remove an extension point.""" self.extension_registry.remove_extension_point(extension_point_id) 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) ########################################################################### # '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) 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) 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) def unregister_service(self, service_id): """Unregister a service.""" self.service_registry.unregister_service(service_id) ########################################################################### # '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). @observe("plugin_manager") def _update_application(self, event): """Static trait change handler.""" old, new = event.old, event.new if old is not None: old.application = None if new is not None: new.application = self #### Methods ############################################################## def _create_application_event(self): """Create an application event.""" return ApplicationEvent(application=self) def _initialize_application_home(self): """Initialize the application directories.""" os.makedirs(self.home, mode=0o700, exist_ok=True) os.makedirs(self.user_data, exist_ok=True) envisage-7.0.3/envisage/application_event.py000066400000000000000000000012501441257372400212100ustar00rootroot00000000000000# (C) Copyright 2007-2023 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") envisage-7.0.3/envisage/composite_plugin_manager.py000066400000000000000000000125051441257372400225630ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, observe, on_trait_change, provides, ) # 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_managers = [ 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) @observe("application") def _update_application(self, event): new = event.new for plugin_manager in self.plugin_managers: plugin_manager.application = new # 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_on_plugins(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) 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 ValueError("no such plugin %s" % plugin_id) 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) 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 ValueError("no such plugin %s" % plugin_id) envisage-7.0.3/envisage/core_plugin.py000066400000000000000000000141741441257372400200230ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ from contextlib import closing from traits.api import List, on_trait_change, Str # Enthought library imports. from envisage.extension_point import ExtensionPoint from envisage.plugin import Plugin from envisage.service_offer import ServiceOffer class CorePlugin(Plugin): """The Envisage core plugin. The core plugin offers facilities that are generally useful when building extensible applications such as adapters 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 ID for preferences PREFERENCES = "envisage.preferences" #: Extension point ID for service offers 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 ############################## #: preferences ExtensionPoint 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 _update_preferences(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) #: service offers ExtensionPoint 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 _register_new_services(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) #### 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) # 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) ########################################################################### # Private interface. ########################################################################### 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: with closing(resource_manager.file(resource_name)) as f: default.load(f) 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 envisage-7.0.3/envisage/egg_basket_plugin_manager.py000066400000000000000000000143331441257372400226550ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import sys import traceback import warnings import pkg_resources 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. """ def __init__(self, **traits): warnings.warn( ( "The EggBasketPluginManager is deprecated. The recommended " "approach is to install plugin-containing packages into " "site-packages and advertise the plugins via entry points. " ), DeprecationWarning, stacklevel=2, ) super().__init__(**traits) # 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 _update_path_and_reset_plugins(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 RuntimeError("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) envisage-7.0.3/envisage/egg_plugin_manager.py000066400000000000000000000116741441257372400213310ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import re import warnings # 3rd party imports. import pkg_resources # 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. """ def __init__(self, **traits): warnings.warn( ( "The EggPluginManager is deprecated. The recommended " "approach is to install plugin-containing packages into " "site-packages and advertise the plugins via entry points. " ), DeprecationWarning, stacklevel=2, ) super().__init__(**traits) # 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) # 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 def _working_set_default(self): """Trait initializer.""" return pkg_resources.working_set ########################################################################### # 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 envisage-7.0.3/envisage/egg_utils.py000066400000000000000000000065651441257372400175040ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # 3rd party 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 RuntimeError("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) 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 envisage-7.0.3/envisage/examples/000077500000000000000000000000001441257372400167525ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/__init__.py000066400000000000000000000000001441257372400210510ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/_demo.py000066400000000000000000000020071441257372400204060ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ Utilities for supporting Envisage's demo examples. """ import contextlib import os import sys @contextlib.contextmanager def demo_path(path): """Context manager to temporarily insert the directory containing the demo script to sys.path such that demo examples can be run using local packages. This function should only be used by Envisage example files. Parameters ---------- path : Path-like Path to the demo script to be run. """ path = os.path.dirname(os.fspath(path)) try: sys.path.insert(0, path) yield finally: sys.path.remove(path) envisage-7.0.3/envisage/examples/_etsdemo_info.py000066400000000000000000000017171441257372400221440ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 functions to be advertised in the distribution entry points. """ import pkg_resources def info(request): """Return a configuration for contributing examples to the Demo application. Parameters ---------- request : dict Information provided by the demo application. Currently this is a placeholder. Returns ------- response : dict """ return dict( version=1, name="Envisage Examples", root=(pkg_resources.resource_filename("envisage.examples", "demo")), ) envisage-7.0.3/envisage/examples/demo/000077500000000000000000000000001441257372400176765ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/GUI_Application/000077500000000000000000000000001441257372400226455ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/GUI_Application/traitsui_gui_app.py000066400000000000000000000031631441257372400265720ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 example of a GUIApplication which wraps a TraitsUI """ from traits.api import Enum, HasTraits, Instance, Int, on_trait_change, Str from traitsui.api import Item, OKCancelButtons, View from envisage.api import Plugin from envisage.ui.gui_application import GUIApplication class Person(HasTraits): """A typical traits model object""" name = Str("John Doe") age = Int(21) gender = Enum("Male", "Female") view = View( Item("name"), Item("age"), Item("gender"), buttons=OKCancelButtons, ) class PersonViewPlugin(Plugin): """The 'Person View' plugin. This plugin waits for the application to start, and then creates a traits UI. """ ui = Instance("traitsui.ui.UI") @on_trait_change("application:application_initialized") def on_application_start(self): """Start the UI.""" person = Person() # keep a reference to the ui object to avoid garbage collection self.ui = person.edit_traits() # Application entry point. if __name__ == "__main__": # Create the application. application = GUIApplication( id="person_view", plugins=[PersonViewPlugin()] ) # Run it! application.run() envisage-7.0.3/envisage/examples/demo/Hello_World/000077500000000000000000000000001441257372400221105ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/Hello_World/hello_world.py000066400000000000000000000056661441257372400250110ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 version of the old chestnut. """ from traits.api import List, Str # Enthought library imports. from envisage.api import Application, ExtensionPoint, Plugin class HelloWorld(Plugin): """The 'Hello World' plugin. This plugin offers a single extension point 'greetings' which is a list of greetings, one of which is used to produce the ' World' message when the plugin is started. """ # This tells us that the plugin offers the 'greetings' extension point, # and that plugins that want to contribute to it must each provide a list # of strings (Str). greetings = ExtensionPoint( List(Str), id="greetings", desc='Greetings for "Hello World"' ) # Plugin's have two important lifecyle methods, 'start' and 'stop'. These # methods are called automatically by Envisage when the application is # started and stopped respectively. def start(self): """Start the plugin.""" # Standard library imports. # # We put this import here just to emphasize that it is only used in # this specific plugin. import random print("{} World!".format(random.choice(self.greetings))) class Greetings(Plugin): """A plugin that contributes to the 'greetings' extension point.""" # This tells us that the plugin contributes the value of this trait to the # 'greetings' extension point. greetings = List(["Hello", "G'day"], contributes_to="greetings") class MoreGreetings(Plugin): """Another plugin that contributes to the 'greetings' extension point.""" # This tells us that the plugin contributes the value of this trait to the # 'greetings' extension point. greetings = List(contributes_to="greetings") # This shows how you can use a standard trait initializer to populate the # list dynamically. def _greetings_default(self): """Trait initializer.""" extensions = [ "The %s application says %s" % (self.application.id, greeting) for greeting in ["Bonjour", "Hola"] ] return extensions # Application entry point. if __name__ == "__main__": # Create the application. # # An application is simply a collection of plugins. In this case we # specify the plugins explicitly, but the mechanism for finding plugins # is configurable by setting the application's 'plugin_manager' trait. application = Application( id="hello.world", plugins=[HelloWorld(), Greetings(), MoreGreetings()] ) # Run it! application.run() envisage-7.0.3/envisage/examples/demo/MOTD/000077500000000000000000000000001441257372400204415ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/MOTD/acme/000077500000000000000000000000001441257372400213465ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/MOTD/acme/__init__.py000066400000000000000000000006271441257372400234640ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/envisage/examples/demo/MOTD/acme/motd/000077500000000000000000000000001441257372400223115ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/MOTD/acme/motd/__init__.py000066400000000000000000000006271441257372400244270ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/envisage/examples/demo/MOTD/acme/motd/api.py000066400000000000000000000010521441257372400234320ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 acme.motd.i_message import IMessage from acme.motd.i_motd import IMOTD from acme.motd.message import Message from acme.motd.motd import MOTD envisage-7.0.3/envisage/examples/demo/MOTD/acme/motd/i_message.py000066400000000000000000000013201441257372400246130ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'Message of the Day' messages. """ # Enthought library imports. from traits.api import Interface, Str class IMessage(Interface): """The interface for 'Message of the Day' messages.""" # The author of the message. author = Str # The text of the message. text = Str envisage-7.0.3/envisage/examples/demo/MOTD/acme/motd/i_motd.py000066400000000000000000000013351441257372400241400ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'Message of the Day' interface. """ # Enthought library imports. from traits.api import Interface class IMOTD(Interface): """The 'Message of the Day' interface.""" def motd(self): """Return the message of the day. Returns an object that implements the 'IMessage' interface. """ envisage-7.0.3/envisage/examples/demo/MOTD/acme/motd/message.py000066400000000000000000000014661441257372400243160ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'IMessage' interface. """ # Local imports. from acme.motd.i_message import IMessage # Enthought library imports. from traits.api import HasTraits, provides, Str @provides(IMessage) class Message(HasTraits): """The default implementation of the 'IMessage' interface.""" # The author of the message. author = Str # The text of the message. text = Str envisage-7.0.3/envisage/examples/demo/MOTD/acme/motd/motd.py000066400000000000000000000026721441257372400236350ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'Message of the Day' implementation! """ # Standard library imports. from random import choice # Local imports. from acme.motd.i_message import IMessage from acme.motd.i_motd import IMOTD from acme.motd.message import Message # Enthought library imports. from traits.api import HasTraits, List, provides @provides(IMOTD) class MOTD(HasTraits): """The 'Message of the Day' implementation!""" # The default message is used when there are no other messages! DEFAULT_MESSAGE = Message( author="Anon", text="Work hard and be good to your Mother" ) # The list of possible messages. messages = List(IMessage) ########################################################################### # 'IMOTD' interface. ########################################################################### def motd(self): """Prints a random message.""" if len(self.messages) > 0: message = choice(self.messages) else: message = self.DEFAULT_MESSAGE return message envisage-7.0.3/envisage/examples/demo/MOTD/acme/motd/motd_plugin.py000066400000000000000000000075731441257372400252200ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'Message of the Day' plugin """ # In the interest of lazy loading you should only import from the following # packages at the module level of a plugin:: # # - envisage # - traits # # Eveything else should be imported when it is actually required. from traits.api import Instance, List, on_trait_change # Enthought library imports. from envisage.api import ExtensionPoint, Plugin, ServiceOffer class MOTDPlugin(Plugin): """The 'Message of the Day' plugin. This plugin simply prints the 'Message of the Day' to stdout. """ # The Ids of the extension points that this plugin offers. MESSAGES = "acme.motd.messages" # The Ids of the extension points that this plugin contributes to. SERVICE_OFFERS = "envisage.service_offers" #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = "acme.motd" # The plugin's name (suitable for displaying to the user). name = "MOTD" #### Extension points offered by this plugin ############################## # The messages extension point. # # Notice that we use the string name of the 'IMessage' interface rather # than actually importing it. This makes sure that the import only happens # when somebody actually gets the contributions to the extension point. messages = ExtensionPoint( List(Instance("acme.motd.api.IMessage")), id=MESSAGES, desc=""" This extension point allows you to contribute messages to the 'Message Of The Day'. """, ) #### Contributions to extension points made by this plugin ################ service_offers = List(contributes_to=SERVICE_OFFERS) def _service_offers_default(self): """Trait initializer.""" # Register the protocol as a string containing the actual module path # and *not* a module path that goes via an 'api.py' file as this does # not match what Python thinks the module is! This allows the service # to be looked up by passing either the exact same string, or the # actual protocol object itself (the latter is the preferred way of # doing it!). motd_service_offer = ServiceOffer( protocol="acme.motd.i_motd.IMOTD", factory=self._create_motd_service, ) return [motd_service_offer] ########################################################################### # Private interface. ########################################################################### def _create_motd_service(self): """Factory method for the 'MOTD' service.""" # Only do imports when you need to! This makes sure that the import # only happens when somebody needs an 'IMOTD' service. from acme.motd.motd import MOTD return MOTD(messages=self.messages) # This plugin does all of its work in this method which gets called when # the application has started all of its plugins. @on_trait_change("application:started") def _print_motd(self): """Print the 'Message of the Day' to stdout!""" # Note that we always offer the service via its name, but look it up # via the actual protocol. from acme.motd.api import IMOTD # Lookup the MOTD service. motd = self.application.get_service(IMOTD) # Get the message of the day... message = motd.motd() # ... and print it. print('\n"%s"\n\n- %s' % (message.text, message.author)) envisage-7.0.3/envisage/examples/demo/MOTD/acme/motd/software_quotes/000077500000000000000000000000001441257372400255435ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/MOTD/acme/motd/software_quotes/__init__.py000066400000000000000000000006271441257372400276610ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/envisage/examples/demo/MOTD/acme/motd/software_quotes/messages.py000066400000000000000000000045211441257372400277260ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ Some messages! """ from acme.motd.api import Message messages = [ Message( author="Bertrand Meyer", text="You can have quality software, or you can have pointer" " arithmetic; but you cannot have both at the same time.", ), Message( author="Edsger W. Dijkstra", text="The effective programmer is keenly aware of the limited size" " of his own head.", ), Message( author="Richard Buckminster-Fuller", text="When I'm working on a problem, I never think about beauty. I" " think only how to solve the problem. But when I have finished, if" " the solution is not beautiful, I know it is wrong.", ), Message( author="Tom Gilb", text="If you don't know what you're doing, don't do it on a large" " scale.", ), Message( author="Arthur Norman", text="The best way to implement hard code is never to know you're" " implementing it.", ), Message( author="Albert Einstein", text="Any intelligent fool can make things bigger, more complex," " and more violent. It takes a touch of genius - and a lot of courage" " - to move in the opposite direction.", ), Message( author="Martin Fowler", text="Any fool can write code that a computer can understand. Good" " programmers write code that humans can understand.", ), Message( author="Chet Hendrickson", text="The rule is, 'Do the simplest thing that could possibly" " work', not the most stupid.", ), Message( author="Ron Jeffries", text="If you're working sixty hour weeks, you're too tired to be" " doing good software.", ), Message( author="Edward Tufte", text="Clutter and confusion are failures of design, not attributes" " of information. There's no such thing as information overload.", ), ] envisage-7.0.3/envisage/examples/demo/MOTD/acme/motd/software_quotes/software_quotes_plugin.py000066400000000000000000000024771441257372400327370ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'Software Quotes' plugin """ from traits.api import List # Enthought library imports. from envisage.api import Plugin class SoftwareQuotesPlugin(Plugin): """The 'Software Quotes' plugin.""" #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = "acme.motd.software_quotes" # The plugin's name (suitable for displaying to the user). name = "Software Quotes" #### Extension points offered by this plugin ############################## # None #### Contributions to extension points made by this plugin ################ # Messages for the 'Message Of The Day'. messages = List(contributes_to="acme.motd.messages") def _messages_default(self): """Trait initializer.""" # Only do imports when you need to! from acme.motd.software_quotes.messages import messages return messages envisage-7.0.3/envisage/examples/demo/MOTD/run.py000066400000000000000000000025151441257372400216220ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ Run the MOTD example application. """ # Enthought library imports. from envisage.api import Application, CorePlugin def main(): """Run the application.""" # Import here so that this script can be run from anywhere without # having to install the packages. from acme.motd.motd_plugin import MOTDPlugin from acme.motd.software_quotes.software_quotes_plugin import ( SoftwareQuotesPlugin, ) # Create an application containing the appropriate plugins. application = Application( id="acme.motd", plugins=[CorePlugin(), MOTDPlugin(), SoftwareQuotesPlugin()], ) # Run it! return application.run() if __name__ == "__main__": # This context manager is added so that one can run this example from any # directory without necessarily having installed the examples as packages. from envisage.examples._demo import demo_path with demo_path(__file__): main() envisage-7.0.3/envisage/examples/demo/plugins/000077500000000000000000000000001441257372400213575ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/plugins/tasks/000077500000000000000000000000001441257372400225045ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/000077500000000000000000000000001441257372400246725ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/__init__.py000066400000000000000000000000001441257372400267710ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/attractors_application.py000066400000000000000000000044211441257372400320160ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Local imports. from attractors.attractors_preferences import AttractorsPreferences from pyface.tasks.api import TaskWindowLayout from traits.api import Bool, Instance, List, Property # Enthought library imports. from envisage.ui.tasks.api import TasksApplication class AttractorsApplication(TasksApplication): """The chaotic attractors Tasks application.""" #### 'IApplication' interface ############################################# # The application's globally unique identifier. id = "example.attractors" # The application's user-visible name. name = "Attractors" #### 'TasksApplication' interface ######################################### # The default window-level layout for the application. default_layout = List(TaskWindowLayout) # Whether to restore the previous application-level layout when the # applicaton is started. always_use_default_layout = Property(Bool) #### 'AttractorsApplication' interface #################################### preferences_helper = Instance(AttractorsPreferences) ########################################################################### # Private interface. ########################################################################### #### Trait initializers ################################################### def _default_layout_default(self): active_task = self.preferences_helper.default_task tasks = [factory.id for factory in self.task_factories] return [ TaskWindowLayout(*tasks, active_task=active_task, size=(800, 600)) ] def _preferences_helper_default(self): return AttractorsPreferences(preferences=self.preferences) #### Trait property getter/setters ######################################## def _get_always_use_default_layout(self): return self.preferences_helper.always_use_default_layout envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/attractors_plugin.py000066400000000000000000000044501441257372400310130ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 os.path from traits.api import List # Enthought library imports. from envisage.api import Plugin from envisage.ui.tasks.api import TaskFactory class AttractorsPlugin(Plugin): """The chaotic attractors plugin.""" # Extension point IDs. PREFERENCES = "envisage.preferences" PREFERENCES_PANES = "envisage.ui.tasks.preferences_panes" TASKS = "envisage.ui.tasks.tasks" #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = "example.attractors" # The plugin's name (suitable for displaying to the user). name = "Attractors" #### Contributions to extension points made by this plugin ################ preferences = List(contributes_to=PREFERENCES) preferences_panes = List(contributes_to=PREFERENCES_PANES) tasks = List(contributes_to=TASKS) ########################################################################### # Protected interface. ########################################################################### def _preferences_default(self): filename = os.path.join(os.path.dirname(__file__), "preferences.ini") return ["file://" + filename] def _preferences_panes_default(self): from attractors.attractors_preferences import AttractorsPreferencesPane return [AttractorsPreferencesPane] def _tasks_default(self): from attractors.visualize_2d_task import Visualize2dTask from attractors.visualize_3d_task import Visualize3dTask return [ TaskFactory( id="example.attractors.task_2d", name="2D Visualization", factory=Visualize2dTask, ), TaskFactory( id="example.attractors.task_3d", name="3D Visualization", factory=Visualize3dTask, ), ] envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/attractors_preferences.py000066400000000000000000000051641441257372400320210ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 apptools.preferences.api import PreferencesHelper from traits.api import Bool, Dict, Str from traitsui.api import EnumEditor, HGroup, Item, Label, VGroup, View # Enthought library imports. from envisage.ui.tasks.api import PreferencesPane class AttractorsPreferences(PreferencesHelper): """The preferences helper for the Attractors application.""" #### 'PreferencesHelper' interface ######################################## # The path to the preference node that contains the preferences. preferences_path = "example.attractors" #### Preferences ########################################################## # The task to activate on app startup if not restoring an old layout. default_task = Str # Whether to always apply the default application-level layout. # See TasksApplication for more information. always_use_default_layout = Bool class AttractorsPreferencesPane(PreferencesPane): """The preferences pane for the Attractors application.""" #### 'PreferencesPane' interface ########################################## # The factory to use for creating the preferences model object. model_factory = AttractorsPreferences #### 'AttractorsPreferencesPane' interface ################################ task_map = Dict(Str, Str) view = View( VGroup( HGroup( Item("always_use_default_layout"), Label("Always use the default active task on startup"), show_labels=False, ), HGroup( Label("Default active task:"), Item( "default_task", editor=EnumEditor(name="handler.task_map") ), enabled_when="always_use_default_layout", show_labels=False, ), label="Application startup", ), resizable=True, ) ########################################################################### # Private interface. ########################################################################### def _task_map_default(self): return dict( (factory.id, factory.name) for factory in self.dialog.application.task_factories ) envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/000077500000000000000000000000001441257372400256225ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/henon.html000066400000000000000000000032601441257372400276200ustar00rootroot00000000000000

The Hénon map is a discrete-time dynamical system. It is one of the most studied examples of dynamical systems that exhibit chaotic behavior. The Hénon map takes a point (x_n, y_n) in the plane and maps it to a new point

The map depends on two parameters, a and b, which for the canonical Hénon map have values of a = 1.4 and b = 0.3. For the canonical values the Hénon map is chaotic. For other values of a and b the map may be chaotic, intermittent, or converge to a periodic orbit. An overview of the type of behavior of the map at different parameter values may be obtained from its orbit diagram.

The map was introduced by Michel Hénon as a simplified model of the Poincaré section of the Lorenz model. For the canonical map, an initial point of the plane will either approach a set of points known as the Hénon strange attractor, or diverge to infinity. The Hénon attractor is a fractal, smooth in one direction and a Cantor set in another. Numerical estimates yield a correlation dimension of 1.42 ± 0.02[1] and a Hausdorff dimension of 1.261 ± 0.003[2] for the attractor of the canonical map.

As a dynamical system, the canonical Hénon map is interesting because, unlike the logistic map, its orbits defy a simple description.

Text courtesy of Wikipedia under the CC Share-Alike License.

envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/images/000077500000000000000000000000001441257372400270675ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/images/henon1.png000066400000000000000000000016541441257372400307730ustar00rootroot00000000000000PNG  IHDR2#'0PLTEtttbbb@@@000"""PPP 9V7IDATX ?hQǿӗ%iS! Eq8-B-Tp`X %uE(VX:8V vppppPݽwrKӭo }Z3m Ù!'8_jd.ܔkr0Lq}RP =A %'GLX,3mە\|DITmjY|圗jE YZB!2r8P"UvF'Jn/*R},bw@+a)B&U~Å>` z)f% ~WJ^y shܒ)9t!2/z tu%7 cs-ZRc-hQdؘwA!d=' lыBF>gddY~q w-I##4t0ֱThh"kU_t]Ə\kI-sl_m| %7֒jQxP)ι"+xs+T huywҎ*:˨Q[mq{˹q؟s4GIaZk:J20iش<}/ɇa \4[9$S 7pcpp wgqhq}Y<90IENDB`envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/images/lorenz2.png000066400000000000000000000014371441257372400311750ustar00rootroot00000000000000PNG  IHDR)Ը0PLTE@@@PPPtttbbb"""̶ 000bIDATH VMhAd7M)Z.(-ɋ"$bEmA($ſPiCKoz19 CED('A} n &޷3o߼ AIRUڸL5 OMZ~JeqH9wM͢ 5=,fmPKr+Cwd5ۨZA#E-Ƴ8PZ)YrsjM 'ti j,f+|Ňk[@i?k|I!p%K~0TslZ?.BjTJ:ܸ~'"3HQӃFASm~'1g'>TH!G >qژaiϮ,,jT@0cv~֙C.hEF`%Q9@3d#>`qqu)Z_HvNxȞ;&݋k:8:xK_-oioX>Q3hvuz>`_:*{ .$ug"Kaw Tc|Q_wgE/]A_V \;Qr%HUWdNRPe %| =N/szt/mkF60AɑэmSwGIENDB`envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/images/lorenz3.png000066400000000000000000000011731441257372400311730ustar00rootroot00000000000000PNG  IHDRt)=0PLTE@@@PPPtttbbb"""̶ 000LIDATH 1H#Ah1wC#UW{pW"jci,\ppJ;"ih#( 6hv¡Vfgј؝7ٙg3=խQGQ-j4.?P12ݓFV&c8GC |YIKXz*k5X)7-kAf7_yTXP<++E4 |QC iޠ :RKNǕd#ˏX/B^(TwZ p(b=K<+,EHbk\xilo8{fd^G@CKN+ȋ" 8PNut1h˵30D5'Bi O\B)xvMƮ h#v| 0 6Rϯ\;ƹ!?M_vf\1է\֢7Mך(hiG+ j{<ں1hYYrȑ{Nv.ԯ9$6s='.wpx=IENDB`envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/images/rossler1.png000066400000000000000000000010311441257372400313420ustar00rootroot00000000000000PNG  IHDRm)0PLTE@@@PPPtttbbb"""̶ 000bIDATH ?H@ƿJ&ME.,88 8UK Xf-X h6Mnr2#/!w3lR(c T4N<><'Y6ku%Gh@s,% f %،UsQXةŎ^x W'snsqv Y-MFwV Zo_0ёI޽]ŖZiɱ1\$ '津'eoaΏd*^+o "b}`$~xIENDB`envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/images/rossler2.png000066400000000000000000000010571441257372400313530ustar00rootroot00000000000000PNG  IHDRh)+6q[0PLTE@@@PPPtttbbb"""̶ 000bIDATH ?O@ƟZ A$8A'#&tLN$u61,?Rrk{y w/$hmU6m24҃ y#@Z}+;Z1uB$( EKGbzc|0k/8SHKo%(-|0CRkYz8*CZy<&FHCƅ}[q?t)&!;(+CߖudY~8MCbOț`~h+zgb?cG ii\ в& )D?zƥ*5ODq-/n\y'aB2^+S eRse^F^.Q~ʝ1uPh~IENDB`envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/images/rossler3.png000066400000000000000000000014441441257372400313540ustar00rootroot00000000000000PNG  IHDR)Ú50PLTE@@@PPPtttbbb"""̶ 000LIDATH V=ha~&܏5J-$EN[F<&UbDpb""E): ]c\Ŀ޽w$'3D+L1:ldRJ&")eiԲm׻RbeZ jHe /Zx%(Hl ;M^+)h}i㢕'-l5|ǟ8W F>Em߇d1;6[)lEmy &t} %"ԺK]gƘ!1-#c>i ,s8Qb987sVU-rU"ny))O9q7FԥvK30qd*G̔( -yLTZ;? 18&5W=gY`

The Lorenz attractor itself, and the equations from which it is derived, were introduced by Edward Lorenz in 1963, who derived it from the simplified equations of convection rolls arising in the equations of the atmosphere.

In addition to its interest to the field of non-linear mathematics, the Lorenz model has important implications for climate and weather prediction. The model is an explicit statement that planetary and stellar atmospheres may exhibit a variety of quasi-periodic regimes that are, although fully deterministic, subject to abrupt and seemingly random change.

From a technical standpoint, the Lorenz oscillator is nonlinear, three-dimensional and deterministic. In 2001 it was proven by Warwick Tucker that for a certain set of parameters the system exhibits chaotic behavior and displays what is today called a strange attractor. The strange attractor in this case is a fractal of Hausdorff dimension between 2 and 3. Grassberger (1983) has estimated the Hausdorff dimension to be 2.06 ± 0.01 and the correlation dimension to be 2.05 ± 0.01.

The equations that govern the Lorenz oscillator are:

where σ is called the Prandtl number and ρ is called the Rayleigh number. All σ, ρ, β > 0, but usually σ = 10, β = 8/3 and ρ is varied. The system exhibits chaotic behavior for ρ = 28 but displays knotted periodic orbits for other values of ρ. For example, with ρ = 99.96 it becomes a T(3,2) torus knot.

When σ ≠ 0 and β (ρ-1) ≥ 0, the equations generate three critical points. The critical points at (0,0,0) correspond to no convection, and the critical points at (√(β(ρ-1)), √(β(ρ-1)), ρ-1) and (-√(β(ρ-1), -√(β(ρ-1)), ρ-1) correspond to steady convection. This pair is stable only if ρ= (σ(σ+β+3))/(σ-β-1), which can hold only for positive ρ if σ > β+1.

Text courtesy of Wikipedia under the CC Share-Alike License.

envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/help/rossler.html000066400000000000000000000037621441257372400302110ustar00rootroot00000000000000

The Rössler attractor is the attractor for the Rössler system, a system of three non-linear ordinary differential equations. These differential equations define a continuous-time dynamical system that exhibits chaotic dynamics associated with the fractal properties of the attractor. Some properties of the Rössler system can be deduced via linear methods such as eigenvectors, but the main features of the system require non-linear methods such as Poincaré maps and bifurcation diagrams. The original Rössler paper says the Rössler attractor was intended to behave similarly to the Lorenz attractor, but also be easier to analyze qualitatively. An orbit within the attractor follows an outward spiral close to the x,y plane around an unstable fixed point. Once the graph spirals out enough, a second fixed point influences the graph, causing a rise and twist in the z-dimension. In the time domain, it becomes apparent that although each variable is oscillating within a fixed range of values, the oscillations are chaotic. This attractor has some similarities to the Lorenz attractor, but is simpler and has only one manifold. Otto Rössler designed the Rössler attractor in 1976, but the originally theoretical equations were later found to be useful in modeling equilibrium in chemical reactions. The defining equations are:

Rössler studied the chaotic attractor with a = 0.2, b = 0.2, and c = 5.7, though properties of a = 0.1, b = 0.1, and c = 14 have been more commonly used since.

Text courtesy of Wikipedia under the CC Share-Alike License.

envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model/000077500000000000000000000000001441257372400257725ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model/__init__.py000066400000000000000000000000001441257372400300710ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model/henon.py000066400000000000000000000044571441257372400274650ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # System library imports. import numpy as np # Local imports. from attractors.model.i_plottable_2d import IPlottable2d from scipy import array, zeros # Enthought library imports. from traits.api import ( Array, cached_property, Float, HasTraits, Int, Property, provides, Str, ) from traitsui.api import Item, View @provides(IPlottable2d) class Henon(HasTraits): """The model object for the Henon map.""" #### 'Henon' interface #################################################### # Equation parameters. a = Float(1.4, auto_set=False, enter_set=True) b = Float(0.3, auto_set=False, enter_set=True) # Iteration parameters. initial_point = Array(dtype=np.float64, value=[0.1, 0.1]) steps = Int(10000) # Iteration results. points = Property(Array, observe="a, b, initial_point, steps") # Configuration view. view = View( Item("a"), Item("b"), Item("initial_point"), Item("steps"), resizable=True, ) #### 'IPlottable2D' interface ############################################# name = Str("Henon Map") plot_type = Str("scatter") x_data = Property(Array, observe="points") y_data = Property(Array, observe="points") x_label = Str("x") y_label = Str("y") ########################################################################### # Protected interface. ########################################################################### @cached_property def _get_points(self): point = self.initial_point points = zeros((self.steps, 2)) for i in range(self.steps): x, y = points[i] = point point = array([y + 1 - self.a * x**2, self.b * x]) return points @cached_property def _get_x_data(self): return self.points[:, 0] @cached_property def _get_y_data(self): return self.points[:, 1] envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model/i_model_2d.py000066400000000000000000000011451441257372400303420ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 Array, Interface, Str class IModel2d(Interface): # The user-visible name of the model. name = Str() x_data = Array() y_data = Array() envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model/i_model_3d.py000066400000000000000000000041611441257372400303440ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 ( Array, cached_property, DelegatesTo, HasTraits, Instance, Interface, Property, Str, Trait, ) from traitsui.api import Group, Item, View class IModel3d(Interface): """A model object that produces an array of 3D points.""" # The user-visible name of the model. name = Str # An n-by-3 array. points = Array class IModel3dIPlottable2dMixin(HasTraits): """ Mixin class to facilitate defining a IModel3d -> IPlottable2D adapter. """ #### 'Adapter' interface ################################################## adaptee = Instance(IModel3d) #### 'IPlottable2D' interface ############################################# name = DelegatesTo("adaptee") x_data = Property(Array, observe="adaptee.points, x_label") y_data = Property(Array, observe="adaptee.points, y_label") x_label = Trait("x", {"x": 0, "y": 1, "z": 2}) y_label = Trait("y", {"x": 0, "y": 1, "z": 2}) view = View( Group( Group( Item("adaptee", style="custom", show_label=False), label="Model", ), Group( Item("x_label", label="X axis"), Item("y_label", label="Y axis"), label="Plot", ), ) ) ########################################################################### # Protected interface. ########################################################################### @cached_property def _get_x_data(self): return self.adaptee.points[:, self.x_label_] @cached_property def _get_y_data(self): return self.adaptee.points[:, self.y_label_] envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model/i_plottable_2d.py000066400000000000000000000012121441257372400312230ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Local imports. from attractors.model.i_model_2d import IModel2d # Enthought library imports. from traits.api import Enum, Str class IPlottable2d(IModel2d): plot_type = Enum("line", "scatter") x_label = Str() y_label = Str() envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model/lorenz.py000066400000000000000000000060511441257372400276570ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # System library imports. import numpy as np # Local imports from attractors.model.i_model_3d import IModel3d, IModel3dIPlottable2dMixin from attractors.model.i_plottable_2d import IPlottable2d from scipy import arange, array from scipy.integrate import odeint # Enthought libary imports. from traits.api import ( Adapter, Array, cached_property, Float, HasTraits, Instance, Property, provides, register_factory, Str, ) from traitsui.api import Item, View @provides(IModel3d) class Lorenz(HasTraits): """The model object for the Lorenz attractor.""" #### 'IModel3d' interface ################################################# name = Str("Lorenz Attractor") points = Property( Array, observe="prandtl, rayleigh, beta, initial_point, times" ) #### 'Lorenz' interface ################################################### # Equation parameters. prandtl = Float(10.0, auto_set=False, enter_set=True) rayleigh = Float(28.0, auto_set=False, enter_set=True) beta = Float(8.0 / 3.0, auto_set=False, enter_set=True) # Integration parameters. initial_point = Array(dtype=np.float64, value=[0.0, 1.0, 0.0]) time_start = Float(0.0) time_stop = Float(100.0) time_step = Float(0.01) times = Property(Array, observe="time_start, time_stop, time_step") # Configuration view. view = View( Item("prandtl"), Item("rayleigh"), Item("beta"), Item("initial_point"), Item("time_start"), Item("time_stop"), Item("time_step"), resizable=True, ) ########################################################################### # 'Lorenz' interface. ########################################################################### def compute_step(self, point, time): x, y, z = point return array( [ self.prandtl * (y - x), x * (self.rayleigh - z) - y, x * y - self.beta * z, ] ) ########################################################################### # Protected interface. ########################################################################### @cached_property def _get_points(self): return odeint(self.compute_step, self.initial_point, self.times) @cached_property def _get_times(self): return arange(self.time_start, self.time_stop, self.time_step) @provides(IPlottable2d) class LorenzIPlottable2dAdapter(Adapter, IModel3dIPlottable2dMixin): adaptee = Instance(Lorenz) plot_type = Str("line") register_factory(LorenzIPlottable2dAdapter, Lorenz, IPlottable2d) envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model/rossler.py000066400000000000000000000055751441257372400300510ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # System library imports. import numpy as np # Local imports from attractors.model.i_model_3d import IModel3d, IModel3dIPlottable2dMixin from attractors.model.i_plottable_2d import IPlottable2d from scipy import arange, array from scipy.integrate import odeint # Enthought libary imports. from traits.api import ( Adapter, Array, cached_property, Float, HasTraits, Instance, Property, provides, register_factory, Str, ) from traitsui.api import Item, View @provides(IModel3d) class Rossler(HasTraits): """The model object for the Rossler attractor.""" #### 'IModel3d' interface ################################################# name = Str("Rossler Attractor") points = Property(Array, observe="a, b, c, initial_point, times") #### 'Rossler' interface ################################################## # Equation parameters. a = Float(0.2, auto_set=False, enter_set=True) b = Float(0.2, auto_set=False, enter_set=True) c = Float(5.7, auto_set=False, enter_set=True) # Integration parameters. initial_point = Array(dtype=np.float64, value=[0.0, 1.0, 0.0]) time_start = Float(0.0) time_stop = Float(100.0) time_step = Float(0.01) times = Property(Array, observe="time_start, time_stop, time_step") # Configuration view. view = View( Item("a"), Item("b"), Item("c"), Item("initial_point"), Item("time_start"), Item("time_stop"), Item("time_step"), resizable=True, ) ########################################################################### # 'Rossler' interface. ########################################################################### def compute_step(self, point, time): x, y, z = point return array([-y - z, x + self.a * y, self.b + z * (x - self.c)]) ########################################################################### # Protected interface. ########################################################################### @cached_property def _get_points(self): return odeint(self.compute_step, self.initial_point, self.times) @cached_property def _get_times(self): return arange(self.time_start, self.time_stop, self.time_step) @provides(IPlottable2d) class RosslerIPlottable2dAdapter(Adapter, IModel3dIPlottable2dMixin): adaptee = Instance(Rossler) plot_type = Str("line") register_factory(RosslerIPlottable2dAdapter, Rossler, IPlottable2d) envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model_config_pane.py000066400000000000000000000020611441257372400306730ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.api import TraitsDockPane from traits.api import HasTraits, Instance from traitsui.api import Item, View class ModelConfigPane(TraitsDockPane): """A simple dock pane for editing an attractor model's configuration options. """ #### 'ITaskPane' interface ################################################ id = "example.attractors.model_config_pane" name = "Model Configuration" #### 'ModelConfigPane' interface ########################################## model = Instance(HasTraits) view = View( Item("pane.model", style="custom", show_label=False), resizable=True ) envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/model_help_pane.py000066400000000000000000000042561441257372400303660ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 codecs import os.path # Enthought library imports. from pyface.tasks.api import TraitsDockPane from traits.api import cached_property, HasTraits, Instance, Property, Str from traitsui.api import HTMLEditor, Item, View # Constants. HELP_PATH = os.path.join(os.path.dirname(__file__), "help") class ModelHelpPane(TraitsDockPane): """A dock pane for viewing any help associated with a model.""" #### 'ITaskPane' interface ################################################ id = "example.attractors.model_help_pane" name = "Model Information" #### 'ModelConfigPane' interface ########################################## model = Instance(HasTraits) html = Property(Str, observe="model") view = View( Item( "pane.html", editor=HTMLEditor(base_url=HELP_PATH, open_externally=True), show_label=False, ), width=300, resizable=True, ) ########################################################################### # Protected interface. ########################################################################### @cached_property def _get_html(self): """Fetch the help HTML for the current model.""" if self.model is None: return "No model selected." # Determine the name of the model. model = self.model while hasattr(model, "adaptee"): model = model.adaptee name = model.__class__.__name__.lower() # Load HTML file, if possible. path = os.path.join(HELP_PATH, name + ".html") if os.path.isfile(path): with codecs.open(path, "r", "utf-8") as f: return f.read() else: return "No information available for model." envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/plot_2d_pane.py000066400000000000000000000107711441257372400276200ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Local imports. from attractors.model.i_plottable_2d import IPlottable2d # Enthought library imports. from chaco.api import ArrayPlotData, Plot from enable.api import ComponentEditor from pyface.tasks.api import TraitsTaskPane from traits.api import ( Dict, Instance, List, observe, on_trait_change, Property, Str, ) from traitsui.api import EnumEditor, HGroup, Item, Label, UItem, View class Plot2dPane(TraitsTaskPane): #### 'ITaskPane' interface ################################################ id = "example.attractors.plot_2d_pane" name = "Plot 2D Pane" #### 'Plot2dPane' interface ############################################### active_model = Instance(IPlottable2d) models = List(IPlottable2d) plot_type = Property(Str, observe="active_model.plot_type") title = Property(Str, observe="active_model.name") x_data = Property(observe="active_model.x_data") y_data = Property(observe="active_model.y_data") x_label = Property(Str, observe="active_model.x_label") y_label = Property(Str, observe="active_model.y_label") plot = Instance(Plot) def _plot_default(self): plot = Plot(ArrayPlotData(x=self.x_data, y=self.y_data)) plot.x_axis.title = self.x_label plot.y_axis.title = self.y_label plot.plot( ("x", "y"), type=self.plot_type, name=self.title, marker="pixel", color="blue", ) return plot @observe("x_data,y_data") def _update_plot_data(self, event): if event.name == "x_data": self.plot.data.set_data("x", event.new) else: self.plot.data.set_data("y", event.new) self.plot.invalidate_and_redraw() @observe("x_label,y_label") def _update_axis_label(self, event): if event.name == "x_label": self.plot.x_axis.title = event.new else: self.plot.y_axis.title = event.new self.plot.invalidate_and_redraw() @observe("active_model") def _update_plot_new_model(self, event): if event.old: self.plot.delplot(event.old.name) self.plot.data.set_data("x", event.new.x_data) self.plot.data.set_data("y", event.new.y_data) self.plot.plot( ("x", "y"), type=self.plot_type, name=self.title, marker="pixel", color="blue", ) self.plot.invalidate_and_redraw() view = View( HGroup( Label("Model: "), Item("active_model", editor=EnumEditor(name="_enum_map")), show_labels=False, ), UItem("plot", editor=ComponentEditor()), resizable=True, ) #### Private traits ####################################################### _enum_map = Dict(IPlottable2d, Str) ########################################################################### # Protected interface. ########################################################################### #### Trait property getters/setters ####################################### def _get_plot_type(self): return self.active_model.plot_type if self.active_model else "line" def _get_title(self): return self.active_model.name if self.active_model else "" def _get_x_data(self): return self.active_model.x_data if self.active_model else [] def _get_y_data(self): return self.active_model.y_data if self.active_model else [] def _get_x_label(self): return self.active_model.x_label if self.active_model else "" def _get_y_label(self): return self.active_model.y_label if self.active_model else "" #### Trait change handlers ################################################ @on_trait_change("models[]") def _update_models(self): # Make sure that the active model is valid with the new model list. if self.active_model not in self.models: self.active_model = self.models[0] if self.models else None # Refresh the EnumEditor map. self._enum_map = dict((model, model.name) for model in self.models) envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/plot_3d_pane.py000066400000000000000000000050351441257372400276160ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Local imports. from attractors.model.i_model_3d import IModel3d from mayavi.core.ui.mayavi_scene import MayaviScene # Enthought library imports. from mayavi.tools.mlab_scene_model import MlabSceneModel from tvtk.pyface.scene_editor import SceneEditor from pyface.tasks.api import TraitsTaskPane from traits.api import Dict, Instance, List, on_trait_change, Str from traitsui.api import EnumEditor, HGroup, Item, Label, View class Plot3dPane(TraitsTaskPane): #### 'ITaskPane' interface ################################################ id = "example.attractors.plot_3d_pane" name = "Plot 3D Pane" #### 'Plot3dPane' interface ############################################### active_model = Instance(IModel3d) models = List(IModel3d) scene = Instance(MlabSceneModel, ()) view = View( HGroup( Label("Model: "), Item("active_model", editor=EnumEditor(name="_enum_map")), show_labels=False, ), Item( "scene", editor=SceneEditor(scene_class=MayaviScene), show_label=False, ), resizable=True, ) #### Private traits ####################################################### _enum_map = Dict(IModel3d, Str) ########################################################################### # Protected interface. ########################################################################### #### Trait change handlers ################################################ @on_trait_change("active_model.points") def _update_scene(self): self.scene.mlab.clf() if self.active_model: x, y, z = self.active_model.points.swapaxes(0, 1) self.scene.mlab.plot3d(x, y, z, line_width=1.0, tube_radius=None) @on_trait_change("models[]") def _update_models(self): # Make sure that the active model is valid with the new model list. if self.active_model not in self.models: self.active_model = self.models[0] if self.models else None # Refresh the EnumEditor map. self._enum_map = dict((model, model.name) for model in self.models) envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/preferences.ini000066400000000000000000000001411441257372400276700ustar00rootroot00000000000000[example.attractors] default_task = example.attractors.task_3d always_use_default_layout = False envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/visualize_2d_task.py000066400000000000000000000064301441257372400306710ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Local imports. from attractors.model.i_plottable_2d import IPlottable2d from attractors.model_config_pane import ModelConfigPane from attractors.model_help_pane import ModelHelpPane from attractors.plot_2d_pane import Plot2dPane # Enthought library imports. from pyface.tasks.action.api import SMenu, SMenuBar, TaskToggleGroup from pyface.tasks.api import PaneItem, Tabbed, Task, TaskLayout from traits.api import adapt, Any, Instance, List class Visualize2dTask(Task): """A task for visualizing attractors in 2D.""" #### 'Task' interface ##################################################### id = "example.attractors.task_2d" name = "2D Visualization" menu_bar = SMenuBar( SMenu(id="File", name="&File"), SMenu(id="Edit", name="&Edit"), SMenu(TaskToggleGroup(), id="View", name="&View"), ) #### 'Visualize2dTask' interface ########################################## # The attractor model that is currently active (visible in the center # pane). active_model = Any # The list of available attractor models. models = List(Instance(IPlottable2d)) ########################################################################### # 'Task' interface. ########################################################################### def create_central_pane(self): """Create a plot pane with a list of models. Keep track of which model is active so that dock panes can introspect it. """ pane = Plot2dPane(models=self.models) self.active_model = pane.active_model pane.on_trait_change(self._update_active_model, "active_model") return pane def create_dock_panes(self): return [ ModelConfigPane(model=self.active_model), ModelHelpPane(model=self.active_model), ] ########################################################################### # Protected interface. ########################################################################### #### Trait initializers ################################################### def _default_layout_default(self): return TaskLayout( left=Tabbed( PaneItem("example.attractors.model_config_pane"), PaneItem("example.attractors.model_help_pane"), ) ) def _models_default(self): from attractors.model.henon import Henon from attractors.model.lorenz import Lorenz from attractors.model.rossler import Rossler models = [Henon(), Lorenz(), Rossler()] return [adapt(model, IPlottable2d) for model in models] #### Trait change handlers ################################################ def _update_active_model(self): self.active_model = self.window.central_pane.active_model for dock_pane in self.window.dock_panes: dock_pane.model = self.active_model envisage-7.0.3/envisage/examples/demo/plugins/tasks/attractors/visualize_3d_task.py000066400000000000000000000060711441257372400306730ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Local imports. from attractors.model_config_pane import ModelConfigPane from attractors.model_help_pane import ModelHelpPane from attractors.plot_3d_pane import Plot3dPane # Enthought library imports. from pyface.tasks.action.api import SMenu, SMenuBar, TaskToggleGroup from pyface.tasks.api import PaneItem, Tabbed, Task, TaskLayout from traits.api import Any, List class Visualize3dTask(Task): """A task for visualizing attractors in 3D.""" #### 'Task' interface ##################################################### id = "example.attractors.task_3d" name = "3D Visualization" menu_bar = SMenuBar( SMenu(id="File", name="&File"), SMenu(id="Edit", name="&Edit"), SMenu(TaskToggleGroup(), id="View", name="&View"), ) #### 'Visualize3dTask' interface ########################################## # The attractor model that is currently active (visible in the center # pane). active_model = Any # The list of available attractor models. models = List ########################################################################### # 'Task' interface. ########################################################################### def create_central_pane(self): """Create a plot pane with a list of models. Keep track of which model is active so that dock panes can introspect it. """ pane = Plot3dPane(models=self.models) self.active_model = pane.active_model pane.on_trait_change(self._update_active_model, "active_model") return pane def create_dock_panes(self): return [ ModelConfigPane(model=self.active_model), ModelHelpPane(model=self.active_model), ] ########################################################################### # Protected interface. ########################################################################### #### Trait initializers ################################################### def _default_layout_default(self): return TaskLayout( left=Tabbed( PaneItem("example.attractors.model_config_pane"), PaneItem("example.attractors.model_help_pane"), ) ) def _models_default(self): from attractors.model.lorenz import Lorenz from attractors.model.rossler import Rossler return [Lorenz(), Rossler()] #### Trait change handlers ################################################ def _update_active_model(self): self.active_model = self.window.central_pane.active_model for dock_pane in self.window.dock_panes: dock_pane.model = self.active_model envisage-7.0.3/envisage/examples/demo/plugins/tasks/index.rst000066400000000000000000000003121441257372400243410ustar00rootroot00000000000000Welcome to the attractors example. To run the application:: python run_attractor.py Note that this example application depends on the following additional packages:: numpy, scipy, chaco, mayavi envisage-7.0.3/envisage/examples/demo/plugins/tasks/run_attractor.py000066400000000000000000000023221441257372400257440ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 imports. from envisage.api import CorePlugin from envisage.ui.tasks.api import TasksPlugin def main(argv): """Run the application.""" # Import here so that this script can be run from anywhere without # having to install the packages. from attractors.attractors_application import AttractorsApplication from attractors.attractors_plugin import AttractorsPlugin plugins = [CorePlugin(), TasksPlugin(), AttractorsPlugin()] app = AttractorsApplication(plugins=plugins) app.run() if __name__ == "__main__": import sys # This context manager is added so that one can run this example from any # directory without necessarily having installed the examples as packages. from envisage.examples._demo import demo_path with demo_path(__file__): main(sys.argv) envisage-7.0.3/envisage/examples/tests/000077500000000000000000000000001441257372400201145ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/tests/__init__.py000066400000000000000000000000001441257372400222130ustar00rootroot00000000000000envisage-7.0.3/envisage/examples/tests/test__demo.py000066400000000000000000000014431441257372400226120ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 sys import unittest from envisage.examples._demo import demo_path class TestDemoUtilities(unittest.TestCase): """Test utility functions in the _demo module.""" def test_sys_path_inserted(self): path = os.path.join("dirname", "file.py") with demo_path(path): self.assertIn("dirname", sys.path) self.assertNotIn("dirname", sys.path) envisage-7.0.3/envisage/examples/tests/test_etsdemo_info.py000066400000000000000000000012661441257372400242050ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 unittest from envisage.examples._etsdemo_info import info class TestETSDemoInfo(unittest.TestCase): def test_info(self): # input to info is currently just a placeholder response = info({}) self.assertTrue(os.path.exists(response["root"])) envisage-7.0.3/envisage/extension_point.py000066400000000000000000000167441441257372400207470ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import weakref # Enthought library imports. from traits.api import List, provides, TraitType, Undefined # Local imports. from .i_extension_point import IExtensionPoint # 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 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) @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) ########################################################################### # '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().__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() def __repr__(self): """String representation of an ExtensionPoint object""" return "ExtensionPoint(id={!r})".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) ########################################################################### # '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) 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 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] ########################################################################### # 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 envisage-7.0.3/envisage/extension_point_binding.py000066400000000000000000000175271441257372400224410ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # 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 ############################ # Global dictionary used to keep ExtensionPointBinding objects alive. _bindings = {} #### '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().__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._bind() ########################################################################### # 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) 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)) #### 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 #### Methods ############################################################## def _bind(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 ) def _unbind(self): """Undo the effects of _bind""" self.extension_registry.remove_extension_point_listener( self._extension_point_listener, self.extension_point_id ) self.obj.on_trait_change( self._on_trait_items_changed, self.trait_name + "_items", remove=True, ) self.obj.on_trait_change( self._on_trait_changed, self.trait_name, remove=True ) 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) 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 ) def _set_extensions(self, extensions): """Set the extensions to an extension point.""" self.extension_registry.set_extensions( self.extension_point_id, extensions ) # Factory function for creating bindings. def bind_extension_point( obj, trait_name, extension_point_id, extension_registry ): """Create a binding to an extension point. The returned ExtensionPointBinding object is also stored in a (private) global dictionary, so that users aren't required to keep a reference to it. That global dictionary entry can be removed with a matching call to unbind_extension_point. Parameters ---------- obj : HasTraits The HasTraits object that we're binding to trait_name : str The name of the trait on obj to bind to. extension_point_id : str The id of the extension point. extension_registry : IExtensionRegistry The extension registry that the extension point is registered to. Returns ------- ExtensionPointBinding An object that manages the binding. """ binding = ExtensionPointBinding( obj=obj, trait_name=trait_name, extension_point_id=extension_point_id, extension_registry=extension_registry, ) # Keep a reference to each binding in the ExtensionPointBinding._bindings # global dictionary. Without this, the binding would become inactive # if the caller didn't keep a reference. bindings = ExtensionPointBinding._bindings.setdefault(obj, []) bindings.append(binding) return binding def unbind_extension_point( obj, trait_name, extension_point_id, extension_registry ): """ Remove an extension point binding. Changes to extension point contributions will no longer affect the target trait. Also removes the matching ExtensionPointBinding object from the global dictionary. Parameters ---------- obj : HasTraits The HasTraits object that we're binding to trait_name : str The name of the trait on obj to bind to. extension_point_id : str The id of the extension point. extension_registry : IExtensionRegistry The extension registry that the extension point is registered to. """ # Find the corresponding binding in the global dict. bindings = ExtensionPointBinding._bindings # Find the matching ExtensionPointBinding object. Search in reverse order, # since that's most likely to give the right binding quickly under the # normal first-in-last-out pattern for nested setup and teardown. index, binding = next( (index, binding) for index, binding in reversed(list(enumerate(bindings[obj]))) if binding.trait_name == trait_name if binding.extension_point_id == extension_point_id if binding.extension_registry == extension_registry ) # Remove the binding from the global dict, and remove the dict entry # altogether if it's now empty. bindings[obj].pop(index) if not bindings[obj]: bindings.pop(obj) # Undo the binding binding._unbind() envisage-7.0.3/envisage/extension_point_changed_event.py000066400000000000000000000022651441257372400236120ustar00rootroot00000000000000# (C) Copyright 2007-2023 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().__init__(**kw) # We add the extension point Id. self.extension_point_id = extension_point_id def __repr__(self): return ( "ExtensionPointChangedEvent(extension_point_id={!r}, " "index={!r}, removed={!r}, added={!r})" ).format(self.extension_point_id, self.index, self.removed, self.added) envisage-7.0.3/envisage/extension_provider.py000066400000000000000000000035171441257372400214420ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, ) envisage-7.0.3/envisage/extension_registry.py000066400000000000000000000156561441257372400214670ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import types import weakref # 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 .unknown_extension_point import UnknownExtensionPoint # Logging. logger = logging.getLogger(__name__) def _saferef(listener): """ Weak reference for a (possibly bound method) listener. Returns a weakref.WeakMethod reference for bound methods, and a regular weakref.ref otherwise. This means that for example ``_saferef(myobj.mymethod)`` returns a reference whose lifetime is connected to the lifetime of the object ``myobj``, rather than the lifetime of the temporary method ``myobj.mymethod``. Parameters ---------- listener : callable Listener to return a weak reference for. This can be either a plain function, a bound method, or some other form of callable. Returns ------- weakref.ref A weak reference to the listener. This will be a ``weakref.WeakMethod`` object if the listener is an instance of ``types.MethodType``, and a plain ``weakref.ref`` otherwise. """ if isinstance(listener, types.MethodType): return weakref.WeakMethod(listener) else: return weakref.ref(listener) @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(_saferef(listener)) 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) 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(_saferef(listener)) 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) 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) ########################################################################### # 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) def _check_extension_point(self, extension_point_id): """Check to see if the extension point exists. Raise an 'UnknownExtensionPoint' if it does not. """ if extension_point_id not in self._extension_points: raise UnknownExtensionPoint(extension_point_id) 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 envisage-7.0.3/envisage/i_application.py000066400000000000000000000042531441257372400203250ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 from .application_event import ApplicationEvent # 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 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() """ envisage-7.0.3/envisage/i_extension_point.py000066400000000000000000000026131441257372400212450ustar00rootroot00000000000000# (C) Copyright 2007-2023 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) envisage-7.0.3/envisage/i_extension_point_user.py000066400000000000000000000015521441257372400223040ustar00rootroot00000000000000# (C) Copyright 2007-2023 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) envisage-7.0.3/envisage/i_extension_provider.py000066400000000000000000000024611441257372400217470ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ envisage-7.0.3/envisage/i_extension_registry.py000066400000000000000000000054341441257372400217700ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.""" envisage-7.0.3/envisage/i_import_manager.py000066400000000000000000000031451441257372400210250ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ envisage-7.0.3/envisage/i_plugin.py000066400000000000000000000035351441257372400173220ustar00rootroot00000000000000# (C) Copyright 2007-2023 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) """ envisage-7.0.3/envisage/i_plugin_activator.py000066400000000000000000000024001441257372400213640ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.""" envisage-7.0.3/envisage/i_plugin_manager.py000066400000000000000000000043301441257372400210060ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'ValueError' 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 'ValueError' exception is raised. """ envisage-7.0.3/envisage/i_provider_extension_registry.py000066400000000000000000000017161441257372400237010ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ envisage-7.0.3/envisage/i_service_registry.py000066400000000000000000000111251441257372400214060ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ envisage-7.0.3/envisage/i_service_user.py000066400000000000000000000015031441257372400205130ustar00rootroot00000000000000# (C) Copyright 2007-2023 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) envisage-7.0.3/envisage/ids.py000066400000000000000000000040751441257372400162730ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 redefines the Extension Point IDs and Service IDs defined on Plugins provided by Envisage. Note that this module does not contain IDs defined by all of the Plugins available in Envisage. The Plugins themselves remain the ground truth for the IDs. This module is simply a convenient location from which the user can import the IDs. """ #### Extension Points ######################################################### #: Extension Point to contribute preference files, defined on the #: ``CorePlugin``. PREFERENCES = "envisage.preferences" #: Extension Point to contribute ``ServiceOffer`` s, defined on the #: ``CorePlugin``. SERVICE_OFFERS = "envisage.service_offers" # NOTE : The other PythonShellPlugin defines extension points with the same ID. #: Extension Point to contribute name/value pairs that will be bound to the #: ``PythonShell``, defined on the ``PythonShellPlugin``. BINDINGS = "envisage.plugins.python_shell.bindings" #: Extension Point to contribute commands that will be executed in the #: ``PythonShell``, defined on the ``PythonShellPlugin``. COMMANDS = "envisage.plugins.python_shell.commands" #: Extension Point to contribute preferences categories, defined on the #: ``TasksPlugin``. PREFERENCES_CATEGORIES = "envisage.ui.tasks.preferences_categories" #: Extension Point to contribute preference panes, defined on the #: ``TasksPlugin``. PREFERENCES_PANES = "envisage.ui.tasks.preferences_panes" #: Extension Point to contribute task factories, defined on the #: ``TasksPlugin``. TASKS = "envisage.ui.tasks.tasks" #: Extension Point to contribute task extensions, defined on the #: ``TasksPlugin``. TASK_EXTENSIONS = "envisage.ui.tasks.task_extensions" envisage-7.0.3/envisage/import_manager.py000066400000000000000000000034611441257372400205160ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ import importlib # 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 = importlib.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 envisage-7.0.3/envisage/package_plugin_manager.py000066400000000000000000000130271441257372400221540ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import sys import warnings 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. """ def __init__(self, **traits): warnings.warn( ( "The PackagePluginManager is deprecated. The recommended " "approach is to install plugin-containing packages into " "site-packages and advertise the plugins via entry points. " ), DeprecationWarning, stacklevel=2, ) super().__init__(**traits) # 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 _update_path_and_reset_plugins(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) envisage-7.0.3/envisage/plugin.py000066400000000000000000000317311441257372400170110ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 logging import os from os.path import exists, join # Enthought library imports. from traits.api import Instance, List, Property, provides, Str 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) if len(trait_names) == 0: extensions = [] 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 {} has no Id - using <{}>".format( object.__repr__(self), id ) ) return id def _name_default(self): """Trait initializer.""" name = camel_case_to_words(type(self).__name__) logger.warning( "plugin {} has no name - using <{}>".format( object.__repr__(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().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().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) def disconnect_extension_point_traits(self): """Disconnect all of the plugin's extension points.""" ExtensionPoint.disconnect_extension_point_traits(self) 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) 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: try: self.application.get_service_from_id(service_id) except ValueError: # the service may have already been individually unregistered pass else: self.application.unregister_service(service_id) # Just in case the plugin is started again! self._service_ids = [] ########################################################################### # 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, len(old)) # Let the extension registry know about the change. self._fire_extension_point_changed( trait.contributes_to, added, removed, index ) #### 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 Exception: 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 'Supports' 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 _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) ########################################################################### # 'object' interface. ########################################################################### def __repr__(self): """String representation of a Plugin object""" return f"{type(self).__name__}(id={self.id!r}, name={self.name!r})" envisage-7.0.3/envisage/plugin_activator.py000066400000000000000000000031211441257372400210550ustar00rootroot00000000000000# (C) Copyright 2007-2023 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() 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() envisage-7.0.3/envisage/plugin_event.py000066400000000000000000000012101441257372400201770ustar00rootroot00000000000000# (C) Copyright 2007-2023 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") envisage-7.0.3/envisage/plugin_extension_registry.py000066400000000000000000000045731441257372400230410ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, observe, 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 ################################################ @observe("plugin_manager") def _update_providers(self, event): """Static trait change handler.""" old, new = event.old, event.new # 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) @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) @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) envisage-7.0.3/envisage/plugin_manager.py000066400000000000000000000170071441257372400205030ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ import logging import warnings from fnmatch import fnmatch from traits.api import Event, HasTraits, Instance, List, observe, provides, Str 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) @observe("application") def _set_new_application_on_all_plugins(self, event): """Static trait change handler.""" self._update_application_on_plugins([], self._plugins) #: 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'. """ if "include" in traits or "exclude" in traits: warnings.warn( "The 'include' and 'exclude' traits to PluginManager " "are deprecated, and will be removed in a future version " "of Envisage", DeprecationWarning, stacklevel=2, ) super().__init__(**traits) if plugins is not None: self._plugins = plugins 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) 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) def start(self): """Start the plugin manager.""" for plugin in self._plugins: self.start_plugin(plugin) 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 ValueError("no such plugin %s" % plugin_id) 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) 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 ValueError("no such plugin %s" % plugin_id) #### Protected 'PluginManager' ############################################ # The plugins that the manager manages! _plugins = List(IPlugin) @observe("_plugins") def _update_application_on_all_plugins(self, event): """Static trait change handler.""" old, new = event.old, event.new self._update_application_on_plugins(old, new) @observe("_plugins:items") def _update_application_on_changed_plugins(self, event): """Static trait change handler.""" self._update_application_on_plugins(event.removed, event.added) 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_application_on_plugins(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 envisage-7.0.3/envisage/plugins/000077500000000000000000000000001441257372400166155ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/__init__.py000066400000000000000000000000001441257372400207140ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/event_manager/000077500000000000000000000000001441257372400214305ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/event_manager/__init__.py000066400000000000000000000000001441257372400235270ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/event_manager/api.py000066400000000000000000000010041441257372400225460ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ - :class:`~.EventManagerPlugin` """ from envisage.plugins.event_manager.plugin import EventManagerPlugin envisage-7.0.3/envisage/plugins/event_manager/plugin.py000066400000000000000000000025541441257372400233060ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ from traits.api import List # Enthought library imports. from envisage.api import Plugin, ServiceOffer 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] envisage-7.0.3/envisage/plugins/python_shell/000077500000000000000000000000001441257372400213255ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/python_shell/__init__.py000066400000000000000000000000001441257372400234240ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/python_shell/api.py000066400000000000000000000007001441257372400224450ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/plugins/python_shell/i_python_shell.py000066400000000000000000000017741441257372400247300ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.""" envisage-7.0.3/envisage/plugins/python_shell/python_shell_plugin.py000066400000000000000000000051431441257372400257700ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ from traits.api import Dict, List, Str # Enthought library imports. from envisage.api import ExtensionPoint, Plugin 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.namespace_view import NamespaceView from .view.python_shell_view import PythonShellView return [PythonShellView, NamespaceView] envisage-7.0.3/envisage/plugins/python_shell/view/000077500000000000000000000000001441257372400222775ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/python_shell/view/__init__.py000066400000000000000000000000001441257372400243760ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/python_shell/view/api.py000066400000000000000000000007061441257372400234250ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/plugins/python_shell/view/namespace_view.py000066400000000000000000000112501441257372400256360ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Enthought library imports. from pyface.workbench.api import View from traits.api import ( cached_property, DelegatesTo, HasTraits, Instance, List, Property, Str, ) 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 ( EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate, RuleTableFilter, ) from envisage.plugins.python_shell.api import IPythonShell from envisage.plugins.python_shell.view.python_shell_view import ( PythonShellView, ) # 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": 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, observe="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 envisage-7.0.3/envisage/plugins/python_shell/view/python_shell_view.py000066400000000000000000000174221441257372400264210ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import sys from pyface.api import PythonShell from pyface.workbench.api import View from traits.api import Any, Dict, Event, Instance, Property, provides, Str # Enthought library imports. from envisage.api import ExtensionPoint, IExtensionRegistry from envisage.plugins.python_shell.api import IPythonShell # 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(Dict(Str, Any)) # 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().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 ########################################################################### # '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) 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 #### 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) 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) def _on_write_stdout(self, text): """Dynamic trait change handler.""" self.shell.control.write(text) envisage-7.0.3/envisage/plugins/tasks/000077500000000000000000000000001441257372400177425ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/tasks/__init__.py000066400000000000000000000000001441257372400220410ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/tasks/python_shell_plugin.py000066400000000000000000000077631441257372400244170ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 from pyface.tasks.contrib.python_shell import PythonShellTask # Enthought library imports. from traits.api import Dict, Instance, List, Property, Str from envisage.api import ExtensionPoint, IExtensionRegistry, Plugin 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, 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, ), ] envisage-7.0.3/envisage/plugins/text_editor/000077500000000000000000000000001441257372400211475ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/text_editor/__init__.py000066400000000000000000000000001441257372400232460ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/text_editor/actions.py000066400000000000000000000027451441257372400231710ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.action.api import Action from pyface.api import FileDialog, OK 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) envisage-7.0.3/envisage/plugins/text_editor/api.py000066400000000000000000000007721441257372400223000ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 .editor.text_editor import TextEditor from .text_editor_action_set import TextEditorActionSet envisage-7.0.3/envisage/plugins/text_editor/editor/000077500000000000000000000000001441257372400224355ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/text_editor/editor/__init__.py000066400000000000000000000000001441257372400245340ustar00rootroot00000000000000envisage-7.0.3/envisage/plugins/text_editor/editor/text_editor.py000066400000000000000000000150261441257372400253450ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 from pyface.api import CANCEL, FileDialog # Enthought library imports. from pyface.workbench.api import TraitsUIEditor from traits.api import Code, Instance, observe 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 _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: with open(self.obj.path, "w", encoding="utf-8") as f: f.write(self.text) # We have just saved the file so we ain't dirty no more! self.dirty = False 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() ########################################################################### # '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 ) def select_line(self, lineno): """Selects the specified line.""" self.ui.info.text.selected_line = lineno ########################################################################### # 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 ################################################ @observe("obj") def _handle_update_to_object(self, event): """Static trait change handler.""" new = event.new # 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) with open(new.path, "r", encoding="utf-8") as f: self.text = f.read() @observe("text") def _update_dirty(self, event): """Static trait change handler.""" if self.traits_inited(): self.dirty = True @observe("dirty") def _update_name(self, event): """Static trait change handler.""" dirty = event.new if len(self.obj.path) > 0: if dirty: self.name = basename(self.obj.path) + "*" else: self.name = basename(self.obj.path) #### 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 envisage-7.0.3/envisage/plugins/text_editor/editor/text_editor_handler.py000066400000000000000000000022221441257372400270340ustar00rootroot00000000000000# (C) Copyright 2007-2023 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() def save(self, info): """Save the text to disk.""" info.object.save() envisage-7.0.3/envisage/plugins/text_editor/text_editor_action_set.py000066400000000000000000000022151441257372400262630ustar00rootroot00000000000000# (C) Copyright 2007-2023 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", ), ] envisage-7.0.3/envisage/plugins/text_editor/text_editor_plugin.py000066400000000000000000000022541441257372400254340ustar00rootroot00000000000000# (C) Copyright 2007-2023 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] envisage-7.0.3/envisage/provider_extension_registry.py000066400000000000000000000257001441257372400233700ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, on_trait_change, provides # 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 TypeError("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) 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) ########################################################################### # 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 extension_point_id not 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 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] ########################################################################### # 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 extension_point_id not 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 ) #### 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 envisage-7.0.3/envisage/resource/000077500000000000000000000000001441257372400167635ustar00rootroot00000000000000envisage-7.0.3/envisage/resource/__init__.py000066400000000000000000000000001441257372400210620ustar00rootroot00000000000000envisage-7.0.3/envisage/resource/api.py000066400000000000000000000014221441257372400201050ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 .file_resource_protocol import FileResourceProtocol from .http_resource_protocol import HTTPResourceProtocol from .i_resource_manager import IResourceManager from .i_resource_protocol import IResourceProtocol from .no_such_resource_error import NoSuchResourceError from .package_resource_protocol import PackageResourceProtocol from .resource_manager import ResourceManager envisage-7.0.3/envisage/resource/file_resource_protocol.py000066400000000000000000000027451441257372400241140ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/resource/http_resource_protocol.py000066400000000000000000000026171441257372400241520ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 imports here 'cos I'm not sure how much this will actually # be used. from urllib.error import HTTPError from urllib.request import urlopen try: f = urlopen("http://" + address) except HTTPError: raise NoSuchResourceError("http:://" + address) return f envisage-7.0.3/envisage/resource/i_resource_manager.py000066400000000000000000000020371441257372400231700ustar00rootroot00000000000000# (C) Copyright 2007-2023 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') """ envisage-7.0.3/envisage/resource/i_resource_protocol.py000066400000000000000000000016061441257372400234200ustar00rootroot00000000000000# (C) Copyright 2007-2023 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') """ envisage-7.0.3/envisage/resource/no_such_resource_error.py000066400000000000000000000013001441257372400241050ustar00rootroot00000000000000# (C) Copyright 2007-2023 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) envisage-7.0.3/envisage/resource/package_resource_protocol.py000066400000000000000000000036541441257372400245700ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. try: from importlib.resources import files except ImportError: from importlib_resources import files # 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 'importlib.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.""" package, *resource_path = address.split("/") if not package or not resource_path: raise NoSuchResourceError(address) try: f = files(package).joinpath(*resource_path).open("rb") except ( ModuleNotFoundError, TypeError, # TypeError is raised if package is a module FileNotFoundError, IsADirectoryError, PermissionError, ): raise NoSuchResourceError(address) return f envisage-7.0.3/envisage/resource/resource_manager.py000066400000000000000000000043511441257372400226610ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, provides, Str # 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) envisage-7.0.3/envisage/resource/tests/000077500000000000000000000000001441257372400201255ustar00rootroot00000000000000envisage-7.0.3/envisage/resource/tests/__init__.py000066400000000000000000000000001441257372400222240ustar00rootroot00000000000000envisage-7.0.3/envisage/resource/tests/test_resource_manager.py000066400000000000000000000125531441257372400250650ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import urllib.request from io import StringIO from urllib.error import HTTPError try: from importlib.resources import as_file, files except ImportError: from importlib_resources import as_file, files # Enthought library imports. from envisage.resource.api import NoSuchResourceError, ResourceManager # Module to patch urlopen in during testing. url_library = urllib.request # 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("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. resource = files("envisage.resource") / "api.py" with as_file(resource) as path: # Open a file resource. f = rm.file(f"file://{path}") self.assertNotEqual(f, None) contents = f.read() f.close() # Open the api file via the file system. with open(path, "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 bytes of the 'api.py' file. resource = files("envisage.resource") / "api.py" with resource.open("rb") as g: self.assertEqual(g.read(), contents) def test_package_resource_subdir(self): """package resource""" rm = ResourceManager() # Open a package resource. f = rm.file("pkgfile://envisage.resource/tests/__init__.py") self.assertNotEqual(f, None) contents = f.read() f.close() # Get the bytes of the 'api.py' file. resource = files("envisage.resource") / "tests" / "__init__.py" with resource.open("rb") as g: self.assertEqual(g.read(), contents) 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/") with self.assertRaises(NoSuchResourceError): rm.file("pkgfile:///envisage") with self.assertRaises(NoSuchResourceError): rm.file("pkgfile://envisage.resource/bogus.py") with self.assertRaises(NoSuchResourceError): rm.file("pkgfile://completely.bogus/bogus.py") with self.assertRaises(NoSuchResourceError): rm.file("pkgfile://envisage.resource.resource_manager/anything") 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") envisage-7.0.3/envisage/service.py000066400000000000000000000057111441257372400171520ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 TraitError, 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().__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 def __repr__(self): """String representation of a Service object""" return "Service(protocol={!r})".format(self._protocol) ########################################################################### # '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 TraitError("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 envisage-7.0.3/envisage/service_offer.py000066400000000000000000000027101441257372400203270ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, HasTraits, Str, Type, Union 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 = Union(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 = Union(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 envisage-7.0.3/envisage/service_registry.py000066400000000000000000000215101441257372400210750ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 # 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, str): 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) 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) ########################################################################### # 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 Exception: 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, str): 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, str): 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 envisage-7.0.3/envisage/tests/000077500000000000000000000000001441257372400162765ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/__init__.py000066400000000000000000000000001441257372400203750ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/bad_eggs/000077500000000000000000000000001441257372400200315ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/bad_eggs/README.txt000066400000000000000000000001351441257372400215260ustar00rootroot00000000000000This directory contains packages used by the test suite to create eggs for testing purposes. envisage-7.0.3/envisage/tests/bad_eggs/acme-bad/000077500000000000000000000000001441257372400214625ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/bad_eggs/acme-bad/acme_bad/000077500000000000000000000000001441257372400231755ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/bad_eggs/acme-bad/acme_bad/__init__.py000066400000000000000000000006271441257372400253130ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/envisage/tests/bad_eggs/acme-bad/acme_bad/bad_plugin.py000066400000000000000000000024521441257372400256560ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'Bar' plugin """ # Deliberate bad import import busted_module # noqa: F401 from traits.api import Bool # Enthought library imports. from envisage.api import Plugin class BadPlugin(Plugin): """The 'Bad' plugin""" #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = "acme.bad" #### 'BadPlugin' 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 envisage-7.0.3/envisage/tests/bad_eggs/acme-bad/setup.py000066400000000000000000000015211441257372400231730ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Major package imports. from setuptools import find_packages, setup setup( name="acme-bad", version="0.1a1", author="Enthought, Inc", author_email="info@enthought.com", license="BSD", zip_safe=True, packages=find_packages(include="acme_bad*"), include_package_data=True, install_requires=["acme-foo"], entry_points=""" [envisage.plugins] acme.bad = acme_bad.bad_plugin:BadPlugin """, ) envisage-7.0.3/envisage/tests/eggs/000077500000000000000000000000001441257372400172235ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/eggs/README.txt000066400000000000000000000001351441257372400207200ustar00rootroot00000000000000This directory contains packages used by the test suite to create eggs for testing purposes. envisage-7.0.3/envisage/tests/eggs/acme-bar/000077500000000000000000000000001441257372400206725ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/eggs/acme-bar/acme_bar/000077500000000000000000000000001441257372400224235ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/eggs/acme-bar/acme_bar/__init__.py000066400000000000000000000006271441257372400245410ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/envisage/tests/eggs/acme-bar/acme_bar/bar_plugin.py000066400000000000000000000023561441257372400251250ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'Bar' plugin """ from traits.api import Bool # Enthought library imports. from envisage.api import Plugin class BarPlugin(Plugin): """The 'Bar' plugin""" #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = "acme.bar" #### 'BarPlugin' 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 envisage-7.0.3/envisage/tests/eggs/acme-bar/setup.py000066400000000000000000000015211441257372400224030ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Major package imports. from setuptools import find_packages, setup setup( name="acme-bar", version="0.1a1", author="Enthought, Inc", author_email="info@enthought.com", license="BSD", zip_safe=True, packages=find_packages(include="acme_bar*"), include_package_data=True, install_requires=["acme-foo"], entry_points=""" [envisage.plugins] acme.bar = acme_bar.bar_plugin:BarPlugin """, ) envisage-7.0.3/envisage/tests/eggs/acme-baz/000077500000000000000000000000001441257372400207025ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/eggs/acme-baz/acme_baz/000077500000000000000000000000001441257372400224435ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/eggs/acme-baz/acme_baz/__init__.py000066400000000000000000000006271441257372400245610ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/envisage/tests/eggs/acme-baz/acme_baz/baz_plugin.py000066400000000000000000000023561441257372400251550ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'Baz' plugin """ from traits.api import Bool # Enthought library imports. from envisage.api import Plugin class BazPlugin(Plugin): """The 'Baz' plugin""" #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = "acme.baz" #### 'BazPlugin' 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 envisage-7.0.3/envisage/tests/eggs/acme-baz/setup.py000066400000000000000000000015211441257372400224130ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Major package imports. from setuptools import find_packages, setup setup( name="acme-baz", version="0.1a1", author="Enthought, Inc", author_email="info@enthought.com", license="BSD", zip_safe=True, packages=find_packages(include="acme_baz*"), include_package_data=True, install_requires=["acme-bar"], entry_points=""" [envisage.plugins] acme.baz = acme_baz.baz_plugin:BazPlugin """, ) envisage-7.0.3/envisage/tests/eggs/acme-foo/000077500000000000000000000000001441257372400207115ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/eggs/acme-foo/acme_foo/000077500000000000000000000000001441257372400224615ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/eggs/acme-foo/acme_foo/__init__.py000066400000000000000000000006271441257372400245770ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/envisage/tests/eggs/acme-foo/acme_foo/foo_plugin.py000066400000000000000000000023561441257372400252020ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 'Foo' plugin """ from traits.api import Bool # Enthought library imports. from envisage.api import Plugin class FooPlugin(Plugin): """The 'Foo' plugin""" #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = "acme.foo" #### 'FooPlugin' 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 envisage-7.0.3/envisage/tests/eggs/acme-foo/setup.py000066400000000000000000000015071441257372400224260ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! # Major package imports. from setuptools import find_packages, setup setup( name="acme-foo", version="0.1a1", author="Enthought, Inc", author_email="info@enthought.com", license="BSD", zip_safe=True, packages=find_packages(include="acme_foo*"), include_package_data=True, install_requires=[], entry_points=""" [envisage.plugins] acme.foo = acme_foo.foo_plugin:FooPlugin """, ) envisage-7.0.3/envisage/tests/ets_config_patcher.py000066400000000000000000000037431441257372400225050ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/tests/event_tracker.py000066400000000000000000000060571441257372400215140ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 from traits.has_traits import observe 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 ################################################ @observe("subscriptions") def _update_listeners_on_all_subscriptions(self, event): """Static trait change handler.""" old, new = event.old, event.new for subscription in old: self._remove_subscription(subscription) for subscription in new: self._add_subscription(subscription) @observe("subscriptions:items") def _update_listeners_on_changed_subscriptions(self, event): """Static trait change handler.""" for subscription in event.removed: self._remove_subscription(subscription) for subscription in event.added: self._add_subscription(subscription) 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) #### 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) 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) envisage-7.0.3/envisage/tests/foo.py000066400000000000000000000011651441257372400174360ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/tests/i_foo.py000066400000000000000000000010451441257372400177430ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/tests/mutable_extension_registry.py000066400000000000000000000045011441257372400243250ustar00rootroot00000000000000# (C) Copyright 2007-2023 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]) 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) def remove_extension(self, extension_point_id, extension): """Remove a contribution from an extension point.""" self.remove_extensions(extension_point_id, [extension]) 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) envisage-7.0.3/envisage/tests/plugins/000077500000000000000000000000001441257372400177575ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/plugins/banana/000077500000000000000000000000001441257372400211775ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/plugins/banana/__init__.py000066400000000000000000000000001441257372400232760ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/plugins/banana/banana_plugin.py000066400000000000000000000020341441257372400243460ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 traits.api import Bool from envisage.api import Plugin 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 def stop(self): """Stop the plugin.""" self.started = False self.stopped = True #### 'BananaPlugin' protocol ############################################## started = Bool(False) stopped = Bool(False) envisage-7.0.3/envisage/tests/plugins/banana/plugins.py000066400000000000000000000011221441257372400232260ustar00rootroot00000000000000# (C) Copyright 2007-2023 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()] envisage-7.0.3/envisage/tests/plugins/orange/000077500000000000000000000000001441257372400212325ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/plugins/orange/__init__.py000066400000000000000000000000001441257372400233310ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/plugins/orange/orange_plugin.py000066400000000000000000000020341441257372400244340ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 traits.api import Bool from envisage.api import Plugin 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 def stop(self): """Stop the plugin.""" self.started = False self.stopped = True #### 'BananaPlugin' protocol ############################################## started = Bool(False) stopped = Bool(False) envisage-7.0.3/envisage/tests/plugins/orange/plugins.py000066400000000000000000000011221441257372400232610ustar00rootroot00000000000000# (C) Copyright 2007-2023 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()] envisage-7.0.3/envisage/tests/plugins/pear/000077500000000000000000000000001441257372400207065ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/plugins/pear/__init__.py000066400000000000000000000000001441257372400230050ustar00rootroot00000000000000envisage-7.0.3/envisage/tests/plugins/pear/pear_plugin.py000066400000000000000000000020241441257372400235630ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 traits.api import Bool from envisage.api import Plugin 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 def stop(self): """Stop the plugin.""" self.started = False self.stopped = True #### 'BananaPlugin' protocol ############################################## started = Bool(False) stopped = Bool(False) envisage-7.0.3/envisage/tests/preferences.ini000066400000000000000000000000301441257372400212710ustar00rootroot00000000000000[enthought.test] x = 42 envisage-7.0.3/envisage/tests/support.py000066400000000000000000000136231441257372400203710ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ Support utilities for tests in the Envisage test suite. Note: this is intended to provide helpers for testing Envisage itself rather than for external Envisage-based code. The helpers here should be considered private to Envisage. """ import contextlib import pathlib import subprocess import sys import tempfile import unittest from pkg_resources import resource_filename, working_set from pyface.api import GUI from traits.api import Int, List from envisage.api import Application, ExtensionPoint, Plugin # Skip decorator for tests that require a working GUI instance. try: GUI() except NotImplementedError: gui_available = False else: gui_available = True requires_gui = unittest.skipUnless( gui_available, "Test requires a non-null GUI backend" ) # Test for PySide6 being installed try: import PySide6 except ImportError: pyside6_available = False pyside6_version = None else: pyside6_available = True pyside6_version = PySide6.__version_info__ del PySide6 # Various useful context managers. @contextlib.contextmanager def temporary_directory(): """ Create and remove a temporary directory. Yields a pathlib.Path instance. """ with tempfile.TemporaryDirectory() as tempdir: yield pathlib.Path(tempdir) @contextlib.contextmanager def restore_sys_path(): """ Save and restore `sys.path` state. On entering the associated context, this context manager saves the state of `sys.path`. Within the context, code may then make changes to that global state (for example by adding distributions to the working set). On exiting the context, `sys.path` is restored to its original state. """ original_sys_path = sys.path[:] try: yield finally: sys.path[:] = original_sys_path @contextlib.contextmanager def restore_sys_modules(): """ Save and restore `sys.modules` state. On entering the associated context, this context manager saves the state of `sys.modules`. On exiting the context, any additional keys that were added to `sys.modules` are removed. """ original_modules = set(sys.modules) try: yield finally: for name in sys.modules.keys() - original_modules: del sys.modules[name] @contextlib.contextmanager def restore_pkg_resources_working_set(): """ Save and restore `pkg_resources.working_set` state. On entering the associated context, this context manager saves the state of `pkg_resources.working_set`. Within the context, code may then make changes to that global state (for example by adding distributions to the working set). On exiting the context, `pkg_resources.working_set` is restored to its original state. """ original_entries = working_set.entries[:] original_entry_keys = set(working_set.entry_keys) original_by_key = set(working_set.by_key) # Older setuptools versions don't have this attribute; it appears to # be new in setuptools ~ 62. if hasattr(working_set, "normalized_to_canonical_keys"): original_normalized = set(working_set.normalized_to_canonical_keys) else: original_normalized = None try: yield finally: if original_normalized is not None: for key in ( working_set.normalized_to_canonical_keys.keys() - original_normalized ): del working_set.normalized_to_canonical_keys[key] for key in working_set.by_key.keys() - original_by_key: del working_set.by_key[key] for key in working_set.entry_keys.keys() - original_entry_keys: del working_set.entry_keys[key] working_set.entries[:] = original_entries # Egg and package-related functionality. def build_egg(package_dir, dist_dir): """Helper function to build an egg. Parameters ---------- package_dir : pathlib.Path Directory containing the Python package to be built. Should contain a "setup.py" file that can be used with "python setup.py bdist_egg" to build the package. dist_dir : pathlib.Path Directory to place the built egg in. The directory should already exist. """ subprocess.run( [ sys.executable, "setup.py", "bdist_egg", "--dist-dir", dist_dir, ], cwd=package_dir, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Packages containing plugins that are used for testing _PACKAGES_DIR = pathlib.Path(resource_filename("envisage.tests", "eggs")) PLUGIN_PACKAGES = [ _PACKAGES_DIR / "acme-bar", _PACKAGES_DIR / "acme-baz", _PACKAGES_DIR / "acme-foo", ] # acme-bad contains a plugin that depends on a non-existent module. _BAD_PACKAGES_DIR = pathlib.Path( resource_filename("envisage.tests", "bad_eggs") ) BAD_PLUGIN_PACKAGES = [_BAD_PACKAGES_DIR / "acme-bad"] # Application class used in various tests. class SimpleApplication(Application): """The type of application used in the tests.""" id = "test" # Plugins used in multiple tests. 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") envisage-7.0.3/envisage/tests/test_api.py000066400000000000000000000020741441257372400204630ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import envisage.api as api import envisage.ids as ids class TestApi(unittest.TestCase): """Test for API""" def test_import(self): self.assertEqual(api.BINDINGS, ids.BINDINGS) self.assertEqual(api.COMMANDS, ids.COMMANDS) self.assertEqual(api.PREFERENCES, ids.PREFERENCES) self.assertEqual( api.PREFERENCES_CATEGORIES, ids.PREFERENCES_CATEGORIES ) self.assertEqual(api.PREFERENCES_PANES, ids.PREFERENCES_PANES) self.assertEqual(api.SERVICE_OFFERS, ids.SERVICE_OFFERS) self.assertEqual(api.TASKS, ids.TASKS) self.assertEqual(api.TASK_EXTENSIONS, ids.TASK_EXTENSIONS) envisage-7.0.3/envisage/tests/test_application.py000066400000000000000000000336641441257372400222260ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 unittest from traits.api import Bool, Int, List # Enthought library imports. from traits.etsconfig.api import ETSConfig from envisage.api import ExtensionPoint, Plugin, PluginManager from envisage.tests.ets_config_patcher import ETSConfigPatcher # Local imports. from envisage.tests.event_tracker import EventTracker from envisage.tests.support import PluginA, PluginB, PluginC, SimpleApplication def vetoer(event): """An observer that will veto an event.""" event.new.veto = True 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 # PluginD and PluginE each contribute to the other's extension points, but both # expect to be started before contributions are made. # xref: enthought/envisage#417 class PluginD(Plugin): """Plugin that expects to be started before contributing to extension points.""" id = "D" x = ExtensionPoint(List, id="d.x") y = List(Int, contributes_to="e.x") started = Bool(False) def start(self): self.started = True def _y_default(self): if self.started: return [4, 5, 6] else: return [] class PluginE(Plugin): """Another plugin that expects to be started before contributing to extension points.""" id = "E" x = ExtensionPoint(List, id="e.x") y = List(Int, contributes_to="d.x") started = Bool(False) def start(self): self.started = True def _y_default(self): if self.started: return [1, 2, 3] else: return [] class ApplicationTestCase(unittest.TestCase): """Tests for applications and plugins.""" def setUp(self): """Prepares the test fixture before each test method is called.""" ets_config_patcher = ETSConfigPatcher() ets_config_patcher.start() self.addCleanup(ets_config_patcher.stop) def test_home(self): """home""" application = SimpleApplication() # 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 = SimpleApplication() # 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 = SimpleApplication() 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 = SimpleApplication() # This listener will veto the 'starting' event. application.observe(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 = SimpleApplication() # This listener will veto the 'stopping' event. application.observe(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 = SimpleApplication(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(ValueError): application.start_plugin(plugin_id="bogus") # Try to stop a non-existent plugin. with self.assertRaises(ValueError): application.stop_plugin(plugin_id="bogus") def test_extension_point(self): """extension point""" a = PluginA() b = PluginB() c = PluginC() application = SimpleApplication(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_extension_point_resolution_occurs_after_plugin_start(self): # Regression test for enthought/envisage#417 # Given d = PluginD() e = PluginE() application = SimpleApplication(plugins=[d, e]) # When application.start() # Then self.assertEqual( application.get_extensions("d.x"), [1, 2, 3], ) self.assertEqual( application.get_extensions("e.x"), [4, 5, 6], ) 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 = SimpleApplication(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 = SimpleApplication(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 = SimpleApplication(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 = SimpleApplication(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 = SimpleApplication(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 = SimpleApplication( 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")) envisage-7.0.3/envisage/tests/test_composite_plugin_manager.py000066400000000000000000000140271441257372400247650ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ import unittest from traits.api import Bool from envisage.application import Application from envisage.composite_plugin_manager import CompositePluginManager from envisage.plugin import Plugin from envisage.plugin_manager import PluginManager 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) envisage-7.0.3/envisage/tests/test_core_plugin.py000066400000000000000000000157531441257372400222300ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import unittest # Major package imports. from pkg_resources import resource_filename from traits.api import HasTraits, Interface, List, on_trait_change, Str # Enthought library imports. from envisage.api import CorePlugin, Plugin, ServiceOffer from envisage.tests.support import SimpleApplication # This module's package. PKG = "envisage.tests" class CorePluginTestCase(unittest.TestCase): """Tests for the core plugin.""" def test_service_offers(self): """service offers""" 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 = SimpleApplication(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""" 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 = SimpleApplication(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_preferences(self): """preferences""" 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 = SimpleApplication(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""" 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 = SimpleApplication(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")) # regression test for enthought/envisage#251 def test_unregister_service_offer(self): """Unregister a service that is contributed to the "envisage.service_offers" extension point while the application is running. """ class IJunk(Interface): trash = Str() class Junk(HasTraits): trash = Str("garbage") class PluginA(Plugin): # The Ids of the extension points that this plugin contributes to. SERVICE_OFFERS = "envisage.service_offers" service_offers = List(contributes_to=SERVICE_OFFERS) def _service_offers_default(self): a_service_offer = ServiceOffer( protocol=IJunk, factory=self._create_junk_service, ) return [a_service_offer] def _create_junk_service(self): """Factory method for the 'Junk' service.""" return Junk() @on_trait_change("application:started") def _unregister_junk_service(self): # only 1 service is registered so it has service_id of 1 self.application.unregister_service(1) application = SimpleApplication( plugins=[CorePlugin(), PluginA()], ) # Run it! application.run() def test_unregister_service(self): """Unregister a service which was registered on the application directly, not through the CorePlugin's extension point. CorePlugin should not do anything to interfere.""" class IJunk(Interface): trash = Str() class Junk(HasTraits): trash = Str("garbage") some_junk = Junk() application = SimpleApplication( plugins=[CorePlugin()], ) application.start() some_junk_id = application.register_service(IJunk, some_junk) application.unregister_service(some_junk_id) application.stop() envisage-7.0.3/envisage/tests/test_egg_basket_plugin_manager.py000066400000000000000000000262501441257372400250570ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 contextlib import glob import os import shutil import tempfile import unittest from os.path import basename, join import pkg_resources from envisage.egg_basket_plugin_manager import EggBasketPluginManager from envisage.tests.support import ( BAD_PLUGIN_PACKAGES, build_egg, PLUGIN_PACKAGES, restore_pkg_resources_working_set, restore_sys_modules, restore_sys_path, temporary_directory, ) class EggBasketPluginManagerTestCase(unittest.TestCase): """Tests for the 'Egg Basket' plugin manager.""" @classmethod def setUpClass(cls): cls.egg_cleanup_stack = contextlib.ExitStack() cls.eggs_dir = os.fspath( cls.egg_cleanup_stack.enter_context(temporary_directory()) ) cls.bad_eggs_dir = os.fspath( cls.egg_cleanup_stack.enter_context(temporary_directory()) ) # Build eggs for package in PLUGIN_PACKAGES: build_egg(package_dir=package, dist_dir=cls.eggs_dir) for package in BAD_PLUGIN_PACKAGES: build_egg(package_dir=package, dist_dir=cls.bad_eggs_dir) @classmethod def tearDownClass(cls): cls.egg_cleanup_stack.close() def setUp(self): with contextlib.ExitStack() as cleanup_stack: cleanup_stack.enter_context(restore_sys_path()) cleanup_stack.enter_context(restore_sys_modules()) cleanup_stack.enter_context(restore_pkg_resources_working_set()) self.addCleanup(cleanup_stack.pop_all().close) def test_find_plugins_in_eggs_on_the_plugin_path(self): with self.assertWarns(DeprecationWarning): 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"] with self.assertWarns(DeprecationWarning): 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*"] with self.assertWarns(DeprecationWarning): 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"] with self.assertWarns(DeprecationWarning): 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*"] with self.assertWarns(DeprecationWarning): 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): with self.assertWarns(DeprecationWarning): 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): with self.assertWarns(DeprecationWarning): 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 with self.assertWarns(DeprecationWarning): 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): # Make sure that the distributions from eggs are already in the working # set. This includes acme-foo, with version 0.1a1. for dist in pkg_resources.find_distributions(self.eggs_dir): pkg_resources.working_set.add(dist) with self.assertWarns(DeprecationWarning): plugin_manager = EggBasketPluginManager( plugin_path=[ # Attempt to add acme-foo, with conflicting version 0.1a11 self._create_broken_distribution_eggdir("acme_foo*.egg"), ], ) with self.assertRaises(RuntimeError): iter(plugin_manager) def test_ignore_broken_distributions_loads_good_distributions(self): # Make sure that the distributions from eggs are already in the working # set. This includes acme-foo, with version 0.1a1. for dist in pkg_resources.find_distributions(self.eggs_dir): pkg_resources.working_set.add(dist) data = {"count": 0} def on_broken_distribution(dist, exc): data["count"] += 1 data["distribution"] = dist data["exc"] = exc with self.assertWarns(DeprecationWarning): 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.eggs_dir` for example '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 envisage-7.0.3/envisage/tests/test_egg_plugin_manager.py000066400000000000000000000176541441257372400235360ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ import contextlib import os import unittest import pkg_resources from pkg_resources import Environment, working_set from envisage.api import EggPluginManager from envisage.tests.support import ( build_egg, PLUGIN_PACKAGES, restore_pkg_resources_working_set, restore_sys_modules, restore_sys_path, temporary_directory, ) class EggPluginManagerTestCase(unittest.TestCase): """Tests for the Egg plugin manager.""" @classmethod def setUpClass(cls): cls.egg_cleanup_stack = contextlib.ExitStack() cls.egg_dir = os.fspath( cls.egg_cleanup_stack.enter_context(temporary_directory()) ) # Build eggs for package in PLUGIN_PACKAGES: build_egg(package_dir=package, dist_dir=cls.egg_dir) @classmethod def tearDownClass(cls): cls.egg_cleanup_stack.close() def setUp(self): with contextlib.ExitStack() as cleanup_stack: cleanup_stack.enter_context(restore_sys_path()) cleanup_stack.enter_context(restore_sys_modules()) cleanup_stack.enter_context(restore_pkg_resources_working_set()) self.addCleanup(cleanup_stack.pop_all().close) # Make eggs importable # 'find_plugins' identifies those distributions that *could* be added # to the working set without version conflicts or missing requirements. environment = Environment([os.fspath(self.egg_dir)]) distributions, errors = working_set.find_plugins(environment) if len(distributions) == 0 or len(errors) > 0: raise RuntimeError(f"Cannot find eggs {errors}") for distribution in distributions: working_set.add(distribution) def test_no_include_or_exclude(self): """no include or exclude""" # Make sure that the plugin manager only includes those plugins. with self.assertWarns(DeprecationWarning): 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) def test_include_specific(self): """include specific""" # 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 = [r"acme\.foo", r"acme\.bar"] # Make sure that the plugin manager only includes those plugins. with self.assertWarns(DeprecationWarning): 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) def test_include_multiple(self): """include multiple""" # 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. with self.assertWarns(DeprecationWarning): 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) def test_exclude_specific(self): """exclude specific""" # 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 = [r"acme\.foo", r"acme\.baz"] # Make sure that the plugin manager excludes the specified plugins. with self.assertWarns(DeprecationWarning): 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) def test_exclude_multiple(self): """exclude multiple""" # 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 = [r"acme\.b.*"] # Make sure that the plugin manager excludes the specified plugins. with self.assertWarns(DeprecationWarning): 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) def test_uses_global_working_set_by_default(self): original_working_set = pkg_resources.working_set try: # Create fresh working set for this test, to make sure that the # plugin manager picks up the *current* value of # pkg_resources.working_set. fresh_working_set = pkg_resources.WorkingSet() pkg_resources.working_set = fresh_working_set with self.assertWarns(DeprecationWarning): plugin_manager = EggPluginManager() self.assertEqual(plugin_manager.working_set, fresh_working_set) finally: pkg_resources.working_set = original_working_set ########################################################################### # 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) envisage-7.0.3/envisage/tests/test_extension_point.py000066400000000000000000000211001441257372400231260ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import unittest from traits.api import HasTraits, Int, List, TraitError # Enthought library imports. from envisage.api import Application, ExtensionPoint, ExtensionRegistry class HasExtensionPoints(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. HasExtensionPoints.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(HasExtensionPoints): 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_mutate_extension_point_no_effect(self): """Extension point is recomputed so mutation has no effect.""" 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", [1, 2, 3]) # Declare a class that consumes the extension. class Foo(HasExtensionPoints): x = ExtensionPoint(List(Int), id="my.ep") # when f = Foo() f.x.append(42) # then # The registry is not changed, and the extension point is still the # same as before self.assertEqual(registry.get_extensions("my.ep"), [1, 2, 3]) self.assertEqual(f.x, [1, 2, 3]) 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(HasExtensionPoints): 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(HasExtensionPoints): 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(HasExtensionPoints): 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(HasExtensionPoints): 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(HasExtensionPoints): 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(HasExtensionPoints): 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={!r})" 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) envisage-7.0.3/envisage/tests/test_extension_point_binding.py000066400000000000000000000217161441257372400246350ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import unittest import weakref from traits.api import Any, HasTraits, List # Enthought library imports. from envisage.api import ( bind_extension_point, ExtensionPoint, unbind_extension_point, ) # Local imports. from envisage.tests.mutable_extension_registry import MutableExtensionRegistry class BindingTarget(HasTraits): """ Example class whose traits are used as a binding target. """ #: Target trait for extension point binding. target = List(Any()) class ExtensionPointBindingTestCase(unittest.TestCase): """Tests for extension point binding.""" def setUp(self): self.extension_registry = MutableExtensionRegistry() def tearDown(self): del 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 events = [] f = Foo() f.observe(events.append, "x_items") # Make some bindings. bind_extension_point(f, "x", "my.ep", registry) # 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(len(events), 1) event = events[0] self.assertEqual(f, event.object) self.assertEqual("x_items", event.name) self.assertEqual(1, len(event.new.added)) self.assertTrue("a string" in event.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 events = [] f = Foo() f.observe(events.append, "*") # Make some bindings. bind_extension_point(f, "x", "my.ep", registry) # 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(len(events), 1) event = events[0] self.assertEqual(f, event.object) self.assertEqual("x", event.name) self.assertEqual(1, len(event.new)) self.assertTrue("a string" in event.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 events = [] f = Foo() f.observe(events.append, "*") # Make some bindings. bind_extension_point(f, "x", "my.ep", registry) # 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(len(events), 1) event = events[0] self.assertEqual(f, event.object) self.assertEqual("x", event.name) self.assertEqual(1, len(event.new)) self.assertTrue("a string" in event.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() # 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)) def test_unbind_extension_point(self): # Given ... # ... an extension point ... registry = self.extension_registry registry.add_extension_point(self._create_extension_point("my.ep")) # ... and an object with a corresponding trait ... target = BindingTarget() # When we bind the extension point to the trait ... bind_extension_point(target, "target", "my.ep", registry) # Then contributions to the extension point modify the trait. registry.add_extension("my.ep", "a string") self.assertEqual(target.target, ["a string"]) # When we unbind the extension point unbind_extension_point(target, "target", "my.ep", registry) # Then contributions no longer change the trait. registry.add_extension("my.ep", "another string") self.assertEqual(target.target, ["a string"]) def test_unbinding_removes_references(self): # Given an extension point bound to a trait extension_point = self._create_extension_point("my.ep") self.extension_registry.add_extension_point(extension_point) target = BindingTarget() bind_extension_point( target, "target", "my.ep", self.extension_registry ) # Use a weakref finalizer to keep track of whether 'target' still # has references keeping it alive. target_monitor = weakref.finalize(target, lambda: None) # When we unbind and delete the target object unbind_extension_point( target, "target", "my.ep", self.extension_registry ) del target # Then 'target' should no longer be alive. self.assertFalse(target_monitor.alive) ########################################################################### # 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) envisage-7.0.3/envisage/tests/test_extension_point_changed.py000066400000000000000000000317541441257372400246170ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import unittest # Local imports. from envisage.api import ExtensionPointChangedEvent from envisage.tests.support import PluginA, PluginB, PluginC, SimpleApplication class ExtensionPointChangedTestCase(unittest.TestCase): """Tests for the events fired when extension points are changed.""" def test_set_extension_point(self): """set extension point""" a = PluginA() application = SimpleApplication(plugins=[a]) application.start() # Try to set the extension point. with self.assertRaises(TypeError): setattr(a, "x", [1, 2, 3]) def test_mutate_extension_point_no_events(self): """Mutation will not emit change event for name_items""" events = [] a = PluginA() a.observe(events.append, "x_items") b = PluginB() c = PluginC() application = SimpleApplication(plugins=[a, b, c]) application.start() # when a.x.append(42) # then self.assertEqual(events, []) def test_append(self): """append""" events = [] a = PluginA() a.observe(events.append, "x_items") b = PluginB() c = PluginC() application = SimpleApplication(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(len(events), 1) event = events[0] self.assertEqual(a, event.object) self.assertEqual("x_items", event.name) self.assertEqual([4], event.new.added) self.assertEqual([], event.new.removed) self.assertEqual(3, event.new.index) def test_remove(self): """remove""" events = [] a = PluginA() a.observe(events.append, "x_items") b = PluginB() c = PluginC() application = SimpleApplication(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(len(events), 1) event = events[0] self.assertEqual(a, event.object) self.assertEqual("x_items", event.name) self.assertEqual([], event.new.added) self.assertEqual([3], event.new.removed) self.assertEqual(2, event.new.index) def test_assign_empty_list(self): """assign empty list""" events = [] a = PluginA() a.observe(events.append, "x_items") b = PluginB() c = PluginC() application = SimpleApplication(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(len(events), 1) event = events[0] self.assertEqual(a, event.object) self.assertEqual("x_items", event.name) self.assertEqual([], event.new.added) self.assertEqual([1, 2, 3], event.new.removed) self.assertEqual(0, event.new.index.start) self.assertEqual(3, event.new.index.stop) def test_assign_empty_list_no_event(self): """assign empty list no event""" events = [] a = PluginA() a.observe(events.append, "x_items") b = PluginB() c = PluginC() application = SimpleApplication(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(events, []) def test_assign_non_empty_list(self): """assign non-empty list""" events = [] a = PluginA() a.observe(events.append, "x_items") b = PluginB() c = PluginC() application = SimpleApplication(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) # Keep the old values for later slicing check source_values = list(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(len(events), 1) event = events[0] self.assertEqual(a, event.object) self.assertEqual("x_items", event.name) self.assertEqual([2, 4, 6, 8], event.new.added) self.assertEqual([1, 2, 3], event.new.removed) # The removed entry should match what the old values say self.assertEqual(event.new.removed, source_values[event.new.index]) # If we use the index and apply the changes to the old list, we should # recover the new list source_values[event.new.index] = event.new.added self.assertEqual(source_values, application.get_extensions("a.x")) self.assertEqual(0, event.new.index.start) self.assertEqual(3, event.new.index.stop) def test_add_plugin(self): """add plugin""" events = [] a = PluginA() a.observe(events.append, "x_items") b = PluginB() c = PluginC() # Start off with just two of the plugins. application = SimpleApplication(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(len(events), 1) event = events[0] self.assertEqual(a, event.object) self.assertEqual("x_items", event.name) self.assertEqual([98, 99, 100], event.new.added) self.assertEqual([], event.new.removed) self.assertEqual(3, event.new.index) def test_remove_plugin(self): """remove plugin""" events = [] a = PluginA() a.observe(events.append, "x_items") b = PluginB() c = PluginC() application = SimpleApplication(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(len(events), 1) event = events[0] self.assertEqual(a, event.object) self.assertEqual("x_items", event.name) self.assertEqual([], event.new.added) self.assertEqual([1, 2, 3], event.new.removed) self.assertEqual(0, event.new.index) def test_extension_point_change_event_str_representation(self): """ test string representation of the ExtensionPointChangedEvent class """ desired_repr = ( "ExtensionPointChangedEvent(extension_point_id={}, " "index=0, removed=[], added=[])" ) ext_pt_changed_evt = ExtensionPointChangedEvent(extension_point_id=1) self.assertEqual(desired_repr.format(1), str(ext_pt_changed_evt)) self.assertEqual(desired_repr.format(1), repr(ext_pt_changed_evt)) envisage-7.0.3/envisage/tests/test_extension_registry.py000066400000000000000000000153351441257372400236620ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import contextlib import unittest from traits.api import List # Enthought library imports. from envisage.api import Application, ExtensionPoint, ExtensionRegistry from envisage.tests.test_extension_registry_mixin import ( ExtensionRegistryTestMixin, ) class ExtensionRegistryTestCase(ExtensionRegistryTestMixin, 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_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_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")) def make_function_listener(events): """ Return a simple non-method extension point listener. The listener appends events to the ``events`` list. """ def listener(registry, event): events.append(event) return listener class ListensToExtensionPoint: """ Class with a method that can be used as an extension point listener. """ def __init__(self, events): self.events = events def listener(self, registry, event): self.events.append(event) class ExtensionPointListenerLifetimeTestCase(unittest.TestCase): def setUp(self): # We do all of the testing via the application to make sure it offers # the same interface! registry = Application(extension_registry=ExtensionRegistry()) extension_point = ExtensionPoint(id="my.ep", trait_type=List()) registry.add_extension_point(extension_point) self.registry = registry # A place to record events that listeners receive. self.events = [] def test_add_nonmethod_listener(self): listener = make_function_listener(self.events) self.registry.add_extension_point_listener(listener, "my.ep") with self.assertAppendsTo(self.events): self.registry.set_extensions("my.ep", [1, 2, 3]) def test_remove_nonmethod_listener(self): listener = make_function_listener(self.events) self.registry.add_extension_point_listener(listener, "my.ep") self.registry.remove_extension_point_listener(listener, "my.ep") with self.assertDoesNotModify(self.events): self.registry.set_extensions("my.ep", [4, 5, 6, 7]) def test_nonmethod_listener_lifetime(self): listener = make_function_listener(self.events) self.registry.add_extension_point_listener(listener, "my.ep") # The listener should not kept alive by the registry. del listener with self.assertDoesNotModify(self.events): self.registry.set_extensions("my.ep", [4, 5, 6, 7]) def test_add_method_listener(self): obj = ListensToExtensionPoint(self.events) self.registry.add_extension_point_listener(obj.listener, "my.ep") # At this point, the bound method 'obj.listener' no longer # exists; it's already been garbage collected. Nevertheless, the # listener should still fire. with self.assertAppendsTo(self.events): self.registry.set_extensions("my.ep", [1, 2, 3]) def test_remove_method_listener(self): obj = ListensToExtensionPoint(self.events) # The two occurences of `obj.listener` below refer to different # objects. Nevertheless, they _compare_ equal, so the removal # should still be effective. self.registry.add_extension_point_listener(obj.listener, "my.ep") self.registry.remove_extension_point_listener(obj.listener, "my.ep") with self.assertDoesNotModify(self.events): self.registry.set_extensions("my.ep", [1, 2, 3]) def test_method_listener_lifetime(self): obj = ListensToExtensionPoint(self.events) self.registry.add_extension_point_listener(obj.listener, "my.ep") # Removing the last reference to the object should deactivate # the listener. del obj with self.assertDoesNotModify(self.events): self.registry.set_extensions("my.ep", [1, 2, 3]) # Helper assertions ####################################################### @contextlib.contextmanager def assertAppendsTo(self, some_list): """ Assert that exactly one element is appended to a list. Return a context manager that checks that the code in the corresponding with block appends exactly one element to the given list. """ old_length = len(some_list) yield new_length = len(some_list) diff = new_length - old_length self.assertEqual( diff, 1, msg="Expected exactly one new element; got {}".format(diff), ) @contextlib.contextmanager def assertDoesNotModify(self, some_list): """ Assert that a list is unchanged. Return a context manager that checks that the code in the corresponding with block does not modify the length of the given list. """ old_length = len(some_list) yield new_length = len(some_list) diff = new_length - old_length self.assertEqual( diff, 0, msg="Expected no new elements; got {}".format(diff), ) envisage-7.0.3/envisage/tests/test_extension_registry_mixin.py000066400000000000000000000103421441257372400250570ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 set of tests for extension registry and its subclasses wrapped in a mixin class. """ from traits.api import List # Enthought library imports. from envisage.api import ExtensionPoint, UnknownExtensionPoint class ExtensionRegistryTestMixin: """Base set of tests for extension registry and its subclasses. Test cases inherriting from this mixin should define a setUp method that defines self.registry as an instance of ExtensionPointRegistry. """ 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_get_extension_point_return_none_if_not_found(self): """get extension point return None if id is not found.""" self.assertIsNone(self.registry.get_extension_point("i.do.not.exist")) def test_get_extensions_mutation_no_effect_if_undefined(self): """test one cannot mutate the registry by mutating the list.""" # The extension point with id "my.ep" has not been defined extensions = self.registry.get_extensions("my.ep") # when extensions.append([[1, 2]]) # then # the registry is not affected. self.assertEqual(self.registry.get_extensions("my.ep"), []) 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_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 create_extension_point(self, id, trait_type=List, desc=""): """Create an extension point.""" return ExtensionPoint(id=id, trait_type=trait_type, desc=desc) envisage-7.0.3/envisage/tests/test_ids.py000066400000000000000000000043321441257372400204700ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import envisage.ids from envisage.api import CorePlugin from envisage.plugins.python_shell.python_shell_plugin import PythonShellPlugin from envisage.ui.tasks.api import TasksPlugin class TestIds(unittest.TestCase): def test_id_strings(self): extension_point_ids = [ # Extension point IDs "PREFERENCES", "SERVICE_OFFERS", "BINDINGS", "COMMANDS", "PREFERENCES_CATEGORIES", "PREFERENCES_PANES", "TASKS", "TASK_EXTENSIONS", ] for extension_point_id in extension_point_ids: id_value = getattr(envisage.ids, extension_point_id) self.assertIsInstance(id_value, str) def test_id_strings_against_plugin_constants(self): # Check extension point IDs against ground truth on plugins self.check_id_against_plugin("PREFERENCES", CorePlugin) self.check_id_against_plugin("SERVICE_OFFERS", CorePlugin) self.check_id_against_plugin("BINDINGS", PythonShellPlugin) self.check_id_against_plugin("COMMANDS", PythonShellPlugin) self.check_id_against_plugin("PREFERENCES_CATEGORIES", TasksPlugin) self.check_id_against_plugin("PREFERENCES_PANES", TasksPlugin) self.check_id_against_plugin("TASKS", TasksPlugin) self.check_id_against_plugin("TASK_EXTENSIONS", TasksPlugin) def check_id_against_plugin(self, id_string, plugin_klass): """ Check that the value of an id string matches that of a class variable. Parameters ---------- id_string : str Identifier for the id. plugin_klass : type The Plugin subclass to check. """ self.assertEqual( getattr(envisage.ids, id_string), getattr(plugin_klass, id_string) ) envisage-7.0.3/envisage/tests/test_import_manager.py000066400000000000000000000031311441257372400227110ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import unittest # Enthought library imports. from envisage.api import Application, ImportManager 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) envisage-7.0.3/envisage/tests/test_package_plugin_manager.py000066400000000000000000000137641441257372400243650ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ import contextlib import unittest from os.path import dirname, join from envisage.package_plugin_manager import PackagePluginManager from envisage.tests.support import restore_sys_path class PackagePluginManagerTestCase(unittest.TestCase): """Tests for the 'Package' plugin manager.""" def setUp(self): """Prepares the test fixture before each test method is called.""" cleanup_stack = contextlib.ExitStack() self.addCleanup(cleanup_stack.close) cleanup_stack.enter_context(restore_sys_path()) # 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): with self.assertWarns(DeprecationWarning): 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"] with self.assertWarns(DeprecationWarning): 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*"] with self.assertWarns(DeprecationWarning): 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"] with self.assertWarns(DeprecationWarning): 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*"] with self.assertWarns(DeprecationWarning): 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): with self.assertWarns(DeprecationWarning): 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) envisage-7.0.3/envisage/tests/test_plugin.py000066400000000000000000000265031441257372400212130ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ import unittest # Standard library imports. from os.path import exists, join from traits.api import HasTraits, Instance, Int, Interface, List, provides # Enthought library imports. from envisage.api import Application, ExtensionPoint, IPluginActivator, Plugin from envisage.tests.ets_config_patcher import ETSConfigPatcher from envisage.tests.support import SimpleApplication class TestPlugin(Plugin): id = "test_plugin" 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 = SimpleApplication(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 = SimpleApplication(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 = SimpleApplication(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 = SimpleApplication(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 = SimpleApplication(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 = SimpleApplication(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_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 = SimpleApplication() 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 = SimpleApplication(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 = SimpleApplication(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") def test_plugin_str_representation(self): """test the string representation of the plugin""" plugin_repr = "TestPlugin(id={!r}, name={!r})" plugin = TestPlugin(id="Fred", name="Wilma") self.assertEqual(str(plugin), plugin_repr.format("Fred", "Wilma")) self.assertEqual(repr(plugin), plugin_repr.format("Fred", "Wilma")) envisage-7.0.3/envisage/tests/test_plugin_manager.py000066400000000000000000000204371441257372400227050ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import unittest from traits.api import Bool # Enthought library imports. from envisage.api import Plugin, PluginManager 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(ValueError): plugin_manager.start_plugin(plugin_id="bogus") # Try to stop a non-existent plugin. with self.assertRaises(ValueError): 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"] with self.assertWarns(DeprecationWarning): 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*"] with self.assertWarns(DeprecationWarning): 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"] with self.assertWarns(DeprecationWarning): 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*"] with self.assertWarns(DeprecationWarning): 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) envisage-7.0.3/envisage/tests/test_provider_extension_registry.py000066400000000000000000000441011441257372400255650ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 library imports. import unittest from traits.api import Int, List # Enthought library imports. from envisage.api import ( ExtensionPoint, ExtensionProvider, ProviderExtensionRegistry, ) # Local imports. from envisage.tests.test_extension_registry_mixin import ( ExtensionRegistryTestMixin, ) class ProviderExtensionRegistryTestCase( ExtensionRegistryTestMixin, unittest.TestCase ): """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) 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(TypeError): 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")) envisage-7.0.3/envisage/tests/test_service.py000066400000000000000000000045011441257372400213470ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import unittest # Enthought library imports. from traits.api import HasTraits, Instance, TraitError from envisage.api import Plugin, Service from envisage.tests.support import SimpleApplication 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 = SimpleApplication(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(TraitError): 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") def test_service_str_representation(self): """test the string representation of the service""" class Foo(HasTraits): pass service_repr = "Service(protocol={!r})" service = Service(Foo) self.assertEqual(service_repr.format(Foo), str(service)) self.assertEqual(service_repr.format(Foo), repr(service)) envisage-7.0.3/envisage/tests/test_service_registry.py000066400000000000000000000403261441257372400233040ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import unittest from traits.api import HasTraits, Int, Interface, provides # Enthought library imports. from envisage.api import Application, NoSuchServiceError, ServiceRegistry # 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) envisage-7.0.3/envisage/tests/test_slice.py000066400000000000000000000125361441257372400210150ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import unittest # Enthought library imports. from traits.api import HasTraits, List # 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[:] added = event.added # Backwards compatibility for Traits < 6.0, where event.added may # be a list containing a list containing the added elements. This # block can be removed once compatibility with Traits < 6.0 is no # longer needed. Ref: enthought/traits#300. if len(added) == 1 and isinstance(added[0], list): added = added[0] # If nothing was added then this is a 'del' or 'remove' operation. if len(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] = added else: clone[event.index : event.index] = added # Otherwise, it is an assigment ('sort' and 'reverse' fall into this # category). else: if isinstance(event.index, slice): clone[event.index] = added else: clone[event.index : event.index + len(added)] = 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): elts = List self.f = Foo(elts=TEST_LIST) self.f.on_trait_change(listener, "elts_items") def test_append(self): """append""" self.f.elts.append(99) # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_insert(self): """insert""" self.f.elts.insert(3, 99) # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_extend(self): """extend""" self.f.elts.extend([99, 100]) # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_remove(self): """remove""" self.f.elts.remove(5) # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_reverse(self): """reverse""" self.f.elts.reverse() # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_sort(self): """sort""" self.f.elts.sort() # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_pop(self): """remove""" self.f.elts.pop() # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_del_all(self): """del all""" del self.f.elts[:] # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_assign_item(self): """assign item""" self.f.elts[3] = 99 # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_del_item(self): """del item""" del self.f.elts[3] # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_assign_slice(self): """assign slice""" self.f.elts[2:4] = [88, 99] # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_del_slice(self): """del slice""" del self.f.elts[2:5] # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_assign_extended_slice(self): """assign extended slice""" self.f.elts[2:6:2] = [88, 99] # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) def test_del_extended_slice(self): """del extended slice""" del self.f.elts[2:6:2] # Make sure we successfully recreated the operation. self.assertEqual(self.f.elts, listener.clone) envisage-7.0.3/envisage/tests/test_workbench.py000066400000000000000000000011221441257372400216650ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 class TestWorkbenchDefaultAction(unittest.TestCase): def test_workbench_default_action(self): import envisage.ui.workbench.default_action_set # noqa: F401 envisage-7.0.3/envisage/ui/000077500000000000000000000000001441257372400155515ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/__init__.py000066400000000000000000000000001441257372400176500ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/action/000077500000000000000000000000001441257372400170265ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/action/__init__.py000066400000000000000000000000001441257372400211250ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/action/abstract_action_manager_builder.py000066400000000000000000000360201441257372400257410ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, observe, 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(group.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) ########################################################################### # 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 ################################################# @observe("action_sets") def _update_action_sets_on_manager(self, event): """Static trait change handler.""" new = event.new self._action_set_manager.action_sets = new #### 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) 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) 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 manager 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 envisage-7.0.3/envisage/ui/action/action.py000066400000000000000000000023401441257372400206540ustar00rootroot00000000000000# (C) Copyright 2007-2023 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__ envisage-7.0.3/envisage/ui/action/action_set.py000066400000000000000000000067361441257372400215440ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, provides, Str from traits.util.camel_case import camel_case_to_words # Local imports. from .action import Action from .group import Group from .i_action_set import IActionSet from .menu import Menu from .tool_bar import ToolBar # 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 envisage-7.0.3/envisage/ui/action/action_set_manager.py000066400000000000000000000073101441257372400232230ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/ui/action/api.py000066400000000000000000000013151441257372400201510ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 .abstract_action_manager_builder import AbstractActionManagerBuilder from .action import Action from .action_set import ActionSet from .group import Group from .i_action_manager_builder import IActionManagerBuilder from .i_action_set import IActionSet from .menu import Menu from .tool_bar import ToolBar envisage-7.0.3/envisage/ui/action/group.py000066400000000000000000000025311441257372400205350ustar00rootroot00000000000000# (C) Copyright 2007-2023 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__ envisage-7.0.3/envisage/ui/action/i_action_manager_builder.py000066400000000000000000000020501441257372400243620ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.""" envisage-7.0.3/envisage/ui/action/i_action_set.py000066400000000000000000000046731441257372400220520ustar00rootroot00000000000000# (C) Copyright 2007-2023 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) envisage-7.0.3/envisage/ui/action/location.py000066400000000000000000000031571441257372400212160ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/ui/action/menu.py000066400000000000000000000050741441257372400203520ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 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().__init__(klass=Group, **kw) ########################################################################### # 'TraitType' interface. ########################################################################### def validate(self, object, name, value): """Validate a value.""" if isinstance(value, str): value = Group(id=value) return super().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("&") envisage-7.0.3/envisage/ui/action/tests/000077500000000000000000000000001441257372400201705ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/action/tests/__init__.py000066400000000000000000000000001441257372400222670ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/action/tests/dummy_action_manager_builder.py000066400000000000000000000041731441257372400264370ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ from pyface.action.api import Action, Group, MenuBarManager, MenuManager # Enthought library imports. from envisage.ui.action.api import AbstractActionManagerBuilder 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 envisage-7.0.3/envisage/ui/action/tests/test_action_manager_builder.py000066400000000000000000000567361441257372400262770ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ # Standard library imports. import unittest # Enthought library imports. from envisage.ui.action.api import Action, ActionSet, Group, Menu # 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=[ Group( id="NewGroup", path="MenuBar/File", before="ExitGroup", ), Group(id="ExitGroup", path="MenuBar/File"), ], ), 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=[ Group(id="NewGroup", path="MenuBar/File"), Group(id="ExitGroup", path="MenuBar/File"), ], ), ], ), ActionSet( menus=[ Menu( name="&File", path="MenuBar", groups=[ Group(id="ExtraGroup", 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 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=[ Group(id="NewGroup", path="MenuBar/File"), Group(id="ExitGroup", path="MenuBar/File"), ], ), ], ), ActionSet( menus=[ Menu( name="&File", path="MenuBar", groups=[ Group(id="NewGroup", 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 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) envisage-7.0.3/envisage/ui/action/tool_bar.py000066400000000000000000000056231441257372400212070ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 # 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().__init__(klass=Group, **kw) ########################################################################### # 'TraitType' interface. ########################################################################### def validate(self, object, name, value): """Validate a value.""" if isinstance(value, str): value = Group(id=value) return super().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 envisage-7.0.3/envisage/ui/gui_application.py000066400000000000000000000054141441257372400212760ustar00rootroot00000000000000# (C) Copyright 2007-2023 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) envisage-7.0.3/envisage/ui/tasks/000077500000000000000000000000001441257372400166765ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/tasks/__init__.py000066400000000000000000000000001441257372400207750ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/tasks/action/000077500000000000000000000000001441257372400201535ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/tasks/action/__init__.py000066400000000000000000000000001441257372400222520ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/tasks/action/api.py000066400000000000000000000010641441257372400212770ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/ui/tasks/action/exit_action.py000066400000000000000000000022171441257372400230350ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 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() envisage-7.0.3/envisage/ui/tasks/action/preferences_action.py000066400000000000000000000037601441257372400243710ustar00rootroot00000000000000# (C) Copyright 2007-2023 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().__init__(PreferencesAction(), **traits) envisage-7.0.3/envisage/ui/tasks/action/task_window_launch_group.py000066400000000000000000000052301441257372400256240ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.action.api import TaskAction from pyface.tasks.api import TaskWindowLayout from traits.api import List, observe, 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 ################################################ @observe("task") def _update_name(self, event): """Name the action (unless a name has already been assigned).""" task = event.new if task and not self.name: name = "" 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 envisage-7.0.3/envisage/ui/tasks/action/task_window_toggle_group.py000066400000000000000000000100341441257372400256310ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, on_trait_change, Property, Str class TaskWindowToggleAction(Action): """An action for activating an application window.""" #### 'Action' interface ################################################### name = Property(Str, observe="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 "" @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().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 envisage-7.0.3/envisage/ui/tasks/api.py000066400000000000000000000023171441257372400200240ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ - :class:`~.PreferencesCategory` - :class:`~.PreferencesDialog` - :class:`~.PreferencesTab` - :class:`~.PreferencesPane` - :class:`~.TaskExtension` - :class:`~.TaskFactory` - :class:`~.TaskWindow` - :class:`~.TaskWindowEvent` - :class:`~.VetoableTaskWindowEvent` - :class:`~.TasksApplication` - :class:`~.TasksApplicationState` - :class:`~.TasksPlugin` """ from .preferences_category import PreferencesCategory from .preferences_dialog import PreferencesDialog, PreferencesTab from .preferences_pane import PreferencesPane from .task_extension import TaskExtension from .task_factory import TaskFactory from .task_window import TaskWindow from .task_window_event import TaskWindowEvent, VetoableTaskWindowEvent from .tasks_application import TasksApplication, TasksApplicationState from .tasks_plugin import TasksPlugin envisage-7.0.3/envisage/ui/tasks/preferences_category.py000066400000000000000000000022021441257372400234420ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 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 = Str # 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 envisage-7.0.3/envisage/ui/tasks/preferences_dialog.py000066400000000000000000000117421441257372400230750ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.topological_sort import before_after_sort from traits.api import Bool, HasTraits, Instance, List, on_trait_change, Str from traitsui.api import Handler, Item, ListEditor, View # Local imports. from .preferences_category import PreferencesCategory from .preferences_pane import PreferencesPane class PreferencesTab(HasTraits): """An object used internally by PreferencesDialog.""" name = Str 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().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 envisage-7.0.3/envisage/ui/tasks/preferences_pane.py000066400000000000000000000062721441257372400225630ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 PreferencesHelper from traits.api import Callable, 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().close(info, is_ok) envisage-7.0.3/envisage/ui/tasks/task_extension.py000066400000000000000000000020021441257372400223000ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 Callable, HasStrictTraits, 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) envisage-7.0.3/envisage/ui/tasks/task_factory.py000066400000000000000000000027771441257372400217560ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 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 = Str #: 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 envisage-7.0.3/envisage/ui/tasks/task_window.py000066400000000000000000000041751441257372400216100ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 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), observe="_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 = "%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 envisage-7.0.3/envisage/ui/tasks/task_window_event.py000066400000000000000000000014651441257372400230100ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/ui/tasks/tasks_application.py000066400000000000000000000501561441257372400227670ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 import pickle from traits.api import ( Bool, Callable, Directory, Event, HasStrictTraits, Instance, Int, List, Str, Vetoable, ) from traits.etsconfig.api import ETSConfig # Enthought library imports. from envisage.api import Application, ExtensionPoint # 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 ID for task factories TASK_FACTORIES = "envisage.ui.tasks.tasks" #: Extension point ID for task extensions TASK_EXTENSIONS = "envisage.ui.tasks.task_extensions" #: Pickle protocol to use for persisting layout information. layout_save_protocol = Int(4) #### '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.i_gui.IGUI") #: 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 = Str #: 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 = Str(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() self.stop() 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 pyface.tasks.task_window_layout import TaskWindowLayout from .task_window_event import TaskWindowEvent 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, str) 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") def _initialize_application_home(self): """Initialize the application directories.""" # Extend the base class method to ensure the state directory exists. super()._initialize_application_home() state_location = self.state_location logger.debug(f"Creating folder for tasks state: {state_location}") os.makedirs(state_location, mode=0o700, exist_ok=True) #### 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(self.home, "tasks", ETSConfig.toolkit) 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.gui.stop_event_loop() 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) envisage-7.0.3/envisage/ui/tasks/tasks_plugin.py000066400000000000000000000136261441257372400217630ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, Instance, List from envisage.api import ExtensionPoint, Plugin, ServiceOffer # 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 pyface.tasks.action.api import DockPaneToggleGroup, SchemaAddition from .action.exit_action import ExitAction from .action.preferences_action import PreferencesGroup from .task_extension import TaskExtension 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 envisage-7.0.3/envisage/ui/tasks/tests/000077500000000000000000000000001441257372400200405ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/tasks/tests/__init__.py000066400000000000000000000000001441257372400221370ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/tasks/tests/data/000077500000000000000000000000001441257372400207515ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/tasks/tests/data/README.md000066400000000000000000000005711441257372400222330ustar00rootroot00000000000000This directory contains data files used in tests. - application_memento_v2.pkl is a pickled TasksApplicationState object containing layout information. It's pickled using pickle protocol 2. - application_memento_v3.pkg pickles the same object as above, but using pickle protocol 3. - create_pickles.py is a script that can be run to regenerate the above pickle files. envisage-7.0.3/envisage/ui/tasks/tests/data/application_memento_v2.pkl000066400000000000000000000012271441257372400261210ustar00rootroot00000000000000cenvisage.ui.tasks.tasks_application TasksApplicationState q)q}q(Xprevious_window_layoutsqctraits.trait_list_object TraitListObject q)qcpyface.tasks.task_window_layout TaskWindowLayout q)q}q(X active_taskq Xq Xitemsq h)q }q (Xitem_validatorqc__builtin__ getattr qh X_item_validatorqqRqXnameqh X name_itemsqX items_itemsqubXpositionqJJqXsizeqMMqX size_stateqXnormalqX__traits_version__qX6.3.2quba}q(hhhhqRq hhhXprevious_window_layouts_itemsq!ubXwindow_layoutsq"h)q#}q$(hhh#hq%Rq&hh"hXwindow_layouts_itemsq'ubXversionq(Khhub.envisage-7.0.3/envisage/ui/tasks/tests/data/application_memento_v3.pkl000066400000000000000000000012241441257372400261170ustar00rootroot00000000000000cenvisage.ui.tasks.tasks_application TasksApplicationState q)q}q(Xprevious_window_layoutsqctraits.trait_list_object TraitListObject q)qcpyface.tasks.task_window_layout TaskWindowLayout q)q}q(X active_taskq Xq Xitemsq h)q }q (Xitem_validatorqcbuiltins getattr qh X_item_validatorqqRqXnameqh X name_itemsqX items_itemsqubXpositionqJJqXsizeqMMqX size_stateqXnormalqX__traits_version__qX6.3.2quba}q(hhhhqRq hhhXprevious_window_layouts_itemsq!ubXwindow_layoutsq"h)q#}q$(hhh#hq%Rq&hh"hXwindow_layouts_itemsq'ubXversionq(Khhub.envisage-7.0.3/envisage/ui/tasks/tests/data/create_pickles.py000066400000000000000000000020161441257372400242770ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ Script to recreate the test pickles in this directory. No arguments are required. Run with:: $ python create_pickles.py """ import pathlib import pickle from pyface.tasks.api import TaskWindowLayout from envisage.ui.tasks.api import TasksApplicationState state = TasksApplicationState( version=1, window_layouts=[], previous_window_layouts=[ TaskWindowLayout(size=(492, 743)), ], ) v2_pkl = pathlib.Path("application_memento_v2.pkl") v3_pkl = pathlib.Path("application_memento_v3.pkl") v2_pkl.write_bytes(pickle.dumps(state, protocol=2)) v3_pkl.write_bytes(pickle.dumps(state, protocol=3)) envisage-7.0.3/envisage/ui/tasks/tests/test_tasks_application.py000066400000000000000000000162701441257372400251670ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 pathlib import shutil import sys import tempfile import unittest import pkg_resources from pyface.gui import GUI from pyface.i_gui import IGUI from traits.api import Event, HasTraits, provides from envisage.api import Plugin from envisage.tests.support import ( pyside6_available, pyside6_version, requires_gui, ) from envisage.ui.tasks.api import TasksApplication from envisage.ui.tasks.tasks_application import DEFAULT_STATE_FILENAME # There's a PySide6 end-of-process segfault on Linux that's # interfering with our CI runs, so we skip the relevant tests # when running under GitHub Actions CI. # xref: enthought/envisage#476 skip_with_flaky_pyside = unittest.skipIf( ( os.getenv("GITHUB_ACTIONS") == "true" and sys.platform == "linux" and pyside6_available and pyside6_version < (6, 4, 3) ), "Skipping segfault-causing test on Linux. See enthought/envisage#476", ) @provides(IGUI) class DummyGUI(HasTraits): pass class LifecycleRecordingPlugin(Plugin): """ Plugin that fires events when started and stopped. """ #: Event fired when plugin starts started = Event() #: Event fired when plugin stops stopped = Event() def start(self): self.started = True def stop(self): self.stopped = True class LifecycleRecordingGUI(GUI): """ GUI subclass that adds events for watching start and stop of event loop. """ #: Event fired just before we start the event loop. starting = Event #: Event fired just after we've exited the event loop. stopped = Event def start_event_loop(self): """ Extend the base class method to fire the additional events. """ self.starting = True super().start_event_loop() self.stopped = True @requires_gui class TestTasksApplication(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.tmpdir) @skip_with_flaky_pyside 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") @skip_with_flaky_pyside def test_layout_save_creates_directory(self): # Test that state can still be saved if the target directory # doesn't exist. state_location = pathlib.Path(self.tmpdir) / "subdir" state_filename = "memento_test" state_path = state_location / state_filename self.assertFalse(state_location.exists()) self.assertFalse(state_path.exists()) # Create application and set it up to exit as soon as it's launched. app = TasksApplication( state_location=state_location, state_filename=state_filename, ) app.on_trait_change(app.exit, "application_initialized") app.run() self.assertTrue(state_location.exists()) self.assertTrue(state_path.exists()) @skip_with_flaky_pyside 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)) @skip_with_flaky_pyside 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)) def test_gui_trait_expects_IGUI_interface(self): # Trivial test where we simply set the trait # and the test passes because no errors are raised. app = TasksApplication() app.gui = DummyGUI() @skip_with_flaky_pyside def test_simple_lifecycle(self): app = TasksApplication(state_location=self.tmpdir) app.observe(lambda event: app.exit(), "application_initialized") app.run() @skip_with_flaky_pyside def test_lifecycle_with_plugin(self): events = [] plugin = LifecycleRecordingPlugin(record_to=events) plugin.observe(events.append, "started,stopped") gui = LifecycleRecordingGUI() gui.observe(events.append, "starting,stopped") app = TasksApplication( gui=gui, state_location=self.tmpdir, plugins=[plugin] ) app.observe(events.append, "starting,started,stopping,stopped") # When we start and stop the application app.observe(lambda event: app.exit(), "application_initialized") app.run() # Then events occurred in the following order. self.assertEqual( [(event.object, event.name) for event in events], [ (app, "starting"), (plugin, "started"), (app, "started"), (gui, "starting"), (gui, "stopped"), (app, "stopping"), (plugin, "stopped"), (app, "stopped"), ], ) envisage-7.0.3/envisage/ui/workbench/000077500000000000000000000000001441257372400175335ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/workbench/__init__.py000066400000000000000000000000001441257372400216320ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/workbench/action/000077500000000000000000000000001441257372400210105ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/workbench/action/__init__.py000066400000000000000000000000001441257372400231070ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/workbench/action/about_action.py000066400000000000000000000024161441257372400240340ustar00rootroot00000000000000# (C) Copyright 2007-2023 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() envisage-7.0.3/envisage/ui/workbench/action/api.py000066400000000000000000000010341441257372400221310ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/ui/workbench/action/edit_preferences_action.py000066400000000000000000000034721441257372400262330ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ from pyface.action.api import Action # Enthought library imports. from pyface.api import ImageResource 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() envisage-7.0.3/envisage/ui/workbench/action/exit_action.py000066400000000000000000000025341441257372400236740ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ from pyface.action.api import Action # Enthought library imports. from pyface.api import ImageResource 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() envisage-7.0.3/envisage/ui/workbench/action/images/000077500000000000000000000000001441257372400222555ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/workbench/action/images/exit.png000066400000000000000000000015541441257372400237410ustar00rootroot00000000000000PNG  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`envisage-7.0.3/envisage/ui/workbench/action/images/image_LICENSE.txt000066400000000000000000000006721441257372400252470ustar00rootroot00000000000000The 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 envisage-7.0.3/envisage/ui/workbench/action/images/preferences.png000066400000000000000000000015061441257372400252660ustar00rootroot00000000000000PNG  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`envisage-7.0.3/envisage/ui/workbench/api.py000066400000000000000000000012311441257372400206530ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/ui/workbench/default_action_set.py000066400000000000000000000034111441257372400237400ustar00rootroot00000000000000# (C) Copyright 2007-2023 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, Group, 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=[ Group(id="OpenGroup"), Group(id="SaveGroup"), Group(id="ImportGroup"), Group(id="ExitGroup"), ], ), Menu( path="MenuBar", class_name="pyface.workbench.action.api:ViewMenuManager", ), Menu( name="&Tools", path="MenuBar", groups=[Group(id="PreferencesGroup")], ), Menu(name="&Help", path="MenuBar", groups=[Group(id="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", ), ] envisage-7.0.3/envisage/ui/workbench/images/000077500000000000000000000000001441257372400210005ustar00rootroot00000000000000envisage-7.0.3/envisage/ui/workbench/images/about.png000066400000000000000000000210661441257372400226250ustar00rootroot00000000000000PNG  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:??envisage-7.0.3/envisage/ui/workbench/images/image_LICENSE.txt000066400000000000000000000004741441257372400237720ustar00rootroot00000000000000These 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 envisage-7.0.3/envisage/ui/workbench/preferences.ini000066400000000000000000000000701441257372400225320ustar00rootroot00000000000000[enthought.envisage.ui.workbench] prompt_on_exit = True envisage-7.0.3/envisage/ui/workbench/workbench.py000066400000000000000000000040431441257372400220700ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 pyface.api import YES from traits.api import Delegate, Instance from envisage.api import IApplication # 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 envisage-7.0.3/envisage/ui/workbench/workbench_action_manager_builder.py000066400000000000000000000146161441257372400266340ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 from pyface.action.api import Action, Group, MenuManager from pyface.workbench.action.api import MenuBarManager, ToolBarManager from traits.api import Any, Instance # Enthought library imports. from envisage.ui.action.api import AbstractActionManagerBuilder 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) envisage-7.0.3/envisage/ui/workbench/workbench_action_set.py000066400000000000000000000157251441257372400243110ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ from traits.api import Instance, List, Str # Enthought library imports. from envisage.ui.action.api import ActionSet 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) 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) ########################################################################### # '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") ########################################################################### # 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() #### 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 ) 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) # 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) 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) envisage-7.0.3/envisage/ui/workbench/workbench_application.py000066400000000000000000000137011441257372400244540ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 from pyface.api import AboutDialog, Dialog, GUI, ImageResource, SplashScreen from pyface.workbench.api import IWorkbench from traits.api import Callable, Instance, Str, Tuple # Enthought library imports. from envisage.api import Application # 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() ########################################################################### # '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() # 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() ########################################################################### # 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) envisage-7.0.3/envisage/ui/workbench/workbench_editor_manager.py000066400000000000000000000033601441257372400251310ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 envisage-7.0.3/envisage/ui/workbench/workbench_plugin.py000066400000000000000000000202421441257372400234450ustar00rootroot00000000000000# (C) Copyright 2007-2023 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. """ from traits.api import Callable, List # Enthought library imports. from envisage.api import ExtensionPoint, Plugin, ServiceOffer # 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) envisage-7.0.3/envisage/ui/workbench/workbench_preferences.py000066400000000000000000000020061441257372400244460ustar00rootroot00000000000000# (C) Copyright 2007-2023 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) envisage-7.0.3/envisage/ui/workbench/workbench_preferences_page.py000066400000000000000000000030751441257372400254510ustar00rootroot00000000000000# (C) Copyright 2007-2023 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") envisage-7.0.3/envisage/ui/workbench/workbench_window.py000066400000000000000000000212101441257372400234520ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 pyface.action.api import StatusBarManager from traits.api import Delegate, Instance, List, Property, provides from envisage.api import ( ExtensionPoint, IExtensionPointUser, IExtensionRegistry, IServiceRegistry, ServiceRegistry, ) from envisage.ui.action.api import ActionSet # 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) def _closed_changed(self): """Static trait change handler.""" self._unregister_service_offers(self._service_ids) ########################################################################### # '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) def unregister_service(self, service_id): """Unregister a service.""" self.service_registry.unregister_service(service_id) ########################################################################### # 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) envisage-7.0.3/envisage/unknown_extension.py000066400000000000000000000011121441257372400212740ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.""" envisage-7.0.3/envisage/unknown_extension_point.py000066400000000000000000000011331441257372400225100ustar00rootroot00000000000000# (C) Copyright 2007-2023 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.""" envisage-7.0.3/etstool.py000066400000000000000000000412121441257372400153760ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ Tasks for Test Runs =================== This file is intended to be used with a python environment with the click library to automate the process of setting up test environments and running the test within them. This improves repeatability and reliability of tests be removing many of the variables around the developer's particular Python environment. Test environment setup and package management is performed using `EDM `_ To use this to run your tests, you will need to install EDM and click into your working environment. You will also need to have git installed to access required source code from github repositories. You can then do:: python etstool.py install --runtime=... --toolkit=... to create a test environment from the current codebase and:: python etstool.py test --runtime=... --toolkit=... to run tests in that environment. You can remove the environment with:: python etstool.py cleanup --runtime=... --toolkit=... If you make changes you will either need to remove and re-install the environment or manually update the environment using ``edm``, as the install performs a ``pip install`` rather than a ``pip install -e``, so changes in your code will not be automatically mirrored in the test environment. You can update with a command like:: edm run --environment ... -- pip install . You can run all three tasks at once with:: python etstool.py test_clean --runtime=... --toolkit=... which will create, install, run tests, and then clean-up the environment. And you can run tests in all supported runtimes and toolkits (with cleanup) using:: python etstool.py test_all For currently-supported runtime values, see the 'available_runtimes' value. For toolkits, see 'available_toolkits'. Not all combinations of toolkits and runtimes will work; the 'supported_combinations' variable describes which combinations are supported. Tests can still be run via the usual means in other environments if that suits a developer's purpose. Changing This File ------------------ To change the packages installed during a test run, change the dependencies variable below. Other changes to commands should be a straightforward change to the listed commands for each task. See the EDM documentation for more information about how to run commands within an EDM enviornment. """ import glob import os import subprocess import sys from contextlib import contextmanager from shutil import copy as copyfile from shutil import rmtree, which from tempfile import mkdtemp import click # Python runtime versions supported by this tool. available_runtimes = ["3.8"] # Python runtime used by default. default_runtime = "3.8" # Toolkits supported by this tool. # PySide2 and PyQt5 are not (currently) available for EDM + Python 3.8, so we # don't support them here. available_toolkits = ["null", "pyqt6", "pyside6"] # Toolkit used by default. default_toolkit = "null" supported_combinations = { "3.8": {"null", "pyqt6", "pyside6"}, } dependencies = { "apptools", "coverage", "enthought_sphinx_theme", "pyface", "sphinx", "sphinx_copybutton", "traits", "traitsui", } # Dependencies we install from PyPI pypi_dependencies = { # style packages; install from PyPI to make sure that we're compatible # with the style checks used in the check-style.yml workflow "black~=23.0", "flake8", "flake8-ets", "isort", } # Dependencies we install from source for cron tests # Order from packages with the most dependencies to one with the least # dependencies. Packages are forced re-installed in this order. source_dependencies = [ "apptools", "traitsui", "pyface", "traits", ] # Toolkit dependencies installed from EDM. toolkit_dependencies = { "pyside6": {"pyside6"}, "pyqt6": {"pyqt6"}, } runtime_dependencies = {} github_url_fmt = "git+http://github.com/enthought/{0}.git#egg={0}" # Options shared between different click commands. edm_option = click.option( "--edm", help=( "Path to the EDM executable to use. The default is to use the first " "EDM found in the path. The EDM executable can also be specified " "by setting the ETSTOOL_EDM environment variable." ), envvar="ETSTOOL_EDM", ) runtime_option = click.option( "--runtime", default=default_runtime, type=click.Choice(available_runtimes), show_default=True, help="Python runtime version", ) toolkit_option = click.option( "--toolkit", default=default_toolkit, type=click.Choice(available_toolkits), show_default=True, help="GUI toolkit", ) environment_option = click.option( "--environment", default=None, help=( "EDM environment name to use in place of the " "automatically constructed name" ), ) source_option = click.option( "--source/--no-source", default=False, help="Install ETS packages from source", ) editable_option = click.option( "--editable/--not-editable", default=False, help="Install main package in 'editable' mode? [default: --not-editable]", ) @click.group() def cli(): pass # Subgroup for checking and fixing style @cli.group() def style(): """ Commands for checking style and applying style fixes. """ @cli.command() @edm_option @runtime_option @toolkit_option @environment_option @editable_option @source_option def install(edm, runtime, toolkit, environment, editable, source): """Install project and dependencies into a clean EDM environment.""" parameters = get_parameters(edm, runtime, toolkit, environment) packages = " ".join( dependencies | toolkit_dependencies.get(toolkit, set()) | runtime_dependencies.get(runtime, set()) ) # edm commands to setup the development environment commands = [ "{edm} environments create {environment} --force --version={runtime}", "{edm} --config edm.yaml install -y -e {environment} " + packages, ] commands.extend( [ "{edm} run -e {environment} -- pip install " + dep for dep in pypi_dependencies ] ) click.echo("Creating environment '{environment}'".format(**parameters)) execute(commands, parameters) if source: # Remove EDM ETS packages and install them from source cmd_fmt = ( "{edm} plumbing remove-package " "--environment {environment} --force " ) commands = [cmd_fmt + source_pkg for source_pkg in source_dependencies] execute(commands, parameters) source_pkgs = [ github_url_fmt.format(pkg) for pkg in source_dependencies ] # Without the --no-dependencies flag such that new dependencies on # main branch are brought in. commands = [ "python -m pip install --force-reinstall {pkg}".format(pkg=pkg) for pkg in source_pkgs ] commands = [ "{edm} run -e {environment} -- " + command for command in commands ] execute(commands, parameters) # Always install local source at the end to mitigate risk of testing # against a distributed release. if editable: install_cmd = ( "{edm} run -e {environment} -- " "python -m pip install --editable . --no-dependencies" ) else: install_cmd = ( "{edm} run -e {environment} -- " "python -m pip install . --no-dependencies" ) execute([install_cmd], parameters) click.echo("Done install") @cli.command() @edm_option @runtime_option @toolkit_option @environment_option def shell(edm, runtime, toolkit, environment): """Create a shell into the EDM development environment (aka 'activate' it). """ parameters = get_parameters(edm, runtime, toolkit, environment) commands = [ "{edm} shell -e {environment}", ] execute(commands, parameters) @style.command(name="check") @edm_option @runtime_option @toolkit_option @environment_option def style_check(edm, runtime, toolkit, environment): """ Run style checks. """ parameters = get_parameters(edm, runtime, toolkit, environment) commands = [ "{edm} run -e {environment} -- python -m black --check --diff .", "{edm} run -e {environment} -- python -m isort --check --diff .", "{edm} run -e {environment} -- python -m flake8 .", ] execute(commands, parameters) @style.command(name="fix") @edm_option @runtime_option @toolkit_option @environment_option def style_fix(edm, runtime, toolkit, environment): """ Run style fixers. This automatically fixes any black or isort failures, but it won't automatically fix all flake8 errors. """ parameters = get_parameters(edm, runtime, toolkit, environment) commands = [ "{edm} run -e {environment} -- python -m black .", "{edm} run -e {environment} -- python -m isort .", ] execute(commands, parameters) @cli.command() @edm_option @runtime_option @toolkit_option @environment_option def test(edm, runtime, toolkit, environment): """Run the test suite in a given environment with the specified toolkit.""" parameters = get_parameters(edm, runtime, toolkit, environment) environ = dict(PYTHONUNBUFFERED="1") commands = [ ( "{edm} run -e {environment} -- python -W default -m " "coverage run -p -m unittest discover -v envisage" ), ] # We run in a tempdir to avoid accidentally picking up wrong envisage # code from a local dir. We need to ensure a good .coveragerc is in # that directory, plus coverage has a bug that means a non-local coverage # file doesn't get populated correctly. click.echo("Running tests in '{environment}'".format(**parameters)) with do_in_tempdir(files=[".coveragerc"], capture_files=["./.coverage*"]): os.environ.update(environ) execute(commands, parameters) click.echo("Done test") @cli.command() @edm_option @runtime_option @toolkit_option @environment_option def cleanup(edm, runtime, toolkit, environment): """Remove a development environment.""" parameters = get_parameters(edm, runtime, toolkit, environment) commands = [ "{edm} environments remove {environment} --purge -y", ] click.echo("Cleaning up environment '{environment}'".format(**parameters)) execute(commands, parameters) click.echo("Done cleanup") @cli.command() @edm_option @runtime_option @toolkit_option def test_clean(edm, runtime, toolkit): """Run tests in a clean environment, cleaning up afterwards""" args = ["--toolkit={}".format(toolkit), "--runtime={}".format(runtime)] if edm is not None: args.append("--edm={}".format(edm)) try: install(args=args, standalone_mode=False) test(args=args, standalone_mode=False) finally: cleanup(args=args, standalone_mode=False) @cli.command() @edm_option @runtime_option @toolkit_option @environment_option @editable_option def update(edm, runtime, toolkit, environment, editable): """Update/Reinstall package into environment.""" parameters = get_parameters(edm, runtime, toolkit, environment) if editable: install_cmd = ( "{edm} run -e {environment} -- " "python -m pip install --editable . --no-dependencies" ) else: install_cmd = ( "{edm} run -e {environment} -- " "python -m pip install . --no-dependencies" ) commands = [install_cmd] click.echo("Re-installing in '{environment}'".format(**parameters)) execute(commands, parameters) click.echo("Done update") @cli.command() @edm_option def test_all(edm): """Run test_clean across all supported environment combinations.""" failed_command = False for runtime, toolkits in supported_combinations.items(): for toolkit in toolkits: args = [ "--toolkit={}".format(toolkit), "--runtime={}".format(runtime), ] if edm is not None: args.append("--edm={}".format(edm)) try: test_clean(args, standalone_mode=True) except SystemExit: failed_command = True if failed_command: sys.exit(1) @cli.command() @edm_option @runtime_option @toolkit_option @environment_option def docs(edm, runtime, toolkit, environment): """Build HTML documentation.""" parameters = get_parameters(edm, runtime, toolkit, environment) parameters["docs_source"] = "docs/source" parameters["docs_build"] = "docs/build" parameters["docs_source_api"] = docs_source_api = "docs/source/api" parameters["templates_dir"] = "docs/source/api/templates/" # Remove any previously autogenerated API documentation. doc_api_files = os.listdir(docs_source_api) permanent = ["envisage.api.rst", "templates"] previously_autogenerated = [ file for file in doc_api_files if file not in permanent ] for file in previously_autogenerated: os.remove(os.path.join(docs_source_api, file)) html_build_command = ( "{edm} run -e {environment} -- python -m sphinx -b html " "{docs_source} {docs_build}" ) commands = [html_build_command] execute(commands, parameters) # Utility routines def get_parameters(edm, runtime, toolkit, environment): """Set up parameters dictionary for format() substitution""" if edm is None: edm = locate_edm() if environment is None: environment = "envisage-test-{runtime}-{toolkit}".format( runtime=runtime, toolkit=toolkit ) parameters = { "edm": edm, "runtime": runtime, "toolkit": toolkit, "environment": environment, } if toolkit not in supported_combinations[runtime]: msg = ( "Runtime {runtime} and toolkit {toolkit} not supported by " + "test environments" ) raise click.ClickException(msg.format(**parameters)) return parameters @contextmanager def do_in_tempdir(files=(), capture_files=()): """Create a temporary directory, cleaning up after done. Creates the temporary directory, and changes into it. On exit returns to original directory and removes temporary dir. Parameters ---------- files : sequence of filenames Files to be copied across to temporary directory. capture_files : sequence of filenames Files to be copied back from temporary directory. """ path = mkdtemp() old_path = os.getcwd() # send across any files we need for filepath in files: click.echo("copying file to tempdir: {}".format(filepath)) copyfile(filepath, path) os.chdir(path) try: yield path # retrieve any result files we want for pattern in capture_files: for filepath in glob.iglob(pattern): click.echo("copying file back: {}".format(filepath)) copyfile(filepath, old_path) finally: os.chdir(old_path) rmtree(path) def execute(commands, parameters): for command in commands: click.echo("[EXECUTING] {}".format(command.format(**parameters))) try: subprocess.check_call( [arg.format(**parameters) for arg in command.split()] ) except subprocess.CalledProcessError as exc: click.echo(str(exc)) sys.exit(1) def locate_edm(): """ Locate an EDM executable if it exists, else raise an exception. Returns the first EDM executable found on the path. On Windows, if that executable turns out to be the "edm.bat" batch file, replaces it with the executable that it wraps: the batch file adds another level of command-line mangling that interferes with things like specifying version restrictions. Returns ------- edm : str Path to the EDM executable to use. Raises ------ click.ClickException If no EDM executable is found in the path. """ edm = which("edm") if edm is None: raise click.ClickException( "This script requires EDM, but no EDM executable " "was found on the path." ) # Resolve edm.bat on Windows. if sys.platform == "win32" and os.path.basename(edm) == "edm.bat": edm = os.path.join(os.path.dirname(edm), "embedded", "edm.exe") return edm if __name__ == "__main__": cli() envisage-7.0.3/examples/000077500000000000000000000000001441257372400151515ustar00rootroot00000000000000envisage-7.0.3/examples/README.rst000066400000000000000000000003171441257372400166410ustar00rootroot00000000000000Examples have been moved to sit in envisage/examples/demo. The examples remaining here in the legacy directory have known problems. See enthought/envisage#329, enthought/envisage#330, enthought/envisage#381envisage-7.0.3/examples/legacy/000077500000000000000000000000001441257372400164155ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/000077500000000000000000000000001441257372400203775ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/000077500000000000000000000000001441257372400216635ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/000077500000000000000000000000001441257372400225705ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/__init__.py000066400000000000000000000006271441257372400247060ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/examples/legacy/workbench/AcmeLab/acme/acmelab/000077500000000000000000000000001441257372400241545ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/acmelab/__init__.py000066400000000000000000000006271441257372400262720ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/examples/legacy/workbench/AcmeLab/acme/acmelab/acmelab.py000066400000000000000000000035301441257372400261130ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 Acme Lab application. """ # Standard library imports. from logging import DEBUG from pyface.api import AboutDialog, ImageResource, SplashScreen # Enthought library imports. from envisage.ui.workbench.api import WorkbenchApplication class Acmelab(WorkbenchApplication): """The Acme Lab application.""" #### 'IApplication' interface ############################################# # The application's globally unique Id. id = "acme.acmelab" #### 'WorkbenchApplication' interface ##################################### # Branding information. # # The icon used on window title bars etc. icon = ImageResource("acmelab.ico") # The name of the application (also used on window title bars etc). name = "Acme Lab" ########################################################################### # 'WorkbenchApplication' interface. ########################################################################### def _about_dialog_default(self): """Trait initializer.""" about_dialog = AboutDialog( parent=self.workbench.active_window.control, image=ImageResource("about"), ) return about_dialog def _splash_screen_default(self): """Trait initializer.""" splash_screen = SplashScreen( image=ImageResource("splash"), show_log_messages=True, log_level=DEBUG, ) return splash_screen envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/acmelab/api.py000066400000000000000000000006651441257372400253060ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 .acmelab import Acmelab envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/acmelab/images/000077500000000000000000000000001441257372400254215ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/acmelab/images/about.png000066400000000000000000000271701441257372400272500ustar00rootroot00000000000000PNG  IHDR,,SF0PLTEn:}~|CCClb;._ pHYs  tIME IDATx}{׹ 2&PjiuHp{bpALC2nrwʼn:B,qQ"3CQZ+6&R&EQ@]'q+$HsμΙɕ܁ ;|Lu;zӹvvwwwv}_7ݝ7v_'F$曺_߻KZ`?N^m껻gv/]ze$X:{wQ zg]BW;XXk?]z1vdXww_xi~vi v/|^$T(!Xv]5䔲{ ATE[[|^~~=LZK'h4%n)LAd% U]6|팳 )VUG"rH D]$?]yaA2RnZBOOi 1,2g<-X^8j1UBJlɍA؀7c8kS|~a\D7|6LC˻ o:̚C97e9\pbw HNmkEWO5-Vj°ڢF1ubHs'ú3xEIC~HZi\we9 8a o-McOڠ4ahGv8P߹ذ޻jhZWȬK4,yˬ:Iڬ׏ kd ,iXV&xz^aq[Xz??f_xzde;RjPIodXn JyK ||Lm*~M0w/<d[/xӤ?0 4Xy81wG*i&  &%͚(֧6#úI.MQ!_,JBu\kWƖnӺðV\@OG9}Sm+Gr)):. !ಫ5u8N.,EM00^ܤ76p_*zh^%|V\͑?2{NaP0r鰺>>6n]B|Mɘ' da~Flq:QF}v(ikJpRZ5Hg79s*՟@3"Y"j{QhiKE6}ܰDS %U5M C/|xכzS[ BOS8?ڶP~#~D9a(|=O{eQ^g!,- L s١i}/}: #PU͹RY&ډss: iwꆔd\y KC&B8^w:ڶ|?ՈxW LVd?y`0g$HN3]}aN=0c*AƧuTs,MOVȄEǵٰ]_ p5P8u G01 b3ɾkvzkGMVĤ7N2dq=sa?7 +@Z$s>>& taE$_`e0d~Ibxy:L%!--a5I^z+qSȄPatC<^1й`r 2 K`2LgzZ\K&NyZ0=D).'Pwh%~oJyz(cHx4O yN` Q 5>c~<L7a5[cp]!P Ԑ݋xǯ'º>[_fI;ALC:O+٥426-,/D挩8`hyZ dMS85Cez]Ʒ8~B!1)AbmC VǗiOLv"bAYZK~u&p"FU@k6E8{ C=+źVխ iX'YOjx;W(qS_ABh$Ulds L@!5Gbq` rUϡ*EަHױ+O9*f+u-Jd6.5qK#?u=D~6#j)!fo nKT`䆯\gHX? #NR|u؝f04MOp_,_Iu qd#^I35R+j |jva lK $(0v/ͤa ǴVZ/U$ ߯Gt9 f.qq/e 2æQI:whRbr30BqbM^5A*Z)> sID];>,>0RX|`‡Ơ1\/U PS-b(XB0T1(\nP+gʖ3@$z`Έi.XX$!C7)>ti רDC-!2b5Bq)!}rR4Q1aNyp*m֙K qM+pNPԸ,ciCmϧr-.G{b^rl (LIyL<3]O!@XPX_i"2zF,!WH!%q,-0uj?c\0J s1s=1s}l߇4 "ԋ(z4btqVWd"!y:):X C4oSDu`=Hj8A8FYe %1lA5 X&U-I#?1-䙬/rȴp =0&#elbL!MCZ+c_MNVO0rXmZE@ҐIxbXPuGzW%\.hؔ]"tp_nNb8'.9Ά>?VBȴLªƸ3{$'8a}%M(e78 Gg 4+ 4A dbY,Ϊ:7cU̕8!w+BE4ƅt a=`{'y^`3elVLwQĢի3yɦgd٤&}:eSM)G Za1`9cW$`+eVʾ|)f1̊}-z.[3bug YtX3,Ŗ=,:LMsu3|3VX`*c 0C\.%OF7DZ ZH}Cv9Pz{+d4]`]/5ԨǛ̹ML\A(%/)|Ջ%5V׼Rt*cgyw~x4pҨ#thg.e1ASY[f:͛hvը HW Ϧe6T@`$\R ")7N)=+2͹ױ+D{M0$-(%omX?E5R?9Mk<"`E< ae 4℥& - OT-4]LHtmy>7Z%s=I)YrE5T'o1:s>1ICc%4Yeg)uGDnZŒ.,$zSPׄ Fh>MXbafw2 ~9Ebp)3<J9SB:\2E(LrH{kvKiRIjqP%`*f=G0j;=#\)fԹavN J%*<USnp|ê=@Z6oZk5 )R)[rP14:D# KEUr#O WɌPW)?%ðh+uà%q~B^&M ʩANe0;Nb5eh$U6>,>Mɔˠ'WZv<%knYk y+8GucJD4FY yt4X\J (>c@V зA,+*UkjtYr6wIø9 D[Vkh"TEvBDy31IkUUwaLUar„錯,)@ZӰHCeTo|ǯ{IvLa[3b>+O#ƜRD͖T SoU__jW[W_1RFMAZުӥ`~htVUXv/m MeK?O~GWk/9kP>,*y_amt:?T@j)qkX^SG/>y[7@piߡwVjlQҒHf*1ӫ߾ U!cdc?J2dߜldA*\yHc+rygBK2ob?%EQ@_Q\kY09?b~Wlmm+[:$Y .X}`S@ a1_Zdq[ޚ~m8a(~WN/X(@T23k)?Z5L?E}aisXJkxh 0?^t˘BsmG}=n泶{=6LUn2#탲jMkr,PdoL?ll+1̢}/MQ۶i8id~KU) ۩sL H׵7oNX 9ݚWZWL=wEo)]֠ogp: hRg^D:9h`Vm"oB2&dN4dQutBw`xs@JGOgT ~B4;F)]-Z4?<ى ʍafJ+NU},f_~Ҏo9!ayo}ǕQZ"dlϘ+"`CB›+$(Vf"R`.Wf/"oq9QϯY%n[,:%8@+͓U"OV -2gA KFeQ1ĉU\X^AWFYJ/~ɛN3Z54Vz~i" EhO$*lsB| ~rTIJ?@)5%d 5yRs %YlHҬ LMD9A`:g|X S4t !]ҬmPD Fh)EOo72%C\%poݻ촟tYMQbpa)?ZBuyI{UUH&~S~ #Rޝm=Y+u* -BZCQ,gAɳOUpTGm>pf hEr`,kX`٦:+P2׵ zڞ8ʾ@3Y+C`5eQ-M&V(Y,wX{#/ 6l_ȗ}ܕ-[֣!,Er@Q"e,I} a=Mm[i%&^G.:wˮ]jh 2VMAiJ8Ld[@wSOdzB3-~w~skv%d~ 3A1U2F檖-g y9#=k]BMQBƱ >jfeCrCKw"B%HP9;ܳ&hXnITͪ:ryAJ gpX|i: 6"2Cw k'de9Q&nK9*fqFLvB*f6*=umsr빼(h( "@C0d14MYR*eW9,wX'sB^c E/iX眶;(,~3PWhmt<3ޛC!y Tt4JHk&=Em l%C~e K,gRl UX-,!ilL|_塚b[Gmdp0چM"vj\ymYD(/⪮$yN4 dB=U5”UczE-;CZ@z2^ge'Smei6Th%,^t?855sIK5 2;ִ uIRR7} X<jF)&Vl PAÛՋynJ a--"8Ͳ&nr )Ȼ}uCD:BH+v*t/$9HyN;WMK{vr pWX9 h@NG h1a}AtNrj)vJF˂YJKzG=۸Uס_ ʶ@BK3Z8L =Ǽwxp-Zx::Kّ'>2}bjcwx0WXa΄i륭"XgSRPm-%넒+dP҈Tiai&pZđC;#4, +cL}[WO<l+ UiQ=)ڨy^5{FKN1ۇ7|8 |j Up68TȦE7\WXlqiYmT5S֣qv ˣ n懼Q(rayrpQp tVnJBdgCQ{P{ox tw-ӈaߏ*"UyOfw:OF Kj)J.@r=֫Z_ CB u8E`.JI?!-:[F DNn"C*ID74S#}B=VLӫwU['VrɣWXz*0qK I >y=١j@5 K1qͪ&~ƿ𘂏 i:#gDو8[ ?1Am3 i;u:buZu1`Orlb،V͡1}UHYQ"M:`7m / '%ĦGuʪjA3KCzkZZ pyZpI (˴$iDU"zJ5-JyQ_D S1:mk, t)"Aħ([(j ֚b'f|V!XGMmKjtTC"^L7!'k IJ}zxW *#ᜦfw 8xt[cA@E3%cN&w= nU(q Mz(i(oY+q1ǬF`ہ5AXez DH V5JhAvma&¢Dzhɛ:V ~atDP;938N8u2LOztx--bvSSNu6g&n>aa =V +DBvE(\SEGCHi{ҁMύuE_ d - jq& #>֨ڠ*#bwd=AZ\a']`="o.ĸKKsf]o]況ʴ{Q^h5IZ͋\'}uX Bu$int vo$S yeB,.} 3B_8go$tA`CG:9qZ lq5^ZAc\V&_wom# Aj&GIF?¨*oy_jcWI9`9RaBs8iyGL#šE 7jmnL|}8V'P69*GJ8%ES S1RZ޳]V.?{dX'%H _VJ_KQ"'nOkr" i)}߁AYps8&gIZWu#xjW/O֣k֫漃ʈa4!M'N?&+T&F(%K^oOփl,%-7b=_XJ$<{aM+Z Hȧ|oQiig1y|XoR -.̖1)yY'% ~X7(U$$NJ튊֩\p΄5Mf=8b9^ E 㟤mCxΆuw7TED:X %y&߈uZWSm e06c.⧯NALoß~aݽWT|HM'į o0y9WJ0?(&_fxYg۝|^$Ϸh.$/\`MA< '?)gmB8{sq4}B~~理/-n{눏9*"6kpz|DiqcA3fʨ:s4elXPH\!s?|'<>ceI (g,^S$amE_}ǽcÂKHpfǿ~Ne$!{'Bc x{?~Ia{;'O\IvIENDB`envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/acmelab/images/acmelab.ico000066400000000000000000000462461441257372400275150ustar00rootroot00000000000000hV V00%&(  U,O)1I%@C"s: t;P|?z>s:?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:??envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/acmelab/images/image_LICENSE.txt000066400000000000000000000005331441257372400304070ustar00rootroot00000000000000 filename source --------------------------------------------------- about.png Wikipedia: This image is in the public domain because it contains materials that originally came from the United States Central Intelligence Agency's World Factbook. acmelab.ico Gael Varoquaux splash.jpg Bryce Hendrix envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/acmelab/images/splash.jpg000066400000000000000000001112071441257372400274170ustar00rootroot00000000000000JFIFHH1ExifII* z(2i< CanonCanon PowerShot SD630HH2008:10:02 11:07:424<0220DXl t|  X |J 0100,    8 2006:08:31 06:38:042006:08:31 06:38:04? _ ." NZt(C   h J\E"F\@C_ Dm?;IMG:PowerShot SD630 JPEGFirmware Version 1.005)) 5^ sM|Y\]Nr;rYI[''`E uwxtwyx% @0111 '*: R980100 @  ( JFIFC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?XeUjp)D ?S0i*{T0RbZvړm&)C1I~LQpB*Mp"".)Qp)1RIw EJE4.+HEHE!DE4B(jZiZb*2T,qd"V sE:F*TҔ(8qڸΡW2ނ[ijK#t\QRzHLbLS31Iw &)1N#4n(M""w2T"4B),BE!))V!+QU)hU)1^hqjvs\rx0w|$t$_7t汿2;ylT y`*aTq^rĖ9>xk5X]Da)QM*Am}~d2qf!\U\V.!)1KINⰘ-!pN4MiqXB)S4p)S!)S%;yEc3@ jft$g'nTRTC~ƹѧ+5KѺ"gP>bin5IܖKΌlۈ>o8 wVq]D$TGLk+@%/B&@3Io=Veº-Oe:ari m?~t豚LA=S\??:P}꟝!z/LDf7E|_΁\ijOM2@\&j/5?:O0zb%&=EqK`XrNqVXH܌J9.A=H! $' A!?C\A,HI*ZͱH}U[2N隦]3҄KlduxJv=k7r{Տ:Gn3UK78}nVNιa BԒ] ?ZQGLXz\OB6uK2=93թ#7k?ޣ8.IO`D hTw$ |V94!ZW>>r>_žiES+zO1MX]: o!<)N|lyi^rO,?#` EO9|4I>) OEwM'9qҋ/'bqIj6'=8sSߝ\=? (6RŊc̒-':5[[ʥ?ZCm)-޸Σ8Kvyk0'*VU.<87} P!bNx7siЊ$?ZD#*,7˜zoг[_?;G?-DH: Zw`%$ǖބƅ^\–upPK9l/o!<Σ'<׆r\[r|O»a*c;z- ؾЙ*F$R@]+MXV0p5.!"P~U.(-XbpH2R6>e-y58ӄp0b3³m0"7?${w)R}:FLEo(`E۰3퉀(hkh'TpZV o3s7i\BdsjnRv.J)}h DR?M.Ĥ3ڢ%P@|  T,f B rF1I(81lP"9buY%ܵl }PP9%1cOQNC!`U:|1持L#nTzsP2̣zLFt7>EOHM1\twJB+ccwZb=Jc|2^w٭"8?ZR LTdVFkDٓ)-6V.`'=抮uiE;1]G_ `~dzƟ$RPpF_1N6JմmIHˏT>n=܀/x p=kOToc"y}+9lʨis5:orymY.z5 ;)'"6cVPˏ0qDJ>;~f_5]x H HOJ} `8Kuϝmm ~tE=(-H0) OƆJ,>@|A{p<6bm ´hG;@P.zr1P&>l( (M N@>HXnЌ40HiDtQ tʀD\g-EŘ9Rc"f;xT%͞Ā* ;]F!6;W7%Rۜ ;T@ %!ڝ|gB<*,ŃE zs:` 8Ce'>"4"6Cd!4'B<2)؁`sbP9" (lL|(B`]1zP mhMTM M1 |˜w@QsM"& "?Z8qR4w GfBysJz~5%#=ϥZNLCR:f$~83TwqIwmI_ɟJIM1+Ҹ)ix}`JISh2 zЙ0i84tB91H) jnZM3M\PJm2D0?:l(A'N Qbf5VMxqCf#淨;~ X#&91C"v\|(hT`qkSpFGʖњf. @(9])1$qbmȲ(#vE k|Sm6*ќt!8N(z> 0Iz,(C ry,[(3yɃ@u+L@g’c1υ$h%6o@P#:PJmL4yйSzc4i&w ˶(l 'ʂc4&dSgU& & אzzSiHتD.|J6kK"5tH3E[c 4kvJ"DYJ kvAuNpzI;AQ\co:,@\li2*+քS2DxzaB9s*.E]6\xQ` …݁N#4M)1ʝ zuߥ 4L %ddžM݀\l˽ !܊`J&vhLfb; &w=)ܽkʣFy ֨>H {H, l>}*hvVIa76!y+F;ؼ}bpA@ M6^]D2lg&~&#ʒGʨBzPqL@[nqbb|:Pߥ1cʞʆW"e΂A Y@?  4]-7a1PmAdP:T)  P$Ӱcv\@9s:O C D8ޙL4 l{ApZBuHyJrd Lmx+Op:Ҽkg_ŭάS"W;w Di7 <#"$ͷ\c:i6OԎvuw7enJ,P WmKG0CHmN]OfnSx[qgDg~նjɪgD_eg u!i0 ޿ Jb͟$ P$޸f1-d4sH-iHBl&;;P4 u1Azdc6vքI1yv'9phb3`L@ [2ؠB M9Z`y–zn6BlQZ^-0pOZV{!H(^U&$ʫB) )sr6…Bn/hrn h X]1p2vs}x`{y9@,06{*2qv&=ͽ!fu_l8}k5g >h\I|0iyYrX Gy\vd'#1k#'˭ReHƆ^tYV<(@mnF2v>U5ktP\qCjkJoMXݧ/KVrFEY="7HŤfC+6>Ƭ*F}3~uziM cB%HBLƄe{P ބ[ @i78@ZA#'0d\@ZO*?0`h xPo`ݥcwzj6iw H A4ɵGi⽍Zİ:ܼ`wsrjX:Oݬl |+rGٷӖp;DH|?swKiZ'D}żybxÎe?CLC{SM5hܐG_SOn3s1ʞsgl(.Ss35jYW;t,$+d`=kf٥}"]3Qԭ2c=l3*Qt':R6%`R{#5'P91 2q'4 d ˱9i#&(&\ZboM_*jbgڛ<CwM\S$&yrzMBiF4źxWz.+<YGttHi-qenÔͅukt]3fQU!x;ex4,=#zvr#ݧ(f@)˘ՄvOhK X)UM&b`E(8'$r AEX6Qu5 >b&zQN ;E}69~T&OvQz0}Zqm:ay94uz77>&$nuqhCgύ\ ` 6i6 f=Z`1 Iӟ0>L{/-_x Q)%au~U㞁qazĖdq<]mr<"#I,:o!XFflDHB_4n&Ṵay8?[yhx&Kyy4\z y;[Cj|o_,Ծp:%!WR}0o-Р=EvEi5]qo"(ӕFƧ?wxuUQ&1>^?E+ -NP`cN8X,X`:jӏnlgia2w; I>9+n폞-Oۆ@`GSֿEێ#)7źAaeTl%HF|i{[Nה+g>(T/_4ж=5"J0oK#>*zӵj;;恝 LQS8ힵ6$Pj7Q"@;n߈_YmqZx]OW(PŮT/z,Y$P+",d||<6!ٽ4 "K9DžsgWx#\hws:NQ(랣m۩۵XY݇4k'?QqYql ݤ iԂ3qM%}dPzخGH+TȦr7=w|ݰErpWB{tb2]s <7WE'6d]kx$v((`9e;i {`❀F@䑝qi1||N.R:d`q9ӹ_C#rss,.T##!؏u?|6h80f(\`L";8DWY<8;?:4k,? \йڀͪFF'3}U$8`=Geٕspf䞦y *#rvW0W%ۚng!#볫OΣP,/ɥ`sܠ푝<#@`y♼l04i0,lvd[6 X~^^_אz`]0`Rʯqܦ :N;Y$tdQiEl8 3;tyl-| & E2w/EYI y3ikPBSB_yY$>CZ%D7gޛh fÌՖ=N[=&>nRo֞Fؿ0sОAp1 m|uvW7Rj:DIunL$l!*yytQY͹4ǔTYƵi[R,2IUnt>&xwuZq_zGQ˶?>h׺ts M䄜x '\6CT*@FOGO$ 9 $N--c.ޔ#Xb 4R/Z1K#s\u>WwTu84*[YMxJ<֔3l)L(HB>8 I0 ' n> يQX/!?n!0&>t'̶)v8OҀsm*Ġ%$ٜ1G !i]Ѐf4iu63@'p'Fp¿\@cy"F"(#8iܯ|~c; Ε,U|Zyۤ [J7FyN HuDө3Wg 7#R/UA-QFeUZ@^H#/1d$O_:ĻA]3u[d~u>11b/D ^k$gn쾯>sh';-b֤ԓ=+1Y=֑dn b*җwkd$G׿˽}K-n$-Vk&# 3@⛦~X]4 j<%l'k$u^M~#jigoj0gce%-y7?J"`FcuյfJ2{8R;u8֙&]]C,0\र۩\+h?u?ߍ1k>y3%33Uh@./"+k#ܜc}eP 2E €.w;:C:CfKF\ه m64cbI#GMHAg @9AaG/IYYFܡ2)=R?UDYr PyT䪲Zܻʐoa}kթihrh7}P̬sy/y- ws[vBMU M6/ iR B9Ðxd-[4P%9$nr}0hI=Ihu۵[ݫjζ]1c;yVQ7jF>iAkdE@S>;zuD4fWQD_#y)ÅK j'Pt~蹑J0U&@`<юa-Le{yhb[R9XLw UB3?KRH߆)Ů+܎xX=3q6,?Ys"&{۩;$ԥ˛ 3`?:D.TP4Kdgt0du؆ڀ/o6ryQSk!Eܞʝ'gaqzR9ăxy՟unиNAvgmPve5^$K%7.~;*v]s<O}C ҳ#Ke_NΓwbXu0c Ki7pq~fv[hg~υMQ/3m"B>m5m$z=w ޙ#\ 13 |ݔv9cm:iB^>yt{MkwR2rz0qKL%vKR>ǣ}"'UuۙeKm2%d^dWFϭ,=XEEG'\;rjFmİ \G&ezUUdN5x#^rlFW)źvz%@4y~<䓟< L^۲*ӻMBR?fҞ.q©`wF$quo&wK~_!=6RiNNJɎvkà8 x.$_^|rb"EP#ߡ-ji,;X$DzY Nu1)_pz)|v㟕58AIDzwi0$prlA"997SW5od^5}XHc:ּ|Mrucnֻ'b˕6o(:tcl濧[MOrrFq oI<6|N(8FXİp+WuҨ0)g.0܏?o!^ ee8M=Н?; .5Y&<(,Q>P( Y$3˃mM׋n*j&xgFe%-<$ Qnka{03s;23']}1uǷ*i15IꭐMqTHWKJ뽊jQˆSziwQJFݝxtt-AyP?AhCai@Iu4!Pc \t5hY>pH 'p%q#F*#PAsY,8"Tc8 O\35NQL:f A8̌r ^_O50Vl`$>| 9,uު35eD{tߩ_&nkXXw9sNrrzʒ]p;#Iq,; \;+#UY,)EZdU txw6ܧQ]ӎ4 gM+۫}.w27$yI88e/m%;o+ j5`j2Oz49bH8)л=n= ]GF2[9m&9@lɲ ޭ_gm頞] =ߗ~타-QS۫oo{yI_P RVd®f5 Nӯ46cʥ\-_$_`d}-6 X^GpXoFȥ T d<ş#ʨ-_˨#E,j;tZ2ltѨ(+;w[gѿL*VUVt87Rr،T$»B˷?2U".6DY?U[+mDs[ܫ6BtKR\鷋+^_{3q [kG,H,qWQ N8k>.YXgktydLүo DWe;dl8_>C[i?u'彻zՎMV#n{K}rEI!G[Xh1o?:sv] F)#t>}1Z՘Y- 'rC|*yI1ac*:G0;ӹ4D?.7[jbp٠>k:Z>2Eap7,!lc㊲𞩩[jQ;19wyN=4wNW < ҲNT7R$~ Xr1=FְRM٥:RY^NK hx;U't3˷uwVVlOP^݄G7 zyՏL*B91+&]ЉPӮD M F1Mӯ^? g\M,j,ܪdd$ۻK0H dn}~8˪ vۅb {@WGI*[{H@9NT0Trnz趓X=G4UZ!Lu@do-7jg(v-vܶjM4I)́=LYlwH:#U-zRה㕶?Ji.6Um/n%Yd!j_iZ*'XgWRN5\O؎fK[/Tz`tYO#H㍍n,7NƴVJ̲}X Z@ggLdȈsxޞX[X)Y{ivP3avֲk@ V~r))/3 thݺ2:e|O-KֺJ?)P}9}~Yqw%q78Tx,?{7~f[;=^ uբʉ]/|\H2BAvîFsNZ*gjl:a~T6g ̍"zlH*{Mtw[=Ą1ힿ5u}]sm+E;P4KɕzWi}z AipѶ r̸#mlL6\u+,b- bZjS,)*2QpNږO4a#% }OB/ua+6BA6eRtv#.xE2ks%Hl#FoQ]z\Ѵtݙq݌{sԍkmQZheko曻j~"U]71pv064ۖ7'_evZ4z[jvzjMa*0Tm<+ƏnځطUe.pZ$ ҿ3s̋'*`WuVV=x;Q+[؋I4У9%-㍼: h<2j) B]WM3sjI4mG웑"up7>Few&3gd~5ĽE [YѳUrއcBhqZp-y$b{mV "+o@u42 1u=IL.9p͹-gk#$ 2GU^ %:l&w(G?NQjtatIs\}Z VSaqR5gMGc mlMMh_Ix3#)x2d]Zrx͸5^qŗ:9GM:uMZ=ޔ۲b|0VkJ+Z}Ey-,#*]ܙ"]'_٥&NUu@--I'SFrWϯʭZ /i72B /~U-(>.!;#lU[K &1=:| %,itW1Nn Kon1Qu֣* 6 :ݱpC?9j=e (^ʩcr D|(GG#>@SL`[!718uV%2n|{F<5H`vѩA,($V_LvI9#RɔqKլdb$r~(kiSHjjUI5ȷ~hۼSvR]1LHI@^6ӳ!2!߼ 09PSHtuhA-Yy'V@Kr|Zd %#ͷ|꣢ÚTJ2NU Ϋ0%.Ŗ-ӆK1u˓$O268$0G|MDKE>B>ZY܎lkjcqO*P,˘WjovJ$L3EV.8 u+qnp!!mejd{h~aͿEzgGWdl5Ƹ\4]r T0 g3v.4hR=3ZCg\t_t=S ʑEy֬V5 ZT{Ր+a `x>\e@c:t{C+Knm:f}Akjm$h(XrmNiem4W-"ȸbNJSYѹ`\0GޅdIZ@?χFV,' p@\@ZF{ -^s;Ox ӈ4b Zd,ȇe\GZoCX3/nbn= j\e}{9<9 uYZHyr=\![TCql҉\t8veK&R:N$ps*hcsIu։*YlS [q -m&-"չI[9Sf)%=TP/#ȭV#6 = I9} :ΩCnXŒw+)dIxe. kwH@v>9߻-tG~H8eP>93P妡1e<"\#G$[U7q[J_OUsPPǁեvVצ>+{)cŒǠ'|Ze't+6Q$žqr`TCHqN^b[IFb^l0@lACi}@;bA;u장z[zVNx1G]9wbn2rycUYz&J{Y0x;;8+ŷ+o#0,Rɷ7={gXYn5lnxv)b`T`fiJKOggԷg7i<-ogZYzӡs4řv9WL}؜~}>ÈuvKD9r3ZG;vh:?k{CiDD$IsOXH"0i џu[O879m3QR0W\TI!sWw1{<>nqd;.~%nu-ԯ$acsgY׏"S3+Znʗ)t qsC2OV[䑝(o3\DXMr90MsU <d=ԙ#))]ze:ǵ KM3X/8&m?]Q\P6V벞u}f g[mI9DͦFD^'V_? g)8+5.}MG!r|[èk..] 3<@kI5VKd7DO}# <Y8c;TܞQT*{j0i7d=0?SQsHwp:tυEC,+ImMuF"`q¨ZԜ)`;'UhZygƲ.'U^;ŴۤH{}ALJj#Dr[)[q遃JN2լQWЪ&LF~Z>CX#;tǞrǃu?~O12[Dz˩3/23~#; ;"4I/nHP 1do!v铹#<0z=sP, [U-&>G?CO'I,4uAs<-j"{y3ة ʣc6P3N3'4W3F')<`wPziZzUيݫSսeN%x=5:1Q) p#Ѱ6ߺrSu°UY $sX܉bħ0v?Jiķ-Qda=vckQXgQΣ|j+e=噈'7㊦wmqlqwɓoN\{-S-'KimHf?WughH ;է{3qi5L  *Ũ-H#A3ϔO(L ir9#ʲRC.Dȏz ;~KyZB<<Ӊ;E֘#)L=SBS#o3W 61=aV;Y2 a|m֊'qC$, 0G/(#5' ]3# q铵CFD e-E$QHU7+#?g"+n4oB1zӌ.>9eVz<2"s@#XW|t-rv|vηJ6S낻wYn!nl_$4 `GF5=FV.xJ26Qb:A+TgR{]zao҈%kR\!S̱ q }6xUm3ңN-MB!'=1H̃8bnATO8~<։W壐: i+5ΖL;Kn'r1қV3[r$$&hQjz<(ge8\s5V]HRp,@ ֭;^TKesd<NEreGKs/6 Jϐ3P\Ķm:ƅbp|N#o )Oֲg<-sj(ݐPIUa}zzdӉ[vPs1َ1CU NB8H2.*f#foajG؏g@mi|ƒ[+$dS\ ۆU^5O'NEiiJI$FF,12ydӠ?gmtmqChtEب6 P[pM"e ;QAdr׳ʪsh&eg(ːEf:V'y2;eh|ԓ 2ݞ'J2X[bH#~auj# DK rFaсUHk=j+Xmc+dcxxU.LPvp| IћVeazdPNved|3O,t# mv2) UhJIGŪBī CpgG2Fղ? ڂ3}UMGCmr\Y:td*䄖GZ;qܐ! Ȏif_tp6/ g@jaF#,Ta6JMz|rWg+. zc.t$UdHVO4Ҵ_d2]}? >g|V)hXղEtϕo(CQafY <Luw +ImR֯ ,i5L+jQ!Kb_ujG_N8Q_iTv ؏6vsOuKa094OTNw TlLzo3X VNd(d Nbո|iXld+8-VhWeN(Pqޝ8YLr~nQӻQH\1{{WVV1XN7 e8~Ma+F[=Ym4o3sll%qtѳ`/2d65A pg-В;FGLXCOšt=.. 8ۜ瘱zyjyfͮm.Zݐ FIٛJ&yvf80>*hTaX9$E, Qe4{=FI"o*QV$F.6 *2ϟC{["j ~b励iWl&vWlңKTyM:2qْ+e1 n>ߕuG}R?}j(Kw@oàgj2"%ʏ;TԤmJ̜C$I>e:&yrYy=ܟvFJK9Q*Y`y2XJO]ǡ?ߍ&I ;챑ܱ0#o;qVVw(g[E\u{]%rAn]2pTr`-+46[Tڮ+XF 8Fxf~4imp Cd}+>6J%N =c?y*vju/k;MXy$Cȑ8`7&ݿ[Gƫ3;1?xOZ=Ir+(- FwVVQ3I J|!ⵋ8 01A)UpK(=pqcֽ49R)S)wj"l7] -3YP ̎rZ2mS2C\g&'5Un9x.bN9:d5ċxAs"A~TI83DIr4aǹ哏ʜZ+:ǁʾFL]\Wr<=jKc&]@Ke8s㍩7wVq \\$ŗۡ ^MHbp2dْx05BM .bi ϶R"5ˌ{}RrIdS k?k٦ɧSYu<D9+)#1O_wǠW >7k^H;+[ K;Krc=۸&ScY_{zfL1 ρ#onJfhQ/ղ|q]+mÊhٗ1\1QߑQ_CUI"Z1C<7R4zy5,.si XW)(>x½݃.Ӆ"4lePŒ̡F9Q_*9:/~ 䆧xe;iE}(35iR%eBMnVYc!o|½Q -S&V:N@۔U#WLGew纎؅;S񯥮8^%lnm^'A& W=lͥpJ r3iA+"nG2Z.1pq S8 @'>`r=HI<d*үG,vg836Yr gW* HuІ'V!I]AL^X]sŸ^_ZqW.:JP)__WEOoo1<G7֑ϥ9Fp?_֛lݯ]Tﺐ 6l gu^Vã_voSP.ZI12enFrI>0SrX`A8tvOh  NVzjsj >Q_?98ƌRRkǒu"2;(|+1B`ʏ"tI*6ԸsOe:\6ỹ%;U^bNN7 S\DU6 DAuI?/ΥnYۨ6"U c&5͆k{H叛%H'`pz3RjOsY'[[aG<=`Y|g~rrBv7bI#~X_"C xm銯p[I6RtK}Hf6[n%V@rɏu[sQ<;5k˗AU2Jk?SO<*lѦ& ;n 3MѻD}+kz|3k-ԄuP`d6$es}'L%mR(G[~D| zm[F~Al,̂Vs*o$`'.O*\(1'/G-՜D9G|&[Namm&A$uSX*\/fѤa< d % PF֐rԏ;f]WWcFbH.#<f\gó3$kUeVЇ-8q '7Lq>=GaΪzA}qef{ q<vֺRٴ+y/3ѭLܓʅX *kFT]{yg (1 ʤ/ɷ0H^@7N漪=aZdڴf{aNSyjçZ̡%+2yֆE0G,r$03q,u*2mB$G#InK}`BV|0irwL`U3X;&ʗhV%ս15cjm4ӄ&9dt889*Ni==X9 m&1k*XjdkO]J2:X$4XXY+ǭynLpDvL_|O֮G-:s KӮ=j[c̚ڸa% &SS0g|c^ŁƝѪޕ@LF`L<I vP1ͦ% 1*)cS |3Rka%)e*dmMҤdp6wRG,RF< oy}굚}CW3X;l~GOΚn9ZDx]g>"M:^Uդ=:*G>###ux~݁ vFxWDmfã-jmmVN\s6/, E$T"U-sxR%q@ Ywݑ5r61~y5pcƗw<%wVq\e݃Dܹ洹 Dc0kҾE:lOq<\(Ụ> rVMov 8w^"Ԧ=oU%,P h6* PXu_EbkѸh{0$<#1#Ql!tG{%IopcBz? vF39D2<}}*xCh5AOZ+$Ɲdݶv;9noR6RF'DUhs<鍾$.xY AE/[fukd7#ިeD۳DeGFK.̉RP+h~B}AP`{ˋt{{̐.zm .|\_,f^S c| =N:QkH4b#L>`xSKdhԬlLvwR}tj jSs4@9~a䳆($ Vgf=ɹk0 )*C;ՏHNi\w`rEjo?xY䍙 }bau9>?hF-3Ƌ.n{H#QBw-eF#ycj+ǽ1u($AMSJ{7OL1Z6M1JHI愵gM{?cVR][b6}2ݖhe#8qUte4avqi|yx"/86&n.oY9]|&PX2.&{V+Q! X:XQm J[frѢvGMWS <)`NbʣmjəR4Wӣ(Vl/=zE4##IFځ+WČ%}s]훲6.-).aE'8W$^lIvM͸Oo%R%1HG0o·m۴(L7 , ͗ p00r6\I F?i-혵H$dBAOA_Jmtȵ.LՇ6=j.lH*EZK-Bۑq? 1_g_gnjzVA[ﭴ[kx>drNIxd%~Zֵ7WKlݼF gШӥv -_ʨq@3p,rqjMæwPҢI3Y m,.WٮGoEwii1V HcĈ{"Y&p|@ MANsz75ln]fvcE6n1!f dҀ+\O " X&B̋>:#OCԔNUf|({&No:{+)Cq+R[}~ȒV;HGvsD,a\EiBIenvisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/000077500000000000000000000000001441257372400245525ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/__init__.py000066400000000000000000000006271441257372400266700ustar00rootroot00000000000000# (C) Copyright 2007-2023 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-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/acme_preferences_page.py000066400000000000000000000033171441257372400314120ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 preferences for the Acme workbench. """ # Enthought library imports. from apptools.preferences.ui.api import PreferencesPage from traits.api import Float, Int from traitsui.api import Color, Font, View class AcmePreferencesPage(PreferencesPage): """The preferences page for the Acme workbench.""" #### 'PreferencesPage' interface ########################################## # The page's category (e.g. 'General/Appearance'). The empty string means # that this is a top-level page. category = "General" # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = "" # The page name (this is what is shown in the preferences dialog. name = "Acme" # The path to the preference node that contains the preferences. preferences_path = "acme.workbench" #### Preferences ########################################################## # Width. width = Int(100) # Height. height = Int(200) # Ratio. ratio = Float(0.1) # Background color. bgcolor = Color("red") # Text font. font = Font("helvetica") #### Traits UI views ###################################################### trait_view = View("width", "height", "ratio", "font", "bgcolor") envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/acme_workbench_plugin.py000066400000000000000000000045651441257372400314630ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 AcmeLab Workbench plugin. """ from traits.api import List # Enthought library imports. from envisage.api import Plugin class AcmeWorkbenchPlugin(Plugin): """The AcmeLab Workbench plugin. This plugin is part of the 'AcmeLab' example application. """ # Extension points Ids. ACTION_SETS = "envisage.ui.workbench.action_sets" PERSPECTIVES = "envisage.ui.workbench.perspectives" PREFERENCES_PAGES = "envisage.ui.workbench.preferences_pages" VIEWS = "envisage.ui.workbench.views" #### 'IPlugin' interface ################################################## # The plugin's unique identifier. id = "acme.workbench" # The plugin's name (suitable for displaying to the user). name = "Acme Workbench" #### Contributions to extension points made by this plugin ################ # Action sets. action_sets = List(contributes_to=ACTION_SETS) def _action_sets_default(self): """Trait initializer.""" from .example_action_set import ExampleActionSet return [ExampleActionSet] # Perspectives. perspectives = List(contributes_to=PERSPECTIVES) def _perspectives_default(self): """Trait initializer.""" from acme.workbench.perspective.api import ( BarPerspective, FooPerspective, ) return [FooPerspective, BarPerspective] # Preferences pages. preferences_pages = List(contributes_to=PREFERENCES_PAGES) def _preferences_pages_default(self): """Trait initializer.""" from acme_preferences_page import AcmePreferencesPage return [AcmePreferencesPage] # Views. views = List(contributes_to=VIEWS) def _views_default(self): """Trait initializer.""" from acme.workbench.view.api import ( BlackView, BlueView, GreenView, RedView, YellowView, ) return [BlackView, BlueView, GreenView, RedView, YellowView] envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/action/000077500000000000000000000000001441257372400260275ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/action/__init__.py000066400000000000000000000000001441257372400301260ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/action/new_view_action.py000066400000000000000000000031601441257372400315610ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 dynamically creates and adds a view. """ # Enthought library imports. from pyface.action.api import Action from pyface.workbench.api import View class NewViewAction(Action): """An action that dynamically creates and adds a view.""" #### 'Action' interface ################################################### # A longer description of the action. description = "Create and add a new view" # The action's name (displayed on menus/tool bar tools etc). name = "New View" # A short description of the action used for tooltip text etc. tooltip = "Create and add a new view" ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """Perform the action.""" # You can give the view a position... (it default to 'left')... view = View(id="my.view.fred", name="Fred", position="right") self.window.add_view(view) # or you can specify it on the call to 'add_view'... view = View(id="my.view.wilma", name="Wilma") self.window.add_view(view, position="top") return envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/example_action_set.py000066400000000000000000000057621441257372400310010ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 action set. """ # Enthought library imports. from envisage.ui.action.api import Action, Group, Menu, ToolBar from envisage.ui.workbench.api import WorkbenchActionSet class ExampleActionSet(WorkbenchActionSet): """An action test useful for testing.""" #### 'ActionSet' interface ################################################ # The action set's globally unique identifier. id = "envisage.ui.workbench.test" menus = [ Menu( name="&Test", path="MenuBar", before="Help", groups=[Group(id="XGroup"), Group(id="YGroup")], ), Menu( name="Foo", path="MenuBar/Test", groups=[Group(id="XGroup"), Group(id="YGroup")], ), Menu( name="Bar", path="MenuBar/Test", groups=[Group(id="XGroup"), Group(id="YGroup")], ), ] groups = [Group(id="Fred", path="MenuBar/Test")] tool_bars = [ ToolBar(name="Fred", groups=[Group(id="AToolBarGroup")]), ToolBar(name="Wilma"), ToolBar(name="Barney"), ] actions = [ Action( path="MenuBar/Test", group="Fred", class_name="envisage.ui.workbench.action.api:AboutAction", ), Action( path="MenuBar/Test", group="Fred", class_name="acme.workbench.action.new_view_action:NewViewAction", ), Action( path="ToolBar", class_name="envisage.ui.workbench.action.api:AboutAction", ), Action( path="ToolBar", class_name="envisage.ui.workbench.action.api:ExitAction", ), Action( path="ToolBar/Fred", group="AToolBarGroup", class_name="envisage.ui.workbench.action.api:AboutAction", ), Action( path="ToolBar/Wilma", class_name="envisage.ui.workbench.action.api:AboutAction", ), Action( path="ToolBar/Barney", class_name="envisage.ui.workbench.action.api:ExitAction", ), ] #### 'WorkbenchActionSet' interface ####################################### # The Ids of the perspectives that the action set is enabled in. enabled_for_perspectives = ["Foo"] # The Ids of the perspectives that the action set is visible in. visible_for_perspectives = ["Foo", "Bar"] # The Ids of the views that the action set is enabled for. # enabled_for_views = ['Red'] # The Ids of the views that the action set is visible for. # visible_for_views = ['Red'] envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/perspective/000077500000000000000000000000001441257372400271035ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/perspective/__init__.py000066400000000000000000000000001441257372400312020ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/perspective/api.py000066400000000000000000000007601441257372400302310ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 .bar_perspective import BarPerspective from .foo_perspective import FooPerspective envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/perspective/bar_perspective.py000066400000000000000000000016261441257372400326370ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 example perspective. """ # Enthought library imports. from pyface.workbench.api import Perspective, PerspectiveItem class BarPerspective(Perspective): """An example perspective.""" # The perspective's name. name = "Bar" # Should the editor area be shown in this perspective? show_editor_area = False # The contents of the perspective. contents = [ PerspectiveItem(id="Green"), PerspectiveItem(id="Black", position="bottom", relative_to="Green"), ] envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/perspective/foo_perspective.py000066400000000000000000000017251441257372400326560ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 example perspective. """ # Enthought library imports. from pyface.workbench.api import Perspective, PerspectiveItem class FooPerspective(Perspective): """An example perspective.""" # The perspective's name. name = "Foo" # Should the editor area be shown in this perspective? show_editor_area = True # The contents of the perspective. contents = [ PerspectiveItem(id="Blue", position="left"), PerspectiveItem(id="Red", position="with", relative_to="Blue"), PerspectiveItem(id="Green", position="top"), ] envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/view/000077500000000000000000000000001441257372400255245ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/view/__init__.py000066400000000000000000000000001441257372400276230ustar00rootroot00000000000000envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/view/api.py000066400000000000000000000010761441257372400266530ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 .black_view import BlackView from .blue_view import BlueView from .green_view import GreenView from .red_view import RedView from .yellow_view import YellowView envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/view/black_view.py000066400000000000000000000014741441257372400302120ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 a black panel! """ # Local imports. from .color_view import ColorView class BlackView(ColorView): """A view containing a black panel!""" #### 'IView' interface #################################################### # The view's name. name = "Black" # The default position of the view relative to the item specified in the # 'relative_to' trait. position = "bottom" envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/view/blue_view.py000066400000000000000000000014701441257372400300610ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 a blue panel! """ # Local imports. from .color_view import ColorView class BlueView(ColorView): """A view containing a blue panel!""" #### 'IView' interface #################################################### # The view's name. name = "Blue" # The default position of the view relative to the item specified in the # 'relative_to' trait. position = "bottom" envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/view/color_view.py000066400000000000000000000060771441257372400302600ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 a colored panel! """ from pyface.workbench.api import View # Enthought library imports. from traits.etsconfig.api import ETSConfig _TOOLKIT_MAPPING = { "qt4": "qt", } def _normalize_toolkit_name(toolkit): """Convert legacy toolkit names to current toolkit names.""" toolkit = toolkit.lower() return _TOOLKIT_MAPPING.get(toolkit, toolkit) class ColorView(View): """A view containing a colored panel! This view is written so that it works with *both* wx and Qt4. Your own views obviously do not have to do this! """ #### 'IView' interface #################################################### # The category that the view belongs to. category = "Color" ########################################################################### # 'IWorkbenchPart' interface. ########################################################################### #### Trait initializers ################################################### def _id_default(self): """Trait initializer.""" # By making the Id the same as the name, we make it easy to specify # the views in the example perspectives. Note for larger applications # the Id should be globally unique, and by default we use the module # name and class name. return self.name #### Methods ############################################################## 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. """ toolkit = _normalize_toolkit_name(ETSConfig.toolkit) method = getattr(self, f"_{toolkit}_create_control", None) if method is None: raise RuntimeError(f"Unknown toolkit {ETSConfig.toolkit}") color = self.name.lower() return method(parent, color) ########################################################################### # Private interface. ########################################################################### def _wx_create_control(self, parent, color): """Create a wx version of the control.""" import wx panel = wx.Panel(parent, -1) panel.SetBackgroundColour(color) return panel def _qt_create_control(self, parent, color): """Create a Qt version of the control.""" from pyface.qt import QtGui widget = QtGui.QWidget(parent) palette = widget.palette() palette.setColor(QtGui.QPalette.Window, QtGui.QColor(color)) widget.setPalette(palette) widget.setAutoFillBackground(True) return widget envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/view/green_view.py000066400000000000000000000014741441257372400302360ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 a green panel! """ # Local imports. from .color_view import ColorView class GreenView(ColorView): """A view containing a green panel!""" #### 'IView' interface #################################################### # The view's name. name = "Green" # The default position of the view relative to the item specified in the # 'relative_to' trait. position = "bottom" envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/view/red_view.py000066400000000000000000000014641441257372400277070ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 a red panel! """ # Local imports. from .color_view import ColorView class RedView(ColorView): """A view containing a red panel!""" #### 'IView' interface #################################################### # The view's name. name = "Red" # The default position of the view relative to the item specified in the # 'relative_to' trait. position = "bottom" envisage-7.0.3/examples/legacy/workbench/AcmeLab/acme/workbench/view/yellow_view.py000066400000000000000000000015001441257372400304370ustar00rootroot00000000000000# (C) Copyright 2007-2023 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 a yellow panel! """ # Local imports. from .color_view import ColorView class YellowView(ColorView): """A view containing a yellow panel!""" #### 'IView' interface #################################################### # The view's name. name = "Yellow" # The default position of the view relative to the item specified in the # 'relative_to' trait. position = "bottom" envisage-7.0.3/examples/legacy/workbench/AcmeLab/run.py000066400000000000000000000026111441257372400230410ustar00rootroot00000000000000# (C) Copyright 2007-2023 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! """ Run the AcmeLab example application. """ # Standard library imports. import logging # Example imports. from acme.acmelab.api import Acmelab # Example plugins. from acme.workbench.acme_workbench_plugin import AcmeWorkbenchPlugin # Enthought plugins. from envisage.api import CorePlugin from envisage.ui.workbench.workbench_plugin import WorkbenchPlugin # Do whatever you want to do with log messages! Here we create a log file. logger = logging.getLogger() # logger.addHandler(logging.StreamHandler(file('acmelab.log', 'w'))) logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG) def main(): """Run the application.""" # Create an application with the specified plugins. acmelab = Acmelab( plugins=[CorePlugin(), WorkbenchPlugin(), AcmeWorkbenchPlugin()] ) # Run it! This starts the application, starts the GUI event loop, and when # that terminates, stops the application. acmelab.run() return if __name__ == "__main__": main() envisage-7.0.3/image_LICENSE.txt000066400000000000000000000030201441257372400163130ustar00rootroot00000000000000The 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 envisage-7.0.3/image_LICENSE_CP.txt000066400000000000000000000467161441257372400167200ustar00rootroot00000000000000License 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. envisage-7.0.3/pyproject.toml000066400000000000000000000050071441257372400162510ustar00rootroot00000000000000[build-system] requires = ['setuptools', 'wheel'] build-backend = 'setuptools.build_meta' [project] name = 'envisage' version = '7.0.3' description = 'Extensible application framework' readme = 'README.rst' requires-python = '>= 3.7' license = {file = 'LICENSE.txt'} authors = [{name = 'Enthought', email = 'info@enthought.com'}] keywords = ['extensible', 'plugin', 'application', 'framework'] classifiers = [ '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 :: 3', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Scientific/Engineering', 'Topic :: Software Development', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: User Interfaces', ] dependencies = [ 'apptools', 'pyface', 'setuptools', 'traits>=6.2', 'traitsui', 'importlib-resources>=1.1.0; python_version<"3.9"', ] [project.entry-points.'envisage.plugins'] 'envisage.core' = 'envisage.core_plugin:CorePlugin' [project.entry-points.etsdemo_data] demo_examples = 'envisage.examples._etsdemo_info:info' [project.urls] documentation = 'https://docs.enthought.com/envisage/' repository = 'https://github.com/enthought/envisage' issues = 'https://github.com/enthought/envisage/issues' [tool.black] line-length = 79 [tool.isort] profile = 'black' line_length = 79 order_by_type = false sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'ENTHOUGHT', 'FIRSTPARTY', 'LOCALFOLDER'] known_enthought = ['apptools', 'pyface', 'traits', 'traitsui'] [tool.setuptools.packages.find] include = ['envisage*'] [tool.setuptools.package-data] 'envisage.examples' = ['demo/**'] 'envisage.tests' = [ 'bad_eggs/README.txt', 'bad_eggs/acme-bad/setup.py', 'bad_eggs/acme-bad/acme_bad/*.py', 'eggs/README.txt', 'eggs/acme-bar/setup.py', 'eggs/acme-bar/acme_bar/*.py', 'eggs/acme-baz/setup.py', 'eggs/acme-baz/acme_baz/*.py', 'eggs/acme-foo/setup.py', 'eggs/acme-foo/acme_foo/*.py', 'plugins/pear/*.py', 'plugins/banana/*.py', 'plugins/orange/*.py', 'preferences.ini', ] 'envisage.ui.tasks.tests' = ['data/*.pkl'] 'envisage.ui.workbench' = [ 'preferences.ini', 'images/*', ] 'envisage.ui.workbench.action' = [ 'images/*', ]