apptools-5.1.0/0000755000076500000240000000000013777643025014003 5ustar aayresstaff00000000000000apptools-5.1.0/PKG-INFO0000644000076500000240000001062713777643025015106 0ustar aayresstaff00000000000000Metadata-Version: 2.1 Name: apptools Version: 5.1.0 Summary: application tools Home-page: https://docs.enthought.com/apptools Author: Enthought, Inc. Author-email: info@enthought.com Maintainer: ETS Developers Maintainer-email: enthought-dev@enthought.com License: BSD Download-URL: https://www.github.com/enthought/apptools Description: =========================== apptools: application tools =========================== .. image:: https://travis-ci.org/enthought/apptools.svg?branch=master :target: https://travis-ci.org/enthought/apptools :alt: Build status Documentation: http://docs.enthought.com/apptools Source Code: http://www.github.com/enthought/apptools The apptools project includes a set of packages that Enthought has found useful in creating a number of applications. They implement functionality that is commonly needed by many applications - **apptools.io**: Provides an abstraction for files and folders in a file system. - **apptools.logger**: Convenience functions for creating logging handlers - **apptools.naming**: Manages naming contexts, supporting non-string data types and scoped preferences - **apptools.persistence**: Supports pickling the state of a Python object to a dictionary, which can then be flexibly applied in restoring the state of the object. - **apptools.preferences**: Manages application preferences. - **apptools.selection**: Manages the communication between providers and listener of selected items in an application. - **apptools.scripting**: A framework for automatic recording of Python scripts. - **apptools.undo**: Supports undoing and scripting application commands. Prerequisites ------------- All packages in apptools require: * `traits `_ Certain sub-packages within apptools have their own specific dependencies, which are optional for apptools overall. The `apptools.preferences` package requires: * `configobj `_ The `apptools.io.h5` package requires: * `numpy `_ * `pandas `_ * `tables `_ The `apptools.persistence` package requires: * `numpy `_ Many of the packages provide optional user interfaces using Pyface and Traitsui. In additon, many of the packages are designed to work with the Envisage plug-in system, althought most can be used independently: * `envisage `_ * `pyface `_ * `traitsui `_ Installation ------------ To install with `apptools.preferences` dependencies:: $ pip install apptools[preferences] To install with `apptools.io.h5` dependencies:: $ pip install apptools[h5] To install with `apptools.persistence` dependencies:: $ pip install apptools[persistence] To install with additional test dependencies:: $ pip install apptools[test] Platform: Windows Platform: Linux Platform: Mac OS-X Platform: Unix Platform: Solaris Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Requires-Python: >=3.6 Description-Content-Type: text/x-rst Provides-Extra: test Provides-Extra: h5 Provides-Extra: persistence Provides-Extra: preferences apptools-5.1.0/image_LICENSE_CP.txt0000644000076500000240000004671513777642667017402 0ustar aayresstaff00000000000000License 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.apptools-5.1.0/MANIFEST.in0000644000076500000240000000064213777642667015556 0ustar aayresstaff00000000000000include README.rst include CHANGES.txt include LICENSE.txt include MANIFEST.in include TODO.txt include image_LICENSE.txt include image_LICENSE_CP.txt graft docs prune docs/build recursive-exclude docs *.pyc graft examples recursive-exclude examples *.pyc graft integrationtests recursive-exclude integrationtests *.pyc recursive-include apptools *.py recursive-include apptools *.ini recursive-include apptools *.png apptools-5.1.0/integrationtests/0000755000076500000240000000000013777643025017411 5ustar aayresstaff00000000000000apptools-5.1.0/integrationtests/persistence/0000755000076500000240000000000013777643025021735 5ustar aayresstaff00000000000000apptools-5.1.0/integrationtests/persistence/update2.py0000644000076500000240000000171513777642667023672 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Update class names from the immediately prior version only # to ensure that cycles are not possible from apptools.persistence.updater import Updater def update_project(self, state): print('updating to v2') metadata = state['metadata'] metadata['version'] = 2 metadata['updater'] = 22 return state class Update2(Updater): def __init__(self): self.refactorings = { ("__main__", "Foo1"): ("__main__", "Foo2"), } self.setstates = { ("cplab.project", "Project"): update_project } apptools-5.1.0/integrationtests/persistence/update3.py0000644000076500000240000000172013777642667023667 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Update class names from the immediately prior version only # to ensure that cycles are not possible from apptools.persistence.updater import Updater def update_project(self, state): print('updating to v3') metadata = state['metadata'] metadata['version'] = 3 metadata['finished'] = True return state class Update3(Updater): def __init__(self): self.refactorings = { ("__main__", "Foo1"): ("__main__", "Foo2"), } self.setstates = { ("cplab.project", "Project"): update_project } apptools-5.1.0/integrationtests/persistence/test_persistence.py0000644000076500000240000000476013777642667025714 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Foo0: """ The original class written with no expectation of being upgraded """ def __init__(self): self.prenom = 'didier' self.surnom = 'enfant' class Foo1: """ Now to handle both Foo v0 and Foo v1 we need to add more code ...""" def __init__(self, firstname, lastname): """ This does not get called when the class is unpickled.""" self.firstname = firstname self.lastname = lastname class Foo: def __str__(self): result = ['----------------------------------------------------------'] keys = dir(self) for key in keys: result.append('%s ---> %s' % (key, getattr(self, key))) result.append('------------------------------------------------------') return '\n'.join(result) def __setstate__(self, state): print('calling setstate on the real Foo') state['set'] = True self.__dict__.update(state) def save(fname, str): f = open(fname, 'w') f.write(str) f.close() if __name__ == '__main__': # Create dummy test data ....... import pickle obj = Foo0() print(obj) t0 = pickle.dumps(obj) save('foo0.txt', t0) '''obj = Foo1('duncan', 'child') t1 = pickle.dumps(obj).replace('Foo1', 'Foo') save('foo1.txt', t1) obj = Foo2('duncan child') t2 = pickle.dumps(obj).replace('Foo2', 'Foo') save('foo2.txt', t2) obj = Foo3('duncan child') t3 = pickle.dumps(obj).replace('Foo3', 'Foo') save('foo3.txt', t3) ''' print('==================================================================') from apptools.persistence.versioned_unpickler import VersionedUnpickler # Try and read them back in ... f = open('foo0.txt') import sys rev = 1 __import__('integrationtests.persistence.update%d' % rev) mod = sys.modules['integrationtests.persistence.update%d' % rev] klass = getattr(mod, 'Update%d' % rev) updater = klass() print('%s %s' % (rev, updater)) p = VersionedUnpickler(f, updater).load() print(p) print('Restored version %s %s' % (p.lastname, p.firstname)) apptools-5.1.0/integrationtests/persistence/update1.py0000644000076500000240000000241313777642667023665 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Update class names from the immediately prior version only # to ensure that cycles are not possible from apptools.persistence.updater import Updater def cleanup_foo(self, state): print('cleaning up Foo0') state['firstname'] = state['prenom'] state['lastname'] = state['surnom'] del state['prenom'] del state['surnom'] '''for key in state: print('%s state ---> %s' % (key, state[key])) ''' self.__dict__.update(state) def update_project(self, state): print('updating to v1') metadata = state['metadata'] metadata['version'] = 1 metadata['diesel'] = 'E300TD' return state class Update1(Updater): def __init__(self): self.refactorings = { ("__main__", "Foo0"): ("__main__", "Foo"), } self.setstates = { ("cplab.project", "Project"): update_project } apptools-5.1.0/docs/0000755000076500000240000000000013777643025014733 5ustar aayresstaff00000000000000apptools-5.1.0/docs/releases/0000755000076500000240000000000013777643025016536 5ustar aayresstaff00000000000000apptools-5.1.0/docs/releases/README.rst0000644000076500000240000000201613777642667020237 0ustar aayresstaff00000000000000The `upcoming` directory contains news fragments that will be added to the changelog for the NEXT release. Changes that are not of interest to the end-user can skip adding news fragment. Add a news fragment ------------------- Create a new file with a name like ``..rst``, where ```` is a pull request number, and ```` is one of: - ``feature``: New feature - ``bugfix``: Bug fixes - ``deprecation``: Deprecations of public API - ``removal``: Removal of public API - ``doc``: Documentation changes - ``test``: Changes to test suite ('end users' are distribution packagers) - ``build``: Build system changes that affect how the distribution is installed Then write a short sentence in the file that describes the changes for the end users, e.g. in ``123.removal.rst``:: Remove package xyz. Alternatively, use the following command, run from the project root directory and answer the questions:: python etstool.py changelog create (This command requires ``click`` in the environment.) apptools-5.1.0/docs/Makefile0000644000076500000240000000607613777642667016417 0ustar aayresstaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/apptools.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/apptools.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." apptools-5.1.0/docs/source/0000755000076500000240000000000013777643025016233 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/index.rst0000644000076500000240000000042413777642667020107 0ustar aayresstaff00000000000000AppTools Documentation ======================= .. toctree:: :maxdepth: 2 :glob: preferences/* scripting/* undo/* selection/* naming/* io/* API Documentation ----------------- .. toctree:: :maxdepth: 1 api/apptools * :ref:`search` apptools-5.1.0/docs/source/preferences/0000755000076500000240000000000013777643025020534 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/preferences/PreferencesInEnvisage.rst0000644000076500000240000000327113777642667025516 0ustar aayresstaff00000000000000.. _preferences-in-envisage: Preferences in Envisage ======================= This section discusses how an Envisage application uses the preferences mechanism. Envisage tries not to dictate too much, and so this describes the default behaviour, but you are free to override it as desired. Envisage uses the default implementation of the |ScopedPreferences| class which is made available via the application's 'preferences' trait:: >>> application = Application(id='myapplication') >>> application.preferences.set('acme.ui.bgcolor', 'yellow') >>> application.preferences.get('acme.ui.bgcolor') 'yellow' Hence, you use the Envisage preferences just like you would any other scoped preferences. It also registers itself as the default preferences node used by the |PreferencesHelper| class. Hence you don't need to provide a preferences node explicitly to your helper:: >>> helper = SplashScreenPreferences() >>> helper.bgcolor 'blue' >>> helper.width 100 >>> helper.ratio 1.0 >>> helper.visible True The only extra thing that Envisage does for you is to provide an extension point that allows you to contribute any number of '.ini' files that are loaded into the default scope when the application is started. e.g. To contribute a preference file for my plugin I might use:: class MyPlugin(Plugin): ... @contributes_to('envisage.preferences') def get_preferences(self, application): return ['pkgfile://mypackage:preferences.ini'] .. # substitutions .. |PreferencesHelper| replace:: :class:`~apptools.preferences.preferences_helper.PreferencesHelper` .. |ScopedPreferences| replace:: :class:`~apptools.preferences.scoped_preferences.ScopedPreferences` apptools-5.1.0/docs/source/preferences/Preferences.rst0000644000076500000240000002423613777642667023551 0ustar aayresstaff00000000000000Preferences =========== The preferences package provides a simple API for managing application preferences. The classes in the package are implemented using a layered approach where the lowest layer provides access to the raw preferences mechanism and each layer on top providing more convenient ways to get and set preference values. The Basic Preferences Mechanism ------------------------------- Lets start by taking a look at the lowest layer which consists of the |IPreferences| interface and its default implementation in the |Preferences| class. This layer implements the basic preferences system which is a hierarchical arrangement of preferences 'nodes' (where each node is simply an object that implements the |IPreferences| interface). Nodes in the hierarchy can contain preference settings and/or child nodes. This layer also provides a default way to read and write preferences from the filesystem using the excellent `ConfigObj`_ package. This all sounds a bit complicated but, believe me, it isn't! To prove it (hopefully) lets look at an example. Say I have the following preferences in a file 'example.ini':: [acme.ui] bgcolor = blue width = 50 ratio = 1.0 visible = True [acme.ui.splash_screen] image = splash fgcolor = red I can create a preferences hierarchy from this file by:: >>> from apptools.preferences.api import Preferences >>> preferences = Preferences(filename='example.ini') >>> preferences.dump() Node() {} Node(acme) {} Node(ui) {'bgcolor': 'blue', 'width': '50', 'ratio': '1.0', 'visible': 'True'} Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'} The 'dump' method (useful for debugging etc) simply 'pretty prints' a preferences hierarchy. The dictionary next to each node contains the node's actual preferences. In this case, the root node (the node with no name) is the preferences object that we created. This node now has one child node 'acme', which contains no preferences. The 'acme' node has one child, 'ui', which contains some preferences (e.g. 'bgcolor') and also a child node 'splash_screen' which also contains preferences (e.g. 'image'). To look up a preference we use:: >>> preferences.get('acme.ui.bgcolor') 'blue' If no such preferences exists then, by default, None is returned:: >>> preferences.get('acme.ui.bogus') is None True You can also specify an explicit default value:: >>> preferences.get('acme.ui.bogus', 'fred') 'fred' To set a preference we use:: >>> preferences.set('acme.ui.bgcolor', 'red') >>> preferences.get('acme.ui.bgcolor') 'red' And to make sure the preferences are saved back to disk:: >>> preferences.flush() To add a new preference value we simply set it:: >>> preferences.set('acme.ui.fgcolor', 'black') >>> preferences.get('acme.ui.fgcolor') 'black' Any missing nodes in a call to 'set' are created automatically, hence:: >>> preferences.set('acme.ui.button.fgcolor', 'white') >>> preferences.get('acme.ui.button.fgcolor') 'white' Preferences can also be 'inherited'. e.g. Notice that the 'splash_screen' node does not contain a 'bgcolor' preference, and hence:: >>> preferences.get('acme.ui.splash_screen.bgcolor') is None True But if we allow the 'inheritance' of preference values then:: >>> preferences.get('acme.ui.splash_screen.bgcolor', inherit=True) 'red' By using 'inheritance' here the preferences system will try the following preferences:: 'acme.ui.splash_screen.bgcolor' 'acme.ui.bgcolor' 'acme.bgcolor' 'bgcolor' Strings, Glorious Strings ~~~~~~~~~~~~~~~~~~~~~~~~~ At this point it is worth mentioning that preferences are *always* stored and returned as strings. This is because of the limitations of the traditional '.ini' file format i.e. they don't contain any type information! Now before you start panicking, this doesn't mean that all of your preferences have to be strings! Currently the preferences system allows, strings(!), booleans, ints, longs, floats and complex numbers. When you store a non-string value it gets converted to a string for you, but you *always* get a string back:: >>> preferences.get('acme.ui.width') '50' >>> preferences.set('acme.ui.width', 100) >>> preferences.get('acme.ui.width') '100' >>> preferences.get('acme.ui.visible') 'True' >>> preferences.set('acme.ui.visible', False) >>> preferences.get('acme.ui.visible') 'False' This is obviously not terribly convenient, and so the following section discusses how we associate type information with our preferences to make getting and setting them more natural. Preferences and Types --------------------- As mentioned previously, we would like to be able to get and set non-string preferences in a more convenient way. This is where the |PreferencesHelper| class comes in. Let's take another look at 'example.ini':: [acme.ui] bgcolor = blue width = 50 ratio = 1.0 visible = True [acme.ui.splash_screen] image = splash fgcolor = red Say, I am interested in the preferences in the 'acme.ui' section. I can use a preferences helper as follows:: from apptools.preferences.api import PreferencesHelper class SplashScreenPreferences(PreferencesHelper): """ A preferences helper for the splash screen. """ preferences_path = 'acme.ui' bgcolor = Str width = Int ratio = Float visible = Bool >>> preferences = Preferences(filename='example.ini') >>> helper = SplashScreenPreferences(preferences=preferences) >>> helper.bgcolor 'blue' >>> helper.width 50 >>> helper.ratio 1.0 >>> helper.visible True And, obviously, I can set the value of the preferences via the helper too:: >>> helper.ratio = 0.5 And if you want to prove to yourself it really did set the preference:: >>> preferences.get('acme.ui.ratio') '0.5' Using a preferences helper you also get notified via the usual trait mechanism when the preferences are changed (either via the helper or via the preferences node directly:: def listener(obj, trait_name, old, new): print(trait_name, old, new) >>> helper.on_trait_change(listener) >>> helper.ratio = 0.75 ratio 0.5 0.75 >>> preferences.set('acme.ui.ratio', 0.33) ratio 0.75 0.33 Scoped Preferences ------------------ In many applications the idea of preferences scopes is useful. In a scoped system, an actual preference value can be stored in any scope and when a call is made to the 'get' method the scopes are searched in order of precedence. The default implementation (in the |ScopedPreferences| class) provides two scopes by default: 1) The application scope This scope stores itself in the 'ETSConfig.application_home' directory. This scope is generally used when *setting* any user preferences. 2) The default scope This scope is transient (i.e. it does not store itself anywhere). This scope is generally used to load any predefined default values into the preferences system. If you are happy with the default arrangement, then using the scoped preferences is just like using the plain old non-scoped version:: >>> from apptools.preferences.api import ScopedPreferences >>> preferences = ScopedPreferences(filename='example.ini') >>> preferences.load('example.ini') >>> preferences.dump() Node() {} Node(application) {} Node(acme) {} Node(ui) {'bgcolor': 'blue', 'width': '50', 'ratio': '1.0', 'visible': 'True'} Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'} Node(default) {} Here you can see that the root node now has a child node representing each scope. When we are getting and setting preferences using scopes we generally want the following behaviour: a) When we get a preference we want to look it up in each scope in order. The first scope that contains a value 'wins'. b) When we set a preference, we want to set it in the first scope. By default this means that when we set a preference it will be set in the application scope. This is exactly what we want as the application scope is the scope that is persistent. So usually, we just use the scoped preferences as before:: >>> preferences.get('acme.ui.bgcolor') 'blue' >>> preferences.set('acme.ui.bgcolor', 'red') >>> preferences.dump() Node() {} Node(application) {} Node(acme) {} Node(ui) {'bgcolor': 'red', 'width': '50', 'ratio': '1.0', 'visible': 'True'} Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'} Node(default) {} And, conveniently, preference helpers work just the same with scoped preferences too:: >>> helper = SplashScreenPreferences(preferences=preferences) >>> helper.bgcolor 'red' >>> helper.width 50 >>> helper.ratio 1.0 >>> helper.visible True Accessing a particular scope ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Should you care about getting or setting a preference in a particular scope then you use the following syntax:: >>> preferences.set('default/acme.ui.bgcolor', 'red') >>> preferences.get('default/acme.ui.bgcolor') 'red' >>> preferences.dump() Node() {} Node(application) {} Node(acme) {} Node(ui) {'bgcolor': 'red', 'width': '50', 'ratio': '1.0', 'visible': 'True'} Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'} Node(default) {} Node(acme) {} Node(ui) {'bgcolor': 'red'} You can also get hold of a scope via:: >>> default = preferences.get_scope('default') And then perform any of the usual operations on it. Further Reading --------------- So that's a quick tour around the basic usage of the preferences API. For more information about what is provided take a look at the API documentation - :mod:`apptools.preferences`. If you are using Envisage to build your applications then you might also be interested in the |Preferences in Envisage| section. .. external links .. _ConfigObj: https://configobj.readthedocs.io/en/latest .. # substitutions .. |ScopedPreferences| replace:: :class:`~apptools.preferences.scoped_preferences.ScopedPreferences` .. |IPreferences| replace:: :class:`~apptools.preferences.i_preferences.IPreferences` .. |Preferences| replace:: :class:`~apptools.preferences.preferences.Preferences` .. |PreferencesHelper| replace:: :class:`~apptools.preferences.preferences_helper.PreferencesHelper` .. |Preferences in Envisage| replace:: :ref:`preferences-in-envisage` apptools-5.1.0/docs/source/scripting/0000755000076500000240000000000013777643025020235 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/scripting/introduction.rst0000644000076500000240000001473313777642667023533 0ustar aayresstaff00000000000000.. _automatic-script-recording: Automatic script recording =========================== This package provides a very handy and powerful Python script recording facility. This can be used to: - record all actions performed on a traits based UI into a *human readable*, Python script that should be able to recreate your UI actions. - easily learn the scripting API of an application. This package is not just a toy framework and is powerful enough to provide full script recording to the Mayavi_ application. Mayavi is a powerful 3D visualization tool that is part of ETS_. .. _Mayavi: https://docs.enthought.com/mayavi/mayavi/ .. _ETS: https://docs.enthought.com/ets/ .. _scripting-api: The scripting API ------------------ The scripting API primarily allows you to record UI actions for objects that have Traits. Technically the framework listens to all trait changes so will work outside a UI. We do not document the full API here, the best place to look for that is the ``apptools.scripting.recorder`` module which is reasonably well documented. We provide a high level overview of the library. The quickest way to get started is to look at a small example. .. _scripting-api-example: A tour by example ~~~~~~~~~~~~~~~~~~~ The following example is taken from the test suite. Consider a set of simple objects organized in a hierarchy:: from traits.api import (HasTraits, Float, Instance, Str, List, Bool, HasStrictTraits, Tuple, PrefixMap, Range, Trait) from apptools.scripting.api import (Recorder, recordable, set_recorder) class Property(HasStrictTraits): color = Tuple(Range(0.0, 1.0), Range(0.0, 1.0), Range(0.0, 1.0)) opacity = Range(0.0, 1.0, 1.0) representation = PrefixMap( {"surface": 2, "wireframe": 1, "points": 0}, default_value="surface" ) class Toy(HasTraits): color = Str type = Str # Note the use of the trait metadata to ignore this trait. ignore = Bool(False, record=False) class Child(HasTraits): name = Str('child') age = Float(10.0) # The recorder walks through sub-instances if they are marked # with record=True property = Instance(Property, (), record=True) toy = Instance(Toy, record=True) friends = List(Str) # The decorator records the method. @recordable def grow(self, x): """Increase age by x years.""" self.age += x class Parent(HasTraits): children = List(Child, record=True) recorder = Instance(Recorder, record=False) Using these simple classes we first create a simple object hierarchy as follows:: p = Parent() c = Child() t = Toy() c.toy = t p.children.append(c) Given this hierarchy, we'd like to be able to record a script. To do this we setup the recording infrastructure:: from mayavi.core.recorder import Recorder, set_recorder # Create a recorder. r = Recorder() # Set the global recorder so the decorator works. set_recorder(r) r.register(p) r.recording = True The key method here is the ``r.register(p)`` call above. It looks at the traits of ``p`` and finds all traits and nested objects that specify a ``record=True`` in their trait metadata (all methods starting and ending with ``_`` are ignored). All sub-objects are in turn registered with the recorder and so on. Callbacks are attached to traits changes and these are wired up to produce readable and executable code. The ``set_recorder(r)`` call is also very important and sets the global recorder so the framework listens to any functions that are decorated with the ``recordable`` decorator. Now lets test this out like so:: # The following will be recorded. c.name = 'Shiva' c.property.representation = 'w' c.property.opacity = 0.4 c.grow(1) To see what's been recorded do this:: print(r.script) This prints:: child = parent.children[0] child.name = 'Shiva' child.property.representation = 'wireframe' child.property.opacity = 0.40000000000000002 child.grow(1) The recorder internally maintains a mapping between objects and unique names for each object. It also stores the information about the location of a particular object in the object hierarchy. For example, the path to the ``Toy`` instance in the hierarchy above is ``parent.children[0].toy``. Since scripting with lists this way can be tedious, the recorder first instantiates the ``child``:: child = parent.children[0] Subsequent lines use the ``child`` attribute. The recorder always tries to instantiate the object referred to using its path information in this manner. To record a function or method call one must simply decorate the function/method with the ``recordable`` decorator. Nested recordable functions are not recorded and trait changes are also not recorded if done inside a recordable function. .. note:: 1. It is very important to note that the global recorder must be set via the ``set_recorder`` method. The ``recordable`` decorator relies on this being set to work. 2. The ``recordable`` decorator will work with plain Python classes and with functions too. To stop recording do this:: r.unregister(p) r.recording = False The ``r.unregister(p)`` reverses the ``r.register(p)`` call and unregisters all nested objects as well. .. _recorder-advanced-uses: Advanced use cases ~~~~~~~~~~~~~~~~~~~~ Here are a few advanced use cases. - The API also provides a ``RecorderWithUI`` class that provides a simple user interface that prints the recorded script and allows the user to save the script. - Sometimes it is not enough to just record trait changes, one may want to pass an arbitrary string or command when recording is occurring. To allow for this, if one defines a ``recorder`` trait on the object, it is set to the current recorder. One can then use this recorder to do whatever one wants. This is very convenient. - To ignore specific traits one must specify either a ``record=False`` metadata to the trait definition or specify a list of strings to the ``register`` method in the ``ignore`` keyword argument. - If you want to use a specific name for an object on the script you can pass the ``script_id`` parameter to the register function. For more details on the recorder itself we suggest reading the module source code. It is fairly well documented and with the above background should be enough to get you going. apptools-5.1.0/docs/source/conf.py0000644000076500000240000000727713777642667017562 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # -*- coding: utf-8 -*- # # EnvisageCore documentation build configuration file, created by # sphinx-quickstart on Fri Jul 18 17:09:28 2008. # # This file is execfile()d with the current directory set to its containing dir # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed # automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import apptools import enthought_sphinx_theme # 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.githubpages', 'sphinx.ext.napoleon', 'sphinx.ext.viewcode', 'traits.util.trait_documenter', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = 'apptools' copyright = '2008-2021, Enthought' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. version = release = apptools.__version__ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # Options for autodoc # ------------------- # Apptools offers an envisage plugin that requires importing from # envisage. try: import envisage # noqa except ImportError: autodoc_mock_imports = [ 'envisage', ] # 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 = "Apptools Documentation" # 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 # If false, no module index is generated. html_use_modindex = False # Output file base name for HTML help builder. htmlhelp_basename = 'AppToolsdoc' # Options for LaTeX output # ------------------------ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class # [howto/manual]). latex_documents = [ ( 'index', 'AppTools.tex', 'AppTools Documentation', 'Enthought, Inc.', 'manual' ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. latex_logo = "e-logo-rev.png" apptools-5.1.0/docs/source/io/0000755000076500000240000000000013777643025016642 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/io/introduction.rst0000644000076500000240000000142013777642667022125 0ustar aayresstaff00000000000000File I/O ======== The :mod:`apptools.io` package provides a traited |File| object provides properties and methods for common file path manipulation operations. Much of this functionality was implemented before Python 3 `pathlib`_ standard library became available to provide similar support. For new code we encourage users to investigate if `pathlib`_ can satisfy their use cases before they turn to the `apptools.io` |File| object HDF5 File Support ----------------- The :mod:`apptools.io.h5` sub-package provides a wrapper around `PyTables`_ with a dictionary-style mapping. .. external links .. _pathlib: https://docs.python.org/3/library/pathlib.html .. _PyTables: https://www.pytables.org/ .. # substitutions .. |File| replace:: :class:`~apptools.io.file.File` apptools-5.1.0/docs/source/_static/0000755000076500000240000000000013777643025017661 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/_static/e-logo-rev.png0000644000076500000240000000751113777642667022362 0ustar aayresstaff00000000000000‰PNG  IHDRoi»þê*sRGB®Îé pHYs  šœtIMEØ“ïLÛIDATxÚí]ktTÕÙ>ûzÎ\$Üm ¬€ ‰È¥!TDŠˆ j‘V|_CèWp­Š¡hµ ¢PµTº´•›\"ÍÍBÉ$$”6a’IfÎ9ûûqfB¸%™™=“ ýƒµ²Bæìyæ}ÞÛÞÏ; vØo„Ðâ´`‚š!4Ch†VÍv[¸ƒì“1¦2¦ Œ€!ÛôH¥G”iÝêygò×þöÅ):QVd»ª8Sƒm«¨sQAŒ£j2àô§’ß~ýÙ~ÑÝAH3ûÑqÕÕUÅÿ:€÷?!4[a6SRö—u ÇŽÔü7¢H'Ž1"¡_Qqéåÿâ!šŒ©JbBôæwÍx$IÉ-ÿSï^QO>þAòO~ߨ( ‚ ÚÛHAPU–Œ)=»…­úý“£û·ñOjjm+þðþöÇ– ¤ v$~°Ø¦æ"3¦¼ñê3½zviûJnâ[­*.\vÛ(ü/E“1U`ÊÜYcÖ¿¾ í&y3ñg¦>Ð=Êx$÷;G£S@»¿]ÑdŒ1åÁ±ñYûüÓoç"Û¾Å÷ýX²½¾6ÿÛÓn8ñÛÍo2¦ ˆí¶,cú¨ÄÜ_¼¸äÜŠU?y" "ºcÑdL5HfFjÚÔÑ~}ÐÖÏö½œýQ½]€HÀ™”錩ŒÉ/¤OxcÕü¡ƒûùûq'~ lÓ•_´ Å£Íe•W\ÌÈ|»ñ¡ €Š&cLM½hÁd¸È¶¯î9¶òÕ-Õ—ê $þ ¾™Î˜Ú³›9ó¥iË—>x“¼aÅôë•6í>‚Ô#¹E‚ ¹ß_h2¦šŒ$}î¸ì•¿àâ"çä®ßða\lß°0³×/"‰ôÞ‘ƒÒ¦Ž.*:UQyÙ•éÀ‹ø~`:oYf­xuõú?ù b)<Ìœ>ÆÒ—æ'ñù¢ér‘Y‹gÄèãûËÕÔÔ¾µá¯6n««g˜šÑ€˜êìeøSö‹£ïMðõõkm›>ØñÞæýuv" òSnhj ‹ÌŒé’‡ryÁ_îÍüÝšÊêLM˜š01@$™ª(²]vÚÆüòòçúôîî{Ä_ñÊ{#¬ƒˆ¼&>4µl|îcýì.8~ÿÛåÙÇrK05bjÂÔˆ°Þm;€1•©²¢4*Î:½¤ÎŸ3yÉKó|è‘ÜÂÅËÞ©ºT\©> 8šn™µd¦Ù¤çBíÿ]¶jë§»]8#&ú[ù5ÆTEUŠl—uÝ#u\ˆ/Âkþø½¿í¯kð’ø^Çô6õt=Zo­ÿ`ÎÓ‹¿-,£R'*EP1#¼EQ€ ˆDÔfsnýlOYYYÒ¨!’$ú²‡{Gš2y„ÕZ~æ\p·õÛN|olÓ‹žn«ÙϯýîBu-Ñ\$5¶½õ˘ÂTY‘gNR^\8óÙgåFü‹õ{@|Ñ4Å…¿œ0ov2­ÖŠç^È:j)ÆÔD¨"z©‡M øEn¶þ1]Wf¥Nâ@üwßÿǛ뿨k ÛB|™ÞÐÐð«ùcÂÃÂ!>ºÈ×ßÜ8çéÿ©ºT'J©Ô‰ˆaZà†xØèubˆèOWê¶~º»¬Ìê;ñ‡ÝÓöcÉùù%ª®ÀÖÐdLõ<½bj™µ|ïׇΜýÁë]nÞ²}ðð”5olu]%}Õu!b'ìÜ^f'BDÑ1Œêºüý˼÷Í-,>í£ynØ¸í»¢ïÝÇ÷¬ed°çNSÁét~WX\f-–pO¸'¥ÞáœÜÌå,9UŽ©I2˜1`¬ÓxÄ¥d  ,Œ»úåù½{Eúäͽ¢êR•"¨¤Z GÌ 4›¯ÿÔÔîûúPL¿èøq­¿©@ÄÔ$é#15! G®}\.-ý2kEfVö®=G15Q©¡&­phõáî!9ûCeÕÅá CºvéÜj(ê# 5#¢GXò:In¡ˆÈÌHó±¥ÿJöZm«®œÚžÏó¹ÕU__(çhî݆' ¹ÁH›ˆ¢Á„‰ûÁ7cªÉHçκÞì})"´D­òb-¦&Ñ`ÆÄài­ÉóŽ\eUõ®Ý?ň‹éí*³²YJ05ŠúÈë DÈË ¹ô«šS[ÔGabDÄ›­r¾q¨E§ÊªêÃ9–Õ¯m"4LÔwÅÔt›ÑG‹Túúدr{¡OêìÞPÛ¿hjëò?:}•"ˆްžïi ¯bì:/¤7yAí@ ©eKˆÖ#$òt‘’™13mj’ï5Ø1K ¦&Ž^Èw‹€ !æ%¯PSSSûÖ†³×lr{sž^¨#ÜÔæw4ÒDmQI¨ S#ß3aì@ò57Q; S#Â:į v4y…¿R» É+Ô€ÚA&¯P0j/šŒ)©)Ã-ø¹¡&ÔZ4ÙS³Æ.[<£cQ;XÑdÂ{›÷^¾ô±ñ}; µoXA£e‚`n^éÄÔ¥ËW®«©¹êµ_É^7$ñKþ9Q)ê#E]¡&„u€SíÐÑÐa‰Póû[Ž;ï£m;ÛHíû’[óÆfQ)é£D}W"†#b€ˆ^™D:K „;5:õK²6¦>žÑÂÁŽÕZ‘2õ©¹Ïd^üÑ!j8J01ºØ-´ƒj(ÈT«ˆ!–05Q)"¯ jâÔŒå+ÖÞ@üà¡vp£Ùd¤ˆ"¢'4LÔuyÿ£C͉¿cç¾à¡v‡©,@A1„´Ñi_’µqÛöýLq|s4b©½¢vGîz†ˆ!¢ùUªâ Ý!¢KIÈ裸‹øQD”1E3[÷¹1ª­v”I $0¦ýœ›ì0h6øFŒûÂLëÒâÐÌ™–€dªœš’°nõuËž?SScw\L÷G¨aʃcf-Nó]yç/4u:‘© c c P°¥ÙM1›¯8Ù_h>4~´ÁhÞ½¿Äf· ‚l% &N^˜>‰ïü?2=iÔÝC‡ÄÈ)Î9vÞ!Ëœïújæ>‘ìû±h ý¦N'N~(aݘ¯öä}WÕΠx+ïÚ' uŽ0?1sLâÐ _í/>_nƒH Ø!â ÙxöÊ9½zøqè@àbzlLÏØ˜ž¹ÇOíþú_µ6ôAêc6~çt=F&ö™Øמüœcç2ò\¶æ±‹ärÇh‚3ç.Æôòå©“Jx`ÌÀ¿ÿãx^_œ©–¿>‘_¨mqKžk€™jÉ+-++ÿYŸ.z½äõîÁwì3°TUõå+ÿ¶q±áܹ~Mú„ä¡\ÄÉ_î:pð›BÍI¶äš€" Dôß5 ûæ«Jc¯ž‘„øTÅêubdãÐ!ƒ¸PûÅ%+³~ÿfõ¥z*EP]½6Rû2;Ú/uzÓ] ÂâÞg,'ÎNdÔ=Câ„ö^n1¿ê8ö´ü8‚D,ïd©ÕZ}W”/Ñ)~€÷ŸÇŽ/÷¦Íz~÷¾ã*0P]g*…Ý[{ÐÏs‹]ÄÇb„è¥ëŽæ76ÖÇÅö $š…ß?½`éÚõ[í˜JTìDD“ËKÂŽ¦ t ‰"Û÷(µœ(}òñqq±wùûÑZøÎ¦O11ІÈfƒéüÒZ XSXk71E›½~í;{âûGþbö½NòÓ#Ý"kæ"ý« è„r@×4½Ë—m‡rN"¨öîÁ—é‡sr§L›ÿùΌڀ?íVh¿?ÝÝnõ²ãêÄñw'?0lÛ¶¯Žä`bÀÔìõ´·ÿF4]2UUœªÒ ;lŠlgL bðkx'øÍÛ„§&µ%QU#c*€Bð{awš×E|ÈDíǶ¡Bh¶ìZ±–-„fÍš¡åÙúߣÚ/Ùw¿IEND®B`‚apptools-5.1.0/docs/source/_static/et.ico0000644000076500000240000002362613777642667021011 0ustar aayresstaff00000000000000(f èŽ00hvhÞ ¨F00¨î( r5(H73iKD]ZZzol‘ˆ…ž—•¤£¢µ±¯¿½¼ÇÅÅÕÔÔàßßìììùùùìs$jÞì¤V4{Þí¶H¦5ŒÚF™g@H•™gÎ @'ˆ„îè 9ˆc®ìªÎå]ˆsžÕE{Þàˆc@EŒàˆc{bF­‰QE«Pk„G–\Ø 1¾Ö®Ä}à”màðÀ€Àðü( @_-!p3%7)&[B<]VU~okŠ„‚˜”’§¥¤´°®»¸¶ÄÃÃÓÒÒæåå÷÷÷í¥W½àíÆU#XÍàØWµ$iÍàÛUª¥$zÞàìuŠ™¥5{Þí•j™™µ6Œîí¶Y©™ªv1G¬àØW©™šwÞ×"XÎ fª™š‡ÎÅ$¾š™š—¾à´®©™¦àƒ®™˜Ià q¾™§Jà QΙ—JàîÞàë[î™§JàÚ‰Îàî™—JàÜc4jÞ™§JàÄ5{Þ™—Jà 1Fœî™—Jà qG­à™§IÞ Q#WÍà™§6œî Q$jÎઔ#X½àêA${à¦#hÍàç1IÐQ$jÎîÈ1<à s4{ÉAÎ a5S®à Q}àêA\àç1JîÖ7ÞÅΕ¾àÿðÿÿÀÿÿ€þøðÀ€€À€ð€ø€þ€ÿ€€|À€xó€pÿ€`ÿ€`?€`€8€€€À€€àøüÿÿÿÀÿÿðÿÿøÿ(0`W,"4(%p3%v>1IGF~QFja_~wtމ‡ž›š³¯¬¿¼ºÔÔÔêêêùùùí¹›ÎàíÉdF›ÝàÚwµG¬Þà܆›µ h¼Þ즋ª¥" i¼îí·jºª¥""!z½îíÈiºªªµ"""!ŠÍàÚgºªªª¥""""‹ÎàÛ†«ªªªªµ"""""F›Ýàì–‹ªªªªº†""""""G¬Þàí§zºªªª«™Þ¥""""""H¬ÞÈiºªªª«˜Î “""""" iÎàëhºªªª«¨¾à s""""" œà¸«ªªªª¨­à c"""""!Œà›ªªªª¹à R""""!|à›ªªª¹Œàé2"""!|à›ªªª{îØ2""!|à›ªª”­Ç2"1Œà›ªª„½µ"!Œà›ª«„­£5­›ªª„®îà ˆÞ›ª«„­ìÌÞ›ªª„®í¸y¼î›ª«„­íÈ@yÍª„½Ú`"!ŠÎà›ª«„­“"""‹Íà›ªª„­ 2""""G¬Þà›ª«„½ s"""""H¬Þ›ªª„­ b"""" i½Þ›ª«„­à R""""!yÍª„ŒÞéR""""!ŠÍà›ª«qh¼îØ2""""›Þà›ª§0iÌîÇ2""""GœÞ›©R"!zÍà¶"""""hΛr"""ŠÎàìr"""" Ε"""""F›ÝàíÈB""""#9ÞØ3"""""GœÞíÊB"""""6¾Ç2"""""h¬Ëp"""""%à¶""""" ht"""""#|à¥"""""!"""""kà ƒ"""""""""""YÞ s"""""""""8Î b"""""""&¾êR"""""%àØ2"""#|àÇ2"#kà¶"YÞ©ÞÿÿðÿÿÿÿÀÿÿÿÿ€ÿÿþ?ÿÿøÿÿðÿÿÀÿÿ€þøðà€ÀàÀøÀþÀÿÿÀÿÿÀÀÿÿðÀÿÿüÀÿÿþÀþÿƒÀøÿïÀðÿÿÀÀÿÿÀ€ÿÀ€ÿÀ€ÿÀ€ÿÀàÿÀø?ÀüÀÿÀ?ÀÀðÀðÀÀÀðüþÿÿ€ÿÿàÿÿø?ÿÿüÿÿÿÿÿÿÿÀÿÿÿÿðÿÿÿÿø?ÿÿ( f-t1!j/#i1$n3$j2&r4&s4&s5&u5&]0'`1'^2'u5'v5'v6'x6';+(X2(_2(r4(u6(w7(x7(z7(y8(z8({8(P0)s7)z8){8)|9)€:*c5,i9-A1.R4.E3/W7/u=1x?2A97>;:{F:F?>{I>mH?DBAMDB~ODJHHOMLhSOQPPPQQSRRUTTŠaW\YXx_[…c[]]]^^^b_^___ddd‘pghhh‚mhkjijjjrksmnnnrpopppsssuttwwwyyy~{}}}“…‚Ÿ‰ƒ Š„………‹ˆ‡£ˆ‰‰‰œŽ‰ŒŠŠŒŒŠ‹‹¡‘¦“°˜“”””¯š”—••›˜•œ—–™–———˜˜˜žš˜Ÿ›™ žœžžž¡ Ÿ¡¡¡¢¢¢¨¤¢¹¨¤¦¦¦®§¦§§§¼¯¬­­­´°­µ±®²¯¯¶²¯´²±º¶²³³³µµ´µµµ½¹µ¶¶¶¹¸¶½¹¶½½¼ÅÀ½ÆÁ¾¿¿¿ÀÀÀÍÃÀÁÁÁÂÂÂÆÅÃÄÄÄÅÅÅÆÆÆÇÇÇÌÊÉÕËÉËËËÑÑÑÒÒÒÓÓÓÕÕÔÙÖÖ×××ÛÛÛÝÜÜãÝÜÞÞÞßßßáááãããäääæææçææçççéééììëìììïííîîîïïïòðïðððñññòòòôóòõõõööö÷÷÷øøøùùùúúúûúúûûûüüü°št@4Lhޤ´´¢‹O\b$8Pr”©·¸ª–aKxŒ_ *?VŸ®Nd†€cpI 0M}„Wƒ…fm¡µ’:1n{|wFv­¸³³¶u2!5ˆyziA‹²²ž“¢´¯US§yxj?‹±¥]GRr•ª·¸yxj>‡¦E-BY°yzk7l—`, %3Jg§~‚Z+Da‹˜T)9h™zC&6OoŠe" [ ¨q. *>;'#Q›´«X( <‘°œH/s¬¸‰=^£´ðÀ€Àðü( @m,6#>%D&K'O(R)P) Y+ q0!2%"`."8'#g1#4($l2%p3%q3%q4%0'&a0&r4&t5&u5&v5&e2'p4't4't5'u5'u6'v6'w6'x6'/)(r6(v6(x7(Y3)z8)v8*l6+2--m:-v;-w>1543Q93v@3lA7vC8;:9wF:><;N?<VA=?>>@??zK@AAAoJAFFF~SHbNIKKKnQKLLLrTMONNPNNZPQQQSRRVVUdXVXWWaW…aX[ZZwa\d_^‰e^i_```Šiabbbedceedhhh‹ph‘qh}mi’pijjjlkkƒplrlšwmoooqpoppp•xqwrsssyvuwwwyyy˜€{œ{}}}~~…~Ž„š…¤ˆ…‚‚ƒƒƒ¡Œ‡‰ˆˆŠ‰ŠŠŠŽŒŠ•ŽŽŽŽ“Ž¡“š‘—“—”‘ª—’“““•••——•—––›™—©š—š™™ž›™›››ŸŸŸ¦¢Ÿ±¢Ÿ¡¡¡°§£¤¤¤¦¥¤§¥¤«¦¤¬¨¥¦¦¦§§§©©©°«©²®«·®¬³¯¬­­­´°­®®®µ±®¶²¯°°°·³°¸³°²²²¸´²¹µ²Áµ²º¶³´´´·µ´º·´»·´¼¸µ¶¶¶º·¶¸¸¶»¸¶½¹¶¾º¶¸¸¸¿»¸Å»¹ººº»»»¼¼¼¾½½ËÀ½¾¾¾¿¿¿ÁÁ¿ÃÀÀÂÂÂÄÄÄÅÄÄÅÆÄÅÅÅÈÈÈÑÊÉÊÊÊÕÎÌÍÍÍÒÎÍÏÏÏÐÐÐÑÐÐÑÑÑÔÓÓÔÔÔÛÖÔÖÕÕÖÖÖÚ×Ö×××ÚÚÚÞÜÜÞÞÞãßÞàààáááâââæããäääéåäæææçççèèèëèèéééêêêìêêëëëíííîîîïïïðððòñðòòòóóóôôôöôôõõõööö÷÷÷øøøùøøùùùúùùúúúûúúûûûüüüýýýöà¶jj…¿ÞôüëÍybO =iÉæøóÙ™W‚Åa"Hu ÔìüøãÃih®ª²Z!.S¹ÚñþýíÒW•·¡›²Z% 9bŒÆâöõÝ«Xx·¥›ŸÀaBm˜ÐêüüéÉnc¢´Ÿª¯zq2 !*Nw°ÙóøÜV‹·¡¨®ˆŠÝúßr-% 3]Øøâwtµª››¡·Ž‡ÑõÓ\#%XÂ󘚴Ÿ¡´›z¼ðþ½F' D¶ï”®¢ªz™ëý÷:  E¶ï“¤¡–I£íåv- N¿ñ“¤¨}I»ñÙ`')uÕö“¤¨Hºðþðçëöþ¸YÄðþ“¤¨HºðõÞ¾—¦Òìüþþ“¤¨HºðíÍn65S|ºÜ󓤨HºðüÉJ' 9dÉæø“¤¨HºðÖ<%"DpžÕíý“¤¨Hºñès0!.S»Ý󓤨DžäüÛe(! ;gÉæø“¤³†9p°ÚñþÈQ("Gu ×íý“§²‘M8bŒÆâöù©> .U„Çí™­o0@m˜Îêüî~1% K ç•L  *Nw«Øïúï×’7%%?Ëóçƒ4%% 3X…¿Ð¶P& +_Ïõál,$ =g^/#A¬ïþÌT($ ! 1€äüû©>  )[Ñöò‰4 #C±ðþÛk,1{çüÊR(%+_ÔöþœfÁñþÿðÿÿÀÿÿ€þøðÀ€€À€ð€ø€þ€ÿ€€|À€xó€pÿ€`ÿ€`?€`€8€€€À€€àøüÿÿÿÀÿÿðÿÿøÿ(0`<5"=$H&/"S)m-*" -# X+ s0!b."o1"+%#h0#+%$l2$u3$p3%q3%q4%r4%+&&8)&V.&g2&r4&t4&t5&u5&.('O/'t5'u5'v5'u6'v6'q6(v7(y7(o7)}9),+*V2*g4*x9*:.,x;,/..70.C2.z=/211u=1l=2z@3666X?9:::}E:><<PB>~I>AAALAlKCƒODEEERFIIIŒUIƒUJKKK`OKMMM…ZPRRR\TS‡^TUUU€_WŠaWYYYs_Zt`[`^^__^Šg_a``bbaccceee‘ofhggiiillksknmmppp—xptsruuuƒxvxxxyyy}{y‡}z{{{œ‚|}}}€~}Š~€€€‚‚‚‡„‚ˆ„‚ƒƒƒ¡Š…†††Š‡‰‰‰£Ž‰ŒŠŒ‹‹‘ŽŒ’¦–‘“““•••š—•©š•œ™—˜˜˜š™˜›™˜š™™ œ™®žš››œ›¥›œœœžž žžžž¤ ž°¢Ÿ¥¡ ª¡ £££±¦£¦¦¦­©¦®«¨¶«¨©©©²­«¬¬¬³¯¬³°­´°­®®®µ±®¶±®¶²¯º²°·³°±±±½³±¸´±¶µ²¹µ²³³³º¶³½·³·¶´¸·´»·´µµµ¼¸µ½¹¶¾¹¶¾º¶···»¹·À¹·¼»¸À»¸¹¹¹¾¼¹Á½»¼¼¼¿¿¿Ç¿ÀÀÀÂÁÀÂÂÁÉÂÁÂÂÂÅÅÅÆÆÆÎÉÈÉÉÉËËËÐËËÌÌÌÎÎÎÐÐÐÕÑÐÒÒÒÔÔÔÙÖÖ×××ØØØÙÙÙÜÚÙÞÜÛÜÜÜÞÞÞàààåãâãããæääåååæææçççèççèèèéééëêêëëëììëìììíííîîîïïïðððòññòòòóóóôóóôôôõõõööö÷öö÷÷÷øøøùùùúúúûûûüüüýýýùçÈŽÀ×íúûïÕŽV)I¨æ‡¼ŸŸŸªzE¨ìûøùüãl~äø‡¼ŸŸŸªzE¨ìüðÜÓÚêøú‡¼ŸŸŸªzE¨ì÷á½tgÀÛïú‡¼ŸŸŸªzE¨ìûìÏ{=?b–Ìáôü‡¼ŸŸŸªzE¨ìúàžM+$0Lq¨Óêø‡¼ŸŸŸªzE¨ìð‹9!$ 8Y€ÁÜïú‡¼ŸŸŸªzE¨ì¦5$Cd–Ìâöü‡¼ŸŸŸªzE¨ìØc/$0Lt­Õëù‡¼ŸŸŸªzE¨ìÊW$$ :ZÃÜðû‡¼ŸŸŸªzE¢åû©G$!Ce˜Îäöü‡¼ŸŸŸª|?€Ïé÷õŠ>$$0Ov­Õëù‡¼ŸŸ¡¹o*R}ÀÚîúæu7$:ZÃÜðû‡¼Ÿ«Ÿc5 ?a‘ÉÞóûÔc&!Ce˜Ïéú‡¿°D $*JmžÐå÷ÆN$$0R€Óô¯\%$ 4Rx¸×ìùöÜf,! .eÏöF <]…ÇÜðúúéÌv=)6çûçu7!$Ee˜ÌâôúïÚ–M'#(TÄðÙc-#0Ot¨Ð×½a+ !A”ìûÆQ$# 8Yvh=$$6jÚøù—B$ 1!!%TÄóçy7!!!"A’ìûÔ`&$6jÚøÆQ&"%TÅóù—@$A’ìûèy7!$6jÚøÔ`&!%SÄóÆQ'$A”ìûû—•äùÿÿðÿÿÿÿÀÿÿÿÿ€ÿÿþ?ÿÿøÿÿðÿÿÀÿÿ€þøðà€ÀàÀøÀþÀÿÿÀÿÿÀÀÿÿðÀÿÿüÀÿÿþÀþÿƒÀøÿïÀðÿÿÀÀÿÿÀ€ÿÀ€ÿÀ€ÿÀ€ÿÀàÿÀø?ÀüÀÿÀ?ÀÀðÀðÀÀÀðüþÿÿ€ÿÿàÿÿø?ÿÿüÿÿÿÿÿÿÿÀÿÿÿÿðÿÿÿÿø?ÿÿapptools-5.1.0/docs/source/_static/default.css0000644000076500000240000003231313777642667022034 0ustar aayresstaff00000000000000/** * Sphinx Doc Design */ body { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; font-size: 100%; background-color: #333333; color: #000; margin: 0; padding: 0; } /* :::: LAYOUT :::: */ div.document { background-color: #24326e; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 230px; } div.body { background-color: white; padding: 0 20px 30px 20px; } div.sphinxsidebarwrapper { padding: 10px 5px 0 10px; } div.sphinxsidebar { float: left; width: 230px; margin-left: -100%; font-size: 90%; } p.logo { text-align: center; } div.clearer { clear: both; } div.footer { color: #fff; width: 100%; padding: 9px 0 9px 0; text-align: center; font-size: 75%; } div.footer a { color: #fff; text-decoration: underline; } div.related { background-color: #24326e; color: #fff; width: 100%; height: 30px; line-height: 30px; font-size: 90%; } div.related h3 { display: none; } div.related ul { margin: 0; padding: 0 0 0 10px; list-style: none; } div.related li { display: inline; } div.related li.right { float: right; margin-right: 5px; } div.related a { color: white; } /* ::: TOC :::: */ div.sphinxsidebar h3 { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; color: #acafb3; font-size: 1.4em; font-weight: normal; margin: 0; padding: 0; } div.sphinxsidebar h4 { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; color: #acafb3; font-size: 1.3em; font-weight: normal; margin: 5px 0 0 0; padding: 0; } div.sphinxsidebar p { color: white; } div.sphinxsidebar p.topless { margin: 5px 10px 10px 10px; } div.sphinxsidebar ul { margin: 10px; padding: 0; list-style: none; color: white; } div.sphinxsidebar ul ul, div.sphinxsidebar ul.want-points { margin-left: 20px; list-style: square; } div.sphinxsidebar ul ul { margin-top: 0; margin-bottom: 0; } div.sphinxsidebar a { color: #fff; } div.sphinxsidebar form { margin-top: 10px; } div.sphinxsidebar input { border: 1px solid #9bbde2; font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; font-size: 1em; } /* :::: MODULE CLOUD :::: */ div.modulecloud { margin: -5px 10px 5px 10px; padding: 10px; line-height: 160%; border: 1px solid #666666; background-color: #dddddd; } div.modulecloud a { padding: 0 5px 0 5px; } /* :::: SEARCH :::: */ ul.search { margin: 10px 0 0 20px; padding: 0; } ul.search li { padding: 5px 0 5px 20px; background-image: url(file.png); background-repeat: no-repeat; background-position: 0 7px; } ul.search li a { font-weight: bold; } ul.search li div.context { color: #666; margin: 2px 0 0 30px; text-align: left; } ul.keywordmatches li.goodmatch a { font-weight: bold; } /* :::: COMMON FORM STYLES :::: */ div.actions { padding: 5px 10px 5px 10px; border-top: 1px solid #598ec0; border-bottom: 1px solid #598ec0; background-color: #9bbde2; } form dl { color: #333; } form dt { clear: both; float: left; min-width: 110px; margin-right: 10px; padding-top: 2px; } input#homepage { display: none; } div.error { margin: 5px 20px 0 0; padding: 5px; border: 1px solid #db7d46; font-weight: bold; } /* :::: INLINE COMMENTS :::: */ div.inlinecomments { position: absolute; right: 20px; } div.inlinecomments a.bubble { display: block; float: right; background-image: url(style/comment.png); background-repeat: no-repeat; width: 25px; height: 25px; text-align: center; padding-top: 3px; font-size: 0.9em; line-height: 14px; font-weight: bold; color: black; } div.inlinecomments a.bubble span { display: none; } div.inlinecomments a.emptybubble { background-image: url(style/nocomment.png); } div.inlinecomments a.bubble:hover { background-image: url(style/hovercomment.png); text-decoration: none; color: #598ec0; } div.inlinecomments div.comments { float: right; margin: 25px 5px 0 0; max-width: 50em; min-width: 30em; border: 1px solid #598ec0; background-color: #9bbde2; z-index: 150; } div#comments { border: 1px solid #598ec0; margin-top: 20px; } div#comments div.nocomments { padding: 10px; font-weight: bold; } div.inlinecomments div.comments h3, div#comments h3 { margin: 0; padding: 0; background-color: #598ec0; color: white; border: none; padding: 3px; } div.inlinecomments div.comments div.actions { padding: 4px; margin: 0; border-top: none; } div#comments div.comment { margin: 10px; border: 1px solid #598ec0; } div.inlinecomments div.comment h4, div.commentwindow div.comment h4, div#comments div.comment h4 { margin: 10px 0 0 0; background-color: #2eabb0; color: white; border: none; padding: 1px 4px 1px 4px; } div#comments div.comment h4 { margin: 0; } div#comments div.comment h4 a { color: #9bbde2; } div.inlinecomments div.comment div.text, div.commentwindow div.comment div.text, div#comments div.comment div.text { margin: -5px 0 -5px 0; padding: 0 10px 0 10px; } div.inlinecomments div.comment div.meta, div.commentwindow div.comment div.meta, div#comments div.comment div.meta { text-align: right; padding: 2px 10px 2px 0; font-size: 95%; color: #598ec0; border-top: 1px solid #598ec0; background-color: #9bbde2; } div.commentwindow { position: absolute; width: 500px; border: 1px solid #598ec0; background-color: #9bbde2; display: none; z-index: 130; } div.commentwindow h3 { margin: 0; background-color: #598ec0; color: white; border: none; padding: 5px; font-size: 1.5em; cursor: pointer; } div.commentwindow div.actions { margin: 10px -10px 0 -10px; padding: 4px 10px 4px 10px; color: #598ec0; } div.commentwindow div.actions input { border: 1px solid #598ec0; background-color: white; color: #073d61; cursor: pointer; } div.commentwindow div.form { padding: 0 10px 0 10px; } div.commentwindow div.form input, div.commentwindow div.form textarea { border: 1px solid #598ec0; background-color: white; color: black; } div.commentwindow div.error { margin: 10px 5px 10px 5px; background-color: #fff2b0; display: none; } div.commentwindow div.form textarea { width: 99%; } div.commentwindow div.preview { margin: 10px 0 10px 0; background-color: ##9bbde2; padding: 0 1px 1px 25px; } div.commentwindow div.preview h4 { margin: 0 0 -5px -20px; padding: 4px 0 0 4px; color: white; font-size: 1.3em; } div.commentwindow div.preview div.comment { background-color: #f2fbfd; } div.commentwindow div.preview div.comment h4 { margin: 10px 0 0 0!important; padding: 1px 4px 1px 4px!important; font-size: 1.2em; } /* :::: SUGGEST CHANGES :::: */ div#suggest-changes-box input, div#suggest-changes-box textarea { border: 1px solid #666; background-color: white; color: black; } div#suggest-changes-box textarea { width: 99%; height: 400px; } /* :::: PREVIEW :::: */ div.preview { background-image: url(style/preview.png); padding: 0 20px 20px 20px; margin-bottom: 30px; } /* :::: INDEX PAGE :::: */ table.contentstable { width: 90%; } table.contentstable p.biglink { line-height: 150%; } a.biglink { font-size: 1.3em; } span.linkdescr { font-style: italic; padding-top: 5px; font-size: 90%; } /* :::: INDEX STYLES :::: */ table.indextable td { text-align: left; vertical-align: top; } table.indextable dl, table.indextable dd { margin-top: 0; margin-bottom: 0; } table.indextable tr.pcap { height: 10px; } table.indextable tr.cap { margin-top: 10px; background-color: #dddddd; } img.toggler { margin-right: 3px; margin-top: 3px; cursor: pointer; } form.pfform { margin: 10px 0 20px 0; } /* :::: GLOBAL STYLES :::: */ .docwarning { background-color: #fff2b0; padding: 10px; margin: 0 -20px 0 -20px; border-bottom: 1px solid #db7d46; } p.subhead { font-weight: bold; margin-top: 20px; } a { color: #24326e; text-decoration: none; } a:hover { text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; background-color: #dddddd; font-weight: normal; color: #073d61; border-bottom: 1px solid #666; margin: 20px -20px 10px -20px; padding: 3px 0 3px 10px; } div.body h1 { margin-top: 0; font-size: 200%; } div.body h2 { font-size: 160%; } div.body h3 { font-size: 140%; } div.body h4 { font-size: 120%; } div.body h5 { font-size: 110%; } div.body h6 { font-size: 100%; } a.headerlink { color: #edaa1e; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; visibility: hidden; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink { visibility: visible; } a.headerlink:hover { background-color: #edaa1e; color: white; } div.body p, div.body dd, div.body li { text-align: left; line-height: 130%; } div.body p.caption { text-align: inherit; } div.body td { text-align: left; } ul.fakelist { list-style: none; margin: 10px 0 10px 20px; padding: 0; } .field-list ul { padding-left: 1em; } .first { margin-top: 0 !important; } /* "Footnotes" heading */ p.rubric { margin-top: 30px; font-weight: bold; } /* "Topics" */ div.topic { background-color: #ddd; border: 1px solid #666; padding: 0 7px 0 7px; margin: 10px 0 10px 0; } p.topic-title { font-size: 1.1em; font-weight: bold; margin-top: 10px; } /* Admonitions */ div.admonition { margin-top: 10px; margin-bottom: 10px; padding: 7px; } div.admonition dt { font-weight: bold; } div.admonition dl { margin-bottom: 0; } div.admonition p { display: inline; } div.seealso { background-color: #fff2b0; border: 1px solid #edaa1e; } div.warning { background-color: #fff2b0; border: 1px solid ##db7d46; } div.note { background-color: #eee; border: 1px solid #666; } p.admonition-title { margin: 0px 10px 5px 0px; font-weight: bold; display: inline; } p.admonition-title:after { content: ":"; } div.body p.centered { text-align: center; margin-top: 25px; } table.docutils { border: 0; } table.docutils td, table.docutils th { padding: 1px 8px 1px 0; border-top: 0; border-left: 0; border-right: 0; border-bottom: 1px solid #a9a6a2; } table.field-list td, table.field-list th { border: 0 !important; } table.footnote td, table.footnote th { border: 0 !important; } .field-list ul { margin: 0; padding-left: 1em; } .field-list p { margin: 0; } dl { margin-bottom: 15px; clear: both; } dd p { margin-top: 0px; } dd ul, dd table { margin-bottom: 10px; } dd { margin-top: 3px; margin-bottom: 10px; margin-left: 30px; } .refcount { color: #24326e; } dt:target, .highlight { background-color: #edaa1e1; } dl.glossary dt { font-weight: bold; font-size: 1.1em; } th { text-align: left; padding-right: 5px; } pre { padding: 5px; background-color: #e6f3ff; color: #333; border: 1px solid #24326e; border-left: none; border-right: none; overflow: auto; } td.linenos pre { padding: 5px 0px; border: 0; background-color: transparent; color: #aaa; } table.highlighttable { margin-left: 0.5em; } table.highlighttable td { padding: 0 0.5em 0 0.5em; } tt { background-color: #ddd; padding: 0 1px 0 1px; font-size: 0.95em; } tt.descname { background-color: transparent; font-weight: bold; font-size: 1.2em; } tt.descclassname { background-color: transparent; } tt.xref, a tt { background-color: transparent; font-weight: bold; } .footnote:target { background-color: #fff2b0 } h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { background-color: transparent; } .optional { font-size: 1.3em; } .versionmodified { font-style: italic; } form.comment { margin: 0; padding: 10px 30px 10px 30px; background-color: #ddd; } form.comment h3 { background-color: #598ec0; color: white; margin: -10px -30px 10px -30px; padding: 5px; font-size: 1.4em; } form.comment input, form.comment textarea { border: 1px solid #ddd; padding: 2px; font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; font-size: 100%; } form.comment input[type="text"] { width: 240px; } form.comment textarea { width: 100%; height: 200px; margin-bottom: 10px; } .system-message { background-color: #edaa1e; padding: 5px; border: 3px solid red; } /* :::: PRINT :::: */ @media print { div.document, div.documentwrapper, div.bodywrapper { margin: 0; width : 100%; } div.sphinxsidebar, div.related, div.footer, div#comments div.new-comment-box, #top-link { display: none; } } apptools-5.1.0/docs/source/undo/0000755000076500000240000000000013777643025017200 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/undo/Introduction.rst0000644000076500000240000002413113777642667022427 0ustar aayresstaff00000000000000Undo Framework ============== .. warning:: apptools.undo has been deprecated and moved to `Pyface `_. It will be removed in a future release. Please use `pyface.undo `_ instead. The Undo Framework is a component of the Enthought Tool Suite that provides developers with an API that implements the standard pattern for do/undo/redo commands. The framework is completely configurable. Alternate implementations of all major components can be provided if necessary. Framework Concepts ------------------ The following are the concepts supported by the framework. - Command A command is an application defined operation that can be done (i.e. executed), undone (i.e. reverted) and redone (i.e. repeated). A command operates on some data and maintains sufficient state to allow it to revert or repeat a change to the data. Commands may be merged so that potentially long sequences of similar commands (e.g. to add a character to some text) can be collapsed into a single command (e.g. to add a word to some text). - Macro A macro is a sequence of commands that is treated as a single command when being undone or redone. - Command Stack A command is done by pushing it onto a command stack. The last command can be undone and redone by calling appropriate command stack methods. It is also possible to move the stack's position to any point and the command stack will ensure that commands are undone or redone as required. A command stack maintains a *clean* state which is updated as commands are done and undone. It may be explicitly set, for example when the data being manipulated by the commands is saved to disk. Canned PyFace actions are provided as wrappers around command stack methods to implement common menu items. - Undo Manager An undo manager is responsible for one or more command stacks and maintains a reference to the currently active stack. It provides convenience undo and redo methods that operate on the currently active stack. An undo manager ensures that each command execution is allocated a unique sequence number, irrespective of which command stack it is pushed to. Using this it is possible to synchronise multiple command stacks and restore them to a particular point in time. An undo manager will generate an event whenever the clean state of the active stack changes. This can be used to maintain some sort of GUI status indicator to tell the user that their data has been modified since it was last saved. Typically an application will have one undo manager and one undo stack for each data type that can be edited. However this is not a requirement: how the command stack's in particular are organised and linked (with the user manager's sequence number) can need careful thought so as not to confuse the user - particularly in a plugin based application that may have many editors. To support this typical usage the PyFace ``Workbench`` class has an ``undo_manager`` trait and the PyFace ``Editor`` class has a ``command_stack`` trait. Both are lazy loaded so can be completely ignored if they are not used. API Overview ------------ This section gives a brief overview of the various classes implemented in the framework. The complete API_ documentation is available as endo generated HTML. The example_ application demonstrates all the major features of the framework. UndoManager ........... The ``UndoManager`` class is the default implementation of the ``IUndoManager`` interface. ``active_stack`` This trait is a reference to the currently active command stack and may be None. Typically it is set when some sort of editor becomes active. ``active_stack_clean`` This boolean trait reflects the clean state of the currently active command stack. It is intended to support a "document modified" indicator in the GUI. It is maintained by the undo manager. ``stack_updated`` This event is fired when the index of a command stack is changed. A reference to the stack is passed as an argument to the event and may not be the currently active stack. ``undo_name`` This Str trait is the name of the command that can be undone, and will be empty if there is no such command. It is maintained by the undo manager. ``redo_name`` This Str trait is the name of the command that can be redone, and will be empty if there is no such command. It is maintained by the undo manager. ``sequence_nr`` This integer trait is the sequence number of the next command to be executed. It is incremented immediately before a command's ``do()`` method is called. A particular sequence number identifies the state of all command stacks handled by the undo manager and allows those stacks to be set to the point they were at at a particular point in time. In other words, the sequence number allows otherwise independent command stacks to be synchronised. ``undo()`` This method calls the ``undo()`` method of the last command on the active command stack. ``redo()`` This method calls the ``redo()`` method of the last undone command on the active command stack. CommandStack ............ The ``CommandStack`` class is the default implementation of the ``ICommandStack`` interface. ``clean`` This boolean traits reflects the clean state of the command stack. Its value changes as commands are executed, undone and redone. It may also be explicitly set to mark the current stack position as being clean (when data is saved to disk for example). ``undo_name`` This Str trait is the name of the command that can be undone, and will be empty if there is no such command. It is maintained by the command stack. ``redo_name`` This Str trait is the name of the command that can be redone, and will be empty if there is no such command. It is maintained by the command stack. ``undo_manager`` This trait is a reference to the undo manager that manages the command stack. ``push(command)`` This method executes the given command by calling its ``do()`` method. Any value returned by ``do()`` is returned by ``push()``. If the command couldn't be merged with the previous one then it is saved on the command stack. ``undo(sequence_nr=0)`` This method undoes the last command. If a sequence number is given then all commands are undone up to an including the sequence number. ``redo(sequence_nr=0)`` This method redoes the last command and returns any result. If a sequence number is given then all commands are redone up to an including the sequence number and any result of the last of these is returned. ``clear()`` This method clears the command stack, without undoing or redoing any commands, and leaves the stack in a clean state. It is typically used when all changes to the data have been abandoned. ``begin_macro(name)`` This method begins a macro by creating an empty command with the given name. The commands passed to all subsequent calls to ``push()`` will be contained in the macro until the next call to ``end_macro()``. Macros may be nested. The command stack is disabled (ie. nothing can be undone or redone) while a macro is being created (ie. while there is an outstanding ``end_macro()`` call). ``end_macro()`` This method ends the current macro. ICommand ........ The ``ICommand`` interface defines the interface that must be implemented by any undoable/redoable command. ``data`` This optional trait is a reference to the data object that the command operates on. It is not used by the framework itself. ``name`` This Str trait is the name of the command as it will appear in any GUI element (e.g. in the text of an undo and redo menu entry). It may include ``&`` to indicate a keyboard shortcut which will be automatically removed whenever it is inappropriate. ``__init__(*args)`` If the command takes arguments then the command must ensure that deep copies should be made if appropriate. ``do()`` This method is called by a command stack to execute the command and to return any result. The command must save any state necessary for the ``undo()`` and ``redo()`` methods to work. It is guaranteed that this will only ever be called once and that it will be called before any call to ``undo()`` or ``redo()``. ``undo()`` This method is called by a command stack to undo the command. ``redo()`` This method is called by a command stack to redo the command and to return any result. ``merge(other)`` This method is called by the command stack to try and merge the ``other`` command with this one. True should be returned if the commands were merged. If the commands are merged then ``other`` will not be placed on the command stack. A subsequent undo or redo of this modified command must have the same effect as the two original commands. AbstractCommand ............... ``AbstractCommand`` is an abstract base class that implements the ``ICommand`` interface. It provides a default implementation of the ``merge()`` method. CommandAction ............. The ``CommandAction`` class is a sub-class of the PyFace ``Action`` class that is used to wrap commands. ``command`` This callable trait must be set to a factory that will return an object that implements ``ICommand``. It will be called when the action is invoked and the object created pushed onto the command stack. ``command_stack`` This instance trait must be set to the command stack that commands invoked by the action are pushed to. ``data`` This optional trait is a reference to the data object that will be passed to the ``command`` factory when it is called. UndoAction .......... The ``UndoAction`` class is a canned PyFace action that undoes the last command of the active command stack. RedoAction .......... The ``RedoAction`` class is a canned PyFace action that redoes the last command undone of the active command stack. .. _API: api/index.html .. _example: https://svn.enthought.com/enthought/browser/AppTools/trunk/examples/undo/ apptools-5.1.0/docs/source/naming/0000755000076500000240000000000013777643025017504 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/naming/Introduction.rst0000644000076500000240000000055613777642667022740 0ustar aayresstaff00000000000000Naming ====== :mod:`apptools.naming` package is a Python implementation of the Naming portion of the `Java Naming and Directory Interface `_, including specific implementations for a heirarchy of Python objects. You can also find the Java JNDI tutorial `here `_. apptools-5.1.0/docs/source/api/0000755000076500000240000000000013777643025017004 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/api/templates/0000755000076500000240000000000013777643025021002 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/api/templates/package.rst_t0000644000076500000240000000162213777642667023466 0ustar aayresstaff00000000000000.. (C) Copyright 2006-2021 Enthought, Inc., Austin, TX All rights reserved. This software is provided without warranty under the terms of the BSD license included in LICENSE.txt and may be redistributed only under the conditions described 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 %} apptools-5.1.0/docs/source/api/templates/module.rst_t0000644000076500000240000000123313777642667023356 0ustar aayresstaff00000000000000.. (C) Copyright 2006-2021 Enthought, Inc., Austin, TX All rights reserved. This software is provided without warranty under the terms of the BSD license included in LICENSE.txt and may be redistributed only under the conditions described 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) }} apptools-5.1.0/docs/source/selection/0000755000076500000240000000000013777643025020220 5ustar aayresstaff00000000000000apptools-5.1.0/docs/source/selection/selection.rst0000644000076500000240000001362113777642667022755 0ustar aayresstaff00000000000000.. _selection_service: The selection service ===================== It is quite common in GUI applications to have a UI element displaying a collection of items that a user can select ("selection providers"), while other parts of the application must react to changes in the selection ("selection listeners"). Ideally, the listeners would not have a direct dependency on the UI object. This is especially important in extensible `envisage`_ applications, where a plugin might need to react to a selection change, but we do not want to expose the internal organization of the application to external developers. This package defines a selection service that manages the communication between providers and listener. The :class:`~.SelectionService` object -------------------------------------- The :class:`~.SelectionService` object is the central manager that handles the communication between selection providers and listener. :ref:`Selection providers ` are components that wish to publish information about their current selection for public consumption. They register to a selection service instance when they first have a selection available (e.g., when the UI showing a list of selectable items is initialized), and un-register as soon as the selection is not available anymore (e.g., the UI is destroyed when the windows is closed). :ref:`Selection listeners ` can query the selection service to get the current selection published by a provider, using the provider unique ID. The service acts as a broker between providers and listeners, making sure that they are notified when the :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.selection` event is fired. .. _selection_providers: Selection providers ------------------- Any object can become a selection provider by implementing the :class:`~apptools.selection.i_selection_provider.ISelectionProvider` interface, and registering to the selection service. Selection providers must provide a unique ID :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.provider_id`, which is used by listeners to request its current selection. Whenever its selection changes, providers fire a :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.selection` event. The content of the event is an instance implementing :class:`~.ISelection` that contains information about the selected items. For example, a :class:`~.ListSelection` object contains a list of selected items, and their indices. Selection providers can also be queried directly about their current selection using the :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.get_selection` method, and can be requested to change their selection to a new one with the :attr:`~apptools.selection.i_selection_provider.ISelectionProvider.set_selection` method. Registration ~~~~~~~~~~~~ Selection providers publish their selection by registering to the selection service using the :attr:`~apptools.selection.selection_service.SelectionService.add_selection_provider` method. When the selection is no longer available, selection providers should un-register through :attr:`~apptools.selection.selection_service.SelectionService.remove_selection_provider`. Typically, selection providers are UI objects showing a list or tree of items, they register as soon as the UI component is initialized, and un-register when the UI component disappears (e.g., because their window has been closed). In more complex applications, the registration could be done by a controller object instead. .. _selection_listeners: Selection listeners ------------------- Selection listeners request information regarding the current selection of a selection provider given their provider ID. The :class:`~.SelectionService` supports two distinct use cases: 1) Passively listening to selection changes: listener connect to a specific provider and are notified when the provider's selection changes. 2) Actively querying a provider for its current selection: the selection service can be used to query a provider using its unique ID. Passive listening ~~~~~~~~~~~~~~~~~ Listeners connect to the selection events for a given provider using the :attr:`~apptools.selection.selection_service.SelectionService.connect_selection_listener` method. They need to provide the unique ID of the provider, and a function (or callable) that is called to send the event. This callback function takes one argument, an implementation of the :class:`~.ISelection` that represents the selection. It is possible for a listener to connect to a provider ID before it is registered. As soon as the provider is registered, the listener will receive a notification containing the provider's initial selection. To disconnect a listener use the methods :attr:`~apptools.selection.selection_service.SelectionService.disconnect_selection_listener`. Active querying ~~~~~~~~~~~~~~~ In other instances, an element of the application only needs the current selection at a specific time. For example, a toolbar button could open dialog representing a user action based on what is currently selected in the active editor. The :attr:`~apptools.selection.selection_service.SelectionService.get_selection` method calls the corresponding method on the provider with the given ID and returns an :class:`~.ISelection` instance. Setting a selection ~~~~~~~~~~~~~~~~~~~ Finally, it is possible to request a provider to set its selection to a given set of objects with :attr:`~apptools.selection.selection_service.SelectionService.set_selection`. The main use case for this method is multiple views of the same list of objects, which need to keep their selection synchronized. If the items specified in the arguments are not available in the provider, a :class:`~apptools.selection.errors.ProviderNotRegisteredError` is raised, unless the optional keyword argument :attr:`ignore_missing` is set to ``True``. .. _envisage: http://docs.enthought.com/envisage/ apptools-5.1.0/image_LICENSE.txt0000644000076500000240000000111413777642667017000 0ustar aayresstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: Crystal Project: 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 original authors: ---------------------------------------------------------------- about.png Crystal Project crit_error.png Crystal Project debug.png Crystal Project error.png Crystal Project warning.png Crystal Project apptools-5.1.0/CHANGES.txt0000644000076500000240000001017613777642674015632 0ustar aayresstaff00000000000000Apptools CHANGELOG ================== Version 5.1.0 ~~~~~~~~~~~~~ Released : 2021-01-13 This is a minor release in which the modules in the apptools.undo subpackage are modified to import from pyface.undo rather than redefining the classes. This should help ease the transition to using pyface.undo in place of the now deprecated apptool.undo. Deprecations ------------ * Import from pyface.undo.* instead of redefining classes in apptools.undo.* (#272) Documentation changes --------------------- * Add module docstrings to the various api modules in apptools subpackages (#274) Version 5.0.0 ~~~~~~~~~~~~~ Released : 2020-12-17 This is a major release mainly relating to code modernization. In this release, support for Python versions < 3.6 have been dropped. Numerous dated sub-packages and code fragments have been removed. Additionally, there were various fixes and documentation updates. Fixes ----- * Fix SyntaxWarning in persistence.file_path (#116) * Fix container items change event being saved in preferences (#196) * Fix synchronizing preference trait with name *_items (#226) Deprecations ------------ * Deprecate apptools.undo subpackage (undo was moved to pyface) (#250) Removals -------- * Remove ``appscripting`` subpackage (#172) * Remove ``template`` subpackage (#173) * Remove ``permission`` subpackage (#175) * Remove ``lru_cache`` subpackage (#184) * Remove support for Python 2.7 and 3.5 (#190) * Remove the ``apptools.sweet_pickle`` subpackage. Note that users of sweet_pickle can in some cases transition to using ``apptools.persistence`` and pickle from the python standard library (see changes made in this PR to ``apptools.naming`` for more info) (#199) * Remove ``help`` subpackage (#215) * Remove NullHandler from ``apptools.logger`` (#216) * Remove ``apptools.logger.filtering_handler`` and ``apptools.logger.util`` submodules (#217) * Remove deprecated create_log_file_handler function (#218) * Remove use of ``apptools.type_manager`` from ``apptools.naming``. Then, remove ``apptools.type_manager`` entirely. Finally, remove ``apptools.naming.adapter``. (#219) * Remove ``apptools.persistence.spickle`` submodule (#220) * Remove ``apptools.naming.ui`` sub package (#233) Documentation changes --------------------- * Update documentation for Preferences (#198) * Add a brief section to documentation for ``apptools.naming`` (#221) * Document the ``apptools.io`` and ``apptools.io.h5`` sub packages (#237) * Fix a few broken links in the documentation (#248) Test suite ---------- * Fix AttributeError on Python 3.9 due to usage of ``base64.decodestring`` in tests (#210) * Make optional dependencies optional for tests (#260) Build System ------------ * Add extras_require to setup.py for optional dependencies (#257) Version 4.5.0 ~~~~~~~~~~~~~ Released : 10 October 2019 * Add missing `long_description_content_type` field in setup. (#108) * Remove use of `2to3`. (#90) * Use etstool for CI tasks. Setup travis macos and appveyor CI. (#92) * Temporarily change cwd when running tests. (#104) * Update broken imports. (#95) * Add `six` to requirements. (#101) * Remove one more use of the deprecated `set` method. (#103) * Use `trait_set` instead of the now deprecated `set` method. (#82) * Address one more numpy deprecation warning. (#100) * Address numpy deprecation warnings. (#83) * Test the package on Python 3.5, 3.6 on CI. (#78) * Fix mismatched pyface and traitsui requirements. (#73) * Drop support for Python 2.6. (#63) * Fix `state_pickler.dump` on Python 2. (#61) * Fix a few spelling mistakes in documentation. (#87) Version 4.4.0 ~~~~~~~~~~~~~ * Apptools now works with Python-3.x. (#54) * Travis-ci support with testing on Python 2.6, 2.7 and 3.4. (#55) Change summary since 4.2.1 ~~~~~~~~~~~~~~~~~~~~~~~~~~ Enhancements * Apptools now have a changelog! * Preferences system defaults to utf-8 encoded string with ConfigObj providing better support for unicode in the PreferenceHelper (#41, #45). * Added a traitsified backport of Python 3's lru_cache (#39). * Added PyTables support to the io submodule (#19, #20, and #24 through #34). * Added a SelectionService for managing selections within an application (#15, #16, #17, #23). apptools-5.1.0/setup.py0000644000076500000240000002345713777642674015541 0ustar aayresstaff00000000000000# (C) Copyright 2008-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 runpy import subprocess from setuptools import setup, find_packages # Version information; update this by hand when making a new bugfix or feature # release. The actual package version is autogenerated from this information # together with information from the version control system, and then injected # into the package source. MAJOR = 5 MINOR = 1 MICRO = 0 PRERELEASE = "" IS_RELEASED = True # If this file is part of a Git export (for example created with "git archive", # or downloaded from GitHub), ARCHIVE_COMMIT_HASH gives the full hash of the # commit that was exported. ARCHIVE_COMMIT_HASH = "$Format:%H$" # Templates for version strings. RELEASED_VERSION = "{major}.{minor}.{micro}{prerelease}" UNRELEASED_VERSION = "{major}.{minor}.{micro}{prerelease}.dev{dev}" # Paths to the autogenerated version file and the Git directory. HERE = os.path.abspath(os.path.dirname(__file__)) VERSION_FILE = os.path.join(HERE, "apptools", "version.py") GIT_DIRECTORY = os.path.join(HERE, ".git") # Template for the autogenerated version file. VERSION_FILE_TEMPLATE = '''\ # (C) Copyright 2008-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Version information for this Apptools distribution. This file is autogenerated by the Apptools setup.py script. """ #: The full version of the package, including a development suffix #: for unreleased versions of the package. version = "{version}" #: The Git revision from which this release was made. git_revision = "{git_revision}" #: Flag whether this is a final release is_released = {is_released} ''' # Git executable to use to get revision information. GIT = "git" def _git_output(args): """ Call Git with the given arguments and return the output as (Unicode) text. """ return subprocess.check_output([GIT] + args).decode("utf-8") def _git_info(commit="HEAD"): """ Get information about the given commit from Git. Parameters ---------- commit : str, optional Commit to provide information for. Defaults to "HEAD". Returns ------- git_count : int Number of revisions from this commit to the initial commit. git_revision : unicode Commit hash for HEAD. Raises ------ EnvironmentError If Git is not available. subprocess.CalledProcessError If Git is available, but the version command fails (most likely because there's no Git repository here). """ count_args = ["rev-list", "--count", "--first-parent", commit] git_count = int(_git_output(count_args)) revision_args = ["rev-list", "--max-count", "1", commit] git_revision = _git_output(revision_args).rstrip() return git_count, git_revision def write_version_file(version, git_revision): """ Write version information to the version file. Overwrites any existing version file. Parameters ---------- version : unicode Package version. git_revision : unicode The full commit hash for the current Git revision. """ with open(VERSION_FILE, "w", encoding="ascii") as version_file: version_file.write( VERSION_FILE_TEMPLATE.format( version=version, git_revision=git_revision, is_released=IS_RELEASED, ) ) def read_version_file(): """ Read version information from the version file, if it exists. Returns ------- version : unicode The full version, including any development suffix. git_revision : unicode The full commit hash for the current Git revision. Raises ------ EnvironmentError If the version file does not exist. """ version_info = runpy.run_path(VERSION_FILE) return (version_info["version"], version_info["git_revision"]) def git_version(): """ Construct version information from local variables and Git. Returns ------- version : unicode Package version. git_revision : unicode The full commit hash for the current Git revision. Raises ------ EnvironmentError If Git is not available. subprocess.CalledProcessError If Git is available, but the version command fails (most likely because there's no Git repository here). """ git_count, git_revision = _git_info() version_template = RELEASED_VERSION if IS_RELEASED else UNRELEASED_VERSION version = version_template.format( major=MAJOR, minor=MINOR, micro=MICRO, prerelease=PRERELEASE, dev=git_count, ) return version, git_revision def archive_version(): """ Construct version information for an archive. Returns ------- version : str Package version. git_revision : str The full commit hash for the current Git revision. Raises ------ ValueError If this does not appear to be an archive. """ if "$" in ARCHIVE_COMMIT_HASH: raise ValueError("This does not appear to be an archive.") version_template = RELEASED_VERSION if IS_RELEASED else UNRELEASED_VERSION version = version_template.format( major=MAJOR, minor=MINOR, micro=MICRO, prerelease=PRERELEASE, dev="-unknown", ) return version, ARCHIVE_COMMIT_HASH def resolve_version(): """ Process version information and write a version file if necessary. Returns the current version information. Returns ------- version : unicode Package version. git_revision : unicode The full commit hash for the current Git revision. """ if os.path.isdir(GIT_DIRECTORY): # This is a local clone; compute version information and write # it to the version file, overwriting any existing information. version = git_version() print("Computed package version: {}".format(version)) print("Writing version to version file {}.".format(VERSION_FILE)) write_version_file(*version) elif "$" not in ARCHIVE_COMMIT_HASH: # This is a source archive. version = archive_version() print("Archive package version: {}".format(version)) print("Writing version to version file {}.".format(VERSION_FILE)) write_version_file(*version) elif os.path.isfile(VERSION_FILE): # This is a source distribution. Read the version information. print("Reading version file {}".format(VERSION_FILE)) version = read_version_file() print("Package version from version file: {}".format(version)) else: # This is a source archive for an unreleased version. raise RuntimeError( "Unable to determine package version. No local Git clone " "detected, and no version file found at {}." "Please use a source dist or a git clone.".format(VERSION_FILE) ) return version def get_long_description(): """ Read long description from README.txt. """ with open("README.rst", "r", encoding="utf-8") as readme: return readme.read() if __name__ == "__main__": version, git_revision = resolve_version() setup( name='apptools', version=version, author='Enthought, Inc.', author_email='info@enthought.com', maintainer='ETS Developers', maintainer_email='enthought-dev@enthought.com', url='https://docs.enthought.com/apptools', download_url=('https://www.github.com/enthought/apptools'), classifiers=[ c.strip() for c in """\ Development Status :: 5 - Production/Stable Intended Audience :: Developers Intended Audience :: Science/Research License :: OSI Approved :: BSD License Operating System :: MacOS Operating System :: Microsoft :: Windows Operating System :: OS Independent Operating System :: POSIX Operating System :: Unix Programming Language :: Python Topic :: Scientific/Engineering Topic :: Software Development Topic :: Software Development :: Libraries """.splitlines() if len(c.strip()) > 0], description='application tools', long_description=get_long_description(), long_description_content_type="text/x-rst", include_package_data=True, package_data={ 'apptools': [ 'logger/plugin/*.ini', 'logger/plugin/view/images/*.png', 'preferences/tests/*.ini' ] }, install_requires=[ 'configobj', 'traitsui', ], extras_require={ "test": [ "importlib-resources>=1.1.0", ], "h5": [ "numpy", "pandas", "tables", ], "persistence": [ "numpy", ], "preferences": [ "configobj", ], }, license='BSD', packages=find_packages(), platforms=["Windows", "Linux", "Mac OS-X", "Unix", "Solaris"], zip_safe=False, python_requires=">=3.6", ) apptools-5.1.0/examples/0000755000076500000240000000000013777643025015621 5ustar aayresstaff00000000000000apptools-5.1.0/examples/preferences/0000755000076500000240000000000013777643025020122 5ustar aayresstaff00000000000000apptools-5.1.0/examples/preferences/example.ini0000644000076500000240000000011613777642667022267 0ustar aayresstaff00000000000000[acme] width = 99 height = 600 [acme.workbench] bgcolor = red fgcolor = blue apptools-5.1.0/examples/preferences/preferences_manager.py0000644000076500000240000000617713777642667024515 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 of using the preferences manager. """ # Enthought library imports. from traits.api import Color, Int from traitsui.api import View # Local imports. from apptools.preferences.api import Preferences from apptools.preferences.api import get_default_preferences from apptools.preferences.api import set_default_preferences from apptools.preferences.ui.api import PreferencesManager, PreferencesPage # Create a preferences collection from a file and make it the default root # preferences node for all preferences helpers etc. set_default_preferences(Preferences(filename='example.ini')) class AcmePreferencesPage(PreferencesPage): """ A preference page for the Acme preferences. """ #### 'IPreferencesPage' interface ######################################### # The page's category (e.g. 'General/Appearence'). The empty string means # that this is a top-level page. category = '' # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = '' # The page name (this is what is shown in the preferences dialog. name = 'Acme' # The path to the preferences node that contains our preferences. preferences_path = 'acme' #### Preferences ########################################################## width = Int(800) height = Int(600) #### Traits UI views ###################################################### view = View('width', 'height') class AcmeWorkbenchPreferencesPage(PreferencesPage): """ A preference page for the Acme workbench preferences. """ #### 'IPreferencesPage' interface ######################################### # The page's category (e.g. 'General/Appearence'). The empty string means # that this is a top-level page. category = 'Acme' # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = '' # The page name (this is what is shown in the preferences dialog. name = 'Workbench' # The path to the preferences node that contains our preferences. preferences_path = 'acme' #### Preferences ########################################################## bgcolor = Color fgcolor = Color #### Traits UI views ###################################################### view = View('bgcolor', 'fgcolor') # Entry point. if __name__ == '__main__': # Create a manager with some pages. preferences_manager = PreferencesManager( pages=[AcmePreferencesPage(), AcmeWorkbenchPreferencesPage()] ) # Show the UI... preferences_manager.configure_traits() # Save the preferences... get_default_preferences().flush() apptools-5.1.0/examples/undo/0000755000076500000240000000000013777643025016566 5ustar aayresstaff00000000000000apptools-5.1.0/examples/undo/model.py0000644000076500000240000000265713777642667020265 0ustar aayresstaff00000000000000# ----------------------------------------------------------------------------- # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ----------------------------------------------------------------------------- # Enthought library imports. from traits.api import Enum, HasTraits, Int, Str class Label(HasTraits): """The Label class implements the data model for a label.""" #### 'Label' interface #################################################### # The name. name = Str # The size in points. size = Int(18) # The style. style = Enum('normal', 'bold', 'italic') ########################################################################### # 'Label' interface. ########################################################################### def increment_size(self, by): """Increment the current font size.""" self.size += by def decrement_size(self, by): """Decrement the current font size.""" self.size -= by apptools-5.1.0/examples/undo/example_editor_manager.py0000644000076500000240000001054113777642667023647 0ustar aayresstaff00000000000000# ----------------------------------------------------------------------------- # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ----------------------------------------------------------------------------- # Enthought library imports. from traits.etsconfig.api import ETSConfig from pyface.workbench.api import Editor, EditorManager class _wxLabelEditor(Editor): """ _wxLabelEditor is the wx implementation of a label editor. """ def create_control(self, parent): import wx w = wx.TextCtrl(parent, style=wx.TE_RICH2) style = w.GetDefaultStyle() style.SetAlignment(wx.TEXT_ALIGNMENT_CENTER) w.SetDefaultStyle(style) self._set_text(w) self._set_size_and_style(w) self.obj.on_trait_change(self._update_text, 'text') self.obj.on_trait_change(self._update_size, 'size') self.obj.on_trait_change(self._update_style, 'style') return w def _name_default(self): return self.obj.text def _update_text(self): self._set_text(self.control) def _set_text(self, w): w.SetValue("") w.WriteText( "%s(%d points, %s)" % ( self.obj.text, self.obj.size, self.obj.style ) ) def _update_size(self): self._set_size_and_style(self.control) def _update_style(self): self._set_size_and_style(self.control) def _set_size_and_style(self, w): import wx if self.obj.style == 'normal': style, weight = wx.NORMAL, wx.NORMAL elif self.obj.style == 'italic': style, weight = wx.ITALIC, wx.NORMAL elif self.obj.style == 'bold': style, weight = wx.NORMAL, wx.BOLD else: raise NotImplementedError( "style '%s' not supported" % self.obj.style ) f = wx.Font(self.obj.size, wx.ROMAN, style, weight, False) style = wx.TextAttr("BLACK", wx.NullColour, f) w.SetDefaultStyle(style) self._set_text(w) class _PyQt4LabelEditor(Editor): """ _PyQt4LabelEditor is the PyQt implementation of a label editor. """ def create_control(self, parent): from pyface.qt import QtCore, QtGui w = QtGui.QLabel(parent) w.setAlignment(QtCore.Qt.AlignCenter) self._set_text(w) self._set_size(w) self._set_style(w) self.obj.on_trait_change(self._update_text, 'text') self.obj.on_trait_change(self._update_size, 'size') self.obj.on_trait_change(self._update_style, 'style') return w def _name_default(self): return self.obj.text def _update_text(self): self._set_text(self.control) def _set_text(self, w): w.setText( "%s\n(%d points, %s)" % ( self.obj.text, self.obj.size, self.obj.style ) ) def _update_size(self): self._set_size(self.control) def _set_size(self, w): f = w.font() f.setPointSize(self.obj.size) w.setFont(f) self._set_text(w) def _update_style(self): self._set_style(self.control) def _set_style(self, w): f = w.font() f.setBold(self.obj.style == 'bold') f.setItalic(self.obj.style == 'italic') w.setFont(f) self._set_text(w) class ExampleEditorManager(EditorManager): """ The ExampleEditorManager class creates the example editors. """ def create_editor(self, window, obj, kind): # Create the toolkit specific editor. tk_name = ETSConfig.toolkit if tk_name == 'wx': ed = _wxLabelEditor(window=window, obj=obj) elif tk_name == 'qt4' or tk_name == 'qt': ed = _PyQt4LabelEditor(window=window, obj=obj) else: raise NotImplementedError( "unsupported toolkit: %s" % tk_name ) return ed apptools-5.1.0/examples/undo/example_undo_window.py0000644000076500000240000001204613777642667023225 0ustar aayresstaff00000000000000# ----------------------------------------------------------------------------- # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ----------------------------------------------------------------------------- # Enthought library imports. from pyface.action.api import Action, Group, MenuManager from pyface.workbench.api import WorkbenchWindow from pyface.workbench.action.api import MenuBarManager, ToolBarManager from traits.api import Instance from apptools.undo.action.api import CommandAction, RedoAction, UndoAction # Local imports. from example_editor_manager import ExampleEditorManager from commands import LabelIncrementSizeCommand, LabelDecrementSizeCommand, \ LabelNormalFontCommand, LabelBoldFontCommand, LabelItalicFontCommand class ExampleUndoWindow(WorkbenchWindow): """ The ExampleUndoWindow class is a workbench window that contains example editors that demonstrate the use of the undo framework. """ #### Private interface #################################################### # The action that exits the application. _exit_action = Instance(Action) # The File menu. _file_menu = Instance(MenuManager) # The Label menu. _label_menu = Instance(MenuManager) # The Undo menu. _undo_menu = Instance(MenuManager) ########################################################################### # Private interface. ########################################################################### #### Trait initialisers ################################################### def __file_menu_default(self): """ Trait initialiser. """ return MenuManager(self._exit_action, name="&File") def __undo_menu_default(self): """ Trait initialiser. """ undo_manager = self.workbench.undo_manager undo_action = UndoAction(undo_manager=undo_manager) redo_action = RedoAction(undo_manager=undo_manager) return MenuManager(undo_action, redo_action, name="&Undo") def __label_menu_default(self): """ Trait initialiser. """ size_group = Group(CommandAction(command=LabelIncrementSizeCommand), CommandAction(command=LabelDecrementSizeCommand)) normal = CommandAction(id='normal', command=LabelNormalFontCommand, style='radio', checked=True) bold = CommandAction(id='bold', command=LabelBoldFontCommand, style='radio') italic = CommandAction(id='italic', command=LabelItalicFontCommand, style='radio') style_group = Group(normal, bold, italic, id='style') return MenuManager(size_group, style_group, name="&Label") def __exit_action_default(self): """ Trait initialiser. """ return Action(name="E&xit", on_perform=self.workbench.exit) def _editor_manager_default(self): """ Trait initialiser. """ return ExampleEditorManager() def _menu_bar_manager_default(self): """ Trait initialiser. """ return MenuBarManager( self._file_menu, self._label_menu, self._undo_menu, window=self ) def _tool_bar_manager_default(self): """ Trait initialiser. """ return ToolBarManager(self._exit_action, show_tool_names=False) def _active_editor_changed(self, old, new): """ Trait handler. """ # Tell the undo manager about the new command stack. if old is not None: old.command_stack.undo_manager.active_stack = None if new is not None: new.command_stack.undo_manager.active_stack = new.command_stack # Walk the label editor menu. for grp in self._label_menu.groups: for itm in grp.items: action = itm.action # Enable the action and set the command stack and data if there # is a new editor. if new is not None: action.enabled = True action.command_stack = new.command_stack action.data = new.obj # FIXME v3: We should just be able to check the menu option # corresponding to the style trait - but that doesn't seem # to uncheck the other options in the group. Even then the # first switch to another editor doesn't update the menus # (though subsequent ones do). if grp.id == 'style': action.checked = (action.data.style == action.id) else: action.enabled = False apptools-5.1.0/examples/undo/example.py0000644000076500000240000000446613777642667020620 0ustar aayresstaff00000000000000# ----------------------------------------------------------------------------- # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ----------------------------------------------------------------------------- # Standard library imports. import logging # Enthought library imports. from pyface.api import GUI, YES from pyface.workbench.api import Workbench # Local imports. from example_undo_window import ExampleUndoWindow from model import Label # Log to stderr. logging.getLogger().addHandler(logging.StreamHandler()) logging.getLogger().setLevel(logging.DEBUG) class ExampleUndo(Workbench): """ The ExampleUndo class is a workbench that creates ExampleUndoWindow windows. """ #### 'Workbench' interface ################################################ # The factory (in this case simply a class) that is used to create # workbench windows. window_factory = ExampleUndoWindow ########################################################################### # Private interface. ########################################################################### def _exiting_changed(self, event): """ Called when the workbench is exiting. """ if self.active_window.confirm('Ok to exit?') != YES: event.veto = True def main(argv): """ A simple example of using the the undo framework in a workbench. """ # Create the GUI. gui = GUI() # Create the workbench. workbench = ExampleUndo(state_location=gui.state_location) window = workbench.create_window(position=(300, 300), size=(400, 300)) window.open() # Create some objects to edit. label = Label(text="Label") label2 = Label(text="Label2") # Edit the objects. window.edit(label) window.edit(label2) # Start the GUI event loop. gui.start_event_loop() if __name__ == '__main__': import sys main(sys.argv) apptools-5.1.0/examples/undo/commands.py0000644000076500000240000001350513777642667020760 0ustar aayresstaff00000000000000# ----------------------------------------------------------------------------- # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ----------------------------------------------------------------------------- # Enthought library imports. from traits.api import Instance, Int, Str from apptools.undo.api import AbstractCommand # Local imports. from model import Label class LabelIncrementSizeCommand(AbstractCommand): """ The LabelIncrementSizeCommand class is a command that increases the size of a label's text. This command will merge multiple increments togther. """ #### 'ICommand' interface ################################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Str("&Increment size") #### Private interface #################################################### _incremented_by = Int ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): self.data.increment_size(1) self._incremented_by = 1 def merge(self, other): # We can merge if the other command is the same type (or a sub-type). if isinstance(other, type(self)): self._incremented_by += 1 merged = True else: merged = False return merged def redo(self): self.data.increment_size(self._incremented_by) def undo(self): self.data.decrement_size(self._incremented_by) class LabelDecrementSizeCommand(AbstractCommand): """ The LabelDecrementSizeCommand class is a command that decreases the size of a label's text. This command will merge multiple decrements togther. """ #### 'ICommand' interface ################################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Str("&Decrement size") #### Private interface #################################################### _decremented_by = Int ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): self.data.decrement_size(1) self._decremented_by = 1 def merge(self, other): # We can merge if the other command is the same type (or a sub-type). if isinstance(other, type(self)): self._decremented_by += 1 merged = True else: merged = False return merged def redo(self): self.data.decrement_size(self._decremented_by) def undo(self): self.data.increment_size(self._decremented_by) class LabelNormalFontCommand(AbstractCommand): """ The LabelNormalFontCommand class is a command that sets a normal font for a label's text. """ #### 'ICommand' interface ################################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Str("&Normal font") ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): # Save the old value. self._saved = self.data.style # Calling redo() is a convenient way to update the model now that the # old value is saved. self.redo() def redo(self): self.data.style = 'normal' def undo(self): self.data.style = self._saved class LabelBoldFontCommand(AbstractCommand): """ The LabelNormalFontCommand class is a command that sets a bold font for a label's text. """ #### 'ICommand' interface ############################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Str("&Bold font") ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): # Save the old value. self._saved = self.data.style # Calling redo() is a convenient way to update the model now that the # old value is saved. self.redo() def redo(self): self.data.style = 'bold' def undo(self): self.data.style = self._saved class LabelItalicFontCommand(AbstractCommand): """ The LabelNormalFontCommand class is a command that sets an italic font for a label's text. """ #### 'ICommand' interface ################################################# # The data being operated on. data = Instance(Label) # The name of the command. name = Str("&Italic font") ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): # Save the old value. self._saved = self.data.style # Calling redo() is a convenient way to update the model now that the # old value is saved. self.redo() def redo(self): self.data.style = 'italic' def undo(self): self.data.style = self._saved apptools-5.1.0/examples/naming/0000755000076500000240000000000013777643025017072 5ustar aayresstaff00000000000000apptools-5.1.0/examples/naming/images/0000755000076500000240000000000013777643025020337 5ustar aayresstaff00000000000000apptools-5.1.0/examples/naming/images/document.png0000644000076500000240000000051513777642667022677 0ustar aayresstaff00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  šœtIMEÕ8º`&BÚIDAT8Ë­’=n„0…¿H»[!!ŸŸ³l™"ÇAæ!QÚsú­LŠˆ$^X¢¼j,{Þ|3µmû1 à ÇôZ×õ;ÖÚ騬µÓì”ÌÁ8Žxït/5Ç’ˆã˜4MW(wï=ιUÂR’¨ªêW/ÉòPÅ&AHÉòá’à§ž"Èóh©Û@ü›  æÿ?ÞíÅÀ q ¼s1 31#º~€I0üþr ÃàÜŠµ@2x/!WØ€ÿÿ¾C¹ÿ@>Ó¿>†‚ ‚¯ q2N1HÓ€ðï÷; h(#+XÙ·[¯{ŒËË`W]¹ý»háú¯3jÀ¸‚¿?€5gÏe`bÀv“;‚û€Ô5€‚ðç\âÛ‡« p¹ôÚ/°wþÿyùÃ÷Wë8D¼ÍÑ Hüýæýù~ŸaÕŽï ©)É@C?‚$þÿÿ¡ÿýaøýû/Ûï ó—ìè –ÇõÄoÞ~†%H3Ã_° ¶ÿ…4ÃÏßÿ¸þ~…Ùþˆ<?ù°ãð†Ä7ˆ—þƒbä?Ü ö? µ`ùQ˜íÇø@ xõæ3î£? Þù V áüzáÔ+~þúlûa¯lH³ƒˆ¤!PíHÀýÿwÌ :¨í tÀ Ò @ ¤©ŒÛ« D ý@ªˆŸL  ÔcÄ &D@?Èy€ø(8^ €`™D³C Ã@¹öL3ãÿÿÿ(0ŒþëøIEND®B`‚apptools-5.1.0/examples/naming/images/closed_folder.png0000644000076500000240000000102513777642667023662 0ustar aayresstaff00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<§IDATxÚbd```šÝ,ø— ¤Ö¾×R·øcÈÃ@15ÿÿóã‰倘 ˆY ˜ˆ‘õHá÷—‹'–^’Ú{ ¹ €Àüûó"û(ÆÈ¦¾ß5h_âd@~½aøóýÚU ˆ@ïí‡@P¼kÆn#.  2Ä € ü~úõéÔ+þÿÿ¤}ó ˆ‚Ù o{±‚S,fŠ+@A xË¡?00áùD3ˆþ ûû¨î#Ìa€‚ðjñ'†ÿ@[þÿûÖv и+ Âä5àÄ>Bbh H(ýüÿ÷j;Ä`À®€€B‰FpXüÿq R@ ú6äïßß( ÄéŸ]qDˆ qú?°Ìß Pï€Á€%K-`¼’°¹ H­ ¬@lÄÎ@,D¤þï@|ˆ÷,c€hv¨aÄPî΀bü 4r@€œô2s˜^IEND®B`‚apptools-5.1.0/examples/naming/images/image_LICENSE.txt0000644000076500000240000000067113777642667023343 0ustar aayresstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: GV: Gael Varoquaux: BSD-like Unless stated in this file, icons are work of enthought, and are released under BSD-like license. Files and orginal authors: ---------------------------------------------------------------- closed_folder.png | GV document.png | GV open_folder.png | GV apptools-5.1.0/examples/naming/simple.py0000644000076500000240000000201413777642667020745 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 naming example. """ # Standard library imports. import os # Enthought library imports. from apptools.naming.api import Context, InitialContext # Application entry point. if __name__ == '__main__': # Set up the naming environment. klass_name = "apptools.naming.InitialContextFactory" klass_name = "apptools.naming.PyFSInitialContextFactory" environment = {Context.INITIAL_CONTEXT_FACTORY: klass_name} # Create an initial context. context = InitialContext(environment) context.path = os.getcwd() print('Context', context, context.path) print('Names', context.list_names('')) apptools-5.1.0/setup.cfg0000644000076500000240000000022113777643025015617 0ustar aayresstaff00000000000000[flake8] ignore = E266, W503 per-file-ignores = */api.py:F401, */__init__.py:F401, */undo/*:F401,H101 [egg_info] tag_build = tag_date = 0 apptools-5.1.0/apptools.egg-info/0000755000076500000240000000000013777643025017336 5ustar aayresstaff00000000000000apptools-5.1.0/apptools.egg-info/PKG-INFO0000644000076500000240000001062713777643025020441 0ustar aayresstaff00000000000000Metadata-Version: 2.1 Name: apptools Version: 5.1.0 Summary: application tools Home-page: https://docs.enthought.com/apptools Author: Enthought, Inc. Author-email: info@enthought.com Maintainer: ETS Developers Maintainer-email: enthought-dev@enthought.com License: BSD Download-URL: https://www.github.com/enthought/apptools Description: =========================== apptools: application tools =========================== .. image:: https://travis-ci.org/enthought/apptools.svg?branch=master :target: https://travis-ci.org/enthought/apptools :alt: Build status Documentation: http://docs.enthought.com/apptools Source Code: http://www.github.com/enthought/apptools The apptools project includes a set of packages that Enthought has found useful in creating a number of applications. They implement functionality that is commonly needed by many applications - **apptools.io**: Provides an abstraction for files and folders in a file system. - **apptools.logger**: Convenience functions for creating logging handlers - **apptools.naming**: Manages naming contexts, supporting non-string data types and scoped preferences - **apptools.persistence**: Supports pickling the state of a Python object to a dictionary, which can then be flexibly applied in restoring the state of the object. - **apptools.preferences**: Manages application preferences. - **apptools.selection**: Manages the communication between providers and listener of selected items in an application. - **apptools.scripting**: A framework for automatic recording of Python scripts. - **apptools.undo**: Supports undoing and scripting application commands. Prerequisites ------------- All packages in apptools require: * `traits `_ Certain sub-packages within apptools have their own specific dependencies, which are optional for apptools overall. The `apptools.preferences` package requires: * `configobj `_ The `apptools.io.h5` package requires: * `numpy `_ * `pandas `_ * `tables `_ The `apptools.persistence` package requires: * `numpy `_ Many of the packages provide optional user interfaces using Pyface and Traitsui. In additon, many of the packages are designed to work with the Envisage plug-in system, althought most can be used independently: * `envisage `_ * `pyface `_ * `traitsui `_ Installation ------------ To install with `apptools.preferences` dependencies:: $ pip install apptools[preferences] To install with `apptools.io.h5` dependencies:: $ pip install apptools[h5] To install with `apptools.persistence` dependencies:: $ pip install apptools[persistence] To install with additional test dependencies:: $ pip install apptools[test] Platform: Windows Platform: Linux Platform: Mac OS-X Platform: Unix Platform: Solaris Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Requires-Python: >=3.6 Description-Content-Type: text/x-rst Provides-Extra: test Provides-Extra: h5 Provides-Extra: persistence Provides-Extra: preferences apptools-5.1.0/apptools.egg-info/not-zip-safe0000644000076500000240000000000113777643025021564 0ustar aayresstaff00000000000000 apptools-5.1.0/apptools.egg-info/SOURCES.txt0000644000076500000240000001671313777643025021232 0ustar aayresstaff00000000000000CHANGES.txt LICENSE.txt MANIFEST.in README.rst image_LICENSE.txt image_LICENSE_CP.txt setup.cfg setup.py apptools/__init__.py apptools/version.py apptools.egg-info/PKG-INFO apptools.egg-info/SOURCES.txt apptools.egg-info/dependency_links.txt apptools.egg-info/not-zip-safe apptools.egg-info/requires.txt apptools.egg-info/top_level.txt apptools/_testing/__init__.py apptools/_testing/optional_dependencies.py apptools/io/__init__.py apptools/io/api.py apptools/io/file.py apptools/io/h5/__init__.py apptools/io/h5/dict_node.py apptools/io/h5/file.py apptools/io/h5/table_node.py apptools/io/h5/utils.py apptools/io/h5/tests/__init__.py apptools/io/h5/tests/test_dict_node.py apptools/io/h5/tests/test_file.py apptools/io/h5/tests/test_table_node.py apptools/io/h5/tests/utils.py apptools/io/tests/__init__.py apptools/io/tests/test_file.py apptools/io/tests/test_folder.py apptools/logger/__init__.py apptools/logger/api.py apptools/logger/custom_excepthook.py apptools/logger/log_point.py apptools/logger/log_queue_handler.py apptools/logger/logger.py apptools/logger/ring_buffer.py apptools/logger/agent/__init__.py apptools/logger/agent/attachments.py apptools/logger/agent/quality_agent_mailer.py apptools/logger/agent/quality_agent_view.py apptools/logger/agent/tests/__init__.py apptools/logger/agent/tests/test_attachments.py apptools/logger/plugin/__init__.py apptools/logger/plugin/logger_plugin.py apptools/logger/plugin/logger_preferences.py apptools/logger/plugin/logger_service.py apptools/logger/plugin/preferences.ini apptools/logger/plugin/tests/__init__.py apptools/logger/plugin/tests/test_logger_service.py apptools/logger/plugin/view/__init__.py apptools/logger/plugin/view/logger_preferences_page.py apptools/logger/plugin/view/logger_view.py apptools/logger/plugin/view/images/crit_error.png apptools/logger/plugin/view/images/debug.png apptools/logger/plugin/view/images/error.png apptools/logger/plugin/view/images/info.png apptools/logger/plugin/view/images/warning.png apptools/naming/__init__.py apptools/naming/address.py apptools/naming/api.py apptools/naming/binding.py apptools/naming/context.py apptools/naming/dir_context.py apptools/naming/dynamic_context.py apptools/naming/exception.py apptools/naming/initial_context.py apptools/naming/initial_context_factory.py apptools/naming/naming_event.py apptools/naming/naming_manager.py apptools/naming/object_factory.py apptools/naming/object_serializer.py apptools/naming/py_context.py apptools/naming/py_object_factory.py apptools/naming/pyfs_context.py apptools/naming/pyfs_context_factory.py apptools/naming/pyfs_initial_context_factory.py apptools/naming/pyfs_object_factory.py apptools/naming/pyfs_state_factory.py apptools/naming/reference.py apptools/naming/referenceable.py apptools/naming/referenceable_state_factory.py apptools/naming/state_factory.py apptools/naming/unique_name.py apptools/naming/tests/__init__.py apptools/naming/tests/test_context.py apptools/naming/tests/test_dir_context.py apptools/naming/tests/test_object_serializer.py apptools/naming/tests/test_py_context.py apptools/naming/tests/test_pyfs_context.py apptools/naming/trait_defs/__init__.py apptools/naming/trait_defs/api.py apptools/naming/trait_defs/naming_traits.py apptools/persistence/__init__.py apptools/persistence/file_path.py apptools/persistence/project_loader.py apptools/persistence/state_pickler.py apptools/persistence/updater.py apptools/persistence/version_registry.py apptools/persistence/versioned_unpickler.py apptools/persistence/tests/__init__.py apptools/persistence/tests/state_function_classes.py apptools/persistence/tests/test_class_mapping.py apptools/persistence/tests/test_file_path.py apptools/persistence/tests/test_state_function.py apptools/persistence/tests/test_state_pickler.py apptools/persistence/tests/test_two_stage_unpickler.py apptools/persistence/tests/test_version_registry.py apptools/preferences/__init__.py apptools/preferences/api.py apptools/preferences/i_preferences.py apptools/preferences/package_globals.py apptools/preferences/preference_binding.py apptools/preferences/preferences.py apptools/preferences/preferences_helper.py apptools/preferences/scoped_preferences.py apptools/preferences/tests/__init__.py apptools/preferences/tests/example.ini apptools/preferences/tests/py_config_example.ini apptools/preferences/tests/py_config_example_2.ini apptools/preferences/tests/py_config_file.py apptools/preferences/tests/test_preference_binding.py apptools/preferences/tests/test_preferences.py apptools/preferences/tests/test_preferences_helper.py apptools/preferences/tests/test_py_config_file.py apptools/preferences/tests/test_scoped_preferences.py apptools/preferences/ui/__init__.py apptools/preferences/ui/api.py apptools/preferences/ui/i_preferences_page.py apptools/preferences/ui/preferences_manager.py apptools/preferences/ui/preferences_node.py apptools/preferences/ui/preferences_page.py apptools/preferences/ui/tree_item.py apptools/preferences/ui/widget_editor.py apptools/preferences/ui/tests/__init__.py apptools/preferences/ui/tests/test_preferences_page.py apptools/scripting/__init__.py apptools/scripting/api.py apptools/scripting/package_globals.py apptools/scripting/recordable.py apptools/scripting/recorder.py apptools/scripting/recorder_with_ui.py apptools/scripting/util.py apptools/scripting/tests/__init__.py apptools/scripting/tests/test_recorder.py apptools/selection/__init__.py apptools/selection/api.py apptools/selection/errors.py apptools/selection/i_selection.py apptools/selection/i_selection_provider.py apptools/selection/list_selection.py apptools/selection/selection_service.py apptools/selection/tests/__init__.py apptools/selection/tests/test_list_selection.py apptools/selection/tests/test_selection_service.py apptools/type_registry/__init__.py apptools/type_registry/api.py apptools/type_registry/type_registry.py apptools/type_registry/tests/__init__.py apptools/type_registry/tests/dummies.py apptools/type_registry/tests/test_lazy_registry.py apptools/type_registry/tests/test_type_registry.py apptools/undo/__init__.py apptools/undo/abstract_command.py apptools/undo/api.py apptools/undo/command_stack.py apptools/undo/i_command.py apptools/undo/i_command_stack.py apptools/undo/i_undo_manager.py apptools/undo/undo_manager.py apptools/undo/action/__init__.py apptools/undo/action/abstract_command_stack_action.py apptools/undo/action/api.py apptools/undo/action/command_action.py apptools/undo/action/redo_action.py apptools/undo/action/undo_action.py docs/Makefile docs/releases/README.rst docs/source/conf.py docs/source/index.rst docs/source/_static/default.css docs/source/_static/e-logo-rev.png docs/source/_static/et.ico docs/source/api/templates/module.rst_t docs/source/api/templates/package.rst_t docs/source/io/introduction.rst docs/source/naming/Introduction.rst docs/source/preferences/Preferences.rst docs/source/preferences/PreferencesInEnvisage.rst docs/source/scripting/introduction.rst docs/source/selection/selection.rst docs/source/undo/Introduction.rst examples/naming/simple.py examples/naming/images/closed_folder.png examples/naming/images/document.png examples/naming/images/image_LICENSE.txt examples/naming/images/open_folder.png examples/preferences/example.ini examples/preferences/preferences_manager.py examples/undo/commands.py examples/undo/example.py examples/undo/example_editor_manager.py examples/undo/example_undo_window.py examples/undo/model.py integrationtests/persistence/test_persistence.py integrationtests/persistence/update1.py integrationtests/persistence/update2.py integrationtests/persistence/update3.pyapptools-5.1.0/apptools.egg-info/requires.txt0000644000076500000240000000017613777643025021742 0ustar aayresstaff00000000000000configobj traitsui [h5] numpy pandas tables [persistence] numpy [preferences] configobj [test] importlib-resources>=1.1.0 apptools-5.1.0/apptools.egg-info/top_level.txt0000644000076500000240000000001113777643025022060 0ustar aayresstaff00000000000000apptools apptools-5.1.0/apptools.egg-info/dependency_links.txt0000644000076500000240000000000113777643025023404 0ustar aayresstaff00000000000000 apptools-5.1.0/README.rst0000644000076500000240000000515413777642667015512 0ustar aayresstaff00000000000000=========================== apptools: application tools =========================== .. image:: https://travis-ci.org/enthought/apptools.svg?branch=master :target: https://travis-ci.org/enthought/apptools :alt: Build status Documentation: http://docs.enthought.com/apptools Source Code: http://www.github.com/enthought/apptools The apptools project includes a set of packages that Enthought has found useful in creating a number of applications. They implement functionality that is commonly needed by many applications - **apptools.io**: Provides an abstraction for files and folders in a file system. - **apptools.logger**: Convenience functions for creating logging handlers - **apptools.naming**: Manages naming contexts, supporting non-string data types and scoped preferences - **apptools.persistence**: Supports pickling the state of a Python object to a dictionary, which can then be flexibly applied in restoring the state of the object. - **apptools.preferences**: Manages application preferences. - **apptools.selection**: Manages the communication between providers and listener of selected items in an application. - **apptools.scripting**: A framework for automatic recording of Python scripts. - **apptools.undo**: Supports undoing and scripting application commands. Prerequisites ------------- All packages in apptools require: * `traits `_ Certain sub-packages within apptools have their own specific dependencies, which are optional for apptools overall. The `apptools.preferences` package requires: * `configobj `_ The `apptools.io.h5` package requires: * `numpy `_ * `pandas `_ * `tables `_ The `apptools.persistence` package requires: * `numpy `_ Many of the packages provide optional user interfaces using Pyface and Traitsui. In additon, many of the packages are designed to work with the Envisage plug-in system, althought most can be used independently: * `envisage `_ * `pyface `_ * `traitsui `_ Installation ------------ To install with `apptools.preferences` dependencies:: $ pip install apptools[preferences] To install with `apptools.io.h5` dependencies:: $ pip install apptools[h5] To install with `apptools.persistence` dependencies:: $ pip install apptools[persistence] To install with additional test dependencies:: $ pip install apptools[test] apptools-5.1.0/LICENSE.txt0000644000076500000240000000312013777642667015635 0ustar aayresstaff00000000000000This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. Copyright (c) 2006, Enthought, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Enthought, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. apptools-5.1.0/apptools/0000755000076500000240000000000013777643025015644 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/preferences/0000755000076500000240000000000013777643025020145 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/preferences/preferences_helper.py0000644000076500000240000001572313777642667024402 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 object that can be initialized from a preferences node. """ # Standard library imports. import logging # Enthought library imports. from traits.api import HasTraits, Instance, Str # Local imports. from .i_preferences import IPreferences from .package_globals import get_default_preferences # Logging. logger = logging.getLogger(__name__) class PreferencesHelper(HasTraits): """ A base class for objects that can be initialized from a preferences node. Additional traits defined on subclasses will be listened to. Changes are then synchronized with the preferences. Note that mutations on nested containers e.g. List(List(Str)) cannot be synchronized and should be avoided. """ #### 'PreferencesHelper' interface ######################################## # The preferences node used by the helper. If this trait is not set then # the package-global default preferences node is used. # # fixme: This introduces a 'sneaky' global reference to the preferences # node! preferences = Instance(IPreferences) # The path to the preference node that contains the preferences that we # use to initialize instances of this class. preferences_path = Str ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Constructor. """ super(PreferencesHelper, self).__init__(**traits) # Initialize the object's traits from the preferences node. if self.preferences: self._initialize(self.preferences) ########################################################################### # Private interface. ########################################################################### #### Trait initializers ################################################### def _preferences_default(self): """ Trait initializer. """ # If no specific preferences node is set then we use the package-wide # global node. return get_default_preferences() #### Trait change handlers ################################################ def _anytrait_changed(self, trait_name, old, new): """ Static trait change handler. """ if self.preferences is None: return if self._is_preference_trait(trait_name): self.preferences.set("%s.%s" % (self._get_path(), trait_name), new) # If the trait was a list or dict '_items' trait then just treat it as # if the entire list or dict was changed. elif trait_name.endswith('_items'): trait_name = trait_name[:-6] if self._is_preference_trait(trait_name): self.preferences.set( '%s.%s' % (self._get_path(), trait_name), getattr(self, trait_name) ) # If the change refers to a trait defined on this class, then # the trait is not a preference trait and we do nothing. def _preferences_changed(self, old, new): """ Static trait change handler. """ # Stop listening to the old preferences node. if old is not None: old.remove_preferences_listener( self._preferences_changed_listener, self._get_path() ) if new is not None: # Initialize with the new preferences node (this also adds a # listener for preferences being changed in the new node). self._initialize(new, notify=True) #### Other observer pattern listeners ##################################### def _preferences_changed_listener(self, node, key, old, new): """ Listener called when a preference value is changed. """ if key in self.trait_names(): setattr(self, key, self._get_value(key, new)) #### Methods ############################################################## def _get_path(self): """ Return the path to our preferences node. """ if len(self.preferences_path) > 0: path = self.preferences_path else: path = getattr(self, "PREFERENCES_PATH", None) if path is None: raise SystemError("no preferences path, %s" % self) else: logger.warn('DEPRECATED: use "preferences_path" %s' % self) return path def _get_value(self, trait_name, value): """Get the actual value to set. This method makes sure that any required work is done to convert the preference value from a string. Str traits or those with the metadata 'is_str=True' will just be passed the string itself. """ trait = self.trait(trait_name) handler = trait.handler # If the trait type is 'Str' then we just take the raw value. if isinstance(handler, Str) or trait.is_str: pass # Otherwise, we eval it! else: try: value = eval(value) # If the eval fails then there is probably a syntax error, but # we will let the handler validation throw the exception. except Exception: pass if handler.validate is not None: # Any traits have a validator of None. validated = handler.validate(self, trait_name, value) else: validated = value return validated def _initialize(self, preferences, notify=False): """ Initialize the object's traits from the preferences node. """ path = self._get_path() keys = preferences.keys(path) traits_to_set = {} for trait_name in self.trait_names(): if trait_name in keys: key = "%s.%s" % (path, trait_name) value = self._get_value(trait_name, preferences.get(key)) traits_to_set[trait_name] = value self.trait_set(trait_change_notify=notify, **traits_to_set) # Listen for changes to the node's preferences. preferences.add_preferences_listener( self._preferences_changed_listener, path ) # fixme: Pretty much duplicated in 'PreferencesPage' (except for the # class name of course!). def _is_preference_trait(self, trait_name): """ Return True if a trait represents a preference value. """ if ( trait_name.startswith("_") or trait_name.endswith("_") or trait_name in PreferencesHelper.class_traits() ): return False return trait_name in self.editable_traits() apptools-5.1.0/apptools/preferences/ui/0000755000076500000240000000000013777643025020562 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/preferences/ui/widget_editor.py0000644000076500000240000000614313777642667024004 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 instance editor that allows total control over widget creation. """ # Enthought library imports. from traits.api import Any from traitsui.api import EditorFactory from traitsui.toolkit import toolkit_object Editor = toolkit_object('editor:Editor') class _WidgetEditor(Editor): """ An instance editor that allows total control over widget creation. """ #### '_WidgetEditor' interface ############################################ # The toolkit-specific parent of the editor. parent = Any ########################################################################### # '_WidgetEditor' interface. ########################################################################### def init(self, parent): """ Initialize the editor. """ self.parent = parent # fixme: What if there are no pages?!? page = self.object.pages[0] # Create the editor's control. self.control = page.create_control(parent) # Listen for the page being changed. self.object.on_trait_change(self._on_page_changed, "selected_page") def dispose(self): """ Dispose of the editor. """ page = self.object.selected_page page.destroy_control() def update_editor(self): """ Update the editor. """ pass ########################################################################### # Private interface. ########################################################################### def _on_page_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ if old is not None: old.destroy_control() if new is not None: self.control = new.create_control(self.parent) class WidgetEditor(EditorFactory): """ A factory widget editors. """ ########################################################################### # 'object' interface. ########################################################################### def __call__(self, *args, **traits): """ Call the object. """ return self.trait_set(**traits) ########################################################################### # 'EditorFactory' interface. ########################################################################### def simple_editor(self, ui, object, name, description, parent): """ Create a simple editor. """ editor = _WidgetEditor( parent, factory=self, ui=ui, object=object, name=name, description=description, ) return editor custom_editor = simple_editor text_editor = simple_editor readonly_editor = simple_editor apptools-5.1.0/apptools/preferences/ui/tree_item.py0000644000076500000240000000740313777642667023130 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 generic base-class for items in a tree data structure. An example:- root = TreeItem(data='Root') fruit = TreeItem(data='Fruit') fruit.append(TreeItem(data='Apple', allows_children=False)) fruit.append(TreeItem(data='Orange', allows_children=False)) fruit.append(TreeItem(data='Pear', allows_children=False)) root.append(fruit) veg = TreeItem(data='Veg') veg.append(TreeItem(data='Carrot', allows_children=False)) veg.append(TreeItem(data='Cauliflower', allows_children=False)) veg.append(TreeItem(data='Sprout', allows_children=False)) root.append(veg) """ # Enthought library imports. from traits.api import Any, Bool, HasTraits, Instance, List, Property class TreeItem(HasTraits): """ A generic base-class for items in a tree data structure. """ #### 'TreeItem' interface ################################################# # Does this item allow children? allows_children = Bool(True) # The item's children. children = List(Instance("TreeItem")) # Arbitrary data associated with the item. data = Any # Does the item have any children? has_children = Property(Bool) # The item's parent. parent = Instance("TreeItem") ########################################################################### # 'object' interface. ########################################################################### def __str__(self): """ Returns the informal string representation of the object. """ if self.data is None: s = "" else: s = str(self.data) return s ########################################################################### # 'TreeItem' interface. ########################################################################### #### Properties ########################################################### # has_children def _get_has_children(self): """ True iff the item has children. """ return len(self.children) != 0 #### Methods ############################################################## def append(self, child): """Appends a child to this item. This removes the child from its current parent (if it has one). """ return self.insert(len(self.children), child) def insert(self, index, child): """Inserts a child into this item at the specified index. This removes the child from its current parent (if it has one). """ if child.parent is not None: child.parent.remove(child) child.parent = self self.children.insert(index, child) return child def remove(self, child): """ Removes a child from this item. """ child.parent = None self.children.remove(child) return child def insert_before(self, before, child): """Inserts a child into this item before the specified item. This removes the child from its current parent (if it has one). """ index = self.children.index(before) self.insert(index, child) return (index, child) def insert_after(self, after, child): """Inserts a child into this item after the specified item. This removes the child from its current parent (if it has one). """ index = self.children.index(after) self.insert(index + 1, child) return (index, child) apptools-5.1.0/apptools/preferences/ui/preferences_page.py0000644000076500000240000000742113777642667024450 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 page in a preferences dialog. """ # Enthought library imports. from apptools.preferences.api import PreferencesHelper from traits.api import Any, Dict, Str, provides # Local imports. from .i_preferences_page import IPreferencesPage @provides(IPreferencesPage) class PreferencesPage(PreferencesHelper): """ A page in a preferences dialog. """ #### 'IPreferencesPage' interface ######################################### # The page's category (e.g. 'General/Appearance'). The empty string means # that this is a top-level page. category = Str # DEPRECATED: The help_id was never fully implemented, and it's been # over two years (now 4/2009). The original goal was for the the Help # button to automatically appear and connect to a help page with a # help_id. Not removing the trait right now to avoid breaking code # that may be checking for this. # # Use PreferencesManager.show_help and trait show_help metadata instead. help_id = Str # The page name (this is what is shown in the preferences dialog. name = Str #### Private interface #################################################### # The traits UI that represents the page. _ui = Any # A dictionary containing the traits that have been changed since the # last call to 'apply'. _changed = Dict ########################################################################### # 'IPreferencesPage' interface. ########################################################################### def apply(self): """ Apply the page's preferences. """ path = self._get_path() for trait_name, value in self._changed.items(): if self._is_preference_trait(trait_name): self.preferences.set("%s.%s" % (path, trait_name), value) self._changed.clear() ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ def _anytrait_changed(self, trait_name, old, new): """Static trait change handler. This is an important override! In the base-class when a trait is changed the preferences node is updated too. Here, we stop that from happening and just make a note of what changes have been made. The preferences node gets updated when the 'apply' method is called. """ if self._is_preference_trait(trait_name): self._changed[trait_name] = new elif trait_name.endswith("_items"): # If the trait was a list or dict '_items' trait then just treat it # as if the entire list or dict was changed. trait_name = trait_name[:-6] if self._is_preference_trait(trait_name): self._changed[trait_name] = getattr(self, trait_name) # fixme: Pretty much duplicated in 'PreferencesHelper' (except for the # class name of course!). def _is_preference_trait(self, trait_name): """ Return True if a trait represents a preference value. """ if ( trait_name.startswith("_") or trait_name.endswith("_") or trait_name in PreferencesPage.class_traits() ): return False return trait_name in self.editable_traits() apptools-5.1.0/apptools/preferences/ui/tests/0000755000076500000240000000000013777643025021724 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/preferences/ui/tests/__init__.py0000644000076500000240000000062713777642667024055 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/preferences/ui/tests/test_preferences_page.py0000644000076500000240000000727613777642667026661 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 preferences page. """ import unittest from traits.api import ( Enum, List, Str, pop_exception_handler, push_exception_handler, ) from traitsui.api import Group, Item, View from apptools.preferences.api import Preferences from apptools.preferences.ui.api import PreferencesPage class TestPreferencesPage(unittest.TestCase): """ Non-GUI Tests for PreferencesPage.""" def setUp(self): push_exception_handler(reraise_exceptions=True) self.addCleanup(pop_exception_handler) def test_preferences_page_apply(self): """ Test applying the preferences """ # this sets up imitate Mayavi usage. class MyPreferencesPage(PreferencesPage): # the following set default values for class traits category = "Application" help_id = "" name = "Note" preferences_path = "my_ref.pref" # custom preferences backend = Enum("auto", "simple", "test") traits_view = View(Group(Item("backend"))) preferences = Preferences() pref_page = MyPreferencesPage( preferences=preferences, category="Another Application", help_id="this_wont_be_saved", name="Different Note", # custom preferences backend="simple", ) pref_page.apply() self.assertEqual(preferences.get("my_ref.pref.backend"), "simple") self.assertEqual(preferences.keys("my_ref.pref"), ["backend"]) # this is not saved by virtue of it being static and never assigned to self.assertIsNone(preferences.get("my_ref.pref.traits_view")) # These are skipped because this trait is defined on the # PreferencesPage. self.assertIsNone(preferences.get("my_ref.pref.help_id")) self.assertIsNone(preferences.get("my_ref.pref.category")) self.assertIsNone(preferences.get("my_ref.pref.name")) def test_preferences_page_apply_skip_items_traits(self): """ Test _items traits from List mutation are skipped. """ # Regression test for enthought/apptools#129 class MyPreferencesPage(PreferencesPage): preferences_path = "my_ref.pref" names = List(Str()) preferences = Preferences() pref_page = MyPreferencesPage( preferences=preferences, names=["1"], ) pref_page.names.append("2") pref_page.apply() self.assertEqual(preferences.get("my_ref.pref.names"), str(["1", "2"])) self.assertEqual(preferences.keys("my_ref.pref"), ["names"]) def test_sync_anytrait_items_overload(self): """ Test sychronizing trait with name *_items not to be mistaken as the event trait for mutating list/dict/set """ class MyPreferencesPage(PreferencesPage): preferences_path = Str('my_section') names_items = Str() preferences = Preferences() pref_page = MyPreferencesPage(preferences=preferences) pref_page.names_items = "Hello" pref_page.apply() self.assertEqual( sorted(preferences.keys("my_section")), ["names_items"] ) self.assertEqual( preferences.get("my_section.names_items"), "Hello", ) apptools-5.1.0/apptools/preferences/ui/__init__.py0000644000076500000240000000062713777642667022713 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/preferences/ui/preferences_manager.py0000644000076500000240000002212513777642667025144 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 manager. """ # Enthought library imports. from traits.api import HasTraits, Instance, List, Property, Bool from traitsui.api import Handler, HSplit, Item, TreeEditor from traitsui.api import TreeNode, View, HTMLEditor from traitsui.menu import Action # Local imports. from .preferences_node import PreferencesNode from .preferences_page import PreferencesPage # A tree editor for preferences nodes. tree_editor = TreeEditor( nodes=[ TreeNode( node_for=[PreferencesNode], auto_open=False, children="children", label="name", rename=False, copy=False, delete=False, insert=False, menu=None, ), ], editable=False, hide_root=True, selected="selected_node", show_icons=False, ) class PreferencesHelpWindow(HasTraits): """ Container class to present a view with string info. """ def traits_view(self): """ Default view to show for this class. """ args = [] kw_args = { "title": "Preferences Page Help", "buttons": ["OK"], "width": 800, "height": 800, "resizable": True, "id": "apptools.preferences.ui.preferences_manager.help", } to_show = {} for name, trait_obj in self.traits().items(): if name != "trait_added" and name != "trait_modified": to_show[name] = trait_obj.help for name in to_show: args.append(Item(name, style="readonly", editor=HTMLEditor())) view = View(*args, **kw_args) return view class PreferencesManagerHandler(Handler): """ The traits UI handler for the preferences manager. """ model = Instance(HasTraits) ########################################################################### # 'Handler' interface. ########################################################################### def apply(self, info): """ Handle the **Apply** button being clicked. """ info.object.apply() def init(self, info): """ Initialize the controls of a user interface. """ # Select the first node in the tree (if there is one). self._select_first_node(info) return super(PreferencesManagerHandler, self).init(info) def close(self, info, is_ok): """ Close a dialog-based user interface. """ if is_ok: info.object.apply() return super(PreferencesManagerHandler, self).close(info, is_ok) def preferences_help(self, info): """ Custom preferences help panel. The Traits help doesn't work.""" current_page = self.model.selected_page to_show = {} for trait_name, trait_obj in current_page.traits().items(): if hasattr(trait_obj, "show_help") and trait_obj.show_help: to_show[trait_name] = trait_obj.help help_obj = PreferencesHelpWindow(**to_show) help_obj.edit_traits(kind="livemodal") ########################################################################### # Private interface. ########################################################################### def _select_first_node(self, info): """ Select the first node in the tree (if there is one). """ root = info.object.root if len(root.children) > 0: node = root.children[0] info.object.selected_page = node.page class PreferencesManager(HasTraits): """ The preferences manager. """ # All of the preferences pages known to the manager. pages = List(PreferencesPage) # The root of the preferences node tree. root = Property(Instance(PreferencesNode)) # The preferences node currently selected in the tree. selected_node = Instance(PreferencesNode) # The preferences associated with the currently selected preferences node. selected_page = Instance(PreferencesPage) # Should the custom Info button be shown? If this is True, then an # Info button is shown that pops up a trait view with an HTML entry # for each trait of the *selected_page* with the metadata 'show_help' # set to True. show_help = Bool(False) # Should the Apply button be shown? show_apply = Bool(False) #### Traits UI views ###################################################### def traits_view(self): """ Default traits view for this class. """ help_action = Action(name="Info", action="preferences_help") buttons = ["OK", "Cancel"] if self.show_apply: buttons = ["Apply"] + buttons if self.show_help: buttons = [help_action] + buttons # A tree editor for preferences nodes. tree_editor = TreeEditor( nodes=[ TreeNode( node_for=[PreferencesNode], auto_open=False, children="children", label="name", rename=False, copy=False, delete=False, insert=False, menu=None, ), ], on_select=self._selection_changed, editable=False, hide_root=True, selected="selected_node", show_icons=False, ) view = View( HSplit( Item( name="root", editor=tree_editor, show_label=False, width=250, ), Item( name="selected_page", # editor = WidgetEditor(), show_label=False, width=450, style="custom", ), ), buttons=buttons, handler=PreferencesManagerHandler(model=self), resizable=True, title="Preferences", width=0.3, height=0.3, kind="modal", ) self.selected_page = self.pages[0] return view ########################################################################### # 'PreferencesManager' interface. ########################################################################### #### Trait properties ##################################################### def _get_root(self): """ Property getter. """ # Sort the pages by the length of their category path. This makes it # easy for us to create the preference hierarchy as we know that all of # a node's ancestors will have already been created. def sort_key(a): # We have the guard because if the category is the empty string # then split will still return a list containing one item (and not # the empty list). if len(a.category) == 0: len_a = 0 else: len_a = len(a.category.split("/")) return len_a self.pages.sort(key=sort_key) # Create a corresponding preference node hierarchy (the root of the # hierachy is NOT displayed in the preference dialog). # # fixme: Currently we have to create a dummy page for the root node # event though the root does not get shown in the tree! root_page = PreferencesPage(name="Root", preferences_path="root") root = PreferencesNode(page=root_page) for page in self.pages: # Get the page's parent node. parent = self._get_parent(root, page) # Add a child node representing the page. parent.append(PreferencesNode(page=page)) return root #### Trait change handlers ################################################ def _selection_changed(self, new_selection): self.selected_node = new_selection def _selected_node_changed(self, new): """ Static trait change handler. """ if self.selected_node: self.selected_page = self.selected_node.page #### Methods ############################################################## def apply(self): """ Apply all changes made in the manager. """ for page in self.pages: page.apply() ########################################################################### # Private interface. ########################################################################### def _get_parent(self, root, page): """ Return the page's parent preference node. """ parent = root if len(page.category) > 0: components = page.category.split("/") for component in components: parent = parent.lookup(component) return parent apptools-5.1.0/apptools/preferences/ui/api.py0000644000076500000240000000130113777642667021713 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for the apptools.preferences.ui subpackage. - :class:`~.IPreferencesPage` - :class:`~.PreferencesManager` - :class:`~.PreferencesPage` """ from .i_preferences_page import IPreferencesPage from .preferences_manager import PreferencesManager from .preferences_page import PreferencesPage apptools-5.1.0/apptools/preferences/ui/preferences_node.py0000644000076500000240000000533013777642667024456 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for a node in a preferences dialog. """ # Enthought library imports. from traits.api import Delegate, Instance # Local imports. from .i_preferences_page import IPreferencesPage from .tree_item import TreeItem class PreferencesNode(TreeItem): """Abstract base class for a node in a preferences dialog. A preferences node has a name and an image which are used to represent the node in a preferences dialog (usually in the form of a tree). """ #### 'PreferenceNode' interface ########################################### # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = Delegate("page") # The page name (this is what is shown in the preferences dialog. name = Delegate("page") # The page that we are a node for. page = Instance(IPreferencesPage) ########################################################################### # 'object' interface. ########################################################################### def __str__(self): """ Returns the string representation of the item. """ if self.page is None: s = "root" else: s = self.page.name return s __repr__ = __str__ ########################################################################### # 'PreferencesNode' interface. ########################################################################### def create_page(self, parent): """ Creates the preference page for this node. """ return self.page.create_control(parent) def lookup(self, name): """Returns the child of this node with the specified Id. Returns None if no such child exists. """ for node in self.children: if node.name == name: break else: node = None return node ########################################################################### # Debugging interface. ########################################################################### def dump(self, indent=""): """ Pretty-print the node to stdout. """ print(indent, "Node", str(self)) for child in self.children: child.dump(indent + " ") apptools-5.1.0/apptools/preferences/ui/i_preferences_page.py0000644000076500000240000000211013777642667024746 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pages in a preferences dialog. """ # Enthought library imports. from traits.api import Interface, Str class IPreferencesPage(Interface): """ The interface for pages in a preferences dialog. """ # The page's category (e.g. 'General/Appearence'). The empty string means # that this is a top-level page. category = Str # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = Str # The page name (this is what is shown in the preferences dialog). name = Str def apply(self): """ Apply the page's preferences. """ pass apptools-5.1.0/apptools/preferences/preference_binding.py0000644000076500000240000001371013777642667024344 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 a preference value. """ # Enthought library imports. from traits.api import Any, HasTraits, Instance, Str, Undefined # Local imports. from .i_preferences import IPreferences from .package_globals import get_default_preferences class PreferenceBinding(HasTraits): """ A binding between a trait on an object and a preference value. """ #### 'PreferenceBinding' interface ######################################## # The object that we are binding the preference to. obj = Any # The preferences node used by the binding. If this trait is not set then # the package-global default preferences node is used (and if that is not # set then the binding won't work ;^) preferences = Instance(IPreferences) # The path to the preference value. preference_path = Str # The name of the trait that we are binding the preference to. trait_name = Str ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Constructor. """ super(PreferenceBinding, self).__init__(**traits) # Initialize the object's trait from the preference value. self._set_trait(notify=False) # Wire-up trait change handlers etc. self._initialize() ########################################################################### # 'PreferenceBinding' interface. ########################################################################### #### Trait initializers ################################################### def _preferences_default(self): """ Trait initializer. """ return get_default_preferences() ########################################################################### # Private interface. ########################################################################### #### Trait change handlers ################################################ def _on_trait_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ self.preferences.set(self.preference_path, new) #### Other observer pattern listeners ##################################### def _preferences_listener(self, node, key, old, new): """ Listener called when a preference value is changed. """ components = self.preference_path.split(".") if key == components[-1]: self._set_trait() #### Methods ############################################################## # fixme: This method is mostly duplicated in 'PreferencesHelper' (the only # difference is the line that gets the handler). def _get_value(self, trait_name, value): """Get the actual value to set. This method makes sure that any required work is done to convert the preference value from a string. """ handler = self.obj.trait(trait_name).handler # If the trait type is 'Str' then we just take the raw value. if type(handler) is Str: pass # If the trait type is 'Str' then we convert the raw value. elif type(handler) is Str: value = str(value) # Otherwise, we eval it! else: try: value = eval(value) # If the eval fails then there is probably a syntax error, but # we will let the handler validation throw the exception. except Exception: pass return handler.validate(self, trait_name, value) def _initialize(self): """ Wire-up trait change handlers etc. """ # Listen for the object's trait being changed. self.obj.on_trait_change(self._on_trait_changed, self.trait_name) # Listen for the preference value being changed. components = self.preference_path.split(".") node = ".".join(components[:-1]) self.preferences.add_preferences_listener( self._preferences_listener, node ) def _set_trait(self, notify=True): """ Set the object's trait to the value of the preference. """ value = self.preferences.get(self.preference_path, Undefined) if value is not Undefined: trait_value = self._get_value(self.trait_name, value) traits = {self.trait_name: trait_value} self.obj.trait_set(trait_change_notify=notify, **traits) # Factory function for creating bindings. def bind_preference(obj, trait_name, preference_path, preferences=None): """ Create a new preference binding. """ # This may seem a bit wierd, but we manually build up a dictionary of # the traits that need to be set at the time the 'PreferenceBinding' # instance is created. # # This is because we only want to set the 'preferences' trait iff one # is explicitly specified. If we passed it in with the default argument # value of 'None' then it counts as 'setting' the trait which prevents # the binding instance from defaulting to the package-global preferences. # Also, if we try to set the 'preferences' trait *after* construction time # then it is too late as the binding initialization is done in the # constructor (we could of course split that out, which may be the 'right' # way to do it ;^). traits = { "obj": obj, "trait_name": trait_name, "preference_path": preference_path, } if preferences is not None: traits["preferences"] = preferences return PreferenceBinding(**traits) apptools-5.1.0/apptools/preferences/i_preferences.py0000644000076500000240000001157413777642667023353 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 a node in a preferences hierarchy. """ # Enthought library imports. from traits.api import Instance, Interface, Str class IPreferences(Interface): """ The interface for a node in a preferences hierarchy. """ # The absolute path to this node from the root node (the empty string if # this node *is* the root node). path = Str # The parent node (None if this node *is* the root node). parent = Instance("IPreferences") # The name of the node relative to its parent (the empty string if this # node *is* the root node). name = Str #### Methods where 'path' refers to a preference #### def get(self, path, default=None, inherit=False): """Get the value of the preference at the specified path. If no value exists for the path (or any part of the path does not exist) then return the default value. Preference values are *always* returned as strings. e.g:: preferences.set('acme.ui.bgcolor', 'blue') preferences.get('acme.ui.bgcolor') -> 'blue' preferences.set('acme.ui.width', 100) preferences.get('acme.ui.width') -> '100' preferences.set('acme.ui.visible', True) preferences.get('acme.ui.visible') -> 'True' If 'inherit' is True then we allow 'inherited' preference values. e.g. If we are looking up:: 'acme.ui.widget.bgcolor' and it does not exist then we will also try:: 'acme.ui.bgcolor' 'acme.bgcolor' 'bgcolor' Raise a 'ValueError' exception if the path is the empty string. """ def remove(self, path): """Remove the preference at the specified path. Does nothing if no value exists for the path (or any part of the path does not exist. Raise a 'ValueError' exception if the path is the empty string. e.g.:: preferences.remove('acme.ui.bgcolor') """ def set(self, path, value): """Set the value of the preference at the specified path. Any missing nodes are created automatically. Primitive Python types can be set, but preferences are *always* stored and returned as strings. e.g:: preferences.set('acme.ui.bgcolor', 'blue') preferences.get('acme.ui.bgcolor') -> 'blue' preferences.set('acme.ui.width', 100) preferences.get('acme.ui.width') -> '100' preferences.set('acme.ui.visible', True) preferences.get('acme.ui.visible') -> 'True' Raise a 'ValueError' exception if the path is the empty string. """ #### Methods where 'path' refers to a node #### def clear(self, path=""): """Remove all preference from the node at the specified path. If the path is the empty string (the default) then remove the preferences in *this* node. This does not affect any of the node's children. e.g. To clear the preferences out of a node directly:: preferences.clear() Or to clear the preferences of a node at a given path:: preferences.clear('acme.ui') """ def keys(self, path=""): """Return the preference keys of the node at the specified path. If the path is the empty string (the default) then return the preference keys of *this* node. e.g:: keys = preferences.keys('acme.ui') """ def node(self, path=""): """Return the node at the specified path. If the path is the empty string (the default) then return *this* node. Any missing nodes are created automatically. e.g:: node = preferences.node('acme.ui') bgcolor = node.get('bgcolor') """ def node_exists(self, path=""): """Return True if the node at the specified path exists If the path is the empty string (the default) then return True. e.g:: exists = preferences.exists('acme.ui') """ def node_names(self, path=""): """Return the names of the children of the node at the specified path. If the path is the empty string (the default) then return the names of the children of *this* node. e.g:: names = preferences.node_names('acme.ui') """ #### Persistence methods #### def flush(self): """Force any changes in the node to the backing store. This includes any changes to the node's descendants. """ apptools-5.1.0/apptools/preferences/scoped_preferences.py0000644000076500000240000003431513777642667024376 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 preferences node that adds the notion of preferences scopes. """ # Standard library imports. from os.path import join # Enthought library imports. from traits.etsconfig.api import ETSConfig from traits.api import List, Str, Undefined # Local imports. from .i_preferences import IPreferences from .preferences import Preferences class ScopedPreferences(Preferences): """A preferences node that adds the notion of preferences scopes. Scopes provide a way to access preferences in a precedence order, usually depending on where they came from, for example from the command-line, or set by the user in a preferences file, or the defaults (set by the developer). By default, this class provides two scopes - 'application' which is persistent and 'default' which is not. Path names passed to 'ScopedPreferences' nodes can be either:: a) a preference path as used in a standard 'Preferences' node, e.g:: 'acme.widget.bgcolor'. In this case the operation either takes place in the primary scope (for operations such as 'set' etc), or on all scopes in precedence order (for operations such as 'get' etc). or b) a preference path that refers to a specific scope e.g:: 'default/acme.widget.bgcolor' In this case the operation takes place *only* in the specified scope. There is one drawback to this scheme. If you want to access a scope node itself via the 'clear', 'keys', 'node', 'node_exists' or 'node_names' methods then you have to append a trailing '/' to the path. Without that, the node would try to perform the operation in the primary scope. e.g. To get the names of the children of the 'application' scope, use:: scoped.node_names('application/') If you did this:: scoped.node_names('application') Then the node would get the primary scope and try to find its child node called 'application'. Of course you can just get the scope via:: application_scope = scoped.get_scope('application') and then call whatever methods you like on it - which is definitely more intentional and is highly recommended:: application_scope.node_names() """ #### 'ScopedPreferences' interface ######################################## # The file that the application scope preferences are stored in. # # Defaults to:- # # os.path.join(ETSConfig.application_home, 'preferences.ini') application_preferences_filename = Str # The scopes (in the order that they should be searched when looking up # preferences). # # By default, this class provides two scopes - 'application' which is # persistent and 'default' which is not. scopes = List(IPreferences) # The name of the 'primary' scope. # # This is the scope that operations take place in if no scope is specified # in a given path (for the 'get' operation, if no scope is specified the # operation takes place in *all* scopes in order of precedence). If this is # the empty string (the default) then the primary scope is the first scope # in the 'scopes' list. primary_scope_name = Str ########################################################################### # 'IPreferences' protocol. ########################################################################### #### Methods where 'path' refers to a preference #### def get(self, path, default=None, inherit=False): """ Get the value of the preference at the specified path. """ if len(path) == 0: raise ValueError("empty path") # If the path contains a specific scope then lookup the preference in # just that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, try each scope in turn (i.e. in order of precedence). else: nodes = self.scopes # Try all nodes first (without inheritance even if specified). value = self._get(path, Undefined, nodes, inherit=False) if value is Undefined: if inherit: value = self._get(path, default, nodes, inherit=True) else: value = default return value def remove(self, path): """ Remove the preference at the specified path. """ if len(path) == 0: raise ValueError("empty path") # If the path contains a specific scope then remove the preference from # just that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, remove the preference from the primary scope. else: node = self._get_primary_scope() node.remove(path) def set(self, path, value): """ Set the value of the preference at the specified path. """ if len(path) == 0: raise ValueError("empty path") # If the path contains a specific scope then set the value in that # scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, set the value in the primary scope. else: node = self._get_primary_scope() node.set(path, value) #### Methods where 'path' refers to a node #### def clear(self, path=""): """ Remove all preference from the node at the specified path. """ # If the path contains a specific scope then remove the preferences # from a node in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, remove the preferences from a node in the primary scope. else: node = self._get_primary_scope() return node.clear(path) def keys(self, path=""): """ Return the preference keys of the node at the specified path. """ # If the path contains a specific scope then get the keys of the node # in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, merge the keys of the node in all scopes. else: nodes = self.scopes keys = set() for node in nodes: keys.update(node.node(path).keys()) return list(keys) def node(self, path=""): """ Return the node at the specified path. """ if len(path) == 0: node = self else: # If the path contains a specific scope then we get the node that # scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, get the node from the primary scope. else: node = self._get_primary_scope() node = node.node(path) return node def node_exists(self, path=""): """ Return True if the node at the specified path exists. """ # If the path contains a specific scope then look for the node in that # scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) node = self._get_scope(scope_name) # Otherwise, look for the node in the primary scope. else: node = self._get_primary_scope() return node.node_exists(path) def node_names(self, path=""): """Return the names of the children of the node at the specified path. """ # If the path contains a specific scope then get the names of the # children of the node in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, merge the names of the children of the node in all scopes. else: nodes = self.scopes names = set() for node in nodes: names.update(node.node(path).node_names()) return list(names) ########################################################################### # 'Preferences' protocol. ########################################################################### #### Listener methods #### def add_preferences_listener(self, listener, path=""): """ Add a listener for changes to a node's preferences. """ # If the path contains a specific scope then add a preferences listener # to the node in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, add a preferences listener to the node in all scopes. else: nodes = self.scopes for node in nodes: node.add_preferences_listener(listener, path) def remove_preferences_listener(self, listener, path=""): """ Remove a listener for changes to a node's preferences. """ # If the path contains a specific scope then remove a preferences # listener from the node in that scope. if self._path_contains_scope(path): scope_name, path = self._parse_path(path) nodes = [self._get_scope(scope_name)] # Otherwise, remove a preferences listener from the node in all scopes. else: nodes = self.scopes for node in nodes: node.remove_preferences_listener(listener, path) #### Persistence methods #### def load(self, file_or_filename=None): """Load preferences from a file. This loads the preferences into the primary scope. fixme: I'm not sure it is worth providing an implentation here. I think it would be better to encourage people to explicitly reference a particular scope. """ if file_or_filename is None and len(self.filename) > 0: file_or_filename = self.filename node = self._get_primary_scope() node.load(file_or_filename) def save(self, file_or_filename=None): """Save the node's preferences to a file. This asks each scope in turn to save its preferences. If a file or filename is specified then it is only passed to the primary scope. """ if file_or_filename is None and len(self.filename) > 0: file_or_filename = self.filename self._get_primary_scope().save(file_or_filename) for scope in self.scopes: if scope is not self._get_primary_scope(): scope.save() ########################################################################### # 'ScopedPreferences' protocol. ########################################################################### def _application_preferences_filename_default(self): """ Trait initializer. """ return join(ETSConfig.application_home, "preferences.ini") # fixme: In hindsight, I don't think this class should have provided # default scopes. This should have been an 'abstract' class that could # be subclassed by classes providing specific scopes. def _scopes_default(self): """ Trait initializer. """ scopes = [ Preferences( name="application", filename=self.application_preferences_filename, ), Preferences(name="default"), ] return scopes def get_scope(self, scope_name): """Return the scope with the specified name. Return None if no such scope exists. """ for scope in self.scopes: if scope_name == scope.name: break else: scope = None return scope ########################################################################### # Private protocol. ########################################################################### def _get(self, path, default, nodes, inherit): """ Get a preference from a list of nodes. """ for node in nodes: value = node.get(path, Undefined, inherit) if value is not Undefined: break else: value = default return value def _get_scope(self, scope_name): """Return the scope with the specified name. Raise a 'ValueError' is no such scope exists. """ scope = self.get_scope(scope_name) if scope is None: raise ValueError("no such scope %s" % scope_name) return scope def _get_primary_scope(self): """Return the primary scope. By default, this is the first scope. """ if len(self.primary_scope_name) > 0: scope = self._get_scope(self.primary_scope_name) else: scope = self.scopes[0] return scope def _path_contains_scope(self, path): """ Return True if the path contains a scope component. """ return "/" in path def _parse_path(self, path): """ 'Parse' the path into two parts, the scope name and the rest! """ components = path.split("/") return components[0], "/".join(components[1:]) ########################################################################### # Debugging interface. ########################################################################### def dump(self, indent=""): """ Dump the preferences hierarchy to stdout. """ if indent == "": print() print(indent, "Node(%s)" % self.name, self._preferences) indent += " " for child in self.scopes: child.dump(indent) apptools-5.1.0/apptools/preferences/tests/0000755000076500000240000000000013777643025021307 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/preferences/tests/example.ini0000644000076500000240000000031013777642667023450 0ustar aayresstaff00000000000000[acme.ui] bgcolor = blue width = 50 ratio = 1.0 visible = True description = 'acme ui' offsets = "[1, 2, 3, 4]" names = "['joe', 'fred', 'jane']" [acme.ui.splash_screen] image = splash fgcolor = red apptools-5.1.0/apptools/preferences/tests/test_preferences.py0000644000076500000240000004457013777642667025246 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 preferences nodes. """ # Standard library imports. import os import tempfile import unittest from os.path import join # Major package imports. from importlib_resources import files # Enthought library imports. from apptools.preferences.api import Preferences # This module's package. PKG = "apptools.preferences.tests" class PreferencesTestCase(unittest.TestCase): """ Tests for preferences nodes. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.preferences = Preferences() # The filename of the example preferences file. self.example = os.fspath(files(PKG) / "example.ini") # A temporary directory that can safely be written to. self.tmpdir = tempfile.mkdtemp() def tearDown(self): """ Called immediately after each test method has been called. """ # Remove the temporary directory. os.rmdir(self.tmpdir) ########################################################################### # Tests. ########################################################################### def test_package_global_default_preferences(self): """ package global default preferences """ from apptools.preferences.api import get_default_preferences from apptools.preferences.api import set_default_preferences set_default_preferences(self.preferences) self.assertEqual(self.preferences, get_default_preferences()) def test_get_and_set_str(self): """ get and set str """ p = self.preferences # Set a string preference. p.set("acme.ui.bgcolor", "blue") self.assertEqual("blue", p.get("acme.ui.bgcolor")) def test_get_and_set_int(self): """ get and set int """ p = self.preferences # Note that we can pass an actual 'int' to 'set', but the preference # manager *always* returns preference values as strings. p.set("acme.ui.width", 50) self.assertEqual("50", p.get("acme.ui.width")) def test_get_and_set_float(self): """ get and set float """ p = self.preferences # Note that we can pass an actual 'flaot' to 'set', but the preference # manager *always* returns preference values as strings. p.set("acme.ui.ratio", 1.0) self.assertEqual("1.0", p.get("acme.ui.ratio")) def test_get_and_set_bool(self): """ get and set bool """ p = self.preferences # Note that we can pass an actual 'bool' to 'set', but the preference # manager *always* returns preference values as strings. p.set("acme.ui.visible", True) self.assertEqual("True", p.get("acme.ui.visible")) def test_get_and_set_list_of_str(self): """ get and set list of str """ p = self.preferences # Note that we can pass an actual 'int' to 'set', but the preference # manager *always* returns preference values as strings. p.set("acme.ui.names", ["fred", "wilma", "barney"]) self.assertEqual("['fred', 'wilma', 'barney']", p.get("acme.ui.names")) def test_get_and_set_list_of_int(self): """ get and set list of int """ p = self.preferences # Note that we can pass an actual 'int' to 'set', but the preference # manager *always* returns preference values as strings. p.set("acme.ui.offsets", [1, 2, 3]) self.assertEqual("[1, 2, 3]", p.get("acme.ui.offsets")) def test_empty_path(self): """ empty path """ p = self.preferences self.assertRaises(ValueError, p.get, "") self.assertRaises(ValueError, p.remove, "") self.assertRaises(ValueError, p.set, "", "a value") def test_default_values(self): """ default values """ p = self.preferences # Try non-existent names to get the default-default! self.assertIsNone(p.get("bogus")) self.assertIsNone(p.get("acme.bogus")) self.assertIsNone(p.get("acme.ui.bogus")) # Try non-existent names to get the specified default. self.assertEqual("a value", p.get("bogus", "a value")) self.assertEqual("a value", p.get("acme.bogus", "a value")) self.assertEqual("a value", p.get("acme.ui.bogus", "a value")) def test_keys(self): """ keys """ p = self.preferences # It should be empty to start with! self.assertEqual([], list(p.keys())) # Set some preferences in the node. p.set("a", "1") p.set("b", "2") p.set("c", "3") keys = sorted(p.keys()) self.assertEqual(["a", "b", "c"], keys) # Set some preferences in a child node. p.set("acme.a", "1") p.set("acme.b", "2") p.set("acme.c", "3") keys = sorted(p.keys("acme")) self.assertEqual(["a", "b", "c"], keys) # And, just to be sure, in a child of the child node ;^) p.set("acme.ui.a", "1") p.set("acme.ui.b", "2") p.set("acme.ui.c", "3") keys = sorted(p.keys("acme.ui")) self.assertEqual(["a", "b", "c"], keys) # Test keys of a non-existent node. self.assertEqual([], p.keys("bogus")) self.assertEqual([], p.keys("bogus.blargle")) self.assertEqual([], p.keys("bogus.blargle.foogle")) def test_node(self): """ node """ p = self.preferences # Try an empty path. self.assertEqual(p, p.node()) # Try a simple path. node = p.node("acme") self.assertIsNotNone(node) self.assertEqual("acme", node.name) self.assertEqual("acme", node.path) self.assertEqual(p, node.parent) # Make sure we get the same node each time we ask for it! self.assertEqual(node, p.node("acme")) # Try a nested path. node = p.node("acme.ui") self.assertIsNotNone(node) self.assertEqual("ui", node.name) self.assertEqual("acme.ui", node.path) self.assertEqual(p.node("acme"), node.parent) # And just to be sure, a really nested path. node = p.node("acme.ui.splash_screen") self.assertIsNotNone(node) self.assertEqual("splash_screen", node.name) self.assertEqual("acme.ui.splash_screen", node.path) self.assertEqual(p.node("acme.ui"), node.parent) def test_node_exists(self): """ node exists """ p = self.preferences self.assertTrue(p.node_exists()) self.assertFalse(p.node_exists("acme")) p.node("acme") self.assertTrue(p.node_exists("acme")) def test_node_names(self): """ node names """ p = self.preferences # It should be empty to start with! self.assertEqual([], p.node_names()) # Add some nodes. p.node("a") p.node("b") p.node("c") names = sorted(p.node_names()) self.assertEqual(["a", "b", "c"], names) # Creatd some nodes in a child node. p.node("acme.a") p.node("acme.b") p.node("acme.c") names = sorted(p.node_names("acme")) self.assertEqual(["a", "b", "c"], names) # And, just to be sure, in a child of the child node ;^) p.node("acme.ui.a") p.node("acme.ui.b") p.node("acme.ui.c") names = sorted(p.node_names("acme.ui")) self.assertEqual(["a", "b", "c"], names) # Test keys of a non-existent node. self.assertEqual([], p.node_names("bogus")) self.assertEqual([], p.node_names("bogus.blargle")) self.assertEqual([], p.node_names("bogus.blargle.foogle")) def test_clear(self): """ clear """ p = self.preferences # Set some values. p.set("acme.ui.bgcolor", "blue") self.assertEqual("blue", p.get("acme.ui.bgcolor")) p.set("acme.ui.width", 100) self.assertEqual("100", p.get("acme.ui.width")) # Clear all preferences from the node. p.clear("acme.ui") self.assertIsNone(p.get("acme.ui.bgcolor")) self.assertIsNone(p.get("acme.ui.width")) self.assertEqual(0, len(p.keys("acme.ui"))) def test_remove(self): """ remove """ p = self.preferences # Set a value. p.set("acme.ui.bgcolor", "blue") self.assertEqual("blue", p.get("acme.ui.bgcolor")) # Remove it. p.remove("acme.ui.bgcolor") self.assertIsNone(p.get("acme.ui.bgcolor")) # Make sure we can't remove nodes! p.remove("acme.ui") self.assertTrue(p.node_exists("acme.ui")) def test_flush(self): """ flush """ p = self.preferences # A temporary .ini file for this test. tmp = join(self.tmpdir, "tmp.ini") # This could be set in the constructor of course, its just here we # want to use the instance declared in 'setUp'. p.filename = tmp try: # Load the preferences from an 'ini' file. p.load(self.example) # Flush it. p.flush() # Load it into a new node. p = Preferences() p.load(tmp) # Make sure it was all loaded! self.assertEqual("blue", p.get("acme.ui.bgcolor")) self.assertEqual("50", p.get("acme.ui.width")) self.assertEqual("1.0", p.get("acme.ui.ratio")) self.assertEqual("True", p.get("acme.ui.visible")) self.assertEqual("acme ui", p.get("acme.ui.description")) self.assertEqual("[1, 2, 3, 4]", p.get("acme.ui.offsets")) self.assertEqual("['joe', 'fred', 'jane']", p.get("acme.ui.names")) self.assertEqual("splash", p.get("acme.ui.splash_screen.image")) self.assertEqual("red", p.get("acme.ui.splash_screen.fgcolor")) finally: # Clean up! os.remove(tmp) def test_load(self): """ load """ p = self.preferences # Load the preferences from an 'ini' file. p.load(self.example) # Make sure it was all loaded! self.assertEqual("blue", p.get("acme.ui.bgcolor")) self.assertEqual("50", p.get("acme.ui.width")) self.assertEqual("1.0", p.get("acme.ui.ratio")) self.assertEqual("True", p.get("acme.ui.visible")) self.assertEqual("acme ui", p.get("acme.ui.description")) self.assertEqual("[1, 2, 3, 4]", p.get("acme.ui.offsets")) self.assertEqual("['joe', 'fred', 'jane']", p.get("acme.ui.names")) self.assertEqual("splash", p.get("acme.ui.splash_screen.image")) self.assertEqual("red", p.get("acme.ui.splash_screen.fgcolor")) def test_load_with_filename_trait_set(self): """ load with filename trait set """ p = self.preferences p.filename = self.example # Load the preferences from an 'ini' file. p.load() # Make sure it was all loaded! self.assertEqual("blue", p.get("acme.ui.bgcolor")) self.assertEqual("50", p.get("acme.ui.width")) self.assertEqual("1.0", p.get("acme.ui.ratio")) self.assertEqual("True", p.get("acme.ui.visible")) self.assertEqual("acme ui", p.get("acme.ui.description")) self.assertEqual("[1, 2, 3, 4]", p.get("acme.ui.offsets")) self.assertEqual("['joe', 'fred', 'jane']", p.get("acme.ui.names")) self.assertEqual("splash", p.get("acme.ui.splash_screen.image")) self.assertEqual("red", p.get("acme.ui.splash_screen.fgcolor")) p = self.preferences # Load the preferences from an 'ini' file. p.load(self.example) # Make sure it was all loaded! self.assertEqual("blue", p.get("acme.ui.bgcolor")) self.assertEqual("50", p.get("acme.ui.width")) self.assertEqual("1.0", p.get("acme.ui.ratio")) self.assertEqual("True", p.get("acme.ui.visible")) self.assertEqual("acme ui", p.get("acme.ui.description")) self.assertEqual("[1, 2, 3, 4]", p.get("acme.ui.offsets")) self.assertEqual("['joe', 'fred', 'jane']", p.get("acme.ui.names")) self.assertEqual("splash", p.get("acme.ui.splash_screen.image")) self.assertEqual("red", p.get("acme.ui.splash_screen.fgcolor")) def test_save(self): """ save """ p = self.preferences # Load the preferences from an 'ini' file. p.load(self.example) # Make sure it was all loaded! self.assertEqual("blue", p.get("acme.ui.bgcolor")) self.assertEqual("50", p.get("acme.ui.width")) self.assertEqual("1.0", p.get("acme.ui.ratio")) self.assertEqual("True", p.get("acme.ui.visible")) self.assertEqual("acme ui", p.get("acme.ui.description")) self.assertEqual("[1, 2, 3, 4]", p.get("acme.ui.offsets")) self.assertEqual("['joe', 'fred', 'jane']", p.get("acme.ui.names")) self.assertEqual("splash", p.get("acme.ui.splash_screen.image")) self.assertEqual("red", p.get("acme.ui.splash_screen.fgcolor")) # Make a change. p.set("acme.ui.bgcolor", "yellow") # Save it to another file. tmp = join(self.tmpdir, "tmp.ini") p.save(tmp) try: # Load it into a new node. p = Preferences() p.load(tmp) # Make sure it was all loaded! self.assertEqual("yellow", p.get("acme.ui.bgcolor")) self.assertEqual("50", p.get("acme.ui.width")) self.assertEqual("1.0", p.get("acme.ui.ratio")) self.assertEqual("True", p.get("acme.ui.visible")) self.assertEqual("acme ui", p.get("acme.ui.description")) self.assertEqual("[1, 2, 3, 4]", p.get("acme.ui.offsets")) self.assertEqual("['joe', 'fred', 'jane']", p.get("acme.ui.names")) self.assertEqual("splash", p.get("acme.ui.splash_screen.image")) self.assertEqual("red", p.get("acme.ui.splash_screen.fgcolor")) finally: # Clean up! os.remove(tmp) def SKIPtest_dump(self): """ dump """ # This make look like a weird test, since we don't ever actually check # anything, but it is useful for people to see the structure of a # preferences hierarchy. p = self.preferences # Load the preferences from an 'ini' file. p.load(self.example) p.dump() def test_get_inherited(self): """ get inherited """ p = self.preferences # Set a string preference. p.set("bgcolor", "red") p.set("acme.bgcolor", "green") p.set("acme.ui.bgcolor", "blue") self.assertEqual("blue", p.get("acme.ui.bgcolor", inherit=True)) # Now remove the 'lowest' layer. p.remove("acme.ui.bgcolor") self.assertEqual("green", p.get("acme.ui.bgcolor", inherit=True)) # And the next one. p.remove("acme.bgcolor") self.assertEqual("red", p.get("acme.ui.bgcolor", inherit=True)) # And the last one. p.remove("bgcolor") self.assertEqual(None, p.get("acme.ui.bgcolor", inherit=True)) def test_add_listener(self): """ add listener """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new # Add a listener. p.add_preferences_listener(listener, "acme.ui") # Set a value and make sure the listener was called. p.set("acme.ui.bgcolor", "blue") self.assertEqual(p.node("acme.ui"), listener.node) self.assertEqual("bgcolor", listener.key) self.assertIsNone(listener.old) self.assertEqual("blue", listener.new) # Set it to another value to make sure we get the 'old' value # correctly. p.set("acme.ui.bgcolor", "red") self.assertEqual(p.node("acme.ui"), listener.node) self.assertEqual("bgcolor", listener.key) self.assertEqual("blue", listener.old) self.assertEqual("red", listener.new) def test_remove_listener(self): """ remove listener """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new # Add a listener. p.add_preferences_listener(listener, "acme.ui") # Set a value and make sure the listener was called. p.set("acme.ui.bgcolor", "blue") self.assertEqual(p.node("acme.ui"), listener.node) self.assertEqual("bgcolor", listener.key) self.assertIsNone(listener.old) self.assertEqual("blue", listener.new) # Remove the listener. p.remove_preferences_listener(listener, "acme.ui") # Set a value and make sure the listener was *not* called. listener.node = None p.set("acme.ui.bgcolor", "blue") self.assertIsNone(listener.node) def test_set_with_same_value(self): """ set with same value """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new # Add a listener. p.add_preferences_listener(listener, "acme.ui") # Set a value and make sure the listener was called. p.set("acme.ui.bgcolor", "blue") self.assertEqual(p.node("acme.ui"), listener.node) self.assertEqual("bgcolor", listener.key) self.assertIsNone(listener.old) self.assertEqual("blue", listener.new) # Clear out the listener. listener.node = None # Set the same value and make sure the listener *doesn't* get called. p.set("acme.ui.bgcolor", "blue") self.assertIsNone(listener.node) apptools-5.1.0/apptools/preferences/tests/py_config_file.py0000644000076500000240000002007113777642667024650 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Python based configuration file with hierarchical sections. """ class PyConfigFile(dict): """ A Python based configuration file with hierarchical sections. """ ########################################################################### # 'object' interface. ########################################################################### def __init__(self, file_or_filename=None): """Constructor. If 'file_or_filename' is specified it will be loaded immediately. It can be either:- a) a filename b) a file-like object that must be open for reading """ # A dictionary containing one namespace instance for each root of the # config hierarchy (see the '_Namespace' class for more details). # # e.g. If the following sections have been loaded:- # # [acme.foo] # ... # [acme.bar] # ... # [tds] # ... # [tds.baz] # ... # # Then the dictionary will contain:- # # {'acme' : , 'tds' : } # self._namespaces = {} if file_or_filename is not None: self.load(file_or_filename) ########################################################################### # 'PyConfigFile' interface. ########################################################################### def load(self, file_or_filename): """Load the configuration from a file. 'file_or_filename' can be either:- a) a filename b) a file-like object that must be open for reading """ # Get an open file to read from. f = self._get_file(file_or_filename) section_name = None section_body = "" for line in f: stripped = line.strip() # Is this line a section header? # # If so then parse the preceding section (if there is one) and # start collecting the body of the new section. if stripped.startswith("[") and stripped.endswith("]"): if section_name is not None: self._parse_section(section_name, section_body) section_name = stripped[1:-1] section_body = "" # Otherwise, this is *not* a section header so add the line to the # body of the current section. If there is no current section then # we simply ignore it! else: if section_name is not None: section_body += line # Parse the last section in the file. if section_name is not None: self._parse_section(section_name, section_body) f.close() def save(self, file_or_filename): """Save the configuration to a file. 'file_or_filename' can be either:- a) a filename b) a file-like object that must be open for writing """ f = self._get_file(file_or_filename, "w") for section_name, section_data in self.items(): self._write_section(f, section_name, section_data) f.close() ########################################################################### # Private interface. ########################################################################### def _get_file(self, file_or_filename, mode="r"): """Return an open file object from a file or a filename. The mode is only used if a filename is specified. """ if isinstance(file_or_filename, str): f = open(file_or_filename, mode) else: f = file_or_filename return f def _get_namespace(self, section_name): """ Return the namespace that represents the section. """ components = section_name.split(".") namespace = self._namespaces.setdefault(components[0], _Namespace()) for component in components[1:]: namespace = getattr(namespace, component) return namespace def _parse_section(self, section_name, section_body): """Parse a section. In this implementation, we don't actually 'parse' anything - we just execute the body of the section as Python code ;^) """ # If this is the first time that we have come across the section then # start with an empty dictionary for its contents. Otherwise, we will # update its existing contents. section = self.setdefault(section_name, {}) # Execute the Python code in the section dictionary. # # We use 'self._namespaces' as the globals for the code execution so # that config values can refer to other config values using familiar # Python syntax (see the '_Namespace' class for more details). # # e.g. # # [acme.foo] # bar = 1 # baz = 99 # # [acme.blargle] # blitzel = acme.foo.bar + acme.foo.baz exec(section_body, self._namespaces, section) # The '__builtins__' dictionary gets added to 'self._namespaces' as # by the call to 'exec'. However, we want 'self._namespaces' to only # contain '_Namespace' instances, so we do the cleanup here. del self._namespaces["__builtins__"] # Get the section's corresponding node in the 'dotted' namespace and # update it with the config values. namespace = self._get_namespace(section_name) namespace.__dict__.update(section) def _write_section(self, f, section_name, section_data): """ Write a section to a file. """ f.write("[%s]\n" % section_name) for name, value in section_data.items(): f.write("%s = %s\n" % (name, repr(value))) f.write("\n") ########################################################################### # Debugging interface. ########################################################################### def _pretty_print_namespaces(self): """ Pretty print the 'dotted' namespaces. """ for name, value in self._namespaces.items(): print("Namespace:", name) value.pretty_print(" ") ############################################################################### # Internal use only. ############################################################################### class _Namespace(object): """An object that represents a node in a dotted namespace. We build up a dotted namespace so that config values can refer to other config values using familiar Python syntax. e.g. [acme.foo] bar = 1 baz = 99 [acme.blargle] blitzel = acme.foo.bar + acme.foo.baz """ ########################################################################### # 'object' interface. ########################################################################### def __getattr__(self, name): """ Return the attribute with the specified name. """ # This looks a little weird, but we are simply creating the next level # in the namespace hierarchy 'on-demand'. namespace = self.__dict__[name] = _Namespace() return namespace ########################################################################### # Debugging interface. ########################################################################### def pretty_print(self, indent=""): """ Pretty print the namespace. """ for name, value in self.__dict__.items(): if isinstance(value, _Namespace): print(indent, "Namespace:", name) value.pretty_print(indent + " ") else: print(indent, name, ":", value) apptools-5.1.0/apptools/preferences/tests/py_config_example.ini0000644000076500000240000000137713777642667025523 0ustar aayresstaff00000000000000[acme.ui] bgcolor = "blue" width = 50 ratio = 1.0 visible = True foo = { 'a' : 1, 'b' : 2 } bar = [ 1, 2, 3, 4 ] baz = ( 1, 'a', 6, 4 ) [acme.ui.splash_screen] image = "splash" fgcolor = "red" # You can also reference a previous setting as in the following example, but # note that if you *write* these settings back out again, then any reference # to another setting is lost - just the literal value gets written. # # e.g. The following section would be written as:- # # [acme.ui.other] # fred = "red" # wilma = 100 # [acme.ui.other] fred = acme.ui.splash_screen.fgcolor wilma = acme.ui.foo['a'] + 99 # To show that not every section needs to be from the same root! [tds.foogle] joe = 90 # A non-dotted section name. [simples] animal = "meerkat"apptools-5.1.0/apptools/preferences/tests/__init__.py0000644000076500000240000000062713777642667023440 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/preferences/tests/test_py_config_file.py0000644000076500000240000001572113777642667025715 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Python-esque '.ini' files. """ # Standard library imports. import os import tempfile import unittest from os.path import join # Major package imports. from importlib_resources import files # Enthought library imports. from .py_config_file import PyConfigFile # This module's package. PKG = "apptools.preferences.tests" class PyConfigFileTestCase(unittest.TestCase): """ Tests for Python-esque '.ini' files. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ # The filenames of the example preferences files. self.example = os.fspath(files(PKG) / "py_config_example.ini") self.example_2 = os.fspath(files(PKG) / "py_config_example_2.ini") def tearDown(self): """ Called immediately after each test method has been called. """ ########################################################################### # Tests. ########################################################################### def test_load_from_filename(self): """ load from filename """ config = PyConfigFile(self.example) self.assertEqual("blue", config["acme.ui"]["bgcolor"]) self.assertEqual(50, config["acme.ui"]["width"]) self.assertEqual(1.0, config["acme.ui"]["ratio"]) self.assertEqual(True, config["acme.ui"]["visible"]) self.assertEqual({"a": 1, "b": 2}, config["acme.ui"]["foo"]) self.assertEqual([1, 2, 3, 4], config["acme.ui"]["bar"]) self.assertEqual((1, "a", 6, 4), config["acme.ui"]["baz"]) self.assertEqual("red", config["acme.ui.other"]["fred"]) self.assertEqual(100, config["acme.ui.other"]["wilma"]) self.assertEqual(90, config["tds.foogle"]["joe"]) self.assertEqual("meerkat", config["simples"]["animal"]) def test_load_from_file(self): """ load from file """ config = PyConfigFile(open(self.example)) self.assertEqual("blue", config["acme.ui"]["bgcolor"]) self.assertEqual(50, config["acme.ui"]["width"]) self.assertEqual(1.0, config["acme.ui"]["ratio"]) self.assertEqual(True, config["acme.ui"]["visible"]) self.assertEqual({"a": 1, "b": 2}, config["acme.ui"]["foo"]) self.assertEqual([1, 2, 3, 4], config["acme.ui"]["bar"]) self.assertEqual((1, "a", 6, 4), config["acme.ui"]["baz"]) self.assertEqual("red", config["acme.ui.other"]["fred"]) self.assertEqual(100, config["acme.ui.other"]["wilma"]) self.assertEqual(90, config["tds.foogle"]["joe"]) self.assertEqual("meerkat", config["simples"]["animal"]) def test_save(self): """ save """ config = PyConfigFile(open(self.example)) self.assertEqual("blue", config["acme.ui"]["bgcolor"]) self.assertEqual(50, config["acme.ui"]["width"]) self.assertEqual(1.0, config["acme.ui"]["ratio"]) self.assertEqual(True, config["acme.ui"]["visible"]) self.assertEqual({"a": 1, "b": 2}, config["acme.ui"]["foo"]) self.assertEqual([1, 2, 3, 4], config["acme.ui"]["bar"]) self.assertEqual("red", config["acme.ui.other"]["fred"]) self.assertEqual(100, config["acme.ui.other"]["wilma"]) self.assertEqual(90, config["tds.foogle"]["joe"]) self.assertEqual("meerkat", config["simples"]["animal"]) # Save the config to another file. tmpdir = tempfile.mkdtemp() tmp = join(tmpdir, "tmp.ini") config.save(tmp) try: self.assertTrue(os.path.exists(tmp)) # Make sure we can read the file back in and that we get the same # values! config = PyConfigFile(open(tmp)) self.assertEqual("blue", config["acme.ui"]["bgcolor"]) self.assertEqual(50, config["acme.ui"]["width"]) self.assertEqual(1.0, config["acme.ui"]["ratio"]) self.assertEqual(True, config["acme.ui"]["visible"]) self.assertEqual({"a": 1, "b": 2}, config["acme.ui"]["foo"]) self.assertEqual([1, 2, 3, 4], config["acme.ui"]["bar"]) self.assertEqual((1, "a", 6, 4), config["acme.ui"]["baz"]) self.assertEqual("red", config["acme.ui.other"]["fred"]) self.assertEqual(100, config["acme.ui.other"]["wilma"]) self.assertEqual(90, config["tds.foogle"]["joe"]) self.assertEqual("meerkat", config["simples"]["animal"]) finally: # Clean up! os.remove(tmp) os.rmdir(tmpdir) def test_load_multiple_files(self): """ load multiple files """ config = PyConfigFile(self.example) self.assertEqual("blue", config["acme.ui"]["bgcolor"]) self.assertEqual(50, config["acme.ui"]["width"]) self.assertEqual(1.0, config["acme.ui"]["ratio"]) self.assertEqual(True, config["acme.ui"]["visible"]) self.assertEqual({"a": 1, "b": 2}, config["acme.ui"]["foo"]) self.assertEqual([1, 2, 3, 4], config["acme.ui"]["bar"]) self.assertEqual((1, "a", 6, 4), config["acme.ui"]["baz"]) self.assertEqual("red", config["acme.ui.other"]["fred"]) self.assertEqual(100, config["acme.ui.other"]["wilma"]) self.assertEqual(90, config["tds.foogle"]["joe"]) self.assertEqual("meerkat", config["simples"]["animal"]) # Load another file. config.load(self.example_2) # Make sure we still have the unchanged values... self.assertEqual("red", config["acme.ui"]["bgcolor"]) self.assertEqual(50, config["acme.ui"]["width"]) self.assertEqual(1.0, config["acme.ui"]["ratio"]) self.assertEqual(True, config["acme.ui"]["visible"]) self.assertEqual({"a": 1, "b": 2}, config["acme.ui"]["foo"]) self.assertEqual([1, 2, 3, 4], config["acme.ui"]["bar"]) self.assertEqual((1, "a", 6, 4), config["acme.ui"]["baz"]) self.assertEqual("red", config["acme.ui.other"]["fred"]) self.assertEqual(100, config["acme.ui.other"]["wilma"]) self.assertEqual(90, config["tds.foogle"]["joe"]) self.assertEqual("meerkat", config["simples"]["animal"]) # ... and the values that were overwritten... self.assertEqual("red", config["acme.ui"]["bgcolor"]) # ... and that we have the new ones. self.assertEqual(42, config["acme.ui"]["bazzle"]) # ... and that the new ones can refer to the old ones! self.assertEqual(180, config["acme.ui"]["blimey"]) apptools-5.1.0/apptools/preferences/tests/py_config_example_2.ini0000644000076500000240000000010313777642667025726 0ustar aayresstaff00000000000000[acme.ui] bgcolor = "red" bazzle = 42 blimey = tds.foogle.joe * 2 apptools-5.1.0/apptools/preferences/tests/test_preferences_helper.py0000644000076500000240000004761713777642667026612 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 preferences helper. """ # Standard library imports. import os import shutil import tempfile import unittest # Major package imports. from importlib_resources import files # Enthought library imports. from apptools.preferences.api import Preferences, PreferencesHelper from apptools.preferences.api import ScopedPreferences from apptools.preferences.api import set_default_preferences from traits.api import ( Any, Bool, HasTraits, Int, Float, List, Str, push_exception_handler, pop_exception_handler, ) def width_listener(obj, trait_name, old, new): width_listener.obj = obj width_listener.trait_name = trait_name width_listener.old = old width_listener.new = new def bgcolor_listener(obj, trait_name, old, new): bgcolor_listener.obj = obj bgcolor_listener.trait_name = trait_name bgcolor_listener.old = old bgcolor_listener.new = new # This module's package. PKG = "apptools.preferences.tests" class PreferencesHelperTestCase(unittest.TestCase): """ Tests for the preferences helper. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.preferences = set_default_preferences(Preferences()) # The filename of the example preferences file. self.example = os.fspath(files(PKG) / "example.ini") # A temporary directory that can safely be written to. self.tmpdir = tempfile.mkdtemp() # Path to a temporary file self.tmpfile = os.path.join(self.tmpdir, "tmp.ini") push_exception_handler(reraise_exceptions=True) self.addCleanup(pop_exception_handler) def tearDown(self): """ Called immediately after each test method has been called. """ # Remove the temporary directory. shutil.rmtree(self.tmpdir) ########################################################################### # Tests. ########################################################################### def test_class_scope_preferences_path(self): """ class scope preferences path """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = "acme.ui" # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Str offsets = List(Int) names = List(Str) helper = AcmeUIPreferencesHelper() helper.on_trait_change(bgcolor_listener) # Make sure the helper was initialized properly. self.assertEqual("blue", helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertTrue(helper.visible) self.assertEqual("acme ui", helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(["joe", "fred", "jane"], helper.names) # Make sure we can set the preference via the helper... helper.bgcolor = "yellow" self.assertEqual("yellow", p.get("acme.ui.bgcolor")) self.assertEqual("yellow", helper.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual("bgcolor", bgcolor_listener.trait_name) self.assertEqual("blue", bgcolor_listener.old) self.assertEqual("yellow", bgcolor_listener.new) # Make sure we can set the preference via the preferences node... p.set("acme.ui.bgcolor", "red") self.assertEqual("red", p.get("acme.ui.bgcolor")) self.assertEqual("red", helper.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual("bgcolor", bgcolor_listener.trait_name) self.assertEqual("yellow", bgcolor_listener.old) self.assertEqual("red", bgcolor_listener.new) def test_instance_scope_preferences_path(self): """ instance scope preferences path """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Str offsets = List(Int) names = List(Str) helper = AcmeUIPreferencesHelper(preferences_path="acme.ui") helper.on_trait_change(bgcolor_listener) # Make sure the helper was initialized properly. self.assertEqual("blue", helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertTrue(helper.visible) self.assertEqual("acme ui", helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(["joe", "fred", "jane"], helper.names) # Make sure we can set the preference via the helper... helper.bgcolor = "yellow" self.assertEqual("yellow", p.get("acme.ui.bgcolor")) self.assertEqual("yellow", helper.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual("bgcolor", bgcolor_listener.trait_name) self.assertEqual("blue", bgcolor_listener.old) self.assertEqual("yellow", bgcolor_listener.new) # Make sure we can set the preference via the preferences node... p.set("acme.ui.bgcolor", "red") self.assertEqual("red", p.get("acme.ui.bgcolor")) self.assertEqual("red", helper.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual("bgcolor", bgcolor_listener.trait_name) self.assertEqual("yellow", bgcolor_listener.old) self.assertEqual("red", bgcolor_listener.new) def test_default_values(self): """ default values """ class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = "acme.ui" # The traits that we want to initialize from preferences. bgcolor = Str("blue") width = Int(50) ratio = Float(1.0) visible = Bool(True) description = Str("description") offsets = List(Int, [1, 2, 3, 4]) names = List(Str, ["joe", "fred", "jane"]) helper = AcmeUIPreferencesHelper() # Make sure the helper was initialized properly. self.assertEqual("blue", helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertTrue(helper.visible) self.assertEqual("description", helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(["joe", "fred", "jane"], helper.names) def test_real_unicode_values(self): """ Test with real life unicode values """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = "acme.ui" # The traits that we want to initialize from preferences. bgcolor = Str("blue") width = Int(50) ratio = Float(1.0) visible = Bool(True) description = Str("") offsets = List(Int, [1, 2, 3, 4]) names = List(Str, ["joe", "fred", "jane"]) helper = AcmeUIPreferencesHelper() first_unicode_str = "U\xdc\xf2ser" helper.description = first_unicode_str self.assertEqual(first_unicode_str, helper.description) second_unicode_str = "caf\xe9" helper.description = second_unicode_str self.assertEqual(second_unicode_str, helper.description) self.assertEqual(second_unicode_str, p.get("acme.ui.description")) # Save it to another file. tmp = os.path.join(self.tmpdir, "tmp.ini") p.save(tmp) # Load it into a new node. p = Preferences() p.load(tmp) self.assertEqual(second_unicode_str, p.get("acme.ui.description")) self.assertEqual("True", p.get("acme.ui.visible")) self.assertTrue(helper.visible) def test_mutate_list_of_values(self): """ Mutated list should be saved and _items events not to be saved in the preferences. """ # Regression test for enthought/apptools#129 class MyPreferencesHelper(PreferencesHelper): preferences_path = Str('my_section') list_of_str = List(Str) helper = MyPreferencesHelper(list_of_str=["1"]) # Now modify the list to fire _items event helper.list_of_str.append("2") self.preferences.save(self.tmpfile) new_preferences = Preferences() new_preferences.load(self.tmpfile) self.assertEqual( new_preferences.get("my_section.list_of_str"), str(["1", "2"]) ) self.assertEqual(new_preferences.keys("my_section"), ["list_of_str"]) def test_sync_anytrait_items_not_event(self): """ Test sychronizing trait with name *_items which is a normal trait rather than an event trait for listening to list/dict/set mutation. """ class MyPreferencesHelper(PreferencesHelper): preferences_path = Str('my_section') names_items = Str() helper = MyPreferencesHelper(preferences=self.preferences) helper.names_items = "Hello" self.preferences.save(self.tmpfile) new_preferences = Preferences() new_preferences.load(self.tmpfile) self.assertEqual( sorted(new_preferences.keys("my_section")), ["names_items"] ) self.assertEqual( new_preferences.get("my_section.names_items"), str(helper.names_items), ) def test_no_preferences_path(self): """ no preferences path """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Str offsets = List(Int) names = List(Str) # Cannot create a helper with a preferences path. self.assertRaises(SystemError, AcmeUIPreferencesHelper) def test_sync_trait(self): """ sync trait """ class Widget(HasTraits): """ A widget! """ background_color = Str w = Widget() w.on_trait_change(bgcolor_listener) p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = "acme.ui" # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Str offsets = List(Int) names = List(Str) helper = AcmeUIPreferencesHelper() helper.sync_trait("bgcolor", w, "background_color") # Make sure the helper was initialized properly. self.assertEqual("blue", helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertTrue(helper.visible) self.assertEqual("acme ui", helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(["joe", "fred", "jane"], helper.names) self.assertEqual("blue", w.background_color) # Make sure we can set the preference via the helper... helper.bgcolor = "yellow" self.assertEqual("yellow", p.get("acme.ui.bgcolor")) self.assertEqual("yellow", helper.bgcolor) self.assertEqual("yellow", w.background_color) # ... and that the correct trait change event was fired. self.assertEqual(w, bgcolor_listener.obj) self.assertEqual("background_color", bgcolor_listener.trait_name) self.assertEqual("blue", bgcolor_listener.old) self.assertEqual("yellow", bgcolor_listener.new) # Make sure we can set the preference via the preferences node... p.set("acme.ui.bgcolor", "red") self.assertEqual("red", p.get("acme.ui.bgcolor")) self.assertEqual("red", helper.bgcolor) self.assertEqual("red", w.background_color) # ... and that the correct trait change event was fired. self.assertEqual(w, bgcolor_listener.obj) self.assertEqual("background_color", bgcolor_listener.trait_name) self.assertEqual("yellow", bgcolor_listener.old) self.assertEqual("red", bgcolor_listener.new) def test_scoped_preferences(self): """ scoped preferences """ p = set_default_preferences(ScopedPreferences()) # Set a preference value in the default scope. p.set("default/acme.ui.bgcolor", "blue") class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = "acme.ui" # The traits that we want to initialize from preferences. bgcolor = Str # A trait for a preference that does not exist yet. name = Str helper = AcmeUIPreferencesHelper() # Make sure the trait is set! self.assertEqual("blue", helper.bgcolor) # And that the non-existent trait gets the default value. self.assertEqual("", helper.name) def test_preference_not_in_file(self): """ preference not in file """ class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = "acme.ui" # A trait that has no corresponding value in the file. title = Str("Acme") helper = AcmeUIPreferencesHelper() # Make sure the trait is set! self.assertEqual("Acme", helper.title) # Set a new value. helper.title = "Acme Plus" # Make sure the trait is set! self.assertEqual("Acme Plus", helper.title) self.assertEqual("Acme Plus", self.preferences.get("acme.ui.title")) def test_preferences_node_changed(self): """ preferences node changed """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The path to the preferences node that contains our preferences. preferences_path = "acme.ui" # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Str offsets = List(Int) names = List(Str) helper = AcmeUIPreferencesHelper() # We only listen to some of the traits so the testing is easier. helper.on_trait_change(width_listener, ["width"]) helper.on_trait_change(bgcolor_listener, ["bgcolor"]) # Create a new preference node. p1 = Preferences() p1.load(self.example) p1.set("acme.ui.bgcolor", "red") p1.set("acme.ui.width", 40) # Set the new preferences helper.preferences = p1 # Test event handling. self.assertEqual(helper, width_listener.obj) self.assertEqual("width", width_listener.trait_name) self.assertEqual(50, width_listener.old) self.assertEqual(40, width_listener.new) self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual("bgcolor", bgcolor_listener.trait_name) self.assertEqual("blue", bgcolor_listener.old) self.assertEqual("red", bgcolor_listener.new) # Test re-initialization. self.assertEqual(helper.bgcolor, "red") self.assertEqual(helper.width, 40) # Test event handling. p1.set("acme.ui.bgcolor", "black") self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual("bgcolor", bgcolor_listener.trait_name) self.assertEqual("red", bgcolor_listener.old) self.assertEqual("black", bgcolor_listener.new) # This should not trigger any new changes since we are setting values # on the old preferences node. p.set("acme.ui.bgcolor", "white") self.assertEqual(helper, bgcolor_listener.obj) self.assertEqual("bgcolor", bgcolor_listener.trait_name) self.assertEqual("red", bgcolor_listener.old) self.assertEqual("black", bgcolor_listener.new) def test_nested_set_in_trait_change_handler(self): """ nested set in trait change handler """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): """ A helper! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool description = Str offsets = List(Int) names = List(Str) # When the width changes, change the ratio. def _width_changed(self, trait_name, old, new): """ Static trait change handler. """ self.ratio = 3.0 helper = AcmeUIPreferencesHelper(preferences_path="acme.ui") # Make sure the helper was initialized properly. self.assertEqual("blue", helper.bgcolor) self.assertEqual(50, helper.width) self.assertEqual(1.0, helper.ratio) self.assertTrue(helper.visible) self.assertEqual("acme ui", helper.description) self.assertEqual([1, 2, 3, 4], helper.offsets) self.assertEqual(["joe", "fred", "jane"], helper.names) # Change the width via the preferences node. This should cause the # ratio to get set via the static trait change handler on the helper. p.set("acme.ui.width", 42) self.assertEqual(42, helper.width) self.assertEqual("42", p.get("acme.ui.width")) # Did the ratio get changed? self.assertEqual(3.0, helper.ratio) self.assertEqual("3.0", p.get("acme.ui.ratio")) # fixme: No comments - nice work... I added the doc string and the 'return' # to be compatible with the rest of the module. Interns please note correct # procedure when modifying existing code. If in doubt, ask a developer. def test_unevaluated_strings(self): """ unevaluated strings """ p = self.preferences p.load(self.example) class AcmeUIPreferencesHelper(PreferencesHelper): width = Any(is_str=True) helper = AcmeUIPreferencesHelper(preferences_path="acme.ui") self.assertEqual("50", helper.width) apptools-5.1.0/apptools/preferences/tests/test_preference_binding.py0000644000076500000240000002527613777642667026557 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 preference bindings. """ # Standard library imports. import os import tempfile import unittest from os.path import join # Major package imports. from importlib_resources import files # Enthought library imports. from apptools.preferences.api import Preferences from apptools.preferences.api import bind_preference from apptools.preferences.api import set_default_preferences from traits.api import Bool, HasTraits, Int, Float, Str # This module's package. PKG = "apptools.preferences.tests" def listener(obj, trait_name, old, new): """ A useful trait change handler for testing! """ listener.obj = obj listener.trait_name = trait_name listener.old = old listener.new = new class PreferenceBindingTestCase(unittest.TestCase): """ Tests for preference bindings. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.preferences = set_default_preferences(Preferences()) # The filename of the example preferences file. self.example = os.fspath(files(PKG) / "example.ini") def tearDown(self): """ Called immediately after each test method has been called. """ ########################################################################### # Tests. ########################################################################### def test_preference_binding(self): """ preference binding """ p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool acme_ui = AcmeUI() acme_ui.on_trait_change(listener) # Make some bindings. bind_preference(acme_ui, "bgcolor", "acme.ui.bgcolor") bind_preference(acme_ui, "width", "acme.ui.width") bind_preference(acme_ui, "ratio", "acme.ui.ratio") bind_preference(acme_ui, "visible", "acme.ui.visible") # Make sure the object was initialized properly. self.assertEqual("blue", acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertTrue(acme_ui.visible) # Make sure we can set the preference via the helper... acme_ui.bgcolor = "yellow" self.assertEqual("yellow", p.get("acme.ui.bgcolor")) self.assertEqual("yellow", acme_ui.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(acme_ui, listener.obj) self.assertEqual("bgcolor", listener.trait_name) self.assertEqual("blue", listener.old) self.assertEqual("yellow", listener.new) # Make sure we can set the preference via the preferences node... p.set("acme.ui.bgcolor", "red") self.assertEqual("red", p.get("acme.ui.bgcolor")) self.assertEqual("red", acme_ui.bgcolor) # ... and that the correct trait change event was fired. self.assertEqual(acme_ui, listener.obj) self.assertEqual("bgcolor", listener.trait_name) self.assertEqual("yellow", listener.old) self.assertEqual("red", listener.new) # Make sure we can set a non-string preference via the helper... acme_ui.ratio = 0.5 self.assertEqual("0.5", p.get("acme.ui.ratio")) self.assertEqual(0.5, acme_ui.ratio) # Make sure we can set a non-string preference via the node... p.set("acme.ui.ratio", "0.75") self.assertEqual("0.75", p.get("acme.ui.ratio")) self.assertEqual(0.75, acme_ui.ratio) def test_default_values(self): """ instance scope preferences path """ class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str("blue") width = Int(50) ratio = Float(1.0) visible = Bool(True) acme_ui = AcmeUI() # Make some bindings. bind_preference(acme_ui, "bgcolor", "acme.ui.bgcolor") bind_preference(acme_ui, "width", "acme.ui.width") bind_preference(acme_ui, "ratio", "acme.ui.ratio") bind_preference(acme_ui, "visible", "acme.ui.visible") # Make sure the helper was initialized properly. self.assertEqual("blue", acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertTrue(acme_ui.visible) def test_load_and_save(self): """ load and save """ p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str("red") width = Int(60) ratio = Float(2.0) visible = Bool(False) acme_ui = AcmeUI() # Make some bindings. bind_preference(acme_ui, "bgcolor", "acme.ui.bgcolor") bind_preference(acme_ui, "width", "acme.ui.width") bind_preference(acme_ui, "ratio", "acme.ui.ratio") bind_preference(acme_ui, "visible", "acme.ui.visible") # Make sure the helper was initialized properly (with the values in # the loaded .ini file *not* the trait defaults!). self.assertEqual("blue", acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertTrue(acme_ui.visible) # Make a change to one of the preference values. p.set("acme.ui.bgcolor", "yellow") self.assertEqual("yellow", acme_ui.bgcolor) self.assertEqual("yellow", p.get("acme.ui.bgcolor")) # Save the preferences to a different file. tmpdir = tempfile.mkdtemp() tmp = join(tmpdir, "tmp.ini") p.save(tmp) # Load the preferences again from that file. p = set_default_preferences(Preferences()) p.load(tmp) acme_ui = AcmeUI() # Make some bindings. bind_preference(acme_ui, "bgcolor", "acme.ui.bgcolor") bind_preference(acme_ui, "width", "acme.ui.width") bind_preference(acme_ui, "ratio", "acme.ui.ratio") bind_preference(acme_ui, "visible", "acme.ui.visible") # Make sure the helper was initialized properly (with the values in # the .ini file *not* the trait defaults!). self.assertEqual("yellow", acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertTrue(acme_ui.visible) # Clean up! os.remove(tmp) os.rmdir(tmpdir) def test_explicit_preferences(self): """ explicit preferences """ p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool acme_ui = AcmeUI() acme_ui.on_trait_change(listener) # Create an empty preferences node and use that in some of the # bindings! preferences = Preferences() # Make some bindings. bind_preference(acme_ui, "bgcolor", "acme.ui.bgcolor", preferences) bind_preference(acme_ui, "width", "acme.ui.width") bind_preference(acme_ui, "ratio", "acme.ui.ratio", preferences) bind_preference(acme_ui, "visible", "acme.ui.visible") # Make sure the object was initialized properly. self.assertEqual("", acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(0.0, acme_ui.ratio) self.assertTrue(acme_ui.visible) def test_nested_set_in_trait_change_handler(self): """ nested set in trait change handler """ p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The traits that we want to initialize from preferences. bgcolor = Str width = Int ratio = Float visible = Bool def _width_changed(self, trait_name, old, new): """ Static trait change handler. """ self.ratio = 3.0 acme_ui = AcmeUI() acme_ui.on_trait_change(listener) # Make some bindings. bind_preference(acme_ui, "bgcolor", "acme.ui.bgcolor") bind_preference(acme_ui, "width", "acme.ui.width") bind_preference(acme_ui, "ratio", "acme.ui.ratio") bind_preference(acme_ui, "visible", "acme.ui.visible") # Make sure the object was initialized properly. self.assertEqual("blue", acme_ui.bgcolor) self.assertEqual(50, acme_ui.width) self.assertEqual(1.0, acme_ui.ratio) self.assertTrue(acme_ui.visible) # Change the width via the preferences node. This should cause the # ratio to get set via the static trait change handler on the helper. p.set("acme.ui.width", 42) self.assertEqual(42, acme_ui.width) self.assertEqual("42", p.get("acme.ui.width")) # Did the ratio get changed? self.assertEqual(3.0, acme_ui.ratio) self.assertEqual("3.0", p.get("acme.ui.ratio")) def test_trait_name_different_to_preference_name(self): p = self.preferences p.load(self.example) class AcmeUI(HasTraits): """ The Acme UI class! """ # The test here is to have a different name for the trait than the # preference value (which is 'bgcolor'). color = Str acme_ui = AcmeUI() acme_ui.on_trait_change(listener) # Make some bindings. bind_preference(acme_ui, "color", "acme.ui.bgcolor") # Make sure the object was initialized properly. self.assertEqual("blue", acme_ui.color) # Change the width via the preferences node. p.set("acme.ui.bgcolor", "red") self.assertEqual("color", listener.trait_name) self.assertEqual("blue", listener.old) self.assertEqual("red", listener.new) apptools-5.1.0/apptools/preferences/tests/test_scoped_preferences.py0000644000076500000240000003214313777642667026574 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 scoped preferences. """ # Standard library imports. import os import tempfile from os.path import join # Major package imports. from importlib_resources import files # Enthought library imports. from apptools.preferences.api import Preferences, ScopedPreferences # Local imports. from .test_preferences import PreferencesTestCase # This module's package. PKG = "apptools.preferences.tests" class ScopedPreferencesTestCase(PreferencesTestCase): """ Tests for the scoped preferences. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.preferences = ScopedPreferences() # The filename of the example preferences file. self.example = os.fspath(files(PKG) / "example.ini") # A temporary directory that can safely be written to. self.tmpdir = tempfile.mkdtemp() def tearDown(self): """ Called immediately after each test method has been called. """ # Remove the temporary directory. os.rmdir(self.tmpdir) ########################################################################### # Tests overridden from 'PreferencesTestCase'. ########################################################################### def test_node(self): """ node """ p = self.preferences # Try an empty path. self.assertEqual(p, p.node()) # Try a simple path. node = p.node("acme") self.assertIsNotNone(node) self.assertEqual("acme", node.name) self.assertEqual("acme", node.path) self.assertEqual(p.node("application/"), node.parent) # Make sure we get the same node each time we ask for it! self.assertEqual(node, p.node("acme")) # Try a nested path. node = p.node("acme.ui") self.assertIsNotNone(node) self.assertEqual("ui", node.name) self.assertEqual("acme.ui", node.path) self.assertEqual(p.node("application/acme"), node.parent) # And just to be sure, a really nested path. node = p.node("acme.ui.splash_screen") self.assertIsNotNone(node) self.assertEqual("splash_screen", node.name) self.assertEqual("acme.ui.splash_screen", node.path) self.assertEqual(p.node("application/acme.ui"), node.parent) def test_save(self): """ save """ p = self.preferences # Get the application scope. application = p.node("application/") tmp = join(self.tmpdir, "test.ini") application.filename = tmp # Set a value. p.set("acme.ui.bgcolor", "red") # Save all scopes. p.save() # Make sure a file was written. self.assertTrue(os.path.exists(tmp)) # Load the 'ini' file into a new preferences node and make sure the # preference is in there. p = Preferences() p.load(tmp) self.assertEqual("red", p.get("acme.ui.bgcolor")) # Cleanup. os.remove(tmp) ########################################################################### # Tests. ########################################################################### def test_ability_to_specify_primary_scope(self): preferences = ScopedPreferences( scopes=[ Preferences(name="a"), Preferences(name="b"), Preferences(name="c"), ], primary_scope_name="b", ) # This should set the prefrrence in the primary scope. preferences.set("acme.foo", "bar") # Look it up specifically in the primary scope. self.assertEqual("bar", preferences.get("b/acme.foo")) def test_builtin_scopes(self): """ builtin scopes """ p = self.preferences # Make sure the default built-in scopes get created. self.assertTrue(p.node_exists("application/")) self.assertTrue(p.node_exists("default/")) def test_get_and_set_in_specific_scope(self): """ get and set in specific scope """ p = self.preferences # Set a preference and make sure we can get it again! p.set("default/acme.ui.bgcolor", "red") self.assertEqual("red", p.get("default/acme.ui.bgcolor")) def test_clear_in_specific_scope(self): """ clear in specific scope """ p = self.preferences # Set a value in both the application and default scopes. p.set("application/acme.ui.bgcolor", "red") p.set("default/acme.ui.bgcolor", "yellow") # Make sure when we look it up we get the one in first scope in the # lookup order. self.assertEqual("red", p.get("acme.ui.bgcolor")) # Now clear out the application scope. p.clear("application/acme.ui") self.assertEqual(0, len(p.keys("application/acme.ui"))) # We should now get the value from the default scope. self.assertEqual("yellow", p.get("acme.ui.bgcolor")) def test_remove_in_specific_scope(self): """ remove in specific scope """ p = self.preferences # Set a value in both the application and default scopes. p.set("application/acme.ui.bgcolor", "red") p.set("default/acme.ui.bgcolor", "yellow") # Make sure when we look it up we get the one in first scope in the # lookup order. self.assertEqual("red", p.get("acme.ui.bgcolor")) # Now remove it from the application scope. p.remove("application/acme.ui.bgcolor") # We should now get the value from the default scope. self.assertEqual("yellow", p.get("acme.ui.bgcolor")) def test_keys_in_specific_scope(self): """ keys in specific scope """ p = self.preferences # It should be empty to start with! self.assertEqual([], p.keys("default/")) # Set some preferences in the node. p.set("default/a", "1") p.set("default/b", "2") p.set("default/c", "3") keys = p.keys("default/") keys.sort() self.assertEqual(["a", "b", "c"], keys) # Set some preferences in a child node. p.set("default/acme.a", "1") p.set("default/acme.b", "2") p.set("default/acme.c", "3") keys = p.keys("default/acme") keys.sort() self.assertEqual(["a", "b", "c"], keys) # And, just to be sure, in a child of the child node ;^) p.set("default/acme.ui.a", "1") p.set("default/acme.ui.b", "2") p.set("default/acme.ui.c", "3") keys = p.keys("default/acme.ui") keys.sort() self.assertEqual(["a", "b", "c"], keys) def test_node_in_specific_scope(self): """ node in specific scope """ p = self.preferences # Try an empty path. self.assertEqual(p, p.node()) # Try a simple path. node = p.node("default/acme") self.assertIsNotNone(node) self.assertEqual("acme", node.name) self.assertEqual("acme", node.path) self.assertEqual(p.node("default/"), node.parent) # Make sure we get the same node each time we ask for it! self.assertEqual(node, p.node("default/acme")) # Try a nested path. node = p.node("default/acme.ui") self.assertIsNotNone(node) self.assertEqual("ui", node.name) self.assertEqual("acme.ui", node.path) self.assertEqual(p.node("default/acme"), node.parent) # And just to be sure, a really nested path. node = p.node("default/acme.ui.splash_screen") self.assertIsNotNone(node) self.assertEqual("splash_screen", node.name) self.assertEqual("acme.ui.splash_screen", node.path) self.assertEqual(p.node("default/acme.ui"), node.parent) def test_node_exists_in_specific_scope(self): """ node exists """ p = self.preferences self.assertTrue(p.node_exists()) self.assertFalse(p.node_exists("default/acme")) p.node("default/acme") self.assertTrue(p.node_exists("default/acme")) def test_node_names_in_specific_scope(self): """ node names in specific scope """ p = self.preferences # It should be empty to start with! self.assertEqual([], p.node_names("default/")) # Create some nodes. p.node("default/a") p.node("default/b") p.node("default/c") names = p.node_names("default/") names.sort() self.assertEqual(["a", "b", "c"], names) # Creatd some nodes in a child node. p.node("default/acme.a") p.node("default/acme.b") p.node("default/acme.c") names = p.node_names("default/acme") names.sort() self.assertEqual(["a", "b", "c"], names) # And, just to be sure, in a child of the child node ;^) p.node("default/acme.ui.a") p.node("default/acme.ui.b") p.node("default/acme.ui.c") names = p.node_names("default/acme.ui") names.sort() self.assertEqual(["a", "b", "c"], names) def test_default_lookup_order(self): """ default lookup order """ p = self.preferences # Set a value in both the application and default scopes. p.set("application/acme.ui.bgcolor", "red") p.set("default/acme.ui.bgcolor", "yellow") # Make sure when we look it up we get the one in first scope in the # lookup order. self.assertEqual("red", p.get("acme.ui.bgcolor")) # But we can still get at each scope individually. self.assertEqual("red", p.get("application/acme.ui.bgcolor")) self.assertEqual("yellow", p.get("default/acme.ui.bgcolor")) def test_lookup_order(self): """ lookup order """ p = self.preferences p.lookup_order = ["default", "application"] # Set a value in both the application and default scopes. p.set("application/acme.ui.bgcolor", "red") p.set("default/acme.ui.bgcolor", "yellow") # Make sure when we look it up we get the one in first scope in the # lookup order. self.assertEqual("red", p.get("acme.ui.bgcolor")) # But we can still get at each scope individually. self.assertEqual("red", p.get("application/acme.ui.bgcolor")) self.assertEqual("yellow", p.get("default/acme.ui.bgcolor")) def test_add_listener_in_specific_scope(self): """ add listener in specific scope. """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new # Add a listener. p.add_preferences_listener(listener, "default/acme.ui") # Set a value and make sure the listener was called. p.set("default/acme.ui.bgcolor", "blue") self.assertEqual(p.node("default/acme.ui"), listener.node) self.assertEqual("bgcolor", listener.key) self.assertIsNone(listener.old) self.assertEqual("blue", listener.new) # Set it to another value to make sure we get the 'old' value # correctly. p.set("default/acme.ui.bgcolor", "red") self.assertEqual(p.node("default/acme.ui"), listener.node) self.assertEqual("bgcolor", listener.key) self.assertEqual("blue", listener.old) self.assertEqual("red", listener.new) def test_remove_listener_in_specific_scope(self): """ remove listener in specific scope. """ p = self.preferences def listener(node, key, old, new): """ Listener for changes to a preferences node. """ listener.node = node listener.key = key listener.old = old listener.new = new # Add a listener. p.add_preferences_listener(listener, "default/acme.ui") # Set a value and make sure the listener was called. p.set("default/acme.ui.bgcolor", "blue") self.assertEqual(p.node("default/acme.ui"), listener.node) self.assertEqual("bgcolor", listener.key) self.assertIsNone(listener.old) self.assertEqual("blue", listener.new) # Remove the listener. p.remove_preferences_listener(listener, "default/acme.ui") # Set a value and make sure the listener was *not* called. listener.node = None p.set("default/acme.ui.bgcolor", "blue") self.assertIsNone(listener.node) def test_non_existent_scope(self): """ non existent scope """ p = self.preferences self.assertRaises(ValueError, p.get, "bogus/acme.ui.bgcolor") apptools-5.1.0/apptools/preferences/preferences.py0000644000076500000240000004246313777642667023044 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 a node in a preferences hierarchy. """ # Standard library imports. import logging import threading # Enthought library imports. from traits.api import Any, Callable, Dict, HasTraits, Instance, List from traits.api import Property, Str, Undefined, provides # Local imports. from .i_preferences import IPreferences # Logging. logger = logging.getLogger(__name__) @provides(IPreferences) class Preferences(HasTraits): """ The default implementation of a node in a preferences hierarchy. """ #### 'IPreferences' interface ############################################# # The absolute path to this node from the root node (the empty string if # this node *is* the root node). path = Property(Str) # The parent node (None if this node *is* the root node). parent = Instance(IPreferences) # The name of the node relative to its parent (the empty string if this # node *is* the root node). name = Str #### 'Preferences' interface ############################################## # The default name of the file used to persist the preferences (if no # filename is passed in to the 'load' and 'save' methods, then this is # used instead). filename = Str #### Protected 'Preferences' interface #################################### # A lock to make access to the node thread-safe. # # fixme: There *should* be no need to declare this as a trait, but if we # don't then we have problems using nodes in the preferences manager UI. # It is something to do with 'cloning' the node for use in a 'modal' traits # UI... Hmmm... _lk = Any # The node's children. _children = Dict(Str, IPreferences) # The node's preferences. _preferences = Dict(Str, Any) # Listeners for changes to the node's preferences. # # The callable must take 4 arguments, e.g:: # # listener(node, key, old, new) _preferences_listeners = List(Callable) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Constructor. """ # A lock to make access to the '_children', '_preferences' and # '_preferences_listeners' traits thread-safe. self._lk = threading.Lock() # Base class constructor. super(Preferences, self).__init__(**traits) # If a filename has been specified then load the preferences from it. if len(self.filename) > 0: self.load() ########################################################################### # 'IPreferences' interface. ########################################################################### #### Trait properties ##################################################### def _get_path(self): """ Property getter. """ names = [] node = self while node.parent is not None: names.append(node.name) node = node.parent names.reverse() return ".".join(names) #### Methods ############################################################## #### Methods where 'path' refers to a preference #### def get(self, path, default=None, inherit=False): """ Get the value of the preference at the specified path. """ if len(path) == 0: raise ValueError("empty path") components = path.split(".") # If there is only one component in the path then the operation takes # place in this node. if len(components) == 1: value = self._get(path, Undefined) # Otherwise, find the next node and pass the rest of the path to that. else: node = self._get_child(components[0]) if node is not None: value = node.get(".".join(components[1:]), Undefined) else: value = Undefined # If inherited values are allowed then try those as well. # # e.g. 'acme.ui.widget.bgcolor' # 'acme.ui.bgcolor' # 'acme.bgcolor' # 'bgcolor' while inherit and value is Undefined and len(components) > 1: # Remove the penultimate component... # # e.g. 'acme.ui.widget.bgcolor' -> 'acme.ui.bgcolor' del components[-2] # ... and try that. value = self.get(".".join(components), default=Undefined) if value is Undefined: value = default return value def remove(self, path): """ Remove the preference at the specified path. """ if len(path) == 0: raise ValueError("empty path") components = path.split(".") # If there is only one component in the path then the operation takes # place in this node. if len(components) == 1: self._remove(path) # Otherwise, find the next node and pass the rest of the path to that. else: node = self._get_child(components[0]) if node is not None: node.remove(".".join(components[1:])) def set(self, path, value): """ Set the value of the preference at the specified path. """ if len(path) == 0: raise ValueError("empty path") components = path.split(".") # If there is only one component in the path then the operation takes # place in this node. if len(components) == 1: self._set(path, value) # Otherwise, find the next node (creating it if it doesn't exist) # and pass the rest of the path to that. else: node = self._node(components[0]) node.set(".".join(components[1:]), value) #### Methods where 'path' refers to a node #### def clear(self, path=""): """ Remove all preferences from the node at the specified path. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: self._clear() # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split(".") node = self._get_child(components[0]) if node is not None: node.clear(".".join(components[1:])) def keys(self, path=""): """ Return the preference keys of the node at the specified path. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: keys = self._keys() # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split(".") node = self._get_child(components[0]) if node is not None: keys = node.keys(".".join(components[1:])) else: keys = [] return keys def node(self, path=""): """ Return the node at the specified path. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: node = self # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split(".") node = self._node(components[0]) node = node.node(".".join(components[1:])) return node def node_exists(self, path=""): """ Return True if the node at the specified path exists. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: exists = True # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split(".") node = self._get_child(components[0]) if node is not None: exists = node.node_exists(".".join(components[1:])) else: exists = False return exists def node_names(self, path=""): """Return the names of the children of the node at the specified path. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: names = self._node_names() # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split(".") node = self._get_child(components[0]) if node is not None: names = node.node_names(".".join(components[1:])) else: names = [] return names #### Persistence methods #### def flush(self): """Force any changes in the node to the backing store. This includes any changes to the node's descendants. """ self.save() ########################################################################### # 'Preferences' interface. ########################################################################### #### Listener methods #### def add_preferences_listener(self, listener, path=""): """ Add a listener for changes to a node's preferences. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: self._add_preferences_listener(listener) # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split(".") node = self._node(components[0]) node.add_preferences_listener(listener, ".".join(components[1:])) def remove_preferences_listener(self, listener, path=""): """ Remove a listener for changes to a node's preferences. """ # If the path is empty then the operation takes place in this node. if len(path) == 0: self._remove_preferences_listener(listener) # Otherwise, find the next node and pass the rest of the path to that. else: components = path.split(".") node = self._node(components[0]) node.remove_preferences_listener( listener, ".".join(components[1:]) ) #### Persistence methods #### def load(self, file_or_filename=None): """Load preferences from a file. This is a *merge* operation i.e. the contents of the file are added to the node. This implementation uses 'ConfigObj' files. """ if file_or_filename is None: file_or_filename = self.filename logger.debug("loading preferences from <%s>", file_or_filename) # Do the import here so that we don't make 'ConfigObj' a requirement # if preferences aren't ever persisted (or a derived class chooses to # use a different persistence mechanism). from configobj import ConfigObj config_obj = ConfigObj(file_or_filename, encoding="utf-8") # 'name' is the section name, 'value' is a dictionary containing the # name/value pairs in the section (the actual preferences ;^). for name, value in config_obj.items(): # Create/get the node from the section name. components = name.split(".") node = self for component in components: node = node._node(component) # Add the contents of the section to the node. self._add_dictionary_to_node(node, value) def save(self, file_or_filename=None): """Save the node's preferences to a file. This implementation uses 'ConfigObj' files. """ if file_or_filename is None: file_or_filename = self.filename # If no file or filename is specified then don't save the preferences! if len(file_or_filename) > 0: # Do the import here so that we don't make 'ConfigObj' a # requirement if preferences aren't ever persisted (or a derived # class chooses to use a different persistence mechanism). from configobj import ConfigObj logger.debug("saving preferences to <%s>", file_or_filename) config_obj = ConfigObj(file_or_filename, encoding="utf-8") self._add_node_to_dictionary(self, config_obj) config_obj.write() ########################################################################### # Protected 'Preferences' interface. # # These are the only methods that should access the protected '_children' # and '_preferences' traits. This helps make it easy to subclass this class # to create other implementations (all the subclass has to do is to # implement these protected methods). # ########################################################################### def _add_dictionary_to_node(self, node, dictionary): """ Add the contents of a dictionary to a node's preferences. """ self._lk.acquire() node._preferences.update(dictionary) self._lk.release() def _add_node_to_dictionary(self, node, dictionary): """ Add a node's preferences to a dictionary. """ # This method never manipulates the '_preferences' trait directly. # Instead it does eveything via the other protected methods and hence # doesn't need to grab the lock. if len(node._keys()) > 0: dictionary[node.path] = {} for key in node._keys(): dictionary[node.path][key] = node._get(key) for name in node._node_names(): self._add_node_to_dictionary(node._get_child(name), dictionary) def _add_preferences_listener(self, listener): """ Add a listener for changes to thisnode's preferences. """ self._lk.acquire() self._preferences_listeners.append(listener) self._lk.release() def _clear(self): """ Remove all preferences from this node. """ self._lk.acquire() self._preferences.clear() self._lk.release() def _create_child(self, name): """ Create a child of this node with the specified name. """ self._lk.acquire() child = self._children[name] = Preferences(name=name, parent=self) self._lk.release() return child def _get(self, key, default=None): """ Get the value of a preference in this node. """ self._lk.acquire() value = self._preferences.get(key, default) self._lk.release() return value def _get_child(self, name): """Return the child of this node with the specified name. Return None if no such child exists. """ self._lk.acquire() child = self._children.get(name) self._lk.release() return child def _keys(self): """ Return the preference keys of this node. """ self._lk.acquire() keys = list(self._preferences.keys()) self._lk.release() return keys def _node(self, name): """Return the child of this node with the specified name. Create the child node if it does not exist. """ node = self._get_child(name) if node is None: node = self._create_child(name) return node def _node_names(self): """ Return the names of the children of this node. """ self._lk.acquire() node_names = list(self._children.keys()) self._lk.release() return node_names def _remove(self, name): """ Remove a preference value from this node. """ self._lk.acquire() if name in self._preferences: del self._preferences[name] self._lk.release() def _remove_preferences_listener(self, listener): """ Remove a listener for changes to the node's preferences. """ self._lk.acquire() if listener in self._preferences_listeners: self._preferences_listeners.remove(listener) self._lk.release() def _set(self, key, value): """ Set the value of a preference in this node. """ # everything must be unicode encoded so that ConfigObj configuration # can properly serialize the data. Python str are supposed to be ASCII # encoded. value = str(value) self._lk.acquire() old = self._preferences.get(key) self._preferences[key] = value # If the value is unchanged then don't call the listeners! if old == value: listeners = [] else: listeners = self._preferences_listeners[:] self._lk.release() for listener in listeners: listener(self, key, old, value) ########################################################################### # Debugging interface. ########################################################################### def dump(self, indent=""): """ Dump the preferences hierarchy to stdout. """ if indent == "": print() print(indent, "Node(%s)" % self.name, self._preferences) indent += " " for child in self._children.values(): child.dump(indent) apptools-5.1.0/apptools/preferences/__init__.py0000644000076500000240000000077513777642667022302 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 application preferences. Part of the AppTools project of the Enthought Tool Suite """ apptools-5.1.0/apptools/preferences/api.py0000644000076500000240000000205713777642667021307 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for apptools.preferences subpackage. - :class:`~.Preferences` - :class:`~.PreferenceBinding` - :class:`~.PreferencesHelper` - :class:`~.ScopedPreferences` Interfaces ---------- - :class:`~.IPreferences` Utilities --------- - :func:`~.get_default_preferences` - :func:`~.set_default_preferences` - :func:`~.bind_preference` """ from .i_preferences import IPreferences from .package_globals import get_default_preferences, set_default_preferences from .preferences import Preferences from .preference_binding import PreferenceBinding, bind_preference from .preferences_helper import PreferencesHelper from .scoped_preferences import ScopedPreferences apptools-5.1.0/apptools/preferences/package_globals.py0000644000076500000240000000210413777642667023625 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Package-scope globals. The default preferences node is currently used by 'PreferencesHelper' and 'PreferencesBinding' instances if no specific preferences node is set. This makes it easy for them to access the root node of an application-wide preferences hierarchy. """ # The default preferences node. _default_preferences = None def get_default_preferences(): """ Get the default preferences node. """ return _default_preferences def set_default_preferences(default_preferences): """ Set the default preferences node. """ global _default_preferences _default_preferences = default_preferences # For convenience. return _default_preferences apptools-5.1.0/apptools/scripting/0000755000076500000240000000000013777643025017646 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/scripting/util.py0000644000076500000240000000330413777642667021210 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """Simple utility functions provided by the scripting API. """ from .recorder import Recorder from .recorder_with_ui import RecorderWithUI from .package_globals import get_recorder, set_recorder ############################################################################### # Utility functions. ############################################################################### def start_recording(object, ui=True, **kw): """Convenience function to start recording. Returns the recorder. Parameters ---------- object : object to record. ui : bool specifying if a UI is to be shown or not kw : Keyword arguments to pass to the register function of the recorder. """ if ui: r = RecorderWithUI(root=object) r.edit_traits(kind="live") else: r = Recorder() # Set the global recorder. set_recorder(r) r.recording = True r.register(object, **kw) return r def stop_recording(object, save=True): """Stop recording the object. If `save` is `True`, this will pop up a UI to ask where to save the script. """ recorder = get_recorder() recorder.unregister(object) recorder.recording = False # Set the global recorder back to None set_recorder(None) # Save the script. if save: recorder.ui_save() apptools-5.1.0/apptools/scripting/recordable.py0000644000076500000240000000341213777642667022335 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Decorator to mark functions and methods as recordable. """ from .package_globals import get_recorder # Guard to ensure that only the outermost recordable call is recorded # and nested calls ignored. _outermost_call = True def recordable(func): """A decorator that wraps a function into one that is recordable. This will record the function only if the global recorder has been set via a `set_recorder` function call. """ def _wrapper(*args, **kw): """A wrapper returned to replace the decorated function.""" global _outermost_call # Boolean to specify if the method was recorded or not. record = False if _outermost_call: # Get the recorder. rec = get_recorder() if rec is not None: _outermost_call = False # Record the method if recorder is available. record = True try: result = rec.record_function(func, args, kw) finally: _outermost_call = True if not record: # If the method was not recorded, just call it. result = func(*args, **kw) return result # Mimic the actual function. _wrapper.__name__ = func.__name__ _wrapper.__doc__ = func.__doc__ _wrapper.__dict__.update(func.__dict__) return _wrapper apptools-5.1.0/apptools/scripting/recorder.py0000644000076500000240000006422713777642667022053 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Code to support recording to a readable and executable Python script. FIXME: - Support for dictionaries? """ import builtins import warnings from traits.api import ( HasTraits, List, Str, Dict, Bool, Property, Int, Instance, ) from traits.util.camel_case import camel_case_to_python ############################################################################### # `_RegistryData` class. ############################################################################### class _RegistryData(HasTraits): # Object's script ID script_id = Property(Str) # Path to object in object hierarchy. path = Property(Str) # Parent data for this object if any. parent_data = Instance("_RegistryData", allow_none=True) # The name of the trait on the parent which is this object. trait_name_on_parent = Str("") # List of traits we are listening for on this object. names = List(Str) # Nested recordable instances on the object. sub_recordables = List(Str) # List of traits that are lists. list_names = List(Str) _script_id = Str("") ########################################################################### # Non-public interface. ########################################################################### def _get_path(self): pdata = self.parent_data path = "" if pdata is not None: pid = pdata.script_id ppath = pdata.path tnop = self.trait_name_on_parent if "[" in tnop: # If the object is a nested object through an iterator, # we instantiate it and don't refer to it through the # path, this makes scripting convenient. if len(ppath) == 0: path = pid + "." + tnop else: path = ppath + "." + tnop else: path = ppath + "." + tnop return path def _get_script_id(self): sid = self._script_id if len(sid) == 0: pdata = self.parent_data sid = pdata.script_id + "." + self.trait_name_on_parent return sid def _set_script_id(self, id): self._script_id = id ############################################################################### # `RecorderError` class. ############################################################################### class RecorderError(Exception): pass ############################################################################### # `Recorder` class. ############################################################################### class Recorder(HasTraits): # The lines of code recorded. lines = List(Str) # Are we recording or not? recording = Bool(False, desc="if script recording is enabled or not") # The Python script we have recorded so far. This is just a # convenience trait for the `get_code()` method. script = Property(Str) ######################################## # Private traits. # Dict used to store information on objects registered. It stores a # unique name for the object and its path in the object hierarchy # traversed. _registry = Dict # Reverse registry with keys as script_id and object as value. _reverse_registry = Dict # A mapping to generate unique names for objects. The key is the # name used (which is something derived from the class name of the # object) and the value is an integer describing the number of times # that variable name has been used earlier. _name_map = Dict(Str, Int) # A list of special reserved script IDs. This is handy when you # want a particular object to have an easy to read script ID and not # the default one based on its class name. This leads to slightly # easier to read scripts. _special_ids = List # What are the known names in the script? By known names we mean # names which are actually bound to objects. _known_ids = List(Str) # The known types in the namespace. _known_types = List(Str) # A guard to check if we are currently in a recorded function call, # in which case we don't want to do any recording. _in_function = Bool(False) ########################################################################### # `Recorder` interface. ########################################################################### def record(self, code): """Record a string to be stored to the output file. Parameters ---------- code : str A string of text. """ if self.recording and not self._in_function: lines = self.lines # Analyze the code and add extra code if needed. self._analyze_code(code) # Add the code. lines.append(code) def register( self, object, parent=None, trait_name_on_parent="", ignore=None, known=False, script_id=None, ): """Register an object with the recorder. This sets up the object for recording. By default all traits (except those starting and ending with '_') are recorded. For attributes that are themselves recordable, one may mark traits with a 'record' metadata as follows: - If metadata `record=False` is set, the nested object will not be recorded. - If `record=True`, then that object is also recorded if it is not `None`. If the object is a list or dict that is marked with `record=True`, the list is itself not listened to for changes but all its contents are registered. If the `object` has a trait named `recorder` then this recorder instance will be set to it if possible. Parameters ---------- object : Instance(HasTraits) The object to register in the registry. parent : Instance(HasTraits) An optional parent object in which `object` is contained trait_name_on_parent : str An optional trait name of the `object` in the `parent`. ignore : list(str) An optional list of trait names on the `object` to be ignored. known : bool Optional specification if the `object` id is known on the interpreter. This is needed if you are manually injecting code to define/create an object. script_id : str Optionally specify a script_id to use for this object. It is not guaranteed that this ID will be used since it may already be in use. """ registry = self._registry # Do nothing if the object is already registered. if object in registry: return # When parent is specified the trait_name_on_parent must also be. if parent is not None: assert len(trait_name_on_parent) > 0 if ignore is None: ignore = [] if isinstance(object, HasTraits): # Always ignore these. ignore.extend(["trait_added", "trait_modified"]) sub_recordables = list(object.traits(record=True).keys()) # Find all the trait names we must ignore. ignore.extend(object.traits(record=False).keys()) # The traits to listen for. tnames = [ t for t in object.trait_names() if not t.startswith("_") and not t.endswith("_") and t not in ignore ] # Find all list traits. trts = object.traits() list_names = [] for t in tnames: tt = trts[t].trait_type if ( hasattr(tt, "default_value_type") and tt.default_value_type == 5 ): list_names.append(t) else: # No traits, so we can't do much. sub_recordables = [] tnames = [] list_names = [] # Setup the registry data. # If a script id is supplied try and use it. sid = "" if script_id is not None: r_registry = self._reverse_registry while script_id in r_registry: script_id = "%s1" % script_id sid = script_id # Add the chosen id to special_id list. self._special_ids.append(sid) if parent is None: pdata = None if len(sid) == 0: sid = self._get_unique_name(object) else: pdata = self._get_registry_data(parent) tnop = trait_name_on_parent if "[" in tnop: # If the object is a nested object through an iterator, # we instantiate it and don't refer to it through the # path, this makes scripting convenient. sid = self._get_unique_name(object) # Register the object with the data. data = _RegistryData( script_id=sid, parent_data=pdata, trait_name_on_parent=trait_name_on_parent, names=tnames, sub_recordables=sub_recordables, list_names=list_names, ) registry[object] = data # Now get the script id of the object -- note that if sid is '' # above then the script_id is computed from that of the parent. sid = data.script_id # Setup reverse registry so we can get the object from the # script_id. self._reverse_registry[sid] = object # Record the script_id if the known argument is explicitly set to # True. if known: self._known_ids.append(sid) # Try and set the recorder attribute if necessary. if hasattr(object, "recorder"): try: object.recorder = self except Exception as e: msg = "Cannot set 'recorder' trait of object %r: " "%s" % ( object, e, ) warnings.warn(msg, warnings.RuntimeWarning) if isinstance(object, HasTraits): # Add handler for lists. for name in list_names: object.on_trait_change( self._list_items_listner, "%s_items" % name ) # Register all sub-recordables. for name in sub_recordables: obj = getattr(object, name) if isinstance(obj, list): # Don't register the object itself but register its # children. for i, child in enumerate(obj): attr = "%s[%d]" % (name, i) self.register( child, parent=object, trait_name_on_parent=attr ) elif obj is not None: self.register( obj, parent=object, trait_name_on_parent=name ) # Listen for changes to the trait itself so the newly # assigned object can also be listened to. object.on_trait_change(self._object_changed_handler, name) # Now add listner for the object itself. object.on_trait_change(self._listner, tnames) def unregister(self, object): """Unregister the given object from the recorder. This inverts the logic of the `register(...)` method. """ registry = self._registry # Do nothing if the object isn't registered. if object not in registry: return data = registry[object] # Try and unset the recorder attribute if necessary. if hasattr(object, "recorder"): try: object.recorder = None except Exception as e: msg = "Cannot unset 'recorder' trait of object %r:" "%s" % ( object, e, ) warnings.warn(msg, warnings.RuntimeWarning) if isinstance(object, HasTraits): # Remove all list_items handlers. for name in data.list_names: object.on_trait_change( self._list_items_listner, "%s_items" % name, remove=True ) # Unregister all sub-recordables. for name in data.sub_recordables: obj = getattr(object, name) if isinstance(obj, list): # Unregister the children. for i, child in enumerate(obj): self.unregister(child) elif obj is not None: self.unregister(obj) # Remove the trait handler for trait assignments. object.on_trait_change( self._object_changed_handler, name, remove=True ) # Now remove listner for the object itself. object.on_trait_change(self._listner, data.names, remove=True) # Remove the object data from the registry etc. if data.script_id in self._known_ids: self._known_ids.remove(data.script_id) del self._reverse_registry[data.script_id] del registry[object] def save(self, file): """Save the recorded lines to the given file. It does not close the file. """ file.write(self.get_code()) file.flush() def record_function(self, func, args, kw): """Record a function call given the function and its arguments.""" if self.recording and not self._in_function: # Record the function name and arguments. call_str = self._function_as_string(func, args, kw) # Call the function. try: self._in_function = True result = func(*args, **kw) finally: self._in_function = False # Register the result if it is not None. if func.__name__ == "__init__": f_self = args[0] code = self._import_class_string(f_self.__class__) self.lines.append(code) return_str = self._registry.get(f_self).script_id else: return_str = self._return_as_string(result) if len(return_str) > 0: self.lines.append("%s = %s" % (return_str, call_str)) else: self.lines.append("%s" % (call_str)) else: result = func(*args, **kw) return result def ui_save(self): """Save recording to file, pop up a UI dialog to find out where and close the file when done. """ from pyface.api import FileDialog, OK wildcard = "Python files (*.py)|*.py|" + FileDialog.WILDCARD_ALL dialog = FileDialog( title="Save Script", action="save as", wildcard=wildcard ) if dialog.open() == OK: fname = dialog.path f = open(fname, "w") self.save(f) f.close() def clear(self): """Clears all previous recorded state and unregisters all registered objects.""" # First unregister any registered objects. registry = self._registry while len(registry) > 0: self.unregister(list(registry.keys())[0]) # Clear the various lists. self.lines[:] = [] self._registry.clear() self._known_ids[:] = [] self._name_map.clear() self._reverse_registry.clear() self._known_types[:] = [] self._special_ids[:] = [] def get_code(self): """Returns the recorded lines as a string of printable code.""" return "\n".join(self.lines) + "\n" def is_registered(self, object): """Returns True if the given object is registered with the recorder.""" return object in self._registry def get_script_id(self, object): """Returns the script_id of a registered object. Useful when you want to manually add a record statement.""" return self._get_registry_data(object).script_id def get_object_path(self, object): """Returns the path in the object hierarchy of a registered object. Useful for debugging.""" return self._get_registry_data(object).path def write_script_id_in_namespace(self, script_id): """If a script_id is not known in the current script's namespace, this sets it using the path of the object or actually instantiating it. If this is not possible (since the script_id matches no existing object), nothing is recorded but the framework is notified that the particular script_id is available in the namespace. This is useful when you want to inject code in the namespace to create a particular object. """ if not self.recording: return known_ids = self._known_ids if script_id not in known_ids: obj = self._reverse_registry.get(script_id) # Add the ID to the known_ids. known_ids.append(script_id) if obj is not None: data = self._registry.get(obj) result = "" if len(data.path) > 0: # Record code for instantiation of object. result = "%s = %s" % (script_id, data.path) else: # This is not the best thing to do but better than # nothing. result = self._import_class_string(obj.__class__) cls = obj.__class__.__name__ result += "\n%s = %s()" % (script_id, cls) if len(result) > 0: self.lines.extend(result.split("\n")) ########################################################################### # Non-public interface. ########################################################################### def _get_unique_name(self, obj): """Return a unique object name (a string). Note that this does not cache the object, so if called with the same object 3 times you'll get three different names. """ cname = obj.__class__.__name__ nm = self._name_map result = "" builtin = False if cname in builtins.__dict__: builtin = True if hasattr(obj, "__name__"): cname = obj.__name__ else: cname = camel_case_to_python(cname) special_ids = self._special_ids while len(result) == 0 or result in special_ids: if cname in nm: id = nm[cname] + 1 nm[cname] = id result = "%s%d" % (cname, id) else: nm[cname] = 0 # The first id doesn't need a number if it isn't builtin. if builtin: result = "%s0" % (cname) else: result = cname return result def _get_registry_data(self, object): """Get the data for an object from registry.""" data = self._registry.get(object) if data is None: msg = ( "Recorder: Can't get script_id since object %s not registered" ) raise RecorderError(msg % (object)) return data def _listner(self, object, name, old, new): """The listner for trait changes on an object. This is called by child listners or when any of the recordable object's traits change when recording to a script is enabled. Parameters: ----------- object : Object which has changed. name : extended name of attribute that changed. old : Old value. new : New value. """ if self.recording and not self._in_function: new_repr = repr(new) sid = self._get_registry_data(object).script_id if len(sid) == 0: msg = "%s = %r" % (name, new) else: msg = "%s.%s = %r" % (sid, name, new) if new_repr.startswith("<") and new_repr.endswith(">"): self.record("# " + msg) else: self.record(msg) def _list_items_listner(self, object, name, old, event): """The listner for *_items on list traits of the object.""" # Set the path of registered objects in the modified list and # all their children. This is done by unregistering the object # and re-registering them. This is slow but. registry = self._registry sid = registry.get(object).script_id trait_name = name[:-6] items = getattr(object, trait_name) for (i, item) in enumerate(items): if item in registry: data = registry.get(item) tnop = data.trait_name_on_parent if len(tnop) > 0: data.trait_name_on_parent = "%s[%d]" % (trait_name, i) # Record the change. if self.recording and not self._in_function: index = event.index removed = event.removed added = event.added nr = len(removed) slice = "[%d:%d]" % (index, index + nr) rhs = [self._object_as_string(item) for item in added] rhs = ", ".join(rhs) obj = "%s.%s" % (sid, name[:-6]) msg = "%s%s = [%s]" % (obj, slice, rhs) self.record(msg) def _object_changed_handler(self, object, name, old, new): """Called when a child recordable object has been reassigned.""" registry = self._registry if old is not None: if old in registry: self.unregister(old) if new is not None: if new not in registry: self.register(new, parent=object, trait_name_on_parent=name) def _get_script(self): return self.get_code() def _analyze_code(self, code): """Analyze the code and return extra code if needed.""" lhs = "" try: lhs = code.split()[0] except IndexError: pass if "." in lhs: ob_name = lhs.split(".")[0] self.write_script_id_in_namespace(ob_name) def _function_as_string(self, func, args, kw): """Return a string representing the function call.""" func_name = func.__name__ func_code = func.__code__ # Even if func is really a decorated method it never shows up as # a bound or unbound method here, so we have to inspect the # argument names to figure out if this is a method or function. if func_code.co_argcount > 0 and func_code.co_varnames[0] == "self": # This is a method, the first argument is bound to self. f_self = args[0] # Convert the remaining arguments to strings. argl = [self._object_as_string(arg) for arg in args[1:]] # If this is __init__ we special case it. if func_name == "__init__": # Register the object. self.register(f_self, known=True) func_name = f_self.__class__.__name__ else: sid = self._object_as_string(f_self) func_name = "%s.%s" % (sid, func_name) else: argl = [self._object_as_string(arg) for arg in args] # Convert the keyword args. kwl = [ "%s=%s" % (key, self._object_as_string(value)) for key, value in kw.items() ] argl.extend(kwl) # Make a string representation of the args, kw. argstr = ", ".join(argl) return "%s(%s)" % (func_name, argstr) def _is_arbitrary_object(self, object): """Return True if the object is an arbitrary non-primitive object. We assume that if the hex id of the object is in its string representation then it is an arbitrary object. """ ob_id = id(object) orepr = repr(object) hex_id = "%x" % ob_id return hex_id.upper() in orepr.upper() def _object_as_string(self, object): """Return a string representing the object.""" registry = self._registry if object in registry: # Return script id if the object is known; create the script # id on the namespace if needed before that. sid = registry.get(object).script_id base_id = sid.split(".")[0] self.write_script_id_in_namespace(base_id) return sid else: if not self._is_arbitrary_object(object): return repr(object) # If we get here, we just register the object and call ourselves # again to do the needful. self.register(object) return self._object_as_string(object) def _return_as_string(self, object): """Return a string given a returned object from a function.""" result = "" ignore = (float, complex, bool, int, str) if object is not None and type(object) not in ignore: # If object is not know, register it. registry = self._registry if object not in registry: self.register(object) result = registry.get(object).script_id # Since this is returned it is known on the namespace. known_ids = self._known_ids if result not in known_ids: known_ids.append(result) return result def _import_class_string(self, cls): """Import a class if needed.""" cname = cls.__name__ result = "" if cname not in builtins.__dict__: mod = cls.__module__ typename = "%s.%s" % (mod, cname) if typename not in self._known_types: result = "from %s import %s" % (mod, cname) self._known_types.append(typename) return result apptools-5.1.0/apptools/scripting/tests/0000755000076500000240000000000013777643025021010 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/scripting/tests/test_recorder.py0000644000076500000240000003435013777642667024246 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Unit tests for the script recorder. """ import unittest from traits.api import ( HasTraits, Float, Instance, Str, List, Bool, HasStrictTraits, Tuple, Range, Trait, ) from apptools.scripting.recorder import Recorder from apptools.scripting.recordable import recordable from apptools.scripting.package_globals import set_recorder try: # Require Traits >= 6.1 from traits.api import PrefixMap except ImportError: from traits.api import TraitPrefixMap representation_trait = Trait( "surface", TraitPrefixMap({"surface": 2, "wireframe": 1, "points": 0}) ) else: representation_trait = PrefixMap( {"surface": 2, "wireframe": 1, "points": 0}, default_value="surface" ) ###################################################################### # Test classes. class Property(HasStrictTraits): color = Tuple(Range(0.0, 1.0), Range(0.0, 1.0), Range(0.0, 1.0)) opacity = Range(0.0, 1.0, 1.0) representation = representation_trait class Toy(HasTraits): color = Str type = Str ignore = Bool(False, record=False) class Child(HasTraits): name = Str("child") age = Float(10.0) property = Instance(Property, (), record=True) toy = Instance(Toy, record=True) friends = List(Str) @recordable def grow(self, x): """Increase age by x years.""" self.age += x self.f(1) @recordable def f(self, args): """Function f.""" return args def not_recordable(self): pass class Parent(HasTraits): children = List(Child, record=True) recorder = Instance(Recorder, record=False) class Test(HasTraits): # This should be set. recorder = Instance(HasTraits) # These should be ignored. _ignore = Bool(False) ignore_ = Bool(False) class TestRecorder(unittest.TestCase): def setUp(self): self.tape = Recorder() set_recorder(self.tape) p = Parent() c = Child() toy = Toy(color="blue", type="bunny") c.toy = toy p.children.append(c) self.p = p def tearDown(self): self.tape.clear() set_recorder(None) def test_unique_name(self): "Does the get_unique_id method work." class XMLUnstructuredGridWriter: pass t = XMLUnstructuredGridWriter() tape = self.tape self.assertEqual( tape._get_unique_name(t), "xml_unstructured_grid_writer" ) self.assertEqual( tape._get_unique_name(t), "xml_unstructured_grid_writer1" ) t = Toy() self.assertEqual(tape._get_unique_name(t), "toy") t = (1, 2) self.assertEqual(tape._get_unique_name(t), "tuple0") lst = [1, 2] self.assertEqual(tape._get_unique_name(lst), "list0") d = {"a": 1} self.assertEqual(tape._get_unique_name(d), "dict0") self.assertEqual(tape._get_unique_name(1), "int0") def test_record(self): "Does recording work correctly." tape = self.tape p = self.p c = p.children[0] toy = c.toy # start recording. tape.recording = True tape.register(p) # Test if p's recorder attribute is set. self.assertEqual(tape, p.recorder) # Test script ids and object path. self.assertEqual(tape.get_script_id(p), "parent") self.assertEqual(tape.get_object_path(p), "") self.assertEqual(tape.get_script_id(c), "child") self.assertEqual(tape.get_object_path(c), "parent.children[0]") self.assertEqual(tape.get_script_id(toy), "child.toy") self.assertEqual(tape.get_object_path(toy), "parent.children[0].toy") c.name = "Ram" # The child should first be instantiated. self.assertEqual(tape.lines[-2], "child = parent.children[0]") # Then its trait set. self.assertEqual(tape.lines[-1], "child.name = 'Ram'") c.age = 10.5 self.assertEqual(tape.lines[-1], "child.age = 10.5") c.property.representation = "w" self.assertEqual( tape.lines[-1], "child.property.representation = 'wireframe'" ) c.property.color = (1, 0, 0) self.assertEqual( tape.lines[-1], "child.property.color = (1.0, 0.0, 0.0)" ) toy.color = "red" self.assertEqual(tape.lines[-1], "child.toy.color = 'red'") toy.type = "teddy" self.assertEqual(tape.lines[-1], "child.toy.type = 'teddy'") # This trait should be ignored. toy.ignore = True self.assertEqual(tape.lines[-1], "child.toy.type = 'teddy'") # Turn of recording and test. tape.recording = False toy.type = "rat" self.assertEqual(tape.lines[-1], "child.toy.type = 'teddy'") # Stop recording. n = len(tape.lines) tape.unregister(p) c.property.representation = "points" toy.type = "bunny" self.assertEqual(tape.lines[-1], "child.toy.type = 'teddy'") self.assertEqual(n, len(tape.lines)) # Make sure the internal data of the recorder is cleared. self.assertEqual(0, len(tape._registry)) self.assertEqual(0, len(tape._reverse_registry)) self.assertEqual(0, len(tape._known_ids)) def test_recorded_trait_replaced(self): "Does recording work right when a trait is replaced." tape = self.tape p = self.p c = p.children[0] toy = c.toy # start recording. tape.recording = True tape.register(p) # Test the original trait. toy.color = "red" self.assertEqual(tape.lines[-1], "child.toy.color = 'red'") # Now reassign the toy. t1 = Toy(name="ball") c.toy = t1 t1.color = "yellow" self.assertEqual(tape.lines[-1], "child.toy.color = 'yellow'") def test_clear(self): "Test the clear method." p = self.p tape = self.tape tape.register(p) tape.clear() # Everything should be unregistered. self.assertEqual(p.recorder, None) # Internal data should be wiped clean. self.assertEqual(0, len(tape._registry)) self.assertEqual(0, len(tape._reverse_registry)) self.assertEqual(0, len(tape._known_ids)) self.assertEqual(0, len(tape._name_map)) def test_create_object(self): "Is the object imported and created if unknown?" tape = self.tape tape.recording = True t = Toy() tape.register(t) t.type = "computer" # Since the name toy is unknown, there should be a # line to create it. self.assertEqual(tape.lines[-3][-10:], "import Toy") self.assertEqual(tape.lines[-2], "toy = Toy()") self.assertEqual(tape.lines[-1], "toy.type = 'computer'") # Since this one is known, there should be no imports or # anything. t1 = Toy() tape.register(t1, known=True) t1.type = "ball" self.assertEqual(tape.lines[-2], "toy.type = 'computer'") self.assertEqual(tape.lines[-1], "toy1.type = 'ball'") def test_list_items_changed(self): "Test if a list item is changed does the change get recorded." p = self.p tape = self.tape child = p.children[0] tape.register(p, known=True) tape.recording = True child.friends = ["Krishna", "Ajay", "Ali"] self.assertEqual( tape.lines[-1], "child.friends = ['Krishna', 'Ajay', 'Ali']" ) child.friends[1:] = ["Sam", "Frodo"] self.assertEqual( tape.lines[-1], "child.friends[1:3] = ['Sam', 'Frodo']" ) child.friends[1] = "Hari" self.assertEqual(tape.lines[-1], "child.friends[1:2] = ['Hari']") # What if we change a list where record=True. child1 = Child() tape.register(child1) p.children.append(child1) self.assertEqual(tape.lines[-1], "parent.children[1:1] = [child1]") del p.children[1] self.assertEqual(tape.lines[-1], "parent.children[1:2] = []") p.children[0] = child1 self.assertEqual(tape.lines[-1], "parent.children[0:1] = [child1]") def test_path_change_on_list(self): "Does the object path update when a list has changed?" # Test the case where we have a hierarchy and we change the # list. tape = self.tape p = self.p child1 = Child() p.children.append(child1) tape.register(p) tape.recording = True self.assertEqual(tape.get_object_path(child1), "parent.children[1]") self.assertEqual(tape.get_script_id(child1), "child1") del p.children[0] self.assertEqual(tape.get_object_path(child1), "parent.children[0]") self.assertEqual(tape.get_script_id(child1), "child1") def test_write_script_id_in_namespace(self): "Test the write_script_id_in_namespace method." tape = self.tape tape.recording = True # This should not cause an error but insert the name 'foo' in the # namespace. tape.write_script_id_in_namespace("foo") def test_recorder_and_ignored(self): "Test if recorder trait is set and private traits are ignored." t = Test() self.assertIsNone(t.recorder) self.assertFalse(t._ignore) self.assertFalse(t.ignore_) tape = Recorder() tape.register(t) tape.recording = True self.assertEqual(t.recorder, tape) t._ignore = True t.ignore_ = True self.assertEqual(len(tape.script.strip()), 0) def test_record_function(self): "See if recordable function calls are handled correctly." # Note that the global recorder is set in setUp and removed in # tearDown. tape = self.tape c = self.p.children[0] tape.register(c) tape.recording = True # Setting the age should be recorded. c.age = 11 self.assertEqual(tape.lines[-1], "child.age = 11.0") # This should also work without problems. c.f(c.toy) self.assertEqual(tape.lines[-2], "child.age = 11.0") self.assertEqual(tape.lines[-1], "child.toy = child.f(child.toy)") # Calling f should be recorded. c.f(1) self.assertEqual(tape.lines[-1], "child.f(1)") # This should not record the call to f or the change to the age # trait inside grow. c.grow(1) self.assertEqual(c.age, 12.0) self.assertEqual(tape.lines[-2], "child.f(1)") self.assertEqual(tape.lines[-1], "child.grow(1)") # Non-recordable functions shouldn't be. c.not_recordable() self.assertEqual(tape.lines[-1], "child.grow(1)") # Test a simple recordable function. @recordable def func(x, y): return x, y func(1, 2) self.assertEqual(tape.lines[-1], "tuple0 = func(1, 2)") def test_non_has_traits(self): "Can classes not using traits be handled?" tape = self.tape p = self.p c = p.children[0] class A(object): @recordable def __init__(self, x, y=1): self.x = x self.y = y @recordable def f(self, x, y): return x, y @recordable def g(self, x): return x def not_recordable(self): pass tape.register(p) tape.recording = True # Test if __init__ is recorded correctly. a = A(x=1) # Should record. a.f(1, "asd") self.assertEqual(tape.lines[-3][-8:], "import A") self.assertEqual(tape.lines[-2], "a = A(x=1)") self.assertEqual(tape.lines[-1], "tuple0 = a.f(1, 'asd')") a.f(p, c) # This should instantiate the parent first, get the child from # that and then record the call itself. self.assertEqual(tape.lines[-3], "parent = Parent()") self.assertEqual(tape.lines[-2], "child = parent.children[0]") self.assertEqual(tape.lines[-1], "tuple1 = a.f(parent, child)") # This should simply refer to the child. a.g(c) self.assertEqual(tape.lines[-1], "child = a.g(child)") # Should do nothing. a.not_recordable() self.assertEqual(tape.lines[-1], "child = a.g(child)") # When a function is called with unknown args it should attempt # to create the objects. a.g(Toy()) self.assertEqual(tape.lines[-3][-10:], "import Toy") self.assertEqual(tape.lines[-2], "toy = Toy()") self.assertEqual(tape.lines[-1], "toy = a.g(toy)") def test_set_script_id(self): "Test if setting script_id at registration time works." tape = self.tape p = self.p tape.register(p, script_id="child") tape.recording = True # Ask to be called child. self.assertEqual(tape.get_script_id(p), "child") # Register another Child. c1 = Child() tape.register(c1) # Will be child2 since child1 is taken. self.assertEqual(tape.get_script_id(c1), "child2") # Test if recording works correctly with the changed script_id. p.children.append(c1) self.assertEqual(tape.lines[-1], "child.children[1:1] = [child2]") def test_save(self): "Test if saving tape to file works." tape = self.tape p = self.p c = p.children[0] toy = c.toy # Start recording tape.register(p) tape.recording = True toy.type = "teddy" # Now stop. tape.recording = False tape.unregister(p) import io f = io.StringIO() tape.save(f) # Test if the file is OK. expect = ["child = parent.children[0]\n", "child.toy.type = 'teddy'\n"] f.seek(0) lines = f.readlines() self.assertEqual(expect, lines) f.close() apptools-5.1.0/apptools/scripting/tests/__init__.py0000644000076500000240000000062713777642667023141 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/scripting/__init__.py0000644000076500000240000000100313777642667021764 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Automatic script recording framework, part of the AppTools project of the Enthought Tool Suite. """ apptools-5.1.0/apptools/scripting/api.py0000644000076500000240000000167413777642667021014 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for the apptools.scripting subpackage. - :class:`~.Recorder` - :func:`~.recordable` - :class:`~.RecorderWithUI` Custom Exceptions ----------------- - :class:`~.RecorderError` Utilities --------- - :func:`~.get_recorder` - :func:`~.set_recorder` - :func:`~.start_recording` - :func:`~.stop_recording` """ from .recorder import Recorder, RecorderError from .recordable import recordable from .package_globals import get_recorder, set_recorder from .recorder_with_ui import RecorderWithUI from .util import start_recording, stop_recording apptools-5.1.0/apptools/scripting/package_globals.py0000644000076500000240000000136413777642667023335 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Globals for the scripting package. """ # The global recorder. _recorder = None def get_recorder(): """Return the global recorder. Does not create a new one if none exists. """ global _recorder return _recorder def set_recorder(rec): """Set the global recorder instance.""" global _recorder _recorder = rec apptools-5.1.0/apptools/scripting/recorder_with_ui.py0000644000076500000240000000621513777642667023574 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Recorder subclass that presents a simple user interface. """ from traits.api import Code, Button, Int, on_trait_change, Any from traitsui.api import View, Item, Group, HGroup, CodeEditor, spring, Handler from .recorder import Recorder ############################################################################### # `CloseHandler` class. ############################################################################### class CloseHandler(Handler): """This class cleans up after the UI for the recorder is closed.""" def close(self, info, is_ok): """This method is invoked when the user closes the UI.""" recorder = info.object recorder.on_ui_close() return True ############################################################################### # `RecorderWithUI` class. ############################################################################### class RecorderWithUI(Recorder): """ This class represents a Recorder but with a simple user interface. """ # The code to display code = Code(editor=CodeEditor(line="current_line")) # Button to save script to file. save_script = Button("Save Script") # The current line to show, used by the editor. current_line = Int # The root object which is being recorded. root = Any ######################################## # Traits View. view = View( Group( HGroup( Item("recording", show_label=True), spring, Item("save_script", show_label=False), ), Group(Item("code", show_label=False)), ), width=600, height=360, id="apptools.scripting.recorder_with_ui", buttons=["Cancel"], resizable=True, handler=CloseHandler(), ) ###################################################################### # RecorderWithUI interface. ###################################################################### def on_ui_close(self): """Called from the CloseHandler when the UI is closed. This method basically stops the recording. """ from .util import stop_recording from .package_globals import get_recorder if get_recorder() is self: stop_recording(self.root, save=False) else: self.recording = False self.unregister(self.root) ###################################################################### # Non-public interface. ###################################################################### @on_trait_change("lines[]") def _update_code(self): self.code = self.get_code() self.current_line = len(self.lines) + 1 def _save_script_fired(self): self.ui_save() apptools-5.1.0/apptools/version.py0000644000076500000240000000147713777643024017713 0ustar aayresstaff00000000000000# (C) Copyright 2008-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Version information for this Apptools distribution. This file is autogenerated by the Apptools setup.py script. """ #: The full version of the package, including a development suffix #: for unreleased versions of the package. version = "5.1.0" #: The Git revision from which this release was made. git_revision = "26d0cdea1ff98526afe0af84880cbfe9e948a50c" #: Flag whether this is a final release is_released = True apptools-5.1.0/apptools/logger/0000755000076500000240000000000013777643025017123 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/logger/log_point.py0000644000076500000240000000244713777642667021511 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Prints a stack trace every time it is called but does not halt execution of the application. Copied from Uche Ogbuji's blog """ # Standard library imports. import inspect from io import StringIO def log_point(msg="\n"): stack = inspect.stack() # get rid of logPoint's part of the stack: stack = stack[1:] stack.reverse() output = StringIO() if msg: output.write(str(msg) + "\n") for stackLine in stack: frame, filename, line, funcname, lines, unknown = stackLine if filename.endswith("/unittest.py"): # unittest.py code is a boring part of the traceback continue if filename.startswith("./"): filename = filename[2:] output.write("%s:%s in %s:\n" % (filename, line, funcname)) if lines: output.write(" %s\n" % "".join(lines)[:-1]) s = output.getvalue() return s apptools-5.1.0/apptools/logger/plugin/0000755000076500000240000000000013777643025020421 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/logger/plugin/preferences.ini0000644000076500000240000000015213777642667023434 0ustar aayresstaff00000000000000[enthought.logger] level = 'Info' enable_agent = False smtp_server = '' to_address = '' from_address = '' apptools-5.1.0/apptools/logger/plugin/logger_preferences.py0000644000076500000240000000231213777642667024644 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.preferences.api import PreferencesHelper from traits.api import Bool, Str, Trait class LoggerPreferences(PreferencesHelper): """The persistent service exposing the Logger plugin's API.""" #### Preferences ########################################################## # The log levels level = Trait( "Info", { "Debug": logging.DEBUG, "Info": logging.INFO, "Warning": logging.WARNING, "Error": logging.ERROR, "Critical": logging.CRITICAL, }, is_str=True, ) enable_agent = Bool(False) smtp_server = Str() to_address = Str() from_address = Str() # The path to the preferences node that contains the preferences. preferences_path = Str("apptools.logger") apptools-5.1.0/apptools/logger/plugin/logger_service.py0000644000076500000240000001264213777642667024012 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from io import BytesIO import logging import os import zipfile # Enthought library imports from pyface.workbench.api import View as WorkbenchView from traits.api import ( Any, Callable, HasTraits, Instance, List, Property, Undefined, on_trait_change, ) root_logger = logging.getLogger() logger = logging.getLogger(__name__) class LoggerService(HasTraits): """The persistent service exposing the Logger plugin's API.""" # The Envisage application. application = Any() # The logging Handler we use. handler = Any() # Our associated LoggerPreferences. preferences = Any() # The view we use. plugin_view = Instance(WorkbenchView) # Contributions from other plugins. mail_files = Property(List(Callable)) def save_preferences(self): """Save the preferences.""" self.preferences.preferences.save() def whole_log_text(self): """Return all of the logged data as formatted text.""" lines = [self.handler.format(rec) for rec in self.handler.get()] # Ensure that we end with a newline. lines.append("") text = "\n".join(lines) return text def create_email_message( self, fromaddr, toaddrs, ccaddrs, subject, priority, include_userdata=False, stack_trace="", comments="", include_environment=True, ): """Format a bug report email from the log files.""" from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText message = MIMEMultipart() message["Subject"] = "%s [priority=%s]" % (subject, priority) message["To"] = ", ".join(toaddrs) message["Cc"] = ", ".join(ccaddrs) message["From"] = fromaddr message.preamble = ( "You will not see this in a MIME-aware mail " "reader.\n" ) message.epilogue = " " # To guarantee the message ends with a newline # First section is simple ASCII data ... m = [] m.append("Bug Report") m.append("==============================") m.append("") if len(comments) > 0: m.append("Comments:") m.append("========") m.append(comments) m.append("") if len(stack_trace) > 0: m.append("Stack Trace:") m.append("===========") m.append(stack_trace) m.append("") msg = MIMEText("\n".join(m)) message.attach(msg) # Include the log file ... logtext = self.whole_log_text() msg = MIMEText(logtext) msg.add_header( "Content-Disposition", "attachment", filename="logfile.txt" ) message.attach(msg) # Include the environment variables ... # FIXME: ask the user, maybe? if include_environment: # Transmit the user's environment settings as well. Main purpose # is to work out the user name to help with following up on bug # reports and in future we should probably send less data. entries = [] for key, value in sorted(os.environ.items()): entries.append("%30s : %s\n" % (key, value)) msg = MIMEText("".join(entries)) msg.add_header( "Content-Disposition", "attachment", filename="environment.txt" ) message.attach(msg) if include_userdata and len(self.mail_files) != 0: f = BytesIO() zf = zipfile.ZipFile(f, "w") for mf in self.mail_files: mf(zf) zf.close() msg = MIMEApplication(f.getvalue()) msg.add_header( "Content-Disposition", "attachment", filename="userdata.zip" ) message.attach(msg) return message def send_bug_report( self, smtp_server, fromaddr, toaddrs, ccaddrs, message ): """Send a bug report email.""" try: import smtplib logger.debug("Connecting to: %s" % smtp_server) server = smtplib.SMTP(host=smtp_server) logger.debug("Connected: %s" % server) # server.set_debuglevel(1) server.sendmail(fromaddr, toaddrs + ccaddrs, message.as_string()) server.quit() except Exception: logger.exception("Problem sending error report") #### Traits stuff ######################################################### def _get_mail_files(self): return self.application.get_extensions( "apptools.logger.plugin.mail_files" ) @on_trait_change("preferences.level_") def _level_changed(self, new): if ( new is not None and new is not Undefined and self.handler is not None ): root_logger.setLevel(self.preferences.level_) self.handler.setLevel(self.preferences.level_) apptools-5.1.0/apptools/logger/plugin/tests/0000755000076500000240000000000013777643025021563 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/logger/plugin/tests/__init__.py0000644000076500000240000000062713777642667023714 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/logger/plugin/tests/test_logger_service.py0000644000076500000240000000364613777642667026217 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 email.mime.multipart import MIMEMultipart import unittest from unittest import mock from apptools.logger.plugin.logger_service import LoggerService class LoggerServiceTestCase(unittest.TestCase): def test_create_email_message(self): logger_service = LoggerService() with mock.patch.object( logger_service, "whole_log_text" ) as mocked_log_txt: mocked_log_txt.return_value = "Dummy log data" msg = logger_service.create_email_message( fromaddr="", toaddrs="", ccaddrs="", subject="", priority="" ) self.assertIsInstance(msg, MIMEMultipart) def test_create_email_message_with_user_data(self): # We used a mocked logger service which doesn't depend on the # application trait and the presence of extensions to the extension # point `apptools.logger.plugin.mail_files` class MockedLoggerService(LoggerService): def _get_mail_files(self): return [lambda zip_file: None] logger_service = MockedLoggerService() with mock.patch.object( logger_service, "whole_log_text" ) as mocked_log_txt: mocked_log_txt.return_value = "Dummy log data" msg = logger_service.create_email_message( fromaddr="", toaddrs="", ccaddrs="", subject="", priority="", include_userdata=True, ) self.assertIsInstance(msg, MIMEMultipart) apptools-5.1.0/apptools/logger/plugin/__init__.py0000644000076500000240000000062713777642667022552 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/logger/plugin/logger_plugin.py0000644000076500000240000000663513777642667023655 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Logger plugin. """ # Standard library imports. import logging # Enthought library imports. from envisage.api import ExtensionPoint, Plugin from apptools.logger.log_queue_handler import LogQueueHandler from traits.api import Callable, List # Local imports. from .logger_preferences import LoggerPreferences from .logger_service import LoggerService ID = "apptools.logger" ILOGGER = ID + ".plugin.logger_service.LoggerService" class LoggerPlugin(Plugin): """Logger plugin.""" id = ID name = "Logger plugin" #### Extension points for this plugin ##################################### MAIL_FILES = "apptools.logger.plugin.mail_files" mail_files = ExtensionPoint( List(Callable), id=MAIL_FILES, desc=""" This extension point allows you to contribute functions which will be called to add project files to the zip file that the user mails back with bug reports from the Quality Agent. The function will be passed a zipfile.ZipFile object. """, ) #### Contributions to extension points made by this plugin ################ PREFERENCES = "envisage.preferences" PREFERENCES_PAGES = "envisage.ui.workbench.preferences_pages" VIEWS = "envisage.ui.workbench.views" preferences = List(contributes_to=PREFERENCES) preferences_pages = List(contributes_to=PREFERENCES_PAGES) views = List(contributes_to=VIEWS) def _preferences_default(self): return ["pkgfile://%s/plugin/preferences.ini" % ID] def _preferences_pages_default(self): from apptools.logger.plugin.view.logger_preferences_page import ( LoggerPreferencesPage, ) return [LoggerPreferencesPage] def _views_default(self): return [self._logger_view_factory] #### Plugin interface ##################################################### def start(self): """Starts the plugin.""" preferences = LoggerPreferences() service = LoggerService( application=self.application, preferences=preferences ) formatter = logging.Formatter("%(levelname)s|%(asctime)s|%(message)s") handler = LogQueueHandler() handler.setLevel(preferences.level_) handler.setFormatter(formatter) root_logger = logging.getLogger() root_logger.addHandler(handler) root_logger.setLevel(preferences.level_) service.handler = handler self.application.register_service(ILOGGER, service) def stop(self): """Stops the plugin.""" service = self.application.get_service(ILOGGER) service.save_preferences() #### LoggerPlugin private interface ####################################### def _logger_view_factory(self, **traits): from apptools.logger.plugin.view.logger_view import LoggerView service = self.application.get_service(ILOGGER) view = LoggerView(service=service, **traits) # Record the created view on the service. service.plugin_view = view return view apptools-5.1.0/apptools/logger/plugin/view/0000755000076500000240000000000013777643025021373 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/logger/plugin/view/logger_view.py0000644000076500000240000001471513777642667024301 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from datetime import datetime import logging # Enthought library imports. from pyface.api import ImageResource, clipboard from pyface.workbench.api import TraitsUIView from traits.api import ( Button, Instance, List, Property, Str, cached_property, on_trait_change, ) from traitsui.api import View, Group, Item, CodeEditor, TabularEditor, spring from traitsui.tabular_adapter import TabularAdapter # Local imports from apptools.logger.agent.quality_agent_view import QualityAgentView from apptools.logger.plugin.logger_service import LoggerService # Constants _IMAGE_MAP = { logging.DEBUG: ImageResource("debug"), logging.INFO: ImageResource("info"), logging.WARNING: ImageResource("warning"), logging.ERROR: ImageResource("error"), logging.CRITICAL: ImageResource("crit_error"), } class LogRecordAdapter(TabularAdapter): """A TabularEditor adapter for logging.LogRecord objects.""" columns = [ ("Level", "level"), ("Date", "date"), ("Time", "time"), ("Message", "message"), ] column_widths = [80, 100, 120, -1] level_image = Property level_text = Property(Str) date_text = Property(Str) time_text = Property(Str) message_text = Property(Str) def get_width(self, object, trait, column): return self.column_widths[column] def _get_level_image(self): return _IMAGE_MAP[self.item.levelno] def _get_level_text(self): return self.item.levelname.capitalize() def _get_date_text(self): dt = datetime.fromtimestamp(self.item.created) return dt.date().isoformat() def _get_time_text(self): dt = datetime.fromtimestamp(self.item.created) return dt.time().isoformat() def _get_message_text(self): # Just display the first line of multiline messages, like stacktraces. msg = self.item.getMessage() msgs = msg.strip().split("\n") if len(msgs) > 1: suffix = "... [double click for details]" else: suffix = "" abbrev_msg = msgs[0] + suffix return abbrev_msg class LoggerView(TraitsUIView): """The Workbench View showing the list of log items.""" id = Str("apptools.logger.plugin.view.logger_view.LoggerView") name = Str("Logger") service = Instance(LoggerService) log_records = List(Instance(logging.LogRecord)) formatted_records = Property(Str, depends_on="log_records") activated = Instance(logging.LogRecord) activated_text = Property(Str, depends_on="activated") reset_button = Button("Reset Logs") show_button = Button("Complete Text Log") copy_button = Button("Copy Log to Clipboard") code_editor = CodeEditor(lexer="null", show_line_numbers=False) log_records_editor = TabularEditor( adapter=LogRecordAdapter(), editable=False, activated="activated" ) trait_view = View( Group( Item("log_records", editor=log_records_editor), Group( Item("reset_button"), spring, Item("show_button"), Item("copy_button"), orientation="horizontal", show_labels=False, ), show_labels=False, ) ) ########################################################################### # LogQueueHandler view interface ########################################################################### def update(self, force=False): """Update 'log_records' if our handler has new records or 'force' is set. """ service = self.service if service.handler.has_new_records() or force: log_records = [ rec for rec in service.handler.get() if rec.levelno >= service.preferences.level_ ] log_records.reverse() self.log_records = log_records ########################################################################### # Private interface ########################################################################### @on_trait_change("service.preferences.level_") def _update_log_records(self): self.service.handler._view = self self.update(force=True) def _reset_button_fired(self): self.service.handler.reset() self.log_records = [] def _show_button_fired(self): self.edit_traits( view=View( Item( "formatted_records", editor=self.code_editor, style="readonly", show_label=False, ), width=800, height=600, resizable=True, buttons=["OK"], title="Complete Text Log", ) ) def _copy_button_fired(self): clipboard.text_data = self.formatted_records @cached_property def _get_formatted_records(self): return "\n".join( [ self.service.handler.formatter.format(record) for record in self.log_records ] ) def _activated_changed(self): if self.activated is None: return msg = self.activated.getMessage() if self.service.preferences.enable_agent: dialog = QualityAgentView(msg=msg, service=self.service) dialog.open() else: self.edit_traits( view=View( Item( "activated_text", editor=self.code_editor, style="readonly", show_label=False, ), width=800, height=600, resizable=True, buttons=["OK"], title="Log Message Detail", ) ) @cached_property def _get_activated_text(self): if self.activated is None: return "" else: return self.activated.getMessage() apptools-5.1.0/apptools/logger/plugin/view/images/0000755000076500000240000000000013777643025022640 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/logger/plugin/view/images/info.png0000644000076500000240000000706013777642667024317 0ustar aayresstaff00000000000000‰PNG  IHDRóÿa pHYs  šœ OiCCPPhotoshop ICC profilexÚSgTSé=÷ÞôBKˆ€”KoR RB‹€‘&*! Jˆ!¡ÙQÁEEÈ ˆŽŽ€ŒQ, Š Øä!¢Žƒ£ˆŠÊûá{£kÖ¼÷æÍþµ×>ç¬ó³ÏÀ –H3Q5€ ©BàƒÇÄÆáä.@ $p³d!sý#ø~<<+"À¾xÓ ÀM›À0‡ÿêB™\€„Àt‘8K€@zŽB¦@F€˜&S `ËcbãP-`'æÓ€ø™{[”! ‘ eˆDh;¬ÏVŠEX0fKÄ9Ø-0IWfH°·ÀÎ ² 0Qˆ…){`È##x„™FòW<ñ+®ç*x™²<¹$9E[-qWW.(ÎI+6aaš@.Ây™24àóÌ ‘àƒóýxήÎÎ6޶_-ê¿ÿ"bbãþåÏ«p@át~Ñþ,/³€;€mþ¢%îh^  u÷‹f²@µ éÚWópø~<ß5°j>{‘-¨]cöK'XtÀâ÷ò»oÁÔ(€hƒáÏwÿï?ýG %€fI’q^D$.Tʳ?ÇD *°AôÁ,ÀÁÜÁ ü`6„B$ÄÂBB d€r`)¬‚B(†Í°*`/Ô@4ÀQh†“p.ÂU¸=púažÁ(¼ AÈa!ÚˆbŠX#Ž™…ø!ÁH‹$ ɈQ"K‘5H1RŠT UHò=r9‡\Fº‘;È2‚ü†¼G1”²Q=Ô µC¹¨7„F¢ Ðdt1š ›Ðr´=Œ6¡çЫhÚ>CÇ0Àè3Äl0.ÆÃB±8, “c˱"¬ «Æ°V¬»‰õcϱwEÀ 6wB aAHXLXNØH¨ $4Ú 7 „QÂ'"“¨K´&ºùÄb21‡XH,#Ö/{ˆCÄ7$‰C2'¹I±¤TÒÒFÒnR#é,©›4H#“ÉÚdk²9”, +È…ääÃä3ää!ò[ b@q¤øSâ(RÊjJåå4åe˜2AU£šRݨ¡T5ZB­¡¶R¯Q‡¨4uš9̓IK¥­¢•Óhh÷i¯ètºÝ•N—ÐWÒËéGè—èôw †ƒÇˆg(›gw¯˜L¦Ó‹ÇT071ë˜ç™™oUX*¶*|‘Ê •J•&•*/T©ª¦ªÞª UóUËT©^S}®FU3Sã© Ô–«UªPëSSg©;¨‡ªg¨oT?¤~Yý‰YÃLÃOC¤Q ±_ã¼Æ c³x,!k «†u5Ä&±ÍÙ|v*»˜ý»‹=ª©¡9C3J3W³Ró”f?ã˜qøœtN ç(§—ó~ŠÞï)â)¦4L¹1e\kª–—–X«H«Q«Gë½6®í§¦½E»YûAÇJ'\'GgÎçSÙSݧ §M=:õ®.ªk¥¡»Dw¿n§î˜ž¾^€žLo§Þy½çú}/ýTýmú§õG X³ $Û Î<Å5qo</ÇÛñQC]Ã@C¥a•a—á„‘¹Ñ<£ÕFFŒiÆ\ã$ãmÆmÆ£&&!&KMêMîšRM¹¦)¦;L;LÇÍÌÍ¢ÍÖ™5›=1×2ç›ç›×›ß·`ZxZ,¶¨¶¸eI²äZ¦Yî¶¼n…Z9Y¥XUZ]³F­­%Ö»­»§§¹N“N«žÖgðñ¶É¶©·°åØÛ®¶m¶}agbg·Å®Ã“}º}ý= ‡Ù«Z~s´r:V:ޚΜî?}Åô–é/gXÏÏØ3ã¶Ë)ÄiS›ÓGgg¹sƒóˆ‹‰K‚Ë.—>.›ÆÝȽäJtõq]ázÒõ›³›Âí¨Û¯î6îiî‡ÜŸÌ4Ÿ)žY3sÐÃÈCàQåÑ? Ÿ•0k߬~OCOgµç#/c/‘W­×°·¥wª÷aï>ö>rŸã>ã<7Þ2ÞY_Ì7À·È·ËOÃož_…ßC#ÿdÿzÿѧ€%g‰A[ûøz|!¿Ž?:Ûeö²ÙíAŒ ¹AA‚­‚åÁ­!hÈì­!÷ç˜Î‘Îi…P~èÖÐaæa‹Ã~ '…‡…W†?ŽpˆXÑ1—5wÑÜCsßDúD–DÞ›g1O9¯-J5*>ª.j<Ú7º4º?Æ.fYÌÕXXIlK9.*®6nl¾ßüíó‡ââ ã{˜/È]py¡ÎÂô…§©.,:–@LˆN8”ðA*¨Œ%òw%Ž yÂÂg"/Ñ6шØC\*NòH*Mz’쑼5y$Å3¥,幄'©¼L LÝ›:žšv m2=:½1ƒ’‘qBª!M“¶gêgæfvˬe…²þÅn‹·/•Ék³¬Y- ¶B¦èTZ(×*²geWf¿Í‰Ê9–«ž+Íí̳ÊÛ7œïŸÿíÂá’¶¥†KW-X潬j9²‰Š®Û—Ø(Üxå‡oÊ¿™Ü”´©«Ä¹dÏfÒféæÞ-ž[–ª—æ—n ÙÚ´ ßV´íõöEÛ/—Í(Û»ƒ¶C¹£¿<¸¼e§ÉÎÍ;?T¤TôTúT6îÒݵa×ønÑî{¼ö4ìÕÛ[¼÷ý>ɾÛUUMÕfÕeûIû³÷?®‰ªéø–ûm]­NmqíÇÒý#¶×¹ÔÕÒ=TRÖ+ëGǾþïw- 6 UœÆâ#pDyäé÷ ß÷ :ÚvŒ{¬áÓvg/jBšòšF›Sšû[b[ºOÌ>ÑÖêÞzüGÛœ499â?rýéü§CÏdÏ&žþ¢þË®/~øÕë×Îјѡ—ò—“¿m|¥ýêÀë¯ÛÆÂƾÉx31^ôVûíÁwÜwï£ßOä| (ÿhù±õSЧû“““ÿ˜óüc3-ÛgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFKIDATxÚd“ÍOeÆï;ïìììÌÒý€åc Áµ¢A1 ¥1BXMÚg=·ü jÔ›ÆÄƒ›kL*š¦ RPk -t—»0ì²ììÌ·‡ãïþ<É“F&“0{‹ç”´Í¡½­û4«É®< ]ÇõBJƒn~ú–©¸r}‘¥ßw±M ŒðÚz'BZæBЏY¾ƒ³ù€Dþ,*ÕM¦h§‰â˜A³Q§QÛÆ7 ð›x2IÛÀ2K¨ fÓÌãç¬ó³ÏÀ –H3Q5€ ©BàƒÇÄÆáä.@ $p³d!sý#ø~<<+"À¾xÓ ÀM›À0‡ÿêB™\€„Àt‘8K€@zŽB¦@F€˜&S `ËcbãP-`'æÓ€ø™{[”! ‘ eˆDh;¬ÏVŠEX0fKÄ9Ø-0IWfH°·ÀÎ ² 0Qˆ…){`È##x„™FòW<ñ+®ç*x™²<¹$9E[-qWW.(ÎI+6aaš@.Ây™24àóÌ ‘àƒóýxήÎÎ6޶_-ê¿ÿ"bbãþåÏ«p@át~Ñþ,/³€;€mþ¢%îh^  u÷‹f²@µ éÚWópø~<ß5°j>{‘-¨]cöK'XtÀâ÷ò»oÁÔ(€hƒáÏwÿï?ýG %€fI’q^D$.Tʳ?ÇD *°AôÁ,ÀÁÜÁ ü`6„B$ÄÂBB d€r`)¬‚B(†Í°*`/Ô@4ÀQh†“p.ÂU¸=púažÁ(¼ AÈa!ÚˆbŠX#Ž™…ø!ÁH‹$ ɈQ"K‘5H1RŠT UHò=r9‡\Fº‘;È2‚ü†¼G1”²Q=Ô µC¹¨7„F¢ Ðdt1š ›Ðr´=Œ6¡çЫhÚ>CÇ0Àè3Äl0.ÆÃB±8, “c˱"¬ «Æ°V¬»‰õcϱwEÀ 6wB aAHXLXNØH¨ $4Ú 7 „QÂ'"“¨K´&ºùÄb21‡XH,#Ö/{ˆCÄ7$‰C2'¹I±¤TÒÒFÒnR#é,©›4H#“ÉÚdk²9”, +È…ääÃä3ää!ò[ b@q¤øSâ(RÊjJåå4åe˜2AU£šRݨ¡T5ZB­¡¶R¯Q‡¨4uš9̓IK¥­¢•Óhh÷i¯ètºÝ•N—ÐWÒËéGè—èôw †ƒÇˆg(›gw¯˜L¦Ó‹ÇT071ë˜ç™™oUX*¶*|‘Ê •J•&•*/T©ª¦ªÞª UóUËT©^S}®FU3Sã© Ô–«UªPëSSg©;¨‡ªg¨oT?¤~Yý‰YÃLÃOC¤Q ±_ã¼Æ c³x,!k «†u5Ä&±ÍÙ|v*»˜ý»‹=ª©¡9C3J3W³Ró”f?ã˜qøœtN ç(§—ó~ŠÞï)â)¦4L¹1e\kª–—–X«H«Q«Gë½6®í§¦½E»YûAÇJ'\'GgÎçSÙSݧ §M=:õ®.ªk¥¡»Dw¿n§î˜ž¾^€žLo§Þy½çú}/ýTýmú§õG X³ $Û Î<Å5qo</ÇÛñQC]Ã@C¥a•a—á„‘¹Ñ<£ÕFFŒiÆ\ã$ãmÆmÆ£&&!&KMêMîšRM¹¦)¦;L;LÇÍÌÍ¢ÍÖ™5›=1×2ç›ç›×›ß·`ZxZ,¶¨¶¸eI²äZ¦Yî¶¼n…Z9Y¥XUZ]³F­­%Ö»­»§§¹N“N«žÖgðñ¶É¶©·°åØÛ®¶m¶}agbg·Å®Ã“}º}ý= ‡Ù«Z~s´r:V:ޚΜî?}Åô–é/gXÏÏØ3ã¶Ë)ÄiS›ÓGgg¹sƒóˆ‹‰K‚Ë.—>.›ÆÝȽäJtõq]ázÒõ›³›Âí¨Û¯î6îiî‡ÜŸÌ4Ÿ)žY3sÐÃÈCàQåÑ? Ÿ•0k߬~OCOgµç#/c/‘W­×°·¥wª÷aï>ö>rŸã>ã<7Þ2ÞY_Ì7À·È·ËOÃož_…ßC#ÿdÿzÿѧ€%g‰A[ûøz|!¿Ž?:Ûeö²ÙíAŒ ¹AA‚­‚åÁ­!hÈì­!÷ç˜Î‘Îi…P~èÖÐaæa‹Ã~ '…‡…W†?ŽpˆXÑ1—5wÑÜCsßDúD–DÞ›g1O9¯-J5*>ª.j<Ú7º4º?Æ.fYÌÕXXIlK9.*®6nl¾ßüíó‡ââ ã{˜/È]py¡ÎÂô…§©.,:–@LˆN8”ðA*¨Œ%òw%Ž yÂÂg"/Ñ6шØC\*NòH*Mz’쑼5y$Å3¥,幄'©¼L LÝ›:žšv m2=:½1ƒ’‘qBª!M“¶gêgæfvˬe…²þÅn‹·/•Ék³¬Y- ¶B¦èTZ(×*²geWf¿Í‰Ê9–«ž+Íí̳ÊÛ7œïŸÿíÂá’¶¥†KW-X潬j9²‰Š®Û—Ø(Üxå‡oÊ¿™Ü”´©«Ä¹dÏfÒféæÞ-ž[–ª—æ—n ÙÚ´ ßV´íõöEÛ/—Í(Û»ƒ¶C¹£¿<¸¼e§ÉÎÍ;?T¤TôTúT6îÒݵa×ønÑî{¼ö4ìÕÛ[¼÷ý>ɾÛUUMÕfÕeûIû³÷?®‰ªéø–ûm]­NmqíÇÒý#¶×¹ÔÕÒ=TRÖ+ëGǾþïw- 6 UœÆâ#pDyäé÷ ß÷ :ÚvŒ{¬áÓvg/jBšòšF›Sšû[b[ºOÌ>ÑÖêÞzüGÛœ499â?rýéü§CÏdÏ&žþ¢þË®/~øÕë×Îјѡ—ò—“¿m|¥ýêÀë¯ÛÆÂƾÉx31^ôVûíÁwÜwï£ßOä| (ÿhù±õSЧû“““ÿ˜óüc3-ÛgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFIDATxÚlÒKh\uÇñïùßÇLn&™I2 y™¤Æ„šÚ8ØšÒ`µ…*öÕÄB¥èÎEAЕº ‚Š›€bÓhA¨+¡.ÜøFJ•4µµ±Óh3Iiœt^÷þïq1)nüíÏçÀ9?QUîņ–‹³Ÿâ‹Æ–…ÙãƒQ=´#‡O¶;ÆÿEÞî‡XχTÖÁõ=ŠVéW½×+µÏÞº4j ±*`Àõ`dßž>›h¥D!”n[llÉ¦Ž êíÇ®ÓÿÅ•üòÍÚ¬›€87`6–ãøi²C¢þΩܻ¬Ü„j‘]Ïlz?¬âƒ* ^ÐÜÄÔÊ1+‹ÿpëê½cÙW†÷w§VÎþHÝ£ôné ¬k°czüê…Ï1Sg)=}~ù޽S}'Â*ÉØ*j\?¹¨”Bþ¾²JáÒ*ƒÛºN?øK”ö‰<é‰ç¨‡®C610¼©"8®ƒ›L4€d³OÿX–Áñ,QHÛcùÍ/Æ?ÿ€Úÿ^u{ ˜»Æ¾ÝÎñ0$-ÆE\¯TÖk.Yüu•‘‰ž3ý¹»rëF–ÖÆ°ÕoA y¤ì<°Åûpy±ˆsˆj`k`#Ý3ÕódüÛ5Ú†Rt–6ŠtðGĽÞó ÏÇ4§]¢Ù:™ÍLV$\ ~*à6Añ¨P<*¤¶ƒÀ4m«ËÃãæã¯^­ØJDK»»gw¾#§çæI|¿­@7df”¶…ÐÒh! ûú»Œ/;¿>ù¦ám}ÈýÄ[˜GæcØ d› ¼;¿ãV– ´¶@³¿À°N.'3ßœxÙÈÜãæ…gwĹ>zè » Ö®6Ñ9+×aùÜÁ ¼çqÛÒááSaµfq1I‘dBq›DÚR F£ú]‘ÎÚZUªh\¶Hù²ö Ÿº_ÎLò„c%/‘–]#â jiô@TPâX1±­ ÅHݪâÑT.ñå¿Ý5µªß:IEND®B`‚apptools-5.1.0/apptools/logger/plugin/view/images/debug.png0000644000076500000240000000715413777642667024456 0ustar aayresstaff00000000000000‰PNG  IHDRóÿa pHYs  šœ OiCCPPhotoshop ICC profilexÚSgTSé=÷ÞôBKˆ€”KoR RB‹€‘&*! Jˆ!¡ÙQÁEEÈ ˆŽŽ€ŒQ, Š Øä!¢Žƒ£ˆŠÊûá{£kÖ¼÷æÍþµ×>ç¬ó³ÏÀ –H3Q5€ ©BàƒÇÄÆáä.@ $p³d!sý#ø~<<+"À¾xÓ ÀM›À0‡ÿêB™\€„Àt‘8K€@zŽB¦@F€˜&S `ËcbãP-`'æÓ€ø™{[”! ‘ eˆDh;¬ÏVŠEX0fKÄ9Ø-0IWfH°·ÀÎ ² 0Qˆ…){`È##x„™FòW<ñ+®ç*x™²<¹$9E[-qWW.(ÎI+6aaš@.Ây™24àóÌ ‘àƒóýxήÎÎ6޶_-ê¿ÿ"bbãþåÏ«p@át~Ñþ,/³€;€mþ¢%îh^  u÷‹f²@µ éÚWópø~<ß5°j>{‘-¨]cöK'XtÀâ÷ò»oÁÔ(€hƒáÏwÿï?ýG %€fI’q^D$.Tʳ?ÇD *°AôÁ,ÀÁÜÁ ü`6„B$ÄÂBB d€r`)¬‚B(†Í°*`/Ô@4ÀQh†“p.ÂU¸=púažÁ(¼ AÈa!ÚˆbŠX#Ž™…ø!ÁH‹$ ɈQ"K‘5H1RŠT UHò=r9‡\Fº‘;È2‚ü†¼G1”²Q=Ô µC¹¨7„F¢ Ðdt1š ›Ðr´=Œ6¡çЫhÚ>CÇ0Àè3Äl0.ÆÃB±8, “c˱"¬ «Æ°V¬»‰õcϱwEÀ 6wB aAHXLXNØH¨ $4Ú 7 „QÂ'"“¨K´&ºùÄb21‡XH,#Ö/{ˆCÄ7$‰C2'¹I±¤TÒÒFÒnR#é,©›4H#“ÉÚdk²9”, +È…ääÃä3ää!ò[ b@q¤øSâ(RÊjJåå4åe˜2AU£šRݨ¡T5ZB­¡¶R¯Q‡¨4uš9̓IK¥­¢•Óhh÷i¯ètºÝ•N—ÐWÒËéGè—èôw †ƒÇˆg(›gw¯˜L¦Ó‹ÇT071ë˜ç™™oUX*¶*|‘Ê •J•&•*/T©ª¦ªÞª UóUËT©^S}®FU3Sã© Ô–«UªPëSSg©;¨‡ªg¨oT?¤~Yý‰YÃLÃOC¤Q ±_ã¼Æ c³x,!k «†u5Ä&±ÍÙ|v*»˜ý»‹=ª©¡9C3J3W³Ró”f?ã˜qøœtN ç(§—ó~ŠÞï)â)¦4L¹1e\kª–—–X«H«Q«Gë½6®í§¦½E»YûAÇJ'\'GgÎçSÙSݧ §M=:õ®.ªk¥¡»Dw¿n§î˜ž¾^€žLo§Þy½çú}/ýTýmú§õG X³ $Û Î<Å5qo</ÇÛñQC]Ã@C¥a•a—á„‘¹Ñ<£ÕFFŒiÆ\ã$ãmÆmÆ£&&!&KMêMîšRM¹¦)¦;L;LÇÍÌÍ¢ÍÖ™5›=1×2ç›ç›×›ß·`ZxZ,¶¨¶¸eI²äZ¦Yî¶¼n…Z9Y¥XUZ]³F­­%Ö»­»§§¹N“N«žÖgðñ¶É¶©·°åØÛ®¶m¶}agbg·Å®Ã“}º}ý= ‡Ù«Z~s´r:V:ޚΜî?}Åô–é/gXÏÏØ3ã¶Ë)ÄiS›ÓGgg¹sƒóˆ‹‰K‚Ë.—>.›ÆÝȽäJtõq]ázÒõ›³›Âí¨Û¯î6îiî‡ÜŸÌ4Ÿ)žY3sÐÃÈCàQåÑ? Ÿ•0k߬~OCOgµç#/c/‘W­×°·¥wª÷aï>ö>rŸã>ã<7Þ2ÞY_Ì7À·È·ËOÃož_…ßC#ÿdÿzÿѧ€%g‰A[ûøz|!¿Ž?:Ûeö²ÙíAŒ ¹AA‚­‚åÁ­!hÈì­!÷ç˜Î‘Îi…P~èÖÐaæa‹Ã~ '…‡…W†?ŽpˆXÑ1—5wÑÜCsßDúD–DÞ›g1O9¯-J5*>ª.j<Ú7º4º?Æ.fYÌÕXXIlK9.*®6nl¾ßüíó‡ââ ã{˜/È]py¡ÎÂô…§©.,:–@LˆN8”ðA*¨Œ%òw%Ž yÂÂg"/Ñ6шØC\*NòH*Mz’쑼5y$Å3¥,幄'©¼L LÝ›:žšv m2=:½1ƒ’‘qBª!M“¶gêgæfvˬe…²þÅn‹·/•Ék³¬Y- ¶B¦èTZ(×*²geWf¿Í‰Ê9–«ž+Íí̳ÊÛ7œïŸÿíÂá’¶¥†KW-X潬j9²‰Š®Û—Ø(Üxå‡oÊ¿™Ü”´©«Ä¹dÏfÒféæÞ-ž[–ª—æ—n ÙÚ´ ßV´íõöEÛ/—Í(Û»ƒ¶C¹£¿<¸¼e§ÉÎÍ;?T¤TôTúT6îÒݵa×ønÑî{¼ö4ìÕÛ[¼÷ý>ɾÛUUMÕfÕeûIû³÷?®‰ªéø–ûm]­NmqíÇÒý#¶×¹ÔÕÒ=TRÖ+ëGǾþïw- 6 UœÆâ#pDyäé÷ ß÷ :ÚvŒ{¬áÓvg/jBšòšF›Sšû[b[ºOÌ>ÑÖêÞzüGÛœ499â?rýéü§CÏdÏ&žþ¢þË®/~øÕë×Îјѡ—ò—“¿m|¥ýêÀë¯ÛÆÂƾÉx31^ôVûíÁwÜwï£ßOä| (ÿhù±õSЧû“““ÿ˜óüc3-ÛgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅF‡IDATxÚdÌÿO”uð÷çóy¾Üs÷ÜwÜ7Ndf‚¡KnŶØ"jŽVêš1›eËÍå˜Q™¬1)YÌ¢ ‡´ÑVm©É3—5ÓTÊXCWgŒ€R´ƒørÜÜÝsÏó|>ýÒ}yý/Òöl-t]ÇÙ³”éÅŒc*e¦~øú«ËP C’u¸¼~À­±¤fJÌ"³œÂÃ;›!%°Œ<U²nµê³îCç£k×ÜBÓŽ:c”½ûÉW ÓX†ed lö½¤,¤ {rg³MALÛF̓÷q‡ƒ•ö}xÂíPÝØ»½á·Äl2¹ox`sg&ÂÂüèz³E/þ¹bq ­»ªQ¶y EÅþW;‹ CÞ=k‹|w~o+Ðe¤ó…«K ÉÊ™üR¼Õ§«Íãw:­*ú`ð°%ä6l ÞÝòL­<ðÑÛøØË¿µUŠá+ÅSÕëEHc" ŠÃWŠîMž/¼ þÚ²BÅ+ãÁ9H©‹©qîSŽ{kJÜÑÏߩ߇Ÿ/°¸!£obzû‡±{Möx3МDJøÞJÓ“ »V,¾ˆG›ÐóúnooBÿ*rEœ®]…ÅÜóA±´U#ËÛ<¢ÑAÄ‘8XW¹±ê.Î!]ú.Ý-ßœKoد‰ ]Æv, N@ – ¼·^ «¦A)ñ=Y^@/^xd#è¹oÇžú>vzô×Ûˆªfr1¿ 0Èå3OÀ'€³< ‹sú“)bª³fðê8‘ö>ýЀ/R4 ‡‹ÁmŸÉÂï –EaåÓ$B ENp+61ÿ¾1ŸÜÄm„i˱3XçÔPêPÀ)éIe ^ù.Ũ(q²,äu)ÑK žOòŽ¥ôÒ5‡Ëƒÿ`dR`’*I¸_W+ò5& Ý6Ä”Ìpi>—S&áŸþ€m[ „P‚ú€V˜V©6>“MÎYvš‚ÿúkãQ‡RöèWIEND®B`‚apptools-5.1.0/apptools/logger/plugin/view/images/error.png0000644000076500000240000000701513777642667024515 0ustar aayresstaff00000000000000‰PNG  IHDRóÿa pHYs  šœ OiCCPPhotoshop ICC profilexÚSgTSé=÷ÞôBKˆ€”KoR RB‹€‘&*! Jˆ!¡ÙQÁEEÈ ˆŽŽ€ŒQ, Š Øä!¢Žƒ£ˆŠÊûá{£kÖ¼÷æÍþµ×>ç¬ó³ÏÀ –H3Q5€ ©BàƒÇÄÆáä.@ $p³d!sý#ø~<<+"À¾xÓ ÀM›À0‡ÿêB™\€„Àt‘8K€@zŽB¦@F€˜&S `ËcbãP-`'æÓ€ø™{[”! ‘ eˆDh;¬ÏVŠEX0fKÄ9Ø-0IWfH°·ÀÎ ² 0Qˆ…){`È##x„™FòW<ñ+®ç*x™²<¹$9E[-qWW.(ÎI+6aaš@.Ây™24àóÌ ‘àƒóýxήÎÎ6޶_-ê¿ÿ"bbãþåÏ«p@át~Ñþ,/³€;€mþ¢%îh^  u÷‹f²@µ éÚWópø~<ß5°j>{‘-¨]cöK'XtÀâ÷ò»oÁÔ(€hƒáÏwÿï?ýG %€fI’q^D$.Tʳ?ÇD *°AôÁ,ÀÁÜÁ ü`6„B$ÄÂBB d€r`)¬‚B(†Í°*`/Ô@4ÀQh†“p.ÂU¸=púažÁ(¼ AÈa!ÚˆbŠX#Ž™…ø!ÁH‹$ ɈQ"K‘5H1RŠT UHò=r9‡\Fº‘;È2‚ü†¼G1”²Q=Ô µC¹¨7„F¢ Ðdt1š ›Ðr´=Œ6¡çЫhÚ>CÇ0Àè3Äl0.ÆÃB±8, “c˱"¬ «Æ°V¬»‰õcϱwEÀ 6wB aAHXLXNØH¨ $4Ú 7 „QÂ'"“¨K´&ºùÄb21‡XH,#Ö/{ˆCÄ7$‰C2'¹I±¤TÒÒFÒnR#é,©›4H#“ÉÚdk²9”, +È…ääÃä3ää!ò[ b@q¤øSâ(RÊjJåå4åe˜2AU£šRݨ¡T5ZB­¡¶R¯Q‡¨4uš9̓IK¥­¢•Óhh÷i¯ètºÝ•N—ÐWÒËéGè—èôw †ƒÇˆg(›gw¯˜L¦Ó‹ÇT071ë˜ç™™oUX*¶*|‘Ê •J•&•*/T©ª¦ªÞª UóUËT©^S}®FU3Sã© Ô–«UªPëSSg©;¨‡ªg¨oT?¤~Yý‰YÃLÃOC¤Q ±_ã¼Æ c³x,!k «†u5Ä&±ÍÙ|v*»˜ý»‹=ª©¡9C3J3W³Ró”f?ã˜qøœtN ç(§—ó~ŠÞï)â)¦4L¹1e\kª–—–X«H«Q«Gë½6®í§¦½E»YûAÇJ'\'GgÎçSÙSݧ §M=:õ®.ªk¥¡»Dw¿n§î˜ž¾^€žLo§Þy½çú}/ýTýmú§õG X³ $Û Î<Å5qo</ÇÛñQC]Ã@C¥a•a—á„‘¹Ñ<£ÕFFŒiÆ\ã$ãmÆmÆ£&&!&KMêMîšRM¹¦)¦;L;LÇÍÌÍ¢ÍÖ™5›=1×2ç›ç›×›ß·`ZxZ,¶¨¶¸eI²äZ¦Yî¶¼n…Z9Y¥XUZ]³F­­%Ö»­»§§¹N“N«žÖgðñ¶É¶©·°åØÛ®¶m¶}agbg·Å®Ã“}º}ý= ‡Ù«Z~s´r:V:ޚΜî?}Åô–é/gXÏÏØ3ã¶Ë)ÄiS›ÓGgg¹sƒóˆ‹‰K‚Ë.—>.›ÆÝȽäJtõq]ázÒõ›³›Âí¨Û¯î6îiî‡ÜŸÌ4Ÿ)žY3sÐÃÈCàQåÑ? Ÿ•0k߬~OCOgµç#/c/‘W­×°·¥wª÷aï>ö>rŸã>ã<7Þ2ÞY_Ì7À·È·ËOÃož_…ßC#ÿdÿzÿѧ€%g‰A[ûøz|!¿Ž?:Ûeö²ÙíAŒ ¹AA‚­‚åÁ­!hÈì­!÷ç˜Î‘Îi…P~èÖÐaæa‹Ã~ '…‡…W†?ŽpˆXÑ1—5wÑÜCsßDúD–DÞ›g1O9¯-J5*>ª.j<Ú7º4º?Æ.fYÌÕXXIlK9.*®6nl¾ßüíó‡ââ ã{˜/È]py¡ÎÂô…§©.,:–@LˆN8”ðA*¨Œ%òw%Ž yÂÂg"/Ñ6шØC\*NòH*Mz’쑼5y$Å3¥,幄'©¼L LÝ›:žšv m2=:½1ƒ’‘qBª!M“¶gêgæfvˬe…²þÅn‹·/•Ék³¬Y- ¶B¦èTZ(×*²geWf¿Í‰Ê9–«ž+Íí̳ÊÛ7œïŸÿíÂá’¶¥†KW-X潬j9²‰Š®Û—Ø(Üxå‡oÊ¿™Ü”´©«Ä¹dÏfÒféæÞ-ž[–ª—æ—n ÙÚ´ ßV´íõöEÛ/—Í(Û»ƒ¶C¹£¿<¸¼e§ÉÎÍ;?T¤TôTúT6îÒݵa×ønÑî{¼ö4ìÕÛ[¼÷ý>ɾÛUUMÕfÕeûIû³÷?®‰ªéø–ûm]­NmqíÇÒý#¶×¹ÔÕÒ=TRÖ+ëGǾþïw- 6 UœÆâ#pDyäé÷ ß÷ :ÚvŒ{¬áÓvg/jBšòšF›Sšû[b[ºOÌ>ÑÖêÞzüGÛœ499â?rýéü§CÏdÏ&žþ¢þË®/~øÕë×Îјѡ—ò—“¿m|¥ýêÀë¯ÛÆÂƾÉx31^ôVûíÁwÜwï£ßOä| (ÿhù±õSЧû“““ÿ˜óüc3-ÛgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅF(IDATxÚd“Ío”eÅÏ3ï|´ÓÎà8öCŠÚ¤¨›Ò%éWŒ vD(%"ƒiH¨ñ ]T °qaY`"Á€ŠI ºh')I#&l © iikgÚ™v:ï̼ï3ï\ЦƳ»7çœÜ䞣D„íø<‘H½‹íÒ(P ‘*,g2³ß-.ölç[ۇϚ›'¿½|9 Ö>Ð>@Z(Úv˧ ·¿O§»65jó‚OâñÉÑÑÑdxj ¼*kŸh%` äÛÛeðĉÔÙl×–Á¡htò› ’ ©K…Ká0o‰à¯¯¥qm›‡Z³ÓqhŒÅXìè/OJý´±Ñå{xþ|jøôé‚7o2¿ºJñÈ^döî]"ÓÓ”WW™nm%1<̲1dïÜ!4?¯Þ9z´å‹îî÷éDæ~íì”Ûmmò`hH6±žËɽþ~¹ü¸ …­ý_'OʽŽùcß>9XW7c×õþ\_§¶R!~ëѽ{yu`€ÈŽt^¹J¡µàé¥Kd'&È‹PÙØ jŒg”JÄ«Uò¶=4D)—#qìØ–Ðó<–.^dúìY6ü~Vµ&¯¬jµJzy™’®Rä]k|œðþýhŸ”¢R.s|œôÊ N À?"¬+EU­•â cØm õ®Kco/o|M¡X¤èlÇ¡èöœ;‡ÿ½$%×¥Æ^4”Â× ~Õ©T´ÞóhééaÏÈ®ë‚ßOåÚ5¼Ç)¿þN¹Lc_/êÑ#‚óóÄC!fµÎYÊq|éXÌ{³Tò½ËÎdXe'ucc¬9ƒ¶,¢–ÅB_ñ¿gÙ½¶F¶¾ž©Ú°£W2!%"ðûº›››ÞÍçu¨­H2‰{õ*¾gIËÂ:|{b÷É~‹Dœß3¿T*‰­(,H$š>4F‹1jkñ)…R ñ<*¶  œss™ë®›øO†B ­­M‰h¿Rðü"BE„J¹×gfÒ?;NâeÚÄ¡šš§ÑxÜ<¯ªR Od3™šËå]Ûùÿ:)†œ [šIEND®B`‚apptools-5.1.0/apptools/logger/plugin/view/images/crit_error.png0000644000076500000240000000156713777642667025544 0ustar aayresstaff00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  šœtIMEØ&ãEï+IDAT8Ëm“Kh\e†ŸïÿÏÜǘԤ™6L  T¡jË(šD‘ºÚMW"Š 7-–ØT„6*¨©D”‚BA\(¢ ¨ AŠ©([)ôjÒŽNB’“Î%s9çüçw‘RÚwù}ðÀwyÄŸùø­\<ÑÖËR«.ϘÈn¬ÉFÀG™©}ÏšuBñÕ¦h¬5ˆh<·j¿ùüåÜÁ7ÿ¾ ðáÑôÔsûOd—ŠW°6@)¥ux`|´¡­½×~÷å¡Ü«o†×'ÞØ2õìÞ‰liáÍÆM‘$‘H×­ã¹uD‰d'mûã·ÇrcïÌëxó“Ü螃nüF­²Hßà“ í|‰¹üj•š2áH’‡Ÿ8Œç.3_ø“ÆJIر'óÅɽ)¥Tÿ_—~ææÒ,›º¸ç+$ÛØ5r E(çÑ‘£$îîcè‘$îêb¹”'?}F´vúc|S©”¸qíW6Ÿ?Å}¾H4ÑÍÓûN t€‹gO2›?G` ¶^Æß8ÖZê+DW7ÈýôÍúÛz¥Bxn…‹g?ãß¿ÂÚ€ 0Xk±ÖâXk©ÕÊ(¥pœ"ÂÌ•Óô>ƒˆF‰ÂóêL_>Mµ²„ˆàûA`­E‰ŽB¯l[z;»†ÇðZUŒßÄóê¿ÉîÑ16w÷òÿÙEA)¥u4' “éb÷È!ÜVã7¹vák®_þã71~“ìSclí¹Ç Æåãk¥” G¢ÚV«L4ÖAáú/\8÷Ji´ÓµuõÚ"ÖZ¢±¾ç¶‚ ˆŠ1>“ã³÷tnIiRí)R=CÌ\BDVçTšþÁ,sÿœ§Z.âùn«Tœ+9þozý•'Ç;gS©L*žhWÆx„Â1DdÍ‹ç6pœ+µ¥Öü|¾8>YJß&Óñ#]³=ÛS‰ä&%¢nXP«”ܹ¹éÅÃïÓw´àý×» ±XbeuËj P¯×b¯½»p‹êÿdñV®AFIEND®B`‚apptools-5.1.0/apptools/logger/plugin/view/__init__.py0000644000076500000240000000062713777642667023524 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/logger/plugin/view/logger_preferences_page.py0000644000076500000240000000606513777642667026623 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.preferences.ui.api import PreferencesPage from traits.api import Bool, Trait, Str from traitsui.api import EnumEditor, Group, Item, View class LoggerPreferencesPage(PreferencesPage): """A preference page for the logger plugin.""" #### 'PreferencesPage' interface ########################################## # The page's category (e.g. 'General/Appearance'). The empty string means # that this is a top-level page. category = "" # The page's help identifier (optional). If a help Id *is* provided then # there will be a 'Help' button shown on the preference page. help_id = "" # The page name (this is what is shown in the preferences dialog. name = "Logger" # The path to the preferences node that contains the preferences. preferences_path = "apptools.logger" #### Preferences ########################################################## # The log levels level = Trait( "Info", { "Debug": logging.DEBUG, "Info": logging.INFO, "Warning": logging.WARNING, "Error": logging.ERROR, "Critical": logging.CRITICAL, }, is_str=True, ) enable_agent = Bool(False) smtp_server = Str to_address = Str from_address = Str # The view used to change the plugin preferences traits_view = View( Group( Group( Item( name="level", editor=EnumEditor( values={ "Debug": "1:Debug", "Info": "2:Info", "Warning": "3:Warning", "Error": "4:Error", "Critical": "5:Critical", }, ), style="simple", ), label="Logger Settings", show_border=True, ), Group(Item(name="10")), Group( Group( Group( Item( name="enable_agent", label="Enable quality agent" ), show_left=False, ), Group( Item(name="smtp_server", label="SMTP server"), Item(name="from_address"), Item(name="to_address"), enabled_when="enable_agent==True", ), ), label="Quality Agent Settings", show_border=True, ), ), ) apptools-5.1.0/apptools/logger/__init__.py0000644000076500000240000000077313777642667021256 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Convenience functions for creating logging handlers. Part of the EnthoughtBase project. """ apptools-5.1.0/apptools/logger/agent/0000755000076500000240000000000013777643025020221 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/logger/agent/tests/0000755000076500000240000000000013777643025021363 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/logger/agent/tests/__init__.py0000644000076500000240000000062713777642667023514 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/logger/agent/tests/test_attachments.py0000644000076500000240000000416613777642667025331 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 email.mime.multipart import MIMEMultipart import io import os import shutil import tempfile import unittest from apptools.logger.agent.attachments import Attachments class AttachmentsTestCase(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.tmpdir) self.tmpfile = os.path.join(self.tmpdir, "dummy_file.txt") with io.open(self.tmpfile, "w", encoding="utf8") as filehandle: filehandle.write("Dummy data in dummy file for dummies") def test_attaching_workspace(self): class DummyWorkspace(object): path = self.tmpdir class MockedApplication(object): tmpdir = self.tmpdir def get_service(self, service_id): return DummyWorkspace() attachments = Attachments( application=MockedApplication(), message=MIMEMultipart() ) attachments.package_workspace() message = attachments.message self.assertTrue(message.is_multipart()) payload = message.get_payload() self.assertEqual(len(payload), 1) def test_attaching_single_project(self): class DummySingleProject(object): location = self.tmpdir class MockedApplication(object): tmpdir = self.tmpdir def get_service(self, service_id): return DummySingleProject() attachments = Attachments( application=MockedApplication(), message=MIMEMultipart() ) attachments.package_single_project() message = attachments.message self.assertTrue(message.is_multipart()) payload = message.get_payload() self.assertEqual(len(payload), 1) apptools-5.1.0/apptools/logger/agent/__init__.py0000644000076500000240000000067113777642667022351 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ lib.apptools.logger.agent """ apptools-5.1.0/apptools/logger/agent/attachments.py0000644000076500000240000000625213777642667023126 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Attach relevant project files. FIXME: there are no public project plugins for Envisage 3, yet. In any case, this stuff should not be hard-coded, but extensible via extension points. The code remains here because we can reuse the zip utility code in that extensible rewrite. """ import logging import os.path from email import encoders from email.mime.base import MIMEBase from traits.api import Any, HasTraits logger = logging.getLogger(__name__) class Attachments(HasTraits): application = Any() message = Any() def __init__(self, message, **traits): traits = traits.copy() traits["message"] = message super(Attachments, self).__init__(**traits) # FIXME: all of the package_*() methods refer to deprecated project plugins def package_workspace(self): if self.application is None: pass workspace = self.application.get_service("envisage.project.IWorkspace") if workspace is not None: dir = workspace.path self._attach_directory(dir) def package_single_project(self): if self.application is None: pass single_project = self.application.get_service( "envisage.single_project.ModelService" ) if single_project is not None: dir = single_project.location self._attach_directory(dir) def package_any_relevant_files(self): self.package_workspace() self.package_single_project() def _attach_directory(self, dir): relpath = os.path.basename(dir) import zipfile from io import BytesIO ctype = "application/octet-stream" maintype, subtype = ctype.split("/", 1) msg = MIMEBase(maintype, subtype) file_object = BytesIO() zip = zipfile.ZipFile(file_object, "w") _append_to_zip_archive(zip, dir, relpath) zip.close() msg.set_payload(file_object.getvalue()) encoders.encode_base64(msg) # Encode the payload using Base64 msg.add_header( "Content-Disposition", "attachment", filename="project.zip" ) self.message.attach(msg) file_object.close() def _append_to_zip_archive(zip, dir, relpath): """ Add all files in and below directory dir into zip archive""" for filename in os.listdir(dir): path = os.path.join(dir, filename) if os.path.isfile(path): name = os.path.join(relpath, filename) zip.write(path, name) logger.debug("adding %s to error report" % path) else: if filename != ".svn": # skip svn files if any subdir = os.path.join(dir, filename) _append_to_zip_archive( zip, subdir, os.path.join(relpath, filename) ) apptools-5.1.0/apptools/logger/agent/quality_agent_mailer.py0000644000076500000240000000655013777642667025013 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 # Enthought library imports. from traits.util.home_directory import get_home_directory # Setup a logger for this module. logger = logging.getLogger(__name__) def create_email_message( fromaddr, toaddrs, ccaddrs, subject, priority, include_project=False, stack_trace="", comments="", ): # format a message suitable to be sent to the Roundup bug tracker from email.MIMEMultipart import MIMEMultipart from email.MIMEText import MIMEText from email.MIMEBase import MIMEBase message = MIMEMultipart() message["Subject"] = "%s [priority=%s]" % (subject, priority) message["To"] = ", ".join(toaddrs) message["Cc"] = ", ".join(ccaddrs) message["From"] = fromaddr message.preamble = "You will not see this in a MIME-aware mail reader.\n" message.epilogue = " " # To guarantee the message ends with a newline # First section is simple ASCII data ... m = [] m.append("Bug Report") m.append("==============================") m.append("") if len(comments) > 0: m.append("Comments:") m.append("========") m.append(comments) m.append("") if len(stack_trace) > 0: m.append("Stack Trace:") m.append("===========") m.append(stack_trace) m.append("") msg = MIMEText("\n".join(m)) message.attach(msg) # Include the log file ... if True: try: log = os.path.join(get_home_directory(), "envisage.log") f = open(log, "r") entries = f.readlines() f.close() ctype = "application/octet-stream" maintype, subtype = ctype.split("/", 1) msg = MIMEBase(maintype, subtype) msg = MIMEText("".join(entries)) msg.add_header( "Content-Disposition", "attachment", filename="logfile.txt" ) message.attach(msg) except Exception: logger.exception("Failed to include log file with message") # Include the environment variables ... if True: """ Transmit the user's environment settings as well. Main purpose is to work out the user name to help with following up on bug reports and in future we should probably send less data. """ try: entries = [] for key, value in os.environ.items(): entries.append("%30s : %s\n" % (key, value)) ctype = "application/octet-stream" maintype, subtype = ctype.split("/", 1) msg = MIMEBase(maintype, subtype) msg = MIMEText("".join(entries)) msg.add_header( "Content-Disposition", "attachment", filename="environment.txt" ) message.attach(msg) except Exception: logger.exception( "Failed to include environment variables with message" ) return message apptools-5.1.0/apptools/logger/agent/quality_agent_view.py0000644000076500000240000002754313777642667024521 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Standard library imports. import logging # Enthought library imports. from pyface.api import Dialog from traits.api import Any, Str, Tuple # Setup a logger for this module. logger = logging.getLogger(__name__) priority_levels = ["Low", "Medium", "High", "Critical"] class QualityAgentView(Dialog): size = Tuple((700, 900)) title = Str("Quality Agent") # The associated LoggerService. service = Any() msg = Str("") subject = Str("Untitled Error Report") to_address = Str() cc_address = Str("") from_address = Str() smtp_server = Str() priority = Str(priority_levels[2]) comments = Str("None") include_userdata = Any ########################################################################### # Protected 'Dialog' interface. ########################################################################### # fixme: Ideally, this should be passed in; this topic ID belongs to the # Enlib help project/plug-in. help_id = "enlib|HID_Quality_Agent_Dlg" def _create_dialog_area(self, parent): """ Creates the main content of the dialog. """ import wx parent.SetSizeHints(minW=300, minH=575) # Add the main panel sizer = wx.BoxSizer(wx.VERTICAL) panel = wx.Panel(parent, -1) panel.SetSizer(sizer) panel.SetAutoLayout(True) # Add a descriptive label at the top ... label = wx.StaticText(panel, -1, "Send a comment or bug report ...") sizer.Add(label, 0, wx.ALL, border=5) # Add the stack trace view ... error_panel = self._create_error_panel(panel) sizer.Add( error_panel, 1, wx.ALL | wx.EXPAND | wx.CLIP_CHILDREN, border=5 ) # Update the layout: sizer.Fit(panel) # Add the error report view ... report_panel = self._create_report_panel(panel) sizer.Add( report_panel, 2, wx.ALL | wx.EXPAND | wx.CLIP_CHILDREN, border=5 ) # Update the layout: sizer.Fit(panel) return panel def _create_buttons(self, parent): """ Creates the buttons. """ import wx sizer = wx.BoxSizer(wx.HORIZONTAL) # 'Send' button. send = wx.Button(parent, wx.ID_OK, "Send") wx.EVT_BUTTON(parent, wx.ID_OK, self._on_send) sizer.Add(send) send.SetDefault() # 'Cancel' button. cancel = wx.Button(parent, wx.ID_CANCEL, "Cancel") wx.EVT_BUTTON(parent, wx.ID_CANCEL, self._wx_on_cancel) sizer.Add(cancel, 0, wx.LEFT, 10) # 'Help' button. if len(self.help_id) > 0: help = wx.Button(parent, wx.ID_HELP, "Help") wx.EVT_BUTTON(parent, wx.ID_HELP, self._wx_on_help) sizer.Add(help, 0, wx.LEFT, 10) return sizer ### Utility methods ####################################################### def _create_error_panel(self, parent): import wx box = wx.StaticBox(parent, -1, "Message:") sizer = wx.StaticBoxSizer(box, wx.VERTICAL) # Print the stack trace label2 = wx.StaticText( parent, -1, "The following information will be included in the report:", ) sizer.Add( label2, 0, wx.LEFT | wx.TOP | wx.BOTTOM | wx.CLIP_CHILDREN, border=5, ) details = wx.TextCtrl( parent, -1, self.msg, size=(-1, 75), style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL | wx.VSCROLL | wx.TE_RICH2 | wx.CLIP_CHILDREN, ) details.SetSizeHints(minW=-1, minH=75) # Set the font to not be proportional font = wx.Font(12, wx.MODERN, wx.NORMAL, wx.NORMAL) details.SetStyle(0, len(self.msg), wx.TextAttr(font=font)) sizer.Add(details, 1, wx.EXPAND | wx.ALL | wx.CLIP_CHILDREN, 5) return sizer def _create_report_panel(self, parent): import wx box = wx.StaticBox(parent, -1, "Report Information:") sizer = wx.StaticBoxSizer(box, wx.VERTICAL) # Add email info ... sizer.Add(self._create_email_info(parent), 0, wx.ALL | wx.EXPAND, 5) # Add priority combo: sizer.Add(self._create_priority_combo(parent), 0, wx.ALL | wx.RIGHT, 5) # Extra comments from the user: label3 = wx.StaticText(parent, -1, "Additional Comments:") sizer.Add( label3, 0, wx.LEFT | wx.TOP | wx.BOTTOM | wx.CLIP_CHILDREN, 5 ) comments_field = wx.TextCtrl( parent, -1, self.comments, size=(-1, 75), style=wx.TE_MULTILINE | wx.TE_RICH2 | wx.CLIP_CHILDREN, ) comments_field.SetSizeHints(minW=-1, minH=75) font = wx.Font(12, wx.MODERN, wx.NORMAL, wx.NORMAL) comments_field.SetStyle(0, len(self.comments), wx.TextAttr(font=font)) sizer.Add(comments_field, 1, wx.ALL | wx.EXPAND | wx.CLIP_CHILDREN, 5) wx.EVT_TEXT(parent, comments_field.GetId(), self._on_comments) # Include the project combobox? if len(self.service.mail_files) > 0: sizer.Add(self._create_project_upload(parent), 0, wx.ALL, border=5) return sizer def _create_email_info(self, parent): import wx # Layout setup .. sizer = wx.FlexGridSizer(5, 2, 10, 10) sizer.AddGrowableCol(1) title_label = wx.StaticText(parent, -1, "Subject:") sizer.Add(title_label, 0, wx.ALL | wx.ALIGN_RIGHT) title_field = wx.TextCtrl(parent, -1, self.subject, wx.Point(-1, -1)) sizer.Add( title_field, 1, wx.EXPAND | wx.ALL | wx.ALIGN_RIGHT | wx.CLIP_CHILDREN, ) wx.EVT_TEXT(parent, title_field.GetId(), self._on_subject) to_label = wx.StaticText(parent, -1, "To:") sizer.Add(to_label, 0, wx.ALL | wx.ALIGN_RIGHT) to_field = wx.TextCtrl(parent, -1, self.to_address) sizer.Add( to_field, 1, wx.EXPAND | wx.ALL | wx.ALIGN_RIGHT | wx.CLIP_CHILDREN ) wx.EVT_TEXT(parent, to_field.GetId(), self._on_to) cc_label = wx.StaticText(parent, -1, "Cc:") sizer.Add(cc_label, 0, wx.ALL | wx.ALIGN_RIGHT) cc_field = wx.TextCtrl(parent, -1, "") sizer.Add( cc_field, 1, wx.EXPAND | wx.ALL | wx.ALIGN_RIGHT | wx.CLIP_CHILDREN ) wx.EVT_TEXT(parent, cc_field.GetId(), self._on_cc) from_label = wx.StaticText(parent, -1, "From:") sizer.Add(from_label, 0, wx.ALL | wx.ALIGN_RIGHT) from_field = wx.TextCtrl(parent, -1, self.from_address) sizer.Add( from_field, 1, wx.EXPAND | wx.ALL | wx.ALIGN_RIGHT | wx.CLIP_CHILDREN, ) wx.EVT_TEXT(parent, from_field.GetId(), self._on_from) smtp_label = wx.StaticText(parent, -1, "SMTP Server:") sizer.Add(smtp_label, 0, wx.ALL | wx.ALIGN_RIGHT) smtp_server_field = wx.TextCtrl(parent, -1, self.smtp_server) sizer.Add( smtp_server_field, 1, wx.EXPAND | wx.ALL | wx.ALIGN_RIGHT | wx.CLIP_CHILDREN, ) wx.EVT_TEXT(parent, smtp_server_field.GetId(), self._on_smtp_server) return sizer def _create_priority_combo(self, parent): import wx sizer = wx.BoxSizer(wx.HORIZONTAL) label = wx.StaticText(parent, -1, "How critical is this issue?") sizer.Add(label, 0, wx.ALL, border=0) cb = wx.ComboBox( parent, -1, self.priority, wx.Point(90, 50), wx.Size(95, -1), priority_levels, wx.CB_READONLY, ) sizer.Add(cb, 1, wx.EXPAND | wx.LEFT | wx.CLIP_CHILDREN, border=10) wx.EVT_COMBOBOX(parent, cb.GetId(), self._on_priority) return sizer def _create_project_upload(self, parent): import wx id = wx.NewId() cb = wx.CheckBox( parent, id, "Include Workspace Files (will increase email size) ", wx.Point(65, 80), wx.Size(-1, 20), wx.NO_BORDER, ) wx.EVT_CHECKBOX(parent, id, self._on_project) return cb ## UI Listeners ########################################################### def _on_subject(self, event): self.subject = event.GetEventObject().GetValue() def _on_to(self, event): self.to_address = event.GetEventObject().GetValue() def _on_cc(self, event): self.cc_address = event.GetEventObject().GetValue() def _on_from(self, event): self.from_address = event.GetEventObject().GetValue() def _on_smtp_server(self, event): self.smtp_server = event.GetEventObject().GetValue() def _on_priority(self, event): self.priority = event.GetEventObject().GetStringSelection() def _on_comments(self, event): self.comments = event.GetEventObject().GetValue() def _on_project(self, event): self.include_userdata = event.Checked() cb = event.GetEventObject() if event.Checked(): cb.SetLabel( "Include Workspace Files (approx. %.2f MBytes)" % self._compute_project_size() ) else: cb.SetLabel("Include Workspace Files (will increase email size)") def _on_send(self, event): # Disable the Send button while we go through the possibly # time-consuming email-sending process. button = event.GetEventObject() button.Enable(0) fromaddr, toaddrs, ccaddrs = self._create_email_addresses() message = self._create_email(fromaddr, toaddrs, ccaddrs) self.service.send_bug_report( self.smtp_server, fromaddr, toaddrs, ccaddrs, message ) # save the user's preferences self.service.preferences.smtp_server = self.smtp_server self.service.preferences.to_address = self.to_address self.service.preferences.from_address = self.from_address # finally we close the dialog self._wx_on_ok(event) ## Private ################################################################ def _create_email_addresses(self): # utility function map addresses from ui into the standard format # FIXME: We should use standard To: header parsing instead of this ad # hoc whitespace-only approach. fromaddr = self.from_address if "" == fromaddr.strip(): fromaddr = "anonymous" toaddrs = self.to_address.split() ccaddrs = self.cc_address.split() return fromaddr, toaddrs, ccaddrs def _compute_project_size(self): # determine size of email in MBytes fromaddr, toaddrs, ccaddrs = self._create_email_addresses() message = self._create_email(fromaddr, toaddrs, ccaddrs) return len(message.as_string()) / (2.0 ** 20) def _create_email(self, fromaddr, toaddrs, ccaddrs): return self.service.create_email_message( fromaddr, toaddrs, ccaddrs, self.subject, self.priority, self.include_userdata, self.msg, self.comments, ) def _to_address_default(self): return self.service.preferences.to_address def _from_address_default(self): return self.service.preferences.from_address def _smtp_server_default(self): return self.service.preferences.smtp_server apptools-5.1.0/apptools/logger/logger.py0000644000076500000240000000334613777642667020775 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Convenience functions for creating logging handlers etc. """ # Standard library imports. import logging from logging.handlers import RotatingFileHandler # Local imports. from .log_queue_handler import LogQueueHandler # The default logging level. LEVEL = logging.DEBUG # The default formatter. FORMATTER = logging.Formatter("%(levelname)s|%(asctime)s|%(message)s") class LogFileHandler(RotatingFileHandler): """The default log file handler.""" def __init__( self, path, maxBytes=1000000, backupCount=3, level=None, formatter=None ): RotatingFileHandler.__init__( self, path, maxBytes=maxBytes, backupCount=3 ) if level is None: level = LEVEL if formatter is None: formatter = FORMATTER # Set our default formatter and log level. self.setFormatter(formatter) self.setLevel(level) def add_log_queue_handler(logger, level=None, formatter=None): """Adds a queueing log handler to a logger.""" if level is None: level = LEVEL if formatter is None: formatter = FORMATTER # Add the handler to the root logger. log_queue_handler = LogQueueHandler() log_queue_handler.setLevel(level) log_queue_handler.setFormatter(formatter) logger.addHandler(log_queue_handler) return log_queue_handler apptools-5.1.0/apptools/logger/api.py0000644000076500000240000000130613777642667020261 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for the apptools.logger subpackage. - :func:`~.add_log_queue_handler` - :func:`~.log_point` - :class:`~.LogFileHandler` - :attr:`~.FORMATTER` - :attr:`~.LEVEL` """ from .logger import add_log_queue_handler from .logger import FORMATTER, LEVEL, LogFileHandler from .log_point import log_point apptools-5.1.0/apptools/logger/custom_excepthook.py0000644000076500000240000000206413777642667023255 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Standard library imports. import logging from traceback import format_exception """ To catch exceptions with our own code this code needs to be added sys.excepthook = custom_excepthook """ def custom_excepthook(type, value, traceback): """ Pass on the exception to the logging system. """ msg = "Custom - Traceback (most recent call last):\n" list = format_exception(type, value, traceback) msg = "".join(list) # Try to find the module that the exception actually came from. name = getattr(traceback.tb_frame, "f_globals", {}).get( "__name__", __name__ ) logger = logging.getLogger(name) logger.error(msg) apptools-5.1.0/apptools/logger/log_queue_handler.py0000644000076500000240000000366013777642667023177 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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. from logging import Handler # Local imports. from .ring_buffer import RingBuffer class LogQueueHandler(Handler): """Buffers up the log messages so that we can display them later. This is important on startup when log messages are generated before the ui has started. By putting them in this queue we can display them once the ui is ready. """ # The view where updates will go _view = None def __init__(self, size=1000): Handler.__init__(self) # only buffer 1000 log records self.size = size self.ring = RingBuffer(self.size) self.dirty = False def emit(self, record): """ Actually this is more like an enqueue than an emit().""" self.ring.append(record) if self._view is not None: try: self._view.update() except Exception: pass self.dirty = True def get(self): self.dirty = False try: result = self.ring.get() except Exception: # we did our best and it won't cause too much damage # to just return a bogus message result = [] return result def has_new_records(self): return self.dirty def reset(self): # start over with a new empty buffer self.ring = RingBuffer(self.size) if self._view is not None: try: self._view.update() except Exception: pass self.dirty = True apptools-5.1.0/apptools/logger/ring_buffer.py0000644000076500000240000000263513777642667022006 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Copied from Python Cookbook. """ class RingBuffer: def __init__(self, size_max): self.max = size_max self.data = [] def append(self, x): """append an element at the end of the buffer""" self.data.append(x) if len(self.data) == self.max: self.cur = 0 self.__class__ = RingBufferFull def get(self): """ return a list of elements from the oldest to the newest""" return self.data class RingBufferFull: def __init__(self, n): raise Exception("you should use RingBuffer") def append(self, x): self.data[self.cur] = x self.cur = (self.cur + 1) % self.max def get(self): return self.data[self.cur:] + self.data[: self.cur] # sample of use """x=RingBuffer(5) x.append(1); x.append(2); x.append(3); x.append(4) print(x.__class__,x.get()) x.append(5) print(x.__class__,x.get()) x.append(6) print(x.data,x.get()) x.append(7); x.append(8); x.append(9); x.append(10) print(x.data,x.get())""" apptools-5.1.0/apptools/type_registry/0000755000076500000240000000000013777643025020555 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/type_registry/tests/0000755000076500000240000000000013777643025021717 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/type_registry/tests/test_type_registry.py0000644000076500000240000001227613777642667026264 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from ..type_registry import TypeRegistry from .dummies import A, B, C, D, Mixed, Abstract, Concrete, ConcreteSubclass class TestTypeRegistry(unittest.TestCase): def setUp(self): self.registry = TypeRegistry() def test_deferred_push(self): self.registry.push("dummies:A", "A") self.registry.push("dummies:C", "C") self.assertEqual(self.registry.lookup_by_type(A), "A") self.assertEqual(self.registry.lookup_by_type(B), "A") self.assertEqual(self.registry.lookup_by_type(C), "C") self.assertRaises(KeyError, self.registry.lookup_by_type, D) def test_greedy_push(self): self.registry.push(A, "A") self.registry.push(C, "C") self.assertEqual(self.registry.lookup_by_type(A), "A") self.assertEqual(self.registry.lookup_by_type(B), "A") self.assertEqual(self.registry.lookup_by_type(C), "C") self.assertRaises(KeyError, self.registry.lookup_by_type, D) def test_pop_by_name_from_name(self): self.registry.push("dummies:A", "A") self.registry.pop("dummies:A") self.assertRaises(KeyError, self.registry.lookup_by_type, A) def test_pop_by_name_from_type(self): self.registry.push(A, "A") self.registry.pop("dummies:A") self.assertRaises(KeyError, self.registry.lookup_by_type, A) def test_pop_by_type_from_name(self): self.registry.push("dummies:A", "A") self.registry.pop(A) self.assertRaises(KeyError, self.registry.lookup_by_type, A) def test_pop_by_type_from_type(self): self.registry.push(A, "A") self.registry.pop(A) self.assertRaises(KeyError, self.registry.lookup_by_type, A) def test_mro(self): self.registry.push("dummies:A", "A") self.registry.push("dummies:D", "D") self.assertEqual(self.registry.lookup_by_type(Mixed), "A") def test_lookup_instance(self): self.registry.push(A, "A") self.registry.push(C, "C") self.assertEqual(self.registry.lookup(A()), "A") self.assertEqual(self.registry.lookup(B()), "A") self.assertEqual(self.registry.lookup(C()), "C") self.assertRaises(KeyError, self.registry.lookup, D()) def test_lookup_all(self): self.registry.push(A, "A") self.registry.push(C, "C") self.assertEqual(self.registry.lookup_all(A()), ["A"]) self.assertEqual(self.registry.lookup_all(B()), ["A"]) self.registry.push(A, "A2") self.assertEqual(self.registry.lookup_all(A()), ["A", "A2"]) self.assertEqual(self.registry.lookup_all(B()), ["A", "A2"]) def test_abc(self): self.registry.push_abc(Abstract, "Abstract") self.assertEqual(self.registry.lookup_by_type(Concrete), "Abstract") self.assertEqual( self.registry.lookup_by_type(ConcreteSubclass), "Abstract" ) def test_stack_type(self): self.registry.push(A, "A1") self.registry.push(A, "A2") self.assertEqual(self.registry.lookup_by_type(A), "A2") self.registry.pop(A) self.assertEqual(self.registry.lookup_by_type(A), "A1") self.registry.pop(A) self.assertRaises(KeyError, self.registry.lookup_by_type, A) self.assertRaises(KeyError, self.registry.pop, A) self.assertRaises(KeyError, self.registry.pop, "dummies:A") self.assertEqual(self.registry.type_map, {}) self.assertEqual(self.registry.name_map, {}) self.assertEqual(self.registry.abc_map, {}) def test_stack_mixed_type_and_name(self): self.registry.push(A, "A1") self.registry.push("dummies:A", "A2") self.assertEqual(self.registry.lookup_by_type(A), "A2") self.registry.pop(A) self.assertEqual(self.registry.lookup_by_type(A), "A1") self.registry.pop("dummies:A") self.assertRaises(KeyError, self.registry.lookup_by_type, A) self.assertRaises(KeyError, self.registry.pop, A) self.assertRaises(KeyError, self.registry.pop, "dummies:A") self.assertEqual(self.registry.type_map, {}) self.assertEqual(self.registry.name_map, {}) self.assertEqual(self.registry.abc_map, {}) def test_stack_abc(self): self.registry.push_abc(Abstract, "Abstract1") self.registry.push_abc(Abstract, "Abstract2") self.assertEqual(self.registry.lookup_by_type(Concrete), "Abstract2") self.registry.pop(Abstract) self.assertEqual(self.registry.lookup_by_type(Concrete), "Abstract1") self.registry.pop(Abstract) self.assertRaises(KeyError, self.registry.lookup_by_type, Concrete) self.assertRaises(KeyError, self.registry.pop, Abstract) self.assertEqual(self.registry.type_map, {}) self.assertEqual(self.registry.name_map, {}) self.assertEqual(self.registry.abc_map, {}) apptools-5.1.0/apptools/type_registry/tests/__init__.py0000644000076500000240000000062713777642667024050 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/type_registry/tests/test_lazy_registry.py0000644000076500000240000000471213777642667026256 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from ..type_registry import LazyRegistry from .dummies import A, B, C, D, Mixed, Abstract, Concrete, ConcreteSubclass class TestLazyRegistry(unittest.TestCase): def setUp(self): self.registry = LazyRegistry() # Bodge in a different importer. self.registry._import_object = "Imported {0}".format def test_deferred_push(self): self.registry.push("dummies:A", "foo:A") self.registry.push("dummies:C", "foo:C") self.assertEqual(self.registry.lookup_by_type(A), "Imported foo:A") self.assertEqual(self.registry.lookup_by_type(B), "Imported foo:A") self.assertEqual(self.registry.lookup_by_type(C), "Imported foo:C") self.assertRaises(KeyError, self.registry.lookup_by_type, D) def test_greedy_push(self): self.registry.push(A, "foo:A") self.registry.push(C, "foo:C") self.assertEqual(self.registry.lookup_by_type(A), "Imported foo:A") self.assertEqual(self.registry.lookup_by_type(B), "Imported foo:A") self.assertEqual(self.registry.lookup_by_type(C), "Imported foo:C") self.assertRaises(KeyError, self.registry.lookup_by_type, D) def test_mro(self): self.registry.push("dummies:A", "foo:A") self.registry.push("dummies:D", "foo:D") self.assertEqual(self.registry.lookup_by_type(Mixed), "Imported foo:A") def test_lookup_instance(self): self.registry.push(A, "foo:A") self.registry.push(C, "foo:C") self.assertEqual(self.registry.lookup(A()), "Imported foo:A") self.assertEqual(self.registry.lookup(B()), "Imported foo:A") self.assertEqual(self.registry.lookup(C()), "Imported foo:C") self.assertRaises(KeyError, self.registry.lookup, D()) def test_abc(self): self.registry.push_abc(Abstract, "foo:Abstract") self.assertEqual( self.registry.lookup_by_type(Concrete), "Imported foo:Abstract" ) self.assertEqual( self.registry.lookup_by_type(ConcreteSubclass), "Imported foo:Abstract", ) apptools-5.1.0/apptools/type_registry/tests/dummies.py0000644000076500000240000000145513777642667023754 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 abc class A(object): pass class B(A): pass class C(B): pass class D(object): pass class Mixed(A, D): pass class Abstract(metaclass=abc.ABCMeta): pass class Concrete(object): pass class ConcreteSubclass(Concrete): pass for typ in (A, B, C, D, Mixed, Abstract, Concrete, ConcreteSubclass): typ.__module__ = "dummies" Abstract.register(Concrete) apptools-5.1.0/apptools/type_registry/__init__.py0000644000076500000240000000062713777642667022706 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/type_registry/type_registry.py0000644000076500000240000002100113777642667024045 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! def get_mro(obj_class): """Get a reasonable method resolution order of a class and its superclasses for both old-style and new-style classes. """ if not hasattr(obj_class, "__mro__"): # Old-style class. Mix in object to make a fake new-style class. try: obj_class = type(obj_class.__name__, (obj_class, object), {}) except TypeError: # Old-style extension type that does not descend from object. mro = [obj_class] else: mro = obj_class.__mro__[1:-1] else: mro = obj_class.__mro__ return mro def _mod_name_key(typ): """Return a '__module__:__name__' key for a type.""" module = getattr(typ, "__module__", None) name = getattr(typ, "__name__", None) key = "{0}:{1}".format(module, name) return key class TypeRegistry(object): """Register objects for types. Each type maintains a stack of registered objects that can be pushed and popped. """ def __init__(self): # Map types to lists of registered objects. The last is the most # current and will be the one that is returned on lookup. self.type_map = {} # Map '__module__:__name__' strings to lists of registered objects. self.name_map = {} # Map abstract base classes to lists of registered objects. self.abc_map = {} #### TypeRegistry public interface ######################################## def push(self, typ, obj): """Push an object onto the stack for the given type. Parameters ---------- typ : type or '__module__:__name__' string for a type obj : object The object to register. """ if isinstance(typ, str): # Check the cached types. for cls in self.type_map: if _mod_name_key(cls) == typ: self.type_map[cls].append(obj) break else: if typ not in self.name_map: self.name_map[typ] = [] self.name_map[typ].append(obj) else: if typ not in self.type_map: self.type_map[typ] = [] self.type_map[typ].append(obj) def push_abc(self, typ, obj): """Push an object onto the stack for the given ABC. Parameters ---------- typ : abc.ABCMeta obj : object """ if typ not in self.abc_map: self.abc_map[typ] = [] self.abc_map[typ].append(obj) def pop(self, typ): """Pop a registered object for the given type. Parameters ---------- typ : type or '__module__:__name__' string for a type Returns ------- obj : object The last registered object for the type. Raises ------ KeyError if the type is not registered. """ if isinstance(typ, str): if typ not in self.name_map: # We may have it cached in the type map. We will have to # iterate over all of the types to check. for cls in self.type_map: if _mod_name_key(cls) == typ: old = self._pop_value(self.type_map, cls) break else: raise KeyError("No registered value for {0!r}".format(typ)) else: old = self._pop_value(self.name_map, typ) else: if typ in self.type_map: old = self._pop_value(self.type_map, typ) elif typ in self.abc_map: old = self._pop_value(self.abc_map, typ) else: old = self._pop_value(self.name_map, _mod_name_key(typ)) return old def lookup(self, instance): """Look up the registered object for the given instance. Parameters ---------- instance : object An instance of a possibly registered type. Returns ------- obj : object The registered object for the type of the instance, one of the type's superclasses, or else one of the ABCs the type implements. Raises ------ KeyError if the instance's type has not been registered. """ return self.lookup_by_type(type(instance)) def lookup_by_type(self, typ): """Look up the registered object for a type. Parameters ---------- typ : type Returns ------- obj : object The registered object for the type, one of its superclasses, or else one of the ABCs it implements. Raises ------ KeyError if the type has not been registered. """ return self.lookup_all_by_type(typ)[-1] def lookup_all(self, instance): """Look up all the registered objects for the given instance. Parameters ---------- instance : object An instance of a possibly registered type. Returns ------- objs : list of objects The list of registered objects for the instance. If the given instance is not registered, its superclasses are searched. If none of the superclasses are registered, search the possible ABCs. Raises ------ KeyError if the instance's type has not been registered. """ return self.lookup_all_by_type(type(instance)) def lookup_all_by_type(self, typ): """Look up all the registered objects for a type. Parameters ---------- typ : type Returns ------- objs : list of objects The list of registered objects for the type. If the given type is not registered, its superclasses are searched. If none of the superclasses are registered, search the possible ABCs. Raises ------ KeyError if the type has not been registered. """ # If a concrete superclass is registered use it. for cls in get_mro(typ): if cls in self.type_map or self._in_name_map(cls): objs = self.type_map[cls] if objs: return objs # None of the concrete superclasses. Check the ABCs. for abstract, objs in self.abc_map.items(): if issubclass(typ, abstract) and objs: return objs # If we have reached here, the lookup failed. raise KeyError("No registered value for {0!r}".format(typ)) #### Private implementation ############################################### def _pop_value(self, mapping, key): """Pop a value from a keyed stack in a mapping, taking care to remove the key if the stack is depleted. """ objs = mapping[key] old = objs.pop() if not objs: del mapping[key] return old def _in_name_map(self, typ): """Check if the given type is specified in the name map. Parameters ---------- typ : type Returns ------- is_in_name_map : bool If True, the registered value will be moved over to the type map for future lookups. """ key = _mod_name_key(typ) if key in self.name_map: self.type_map[typ] = self.name_map.pop(key) return True else: return False class LazyRegistry(TypeRegistry): """A type registry that will lazily import the registered objects. Register '__module__:__name__' strings for the lazily imported objects. These will only be imported when the matching type is looked up. The module name must be a fully-qualified absolute name with all of the parent packages specified. """ def lookup_by_type(self, typ): """Look up the registered object for a type.""" mod_name = TypeRegistry.lookup_by_type(self, typ) return self._import_object(mod_name) def _import_object(self, mod_object): module, name = mod_object.split(":") mod = __import__(module, {}, {}, [name], 0) return getattr(mod, name) apptools-5.1.0/apptools/type_registry/api.py0000644000076500000240000000107313777642667021714 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for the apptools.type_registry subpackage. - :class:`~.LazyRegistry` - :class:`~.TypeRegistry` """ from .type_registry import LazyRegistry, TypeRegistry apptools-5.1.0/apptools/io/0000755000076500000240000000000013777643025016253 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/io/h5/0000755000076500000240000000000013777643025016567 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/io/h5/tests/0000755000076500000240000000000013777643025017731 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/io/h5/tests/test_dict_node.py0000644000076500000240000001632413777642667023313 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from apptools._testing.optional_dependencies import ( numpy as np, tables, requires_numpy, requires_tables, ) if np is not None and tables is not None: from ..dict_node import H5DictNode from .utils import open_h5file, temp_h5_file, temp_file NODE = "/dict_node" @requires_tables @requires_numpy class DictNodeTestCase(unittest.TestCase): def test_create(self): with temp_h5_file() as h5: h5dict = H5DictNode.add_to_h5file(h5, NODE) h5dict["a"] = 1 h5dict["b"] = 2 assert h5dict["a"] == 1 assert h5dict["b"] == 2 def test_is_dict_node(self): with temp_h5_file() as h5: node = h5.create_dict(NODE, {}) assert H5DictNode.is_dict_node(node._h5_group) def test_is_not_dict_node(self): with temp_h5_file() as h5: node = h5.create_group(NODE) assert not H5DictNode.is_dict_node(node) assert not H5DictNode.is_dict_node(node._h5_group) def test_create_with_data(self): with temp_h5_file() as h5: data = {"a": 10} h5dict = H5DictNode.add_to_h5file(h5, NODE, data) assert h5dict["a"] == 10 def test_create_with_array_data(self): foo = np.arange(100) bar = np.arange(150) with temp_h5_file() as h5: data = {"a": 10, "foo": foo, "bar": bar} h5dict = H5DictNode.add_to_h5file(h5, NODE, data) assert h5dict["a"] == 10 np.testing.assert_allclose(h5dict["foo"], foo) np.testing.assert_allclose(h5dict["bar"], bar) def test_load_saved_dict_node(self): with temp_file() as filename: # Write data to new dict node and close. with open_h5file(filename, "w") as h5: h5dict = H5DictNode.add_to_h5file(h5, NODE) h5dict["a"] = 1 # Read dict node and make sure the data was saved. with open_h5file(filename, mode="r+") as h5: h5dict = h5[NODE] assert h5dict["a"] == 1 # Change data for next test h5dict["a"] = 2 # Check that data is modified by the previous write. with open_h5file(filename) as h5: h5dict = h5[NODE] assert h5dict["a"] == 2 def test_load_saved_dict_node_with_array(self): arr = np.arange(100) arr1 = np.arange(200) with temp_file() as filename: # Write data to new dict node and close. with open_h5file(filename, "w") as h5: h5dict = H5DictNode.add_to_h5file(h5, NODE) h5dict["arr"] = arr # Read dict node and make sure the data was saved. with open_h5file(filename, mode="r+") as h5: h5dict = h5[NODE] np.testing.assert_allclose(h5dict["arr"], arr) # Change data for next test h5dict["arr"] = arr1 h5dict["arr_old"] = arr # Check that data is modified by the previous write. with open_h5file(filename) as h5: h5dict = h5[NODE] np.testing.assert_allclose(h5dict["arr"], arr1) np.testing.assert_allclose(h5dict["arr_old"], arr) # Make sure that arrays come back as arrays assert isinstance(h5dict["arr"], np.ndarray) assert isinstance(h5dict["arr_old"], np.ndarray) def test_keys(self): with temp_h5_file() as h5: keys = set(("hello", "world", "baz1")) data = dict((n, 1) for n in keys) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) assert set(h5dict.keys()) == keys def test_delete_item(self): with temp_h5_file() as h5: data = dict(a=10) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) del h5dict["a"] assert "a" not in h5dict def test_delete_array(self): with temp_h5_file() as h5: data = dict(a=np.arange(10)) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) del h5dict["a"] assert "a" not in h5dict assert "a" not in h5[NODE] def test_auto_flush(self): with temp_h5_file() as h5: data = dict(a=1) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) # Overwrite existing data, which should get written to disk. new_data = dict(b=2) h5dict.data = new_data # Load data from disk to check that data was automatically flushed. h5dict_from_disk = h5[NODE] assert "a" not in h5dict_from_disk assert h5dict_from_disk["b"] == 2 def test_auto_flush_off(self): with temp_h5_file() as h5: data = dict(a=1) h5dict = H5DictNode.add_to_h5file(h5, NODE, data, auto_flush=False) # Overwrite existing data, but don't write to disk. new_data = dict(b=2) h5dict.data = new_data # Load data from disk to check that it's unchanged. h5dict_from_disk = h5[NODE] assert h5dict_from_disk["a"] == 1 assert "b" not in h5dict_from_disk # Manually flush, and check that data was written h5dict.flush() h5dict_from_disk = h5[NODE] assert "a" not in h5dict_from_disk assert h5dict_from_disk["b"] == 2 def test_undefined_key(self): with temp_h5_file() as h5: data = dict(a="int") h5dict = H5DictNode.add_to_h5file(h5, NODE, data) with self.assertRaises(KeyError): del h5dict["b"] def test_basic_dtypes(self): with temp_h5_file() as h5: data = dict(a_int=1, a_float=1.0, a_str="abc") h5dict = H5DictNode.add_to_h5file(h5, NODE, data) assert isinstance(h5dict["a_int"], int) assert isinstance(h5dict["a_float"], float) assert isinstance(h5dict["a_str"], str) def test_mixed_type_list(self): with temp_h5_file() as h5: data = dict(a=[1, 1.0, "abc"]) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) for value, dtype in zip( h5dict["a"], (int, float, str) ): assert isinstance(value, dtype) def test_dict(self): with temp_h5_file() as h5: data = dict(a=dict(b=1, c=2)) h5dict = H5DictNode.add_to_h5file(h5, NODE, data) sub_dict = h5dict["a"] assert sub_dict["b"] == 1 assert sub_dict["c"] == 2 def test_wrap_self_raises_error(self): with temp_h5_file() as h5: H5DictNode.add_to_h5file(h5, NODE) node = h5[NODE] with self.assertRaises(AssertionError): H5DictNode(node) apptools-5.1.0/apptools/io/h5/tests/test_table_node.py0000644000076500000240000000533613777642667023460 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from apptools._testing.optional_dependencies import ( numpy as np, pandas, tables, requires_numpy, requires_pandas, requires_tables, ) if np is not None and tables is not None: from ..table_node import H5TableNode from .utils import temp_h5_file NODE = "/table_node" @requires_numpy @requires_tables class TableNodeTestCase(unittest.TestCase): def test_basics(self): description = [("a", np.float64), ("b", np.float64)] with temp_h5_file() as h5: h5table = H5TableNode.add_to_h5file(h5, NODE, description) h5table.append({"a": [1, 2], "b": [3, 4]}) np.testing.assert_allclose(h5table["a"], [1, 2]) np.testing.assert_allclose(h5table["b"], [3, 4]) dtype_description = np.dtype([("c", "f4"), ("d", "f4")]) with temp_h5_file() as h5: h5table = H5TableNode.add_to_h5file(h5, NODE, dtype_description) h5table.append({"c": [1.2, 3.4], "d": [5.6, 7.8]}) np.testing.assert_allclose(h5table["c"], [1.2, 3.4]) np.testing.assert_allclose(h5table["d"], [5.6, 7.8]) assert len(repr(h5table)) > 0 def test_getitem(self): description = [("a", np.float64), ("b", np.float64)] with temp_h5_file() as h5: h5table = H5TableNode.add_to_h5file(h5, NODE, description) h5table.append({"a": [1, 2], "b": [3, 4]}) np.testing.assert_allclose(h5table["a"], (1, 2)) np.testing.assert_allclose(h5table[["b", "a"]], [(3, 1), (4, 2)]) def test_keys(self): description = [("hello", "int"), ("world", "int"), ("Qux1", "bool")] with temp_h5_file() as h5: keys = set(list(zip(*description))[0]) h5table = H5TableNode.add_to_h5file(h5, NODE, description) assert set(h5table.keys()) == keys @requires_pandas def test_to_dataframe(self): description = [("a", np.float64)] with temp_h5_file() as h5: h5table = H5TableNode.add_to_h5file(h5, NODE, description) h5table.append({"a": [1, 2, 3]}) df = h5table.to_dataframe() assert isinstance(df, pandas.DataFrame) np.testing.assert_allclose(df["a"], h5table["a"]) if __name__ == "__main__": from numpy import testing testing.run_module_suite() apptools-5.1.0/apptools/io/h5/tests/__init__.py0000644000076500000240000000062713777642667022062 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/io/h5/tests/utils.py0000644000076500000240000000163613777642667021464 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from contextlib import contextmanager import tempfile import os from ..utils import open_h5file SEPARATOR = "-" * 60 @contextmanager def temp_file(suffix="", prefix="tmp", dir=None): fd, filename = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir) try: yield filename finally: os.close(fd) os.unlink(filename) @contextmanager def temp_h5_file(**kwargs): with temp_file(suffix="h5") as fn: with open_h5file(fn, mode="a", **kwargs) as h5: yield h5 apptools-5.1.0/apptools/io/h5/tests/test_file.py0000644000076500000240000005175713777642667022313 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from contextlib import closing import unittest from apptools._testing.optional_dependencies import ( numpy as np, tables, requires_numpy, requires_tables, ) if np is not None and tables is not None: from ..file import H5File from ..dict_node import H5DictNode from ..table_node import H5TableNode from .utils import open_h5file, temp_h5_file H5_TEST_FILE = "_temp_test_filt.h5" @requires_numpy @requires_tables class FileTestCase(unittest.TestCase): def tearDown(self): try: os.remove(H5_TEST_FILE) except OSError: pass def test_reopen(self): h5 = H5File(H5_TEST_FILE, mode="w") assert h5.is_open h5.close() assert not h5.is_open h5.open() assert h5.is_open h5.close() def test_open_from_pytables_object(self): with closing(tables.File(H5_TEST_FILE, "w")) as pyt_file: pyt_file.create_group("/", "my_group") with open_h5file(pyt_file) as h5: assert "/my_group" in h5 def test_open_from_closed_pytables_object(self): with closing(tables.File(H5_TEST_FILE, "w")) as pyt_file: pyt_file.create_group("/", "my_group") pyt_file.close() with open_h5file(pyt_file) as h5: assert "/my_group" in h5 def test_create_array_with_H5File(self): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5array = h5.create_array("/array", array) # Test returned array np.testing.assert_allclose(h5array, array) # Test stored array np.testing.assert_allclose(h5["/array"], array) def test_create_array_with_H5Group(self): array = np.arange(3) node_path = "/tardigrade/array" with open_h5file(H5_TEST_FILE, mode="w") as h5: group = h5.create_group("/tardigrade") h5array = group.create_array("array", array) # Test returned array np.testing.assert_allclose(h5array, array) # Test stored array np.testing.assert_allclose(h5[node_path], array) def test_getitem_failure(self): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.create_array("/array", array) with self.assertRaises(NameError): h5["/not_there"] def test_iteritems(self): node_paths = ["/foo", "/bar", "/baz"] array = np.arange(3) with open_h5file(H5_TEST_FILE, mode="w") as h5: for path in node_paths: h5.create_array(path, array) # We expect to see the root node when calling iteritems... node_paths.append("/") iter_paths = [] # 2to3 converts the iteritems blindly to items which is incorrect, # so we resort to this ugliness. items = getattr(h5, "iteritems")() for path, node in items: iter_paths.append(path) assert set(node_paths) == set(iter_paths) def test_create_plain_array_with_H5File(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: h5array = h5.create_array("/array", np.arange(3), chunked=False) assert isinstance(h5array, tables.Array) assert not isinstance(h5array, tables.CArray) def test_create_plain_array_with_H5Group(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: h5array = h5.root.create_array( "/array", np.arange(3), chunked=False ) assert isinstance(h5array, tables.Array) assert not isinstance(h5array, tables.CArray) def test_create_chunked_array_with_H5File(self): array = np.arange(3, dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5array = h5.create_array("/array", array, chunked=True) np.testing.assert_allclose(h5array, array) assert isinstance(h5array, tables.CArray) def test_create_chunked_array_with_H5Group(self): array = np.arange(3, dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5array = h5.root.create_array("/array", array, chunked=True) np.testing.assert_allclose(h5array, array) assert isinstance(h5array, tables.CArray) def test_create_extendable_array_with_H5File(self): array = np.arange(3, dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5array = h5.create_array("/array", array, extendable=True) np.testing.assert_allclose(h5array, array) assert isinstance(h5array, tables.EArray) def test_create_extendable_array_with_H5Group(self): array = np.arange(3, dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5array = h5.root.create_array("/array", array, extendable=True) np.testing.assert_allclose(h5array, array) assert isinstance(h5array, tables.EArray) def test_str_and_repr(self): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.create_array("/array", array) assert repr(h5) == repr(h5._h5) assert str(h5) == str(h5._h5) def test_shape_and_dtype(self): array = np.ones((3, 4), dtype=np.uint8) with open_h5file(H5_TEST_FILE, mode="w") as h5: for node, chunked in (("/array", False), ("/carray", True)): h5array = h5.create_array( node, array.shape, dtype=array.dtype, chunked=chunked ) assert h5array.dtype == array.dtype assert h5array.shape == array.shape def test_shape_only_raises(self): shape = (3, 4) with open_h5file(H5_TEST_FILE, mode="w") as h5: with self.assertRaises(ValueError): h5.create_array("/array", shape) def test_create_duplicate_array_raises(self): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode="w", delete_existing=False) as h5: h5.create_array("/array", array) with self.assertRaises(ValueError): h5.create_array("/array", array) def test_delete_existing_array_with_H5File(self): old_array = np.arange(3) new_array = np.ones(5) with open_h5file(H5_TEST_FILE, mode="w", delete_existing=True) as h5: h5.create_array("/array", old_array) # New array with the same node name should delete old array h5.create_array("/array", new_array) np.testing.assert_allclose(h5["/array"], new_array) def test_delete_existing_array_with_H5Group(self): old_array = np.arange(3) new_array = np.ones(5) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.create_array("/array", old_array) # New array with the same node name should delete old array h5.root.create_array("/array", new_array, delete_existing=True) np.testing.assert_allclose(h5["/array"], new_array) def test_delete_existing_dict_with_H5File(self): old_dict = {"a": "Goose"} new_dict = {"b": "Quail"} with open_h5file(H5_TEST_FILE, mode="w", delete_existing=True) as h5: h5.create_dict("/dict", old_dict) # New dict with the same node name should delete old dict h5.create_dict("/dict", new_dict) assert h5["/dict"].data == new_dict def test_delete_existing_dict_with_H5Group(self): old_dict = {"a": "Goose"} new_dict = {"b": "Quail"} with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.create_dict("/dict", old_dict) # New dict with the same node name should delete old dict h5.root.create_dict("/dict", new_dict, delete_existing=True) assert h5["/dict"].data == new_dict def test_delete_existing_table_with_H5File(self): old_description = [("Honk", "int"), ("Wink", "float")] new_description = [("Toot", "float"), ("Pop", "int")] with open_h5file(H5_TEST_FILE, mode="w", delete_existing=True) as h5: h5.create_table("/table", old_description) # New table with the same node name should delete old table h5.create_table("/table", new_description) tab = h5["/table"] tab.append({"Pop": (1,), "Toot": (np.pi,)}) assert tab.ix[0][0] == np.pi def test_delete_existing_table_with_H5Group(self): old_description = [("Honk", "int"), ("Wink", "float")] new_description = [("Toot", "float"), ("Pop", "int")] with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.create_table("/table", old_description) # New table with the same node name should delete old table h5.root.create_table( "/table", new_description, delete_existing=True ) tab = h5["/table"] tab.append({"Pop": (1,), "Toot": (np.pi,)}) assert tab.ix[0][0] == np.pi def test_delete_existing_group_with_H5File(self): with open_h5file(H5_TEST_FILE, mode="w", delete_existing=True) as h5: h5.create_group("/group") grp = h5["/group"] grp.attrs["test"] = 4 assert grp.attrs["test"] == 4 h5.create_group("/group") grp = h5["/group"] grp.attrs["test"] = 6 assert grp.attrs["test"] == 6 def test_delete_existing_group_with_H5Group(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.create_group("/group") grp = h5["/group"] grp.attrs["test"] = 4 assert grp.attrs["test"] == 4 h5.root.create_group("/group", delete_existing=True) grp = h5["/group"] grp.attrs["test"] = 6 assert grp.attrs["test"] == 6 def test_remove_group_with_H5File(self): with open_h5file(H5_TEST_FILE, mode="w", delete_existing=True) as h5: h5.create_group("/group") assert "/group" in h5 h5.remove_group("/group") assert "/group" not in h5 def test_remove_group_with_H5Group(self): node_path = "/waterbear/group" with open_h5file(H5_TEST_FILE, mode="w", delete_existing=True) as h5: group = h5.create_group("/waterbear") group.create_group("group") assert node_path in h5 group.remove_group("group") assert node_path not in h5 def test_remove_group_with_remove_node(self): node_path = "/group" with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.create_group(node_path) with self.assertRaises(ValueError): # Groups should be removed w/ `remove_group` h5.remove_node(node_path) def test_remove_node_with_H5File(self): with open_h5file(H5_TEST_FILE, mode="w", delete_existing=True) as h5: h5.create_array("/array", np.arange(3)) assert "/array" in h5 h5.remove_node("/array") assert "/array" not in h5 def test_remove_node_with_H5Group(self): node_path = "/waterbear/array" with open_h5file(H5_TEST_FILE, mode="w", delete_existing=True) as h5: group = h5.create_group("/waterbear") h5.create_array(node_path, np.arange(3)) assert node_path in h5 group.remove_node("array") assert node_path not in h5 def test_read_mode_raises_on_nonexistent_file(self): cm = open_h5file("_nonexistent_.h5", mode="r") with self.assertRaises(IOError): cm.__enter__() cm = open_h5file("_nonexistent_.h5", mode="r+") with self.assertRaises(IOError): cm.__enter__() def test_cleanup(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: h5_pytables = h5._h5 # This reference gets deleted on close assert not h5_pytables.isopen def test_create_group_with_H5File(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.create_group("/group") assert "/group" in h5 def test_create_group_with_H5Group(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: group = h5["/"] group.create_group("group") assert "/group" in h5 def test_split_path(self): path, node = H5File.split_path("/") assert path == "/" assert node == "" path, node = H5File.split_path("/node") assert path == "/" assert node == "node" path, node = H5File.split_path("/group/node") assert path == "/group" assert node == "node" def test_join_path(self): path = H5File.join_path("/", "a", "b", "c") assert path == "/a/b/c" path = H5File.join_path("a", "b/c") assert path == "/a/b/c" path = H5File.join_path("a", "/b", "/c") assert path == "/a/b/c" def test_auto_groups(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.auto_groups = True h5.create_array("/group/array", np.arange(3)) assert "/group/array" in h5 def test_auto_groups_deep(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.auto_groups = True h5.create_array("/group1/group2/array", np.arange(3)) assert "/group1/group2/array" in h5 def test_groups(self): array = np.arange(3) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5.create_array("/a/b/array", array) group_a = h5["/a"] # Check that __contains__ works for groups assert "b" in group_a assert "b/array" in group_a group_b = group_a["b"] # Check that __contains__ works for arrays assert "array" in group_b np.testing.assert_allclose(group_b["array"], array) np.testing.assert_allclose(group_a["b/array"], array) def test_group_attributes(self): value_1 = "foo" value_2 = "bar" with open_h5file(H5_TEST_FILE, mode="w") as h5: h5["/"].attrs["name"] = value_1 assert h5["/"].attrs["name"] == value_1 group = h5["/"] # Make sure changes to the attribute consistent group._h5_group._v_attrs["name"] = value_2 assert group.attrs["name"] == value_2 def test_group_properties(self): with open_h5file(H5_TEST_FILE, mode="w", auto_groups=True) as h5: h5.create_array("/group1/group2/array", np.arange(3)) h5.create_array("/group1/array1", np.arange(3)) assert h5["/group1"].name == "group1" child_names = h5["/group1"].children_names assert sorted(child_names) == sorted(["group2", "array1"]) sub_names = h5["/group1"].subgroup_names assert sub_names == ["group2"] assert h5["/group1"].root.name == "/" assert h5["/group1/group2"].root.name == "/" def test_iter_groups(self): with open_h5file(H5_TEST_FILE, mode="w", auto_groups=True) as h5: h5.create_array("/group1/array", np.arange(3)) h5.create_array("/group1/subgroup/deep_array", np.arange(3)) group = h5["/group1"] assert set(n.name for n in group.iter_groups()) == set( ["subgroup"] ) def test_mapping_interface_for_file(self): with open_h5file(H5_TEST_FILE, mode="w", auto_groups=True) as h5: array = h5.create_array("/array", np.arange(3)) h5.create_array("/group/deep_array", np.arange(3)) # `deep_array` isn't a direct descendent and isn't counted. assert len(h5) == 2 assert "/group" in h5 assert "/array" in h5 np.testing.assert_allclose(h5["/array"], array) assert set(n.name for n in h5) == set(["array", "group"]) def test_mapping_interface_for_group(self): with open_h5file(H5_TEST_FILE, mode="w", auto_groups=True) as h5: array = h5.create_array("/group1/array", np.arange(3)) h5.create_array("/group1/subgroup/deep_array", np.arange(3)) group = h5["/group1"] # `deep_array` isn't a direct descendent and isn't counted. assert len(group) == 2 assert "subgroup" in group assert "array" in group np.testing.assert_allclose(group["array"], array) assert set(n.name for n in group) == set(["array", "subgroup"]) def test_group_str_and_repr(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: group = h5["/"] assert str(group) == str(group._h5_group) assert repr(group) == repr(group._h5_group) def test_attribute_translation(self): value_1 = (1, 2, 3) value_1_array = np.array(value_1) with open_h5file(H5_TEST_FILE, mode="w") as h5: h5["/"].attrs["name"] = value_1 assert isinstance(h5["/"].attrs["name"], np.ndarray) np.testing.assert_allclose(h5["/"].attrs["name"], value_1_array) def test_get_attribute(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: attrs = h5["/"].attrs attrs["name"] = "hello" assert attrs.get("name") == attrs["name"] def test_del_attribute(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: attrs = h5["/"].attrs attrs["name"] = "hello" del attrs["name"] assert "name" not in attrs def test_get_attribute_default(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: assert h5["/"].attrs.get("missing", "null") == "null" def test_attribute_update(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: attrs = h5["/"].attrs attrs.update({"a": 1, "b": 2}) assert attrs["a"] == 1 assert attrs["b"] == 2 attrs.update({"b": 20, "c": 30}) assert attrs["b"] == 20 assert attrs["c"] == 30 def test_attribute_iteration_methods(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: attrs = h5["/"].attrs attrs["organ"] = "gallbladder" attrs["count"] = 42 attrs["alpha"] = 0xFF items = list(attrs.items()) assert all(isinstance(x, tuple) for x in items) # unfold the pairs keys, vals = [list(item) for item in zip(*items)] assert keys == list(attrs.keys()) assert vals == list(attrs.values()) # Check that __iter__ is consistent assert keys == list(iter(attrs)) assert len(attrs) == len(keys) def test_bad_node_name(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: with self.assertRaises(ValueError): h5.create_array("/attrs", np.zeros(3)) def test_bad_group_name(self): with open_h5file(H5_TEST_FILE, mode="w") as h5: with self.assertRaises(ValueError): h5.create_array("/attrs/array", np.zeros(3)) def test_create_dict_with_H5File(self): data = {"a": 1} with temp_h5_file() as h5: h5.create_dict("/dict", data) assert isinstance(h5["/dict"], H5DictNode) assert h5["/dict"]["a"] == 1 def test_create_dict_with_H5Group(self): node_path = "/bananas/dict" data = {"a": 1} with temp_h5_file() as h5: group = h5.create_group("/bananas") group.create_dict("dict", data) assert isinstance(h5[node_path], H5DictNode) assert h5[node_path]["a"] == 1 def test_create_table_with_H5File(self): description = [("foo", "int"), ("bar", "float")] with temp_h5_file() as h5: h5.create_table("/table", description) tab = h5["/table"] assert isinstance(tab, H5TableNode) tab.append({"foo": (1,), "bar": (np.pi,)}) assert tab.ix[0][0] == 1 assert tab.ix[0][1] == np.pi h5.remove_node("/table") assert "/table" not in h5 def test_create_table_with_H5Group(self): node_path = "/rhinocerous/table" description = [("foo", "int"), ("bar", "float")] with temp_h5_file() as h5: group = h5.create_group("/rhinocerous") group.create_table("table", description) tab = h5[node_path] assert isinstance(tab, H5TableNode) tab.append({"foo": (1,), "bar": (np.pi,)}) assert tab.ix[0][0] == 1 assert tab.ix[0][1] == np.pi group.remove_node("table") assert node_path not in h5 apptools-5.1.0/apptools/io/h5/__init__.py0000644000076500000240000000062713777642667020720 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/io/h5/table_node.py0000644000076500000240000001232213777642667021250 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 numpy as np from tables.table import Table as PyTablesTable class _TableRowAccessor(object): """A simple object which provides read access to the rows in a Table.""" def __init__(self, h5_table): self._h5_table = h5_table def __getitem__(self, key): return self._h5_table[key] class H5TableNode(object): """A wrapper for PyTables Table nodes. Parameters ---------- node : tables.Table instance An H5 node which is a pytables.Table or H5TableNode instance """ def __init__(self, node): # Avoid a circular import from .file import H5Attrs assert self.is_table_node(node) self._h5_table = node._h5_table if hasattr(node, "_h5_table") else node self.attrs = H5Attrs(self._h5_table._v_attrs) # -------------------------------------------------------------------------- # Creation methods # -------------------------------------------------------------------------- @classmethod def add_to_h5file(cls, h5, node_path, description, **kwargs): """Add table node to an H5 file at the specified path. Parameters ---------- h5 : H5File The H5 file where the table node will be stored. node_path : str Path to node where data is stored (e.g. '/path/to/my_table') description : list of tuples or numpy dtype object The description of the columns in the table. This is either a list of (column name, dtype, [, shape or itemsize]) tuples or a numpy record array dtype. For more information, see the documentation for `Table` in PyTables. **kwargs : dict Additional keyword arguments to pass to pytables.File.create_table """ if isinstance(description, (tuple, list)): description = np.dtype(description) cls._create_pytables_node(h5, node_path, description, **kwargs) node = h5[node_path] return cls(node) @classmethod def is_table_node(cls, pytables_node): """Return True if pytables_node is a pytables.Table or a H5TableNode. """ return isinstance(pytables_node, (PyTablesTable, H5TableNode)) # -------------------------------------------------------------------------- # Public interface # -------------------------------------------------------------------------- def append(self, data): """Add some data to the table. Parameters ---------- data : dict A dictionary of column name -> values items """ rows = list(zip(*[data[name] for name in self.keys()])) self._h5_table.append(rows) def __getitem__(self, col_or_cols): """Return one or more columns of data from the table. Parameters ---------- col_or_cols : str or list of str A single column name or a list of column names Return ------ data : ndarray An array of column data with the column order matching that of `col_or_cols`. """ if isinstance(col_or_cols, str): return self._h5_table.col(col_or_cols) column_data = [self._h5_table.col(name) for name in col_or_cols] return np.column_stack(column_data) @property def ix(self): """Return an object which provides access to row data.""" return _TableRowAccessor(self._h5_table) def keys(self): return self._h5_table.colnames def to_dataframe(self): """Return table data as a pandas `DataFrame`. XXX: This does not work if the table contains a multidimensional column This method requires pandas to have been installed in the environment. """ from pandas import DataFrame # Slicing rows gives a numpy struct array, which DataFrame understands. return DataFrame(self.ix[:]) # -------------------------------------------------------------------------- # Object interface # -------------------------------------------------------------------------- def __repr__(self): return repr(self._h5_table) def __len__(self): return self._h5_table.nrows # -------------------------------------------------------------------------- # Private interface # -------------------------------------------------------------------------- def _f_remove(self): """Implement the PyTables `Node._f_remove` method so that H5File doesn't choke when trying to remove our node. """ self._h5_table._f_remove() self._h5_table = None @classmethod def _create_pytables_node(cls, h5, node_path, description, **kwargs): path, name = h5.split_path(node_path) pyt_file = h5._h5 pyt_file.create_table(path, name, description, **kwargs) apptools-5.1.0/apptools/io/h5/file.py0000644000076500000240000004031013777642667020071 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 collections import Mapping, MutableMapping from functools import partial import inspect import numpy as np import tables from .dict_node import H5DictNode from .table_node import H5TableNode def get_atom(dtype): """Return a PyTables Atom for the given dtype or dtype string.""" return tables.Atom.from_dtype(np.dtype(dtype)) def iterator_length(iterator): return sum(1 for _ in iterator) def _update_wrapped_docstring(wrapped, original=None): PREAMBLE = """\ ** H5Group wrapper for H5File.{func_name}: ** Note that the first argument is a nodepath relative to the group, rather than an absolute path. Below is the original docstring: """.format( func_name=wrapped.__name__ ) wrapped.__doc__ = PREAMBLE + inspect.cleandoc(original.__doc__) return wrapped def h5_group_wrapper(original): return partial(_update_wrapped_docstring, original=original) class H5File(Mapping): """File object for HDF5 files. This class wraps PyTables to provide a cleaner, but only implements an interface for accessing arrays. Parameters ---------- filename : str or a `tables.File` instance Filename for an HDF5 file, or a PyTables `File` object. mode : str Mode to open the file: 'r' : Read-only 'w' : Write; create new file (an existing file would be deleted). 'a' : Read and write to file; create if not existing 'r+': Read and write to file; must already exist delete_existing : bool If True, an existing node will be deleted when a `create_*` method is called. Otherwise, a ValueError will be raise. auto_groups : bool If True, `create_array` will automatically create parent groups. auto_open : bool If True, open the file automatically on initialization. Otherwise, you can call `H5File.open()` explicitly after initialization. chunked : bool If True, the default behavior of `create_array` will be a chunked array (see PyTables `create_carray`). """ exists_error = ( "'{}' exists in '{}'; set `delete_existing` attribute " "to True to overwrite existing calculations." ) def __init__( self, filename, mode="r+", delete_existing=False, auto_groups=True, auto_open=True, h5filters=None, ): self.mode = mode self.delete_existing = delete_existing self.auto_groups = auto_groups if h5filters is None: self.h5filters = tables.Filters( complib="blosc", complevel=5, shuffle=True ) self._h5 = None if isinstance(filename, tables.File): pyt_file = filename filename = pyt_file.filename if pyt_file.isopen: self._h5 = pyt_file self.filename = filename if auto_open: self.open() def open(self): if not self.is_open: self._h5 = tables.open_file(self.filename, mode=self.mode) def close(self): if self.is_open: self._h5.close() self._h5 = None @property def root(self): return self["/"] @property def is_open(self): return self._h5 is not None def __str__(self): return str(self._h5) def __repr__(self): return repr(self._h5) def __contains__(self, node_path): return node_path in self._h5 def __getitem__(self, node_path): try: node = self._h5.get_node(node_path) except tables.NoSuchNodeError: msg = "Node {0!r} not found in {1!r}" raise NameError(msg.format(node_path, self.filename)) return _wrap_node(node) def __iter__(self): return (_wrap_node(n) for n in self._h5.iter_nodes(where="/")) def __len__(self): return iterator_length(self) def iteritems(self, path="/"): """ Iterate over node paths and nodes of the h5 file. """ for node in self._h5.walk_nodes(where=path): node_path = node._v_pathname yield node_path, _wrap_node(node) def create_array( self, node_path, array_or_shape, dtype=None, chunked=False, extendable=False, **kwargs ): """Create node to store an array. Parameters ---------- node_path : str PyTable node path; e.g. '/path/to/node'. array_or_shape : array or shape tuple Array or shape tuple for an array. If given a shape tuple, the `dtype` parameter must also specified. dtype : str or numpy.dtype Data type of array. Only necessary if `array_or_shape` is a shape. chunked : bool Controls whether the array is chunked. extendable : {None | bool} Controls whether the array is extendable. kwargs : key/value pairs Keyword args passed to PyTables `File.create_(c|e)array`. """ self._check_node(node_path) self._assert_valid_path(node_path) h5 = self._h5 if isinstance(array_or_shape, tuple): if dtype is None: msg = "`dtype` must be specified if only given array shape." raise ValueError(msg) array = None dtype = dtype shape = array_or_shape else: array = array_or_shape dtype = array.dtype.name shape = array.shape path, name = self.split_path(node_path) if extendable: shape = (0,) + shape[1:] atom = get_atom(dtype) node = h5.create_earray( path, name, atom, shape, filters=self.h5filters, **kwargs ) if array is not None: node.append(array) elif chunked: atom = get_atom(dtype) node = h5.create_carray( path, name, atom, shape, filters=self.h5filters, **kwargs ) if array is not None: node[:] = array else: if array is None: array = np.zeros(shape, dtype=dtype) node = h5.create_array(path, name, array, **kwargs) return node def create_group(self, group_path, **kwargs): """Create group. Parameters ---------- group_path : str PyTable group path; e.g. '/path/to/group'. kwargs : key/value pairs Keyword args passed to PyTables `File.create_group`. """ self._check_node(group_path) self._assert_valid_path(group_path) path, name = self.split_path(group_path) self._h5.create_group(path, name, **kwargs) return self[group_path] def create_dict(self, node_path, data=None, **kwargs): """Create dict node at the specified path. Parameters ---------- node_path : str Path to node where data is stored (e.g. '/path/to/my_dict') data : dict Data for initialization, if desired. """ self._check_node(node_path) self._assert_valid_path(node_path) H5DictNode.add_to_h5file(self, node_path, data=data, **kwargs) return self[node_path] def create_table(self, node_path, description, **kwargs): """Create table node at the specified path. Parameters ---------- node_path : str Path to node where data is stored (e.g. '/path/to/my_dict') description : dict or numpy dtype object The description of the columns in the table. This is either a dict of column name -> dtype items or a numpy record array dtype. For more information, see the documentation for Table in pytables. """ self._check_node(node_path) self._assert_valid_path(node_path) H5TableNode.add_to_h5file(self, node_path, description, **kwargs) return self[node_path] def _check_node(self, node_path): """Check if node exists and create parent groups if necessary. Either raise error or delete depending on `delete_existing` attribute. """ if self.auto_groups: path, name = self.split_path(node_path) self._create_required_groups(path) if node_path in self: if self.delete_existing: if isinstance(self[node_path], H5Group): self.remove_group(node_path, recursive=True) else: self.remove_node(node_path) else: msg = self.exists_error.format(node_path, self.filename) raise ValueError(msg) def _create_required_groups(self, path): if path not in self: parent, missing = self.split_path(path) # Call recursively to ensure that all parent groups exist. self._create_required_groups(parent) self.create_group(path) def remove_node(self, node_path): """Remove node Parameters ---------- node_path : str PyTable node path; e.g. '/path/to/node'. """ node = self[node_path] if isinstance(node, H5Group): msg = "{!r} is a group. Use `remove_group` to remove group nodes." raise ValueError(msg.format(node.pathname)) node._f_remove() def remove_group(self, group_path, **kwargs): """Remove group Parameters ---------- group_path : str PyTable group path; e.g. '/path/to/group'. """ self[group_path]._h5_group._g_remove(**kwargs) @classmethod def _assert_valid_path(self, node_path): if "attrs" in node_path.split("/"): raise ValueError("'attrs' is an invalid node name.") @classmethod def split_path(cls, node_path): """Split node path returning the base path and node name. For example: '/path/to/node' will return '/path/to' and 'node' Parameters ---------- node_path : str PyTable node path; e.g. '/path/to/node'. """ i = node_path.rfind("/") if i == 0: return "/", node_path[1:] else: return node_path[:i], node_path[i + 1:] @classmethod def join_path(cls, *args): """Join parts of an h5 path. For example, the 3 argmuments 'path', 'to', 'node' will return '/path/to/node'. Parameters ---------- args : str Parts of path to be joined. """ path = "/".join(part.strip("/") for part in args) if not path.startswith("/"): path = "/" + path return path class H5Attrs(MutableMapping): """An attributes dictionary for an h5 node. This intercepts `__setitem__` so that python sequences can be converted to numpy arrays. This helps preserve the readability of our HDF5 files by other (non-python) programs. """ def __init__(self, node_attrs): self._node_attrs = node_attrs def __delitem__(self, key): del self._node_attrs[key] def __getitem__(self, key): return self._node_attrs[key] def __iter__(self): return iter(self.keys()) def __len__(self): return len(self._node_attrs._f_list()) def __setitem__(self, key, value): if isinstance(value, tuple) or isinstance(value, list): value = np.array(value) self._node_attrs[key] = value def get(self, key, default=None): return default if key not in self else self[key] def keys(self): return self._node_attrs._f_list() def values(self): return [self[k] for k in self.keys()] def items(self): return [(k, self[k]) for k in self.keys()] class H5Group(Mapping): """A group node in an H5File. This is a thin wrapper around PyTables' Group object to expose attributes and maintain the dict interface of H5File. """ def __init__(self, pytables_group): self._h5_group = pytables_group self.attrs = H5Attrs(self._h5_group._v_attrs) def __contains__(self, node_path): return node_path in self._h5_group def __str__(self): return str(self._h5_group) def __repr__(self): return repr(self._h5_group) def __getitem__(self, node_path): parts = node_path.split("/") # PyTables stores children as attributes node = self._h5_group.__getattr__(parts[0]) node = _wrap_node(node) if len(parts) == 1: return node else: return node["/".join(parts[1:])] def __iter__(self): return (_wrap_node(c) for c in self._h5_group) def __len__(self): return iterator_length(self) @property def pathname(self): return self._h5_group._v_pathname @property def name(self): return self._h5_group._v_name @property def filename(self): return self._h5_group._v_file.filename @property def root(self): return _wrap_node(self._h5_group._v_file.root) @property def children_names(self): return list(self._h5_group._v_children.keys()) @property def subgroup_names(self): return list(self._h5_group._v_groups.keys()) def iter_groups(self): """ Iterate over `H5Group` nodes that are children of this group. """ groups = self._h5_group._v_groups # not using the groups.values() method here, because groups is a # `proxydict` object whose .values() method is non-lazy. Related: # PyTables/PyTables#784. return (_wrap_node(groups[group_name]) for group_name in groups) @h5_group_wrapper(H5File.create_group) def create_group(self, group_subpath, delete_existing=False, **kwargs): return self._delegate_to_h5file( "create_group", group_subpath, delete_existing=delete_existing, **kwargs ) @h5_group_wrapper(H5File.remove_group) def remove_group(self, group_subpath, **kwargs): return self._delegate_to_h5file( "remove_group", group_subpath, **kwargs ) @h5_group_wrapper(H5File.create_array) def create_array( self, node_subpath, array_or_shape, dtype=None, chunked=False, extendable=False, **kwargs ): return self._delegate_to_h5file( "create_array", node_subpath, array_or_shape, dtype=dtype, chunked=chunked, extendable=extendable, **kwargs ) @h5_group_wrapper(H5File.create_table) def create_table(self, node_subpath, description, *args, **kwargs): return self._delegate_to_h5file( "create_table", node_subpath, description, *args, **kwargs ) @h5_group_wrapper(H5File.create_dict) def create_dict(self, node_subpath, data=None, **kwargs): return self._delegate_to_h5file( "create_dict", node_subpath, data=data, **kwargs ) @h5_group_wrapper(H5File.remove_node) def remove_node(self, node_subpath, **kwargs): return self._delegate_to_h5file("remove_node", node_subpath, **kwargs) def _delegate_to_h5file( self, function_name, node_subpath, *args, **kwargs ): delete_existing = kwargs.pop("delete_existing", False) h5 = H5File(self._h5_group._v_file, delete_existing=delete_existing) group_path = h5.join_path(self.pathname, node_subpath) func = getattr(h5, function_name) return func(group_path, *args, **kwargs) def _wrap_node(node): """ Wrap PyTables node object, if necessary. """ if isinstance(node, tables.Group): if H5DictNode.is_dict_node(node): node = H5DictNode(node) else: node = H5Group(node) elif H5TableNode.is_table_node(node): node = H5TableNode(node) return node apptools-5.1.0/apptools/io/h5/utils.py0000644000076500000240000000212413777642667020313 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from contextlib import contextmanager from .file import H5File @contextmanager def open_h5file(filename, mode="r+", **kwargs): """Context manager for reading an HDF5 file as an H5File object. Parameters ---------- filename : str HDF5 file name. mode : str Mode to open the file: 'r' : Read-only 'w' : Write; create new file (an existing file would be deleted). 'a' : Read and write to file; create if not existing 'r+': Read and write to file; must already exist See `H5File` for additional keyword arguments. """ h5 = H5File(filename, mode=mode, **kwargs) try: yield h5 finally: h5.close() apptools-5.1.0/apptools/io/h5/dict_node.py0000644000076500000240000001724013777642667021110 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from contextlib import closing import json from numpy import ndarray from tables import Group as PyTablesGroup from tables.nodes import filenode #: The key name which identifies array objects in the JSON dict. ARRAY_PROXY_KEY = "__array__" NODE_KEY = "node_name" class H5DictNode(object): """Dictionary-like node interface. Data for the dict is stored as a JSON file in a PyTables FileNode. This allows easy storage of Python objects, such as dictionaries and lists of different data types. Note that this is implemented using a group-node assuming that arrays are valid inputs and will be stored as H5 array nodes. Parameters ---------- h5_group : H5Group instance Group node which will be used as a dictionary store. auto_flush : bool If True, write data to disk whenever the dict data is altered. Otherwise, call `flush()` explicitly to write data to disk. """ #: Name of filenode where dict data is stored. _pyobject_data_node = "_pyobject_data" def __init__(self, h5_group, auto_flush=True): assert self.is_dict_node(h5_group) h5_group = self._get_pyt_group(h5_group) self._h5_group = h5_group self.auto_flush = auto_flush # Load dict data from the file node. dict_node = getattr(h5_group, self._pyobject_data_node) with closing(filenode.open_node(dict_node)) as f: self._pyobject_data = json.loads( f.read().decode("ascii"), object_hook=self._object_hook ) # -------------------------------------------------------------------------- # Dictionary interface # -------------------------------------------------------------------------- def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value if self.auto_flush: self.flush() def __delitem__(self, key): del self.data[key] if self.auto_flush: self.flush() def __contains__(self, key): return key in self.data def keys(self): return self.data.keys() # -------------------------------------------------------------------------- # Public interface # -------------------------------------------------------------------------- @property def data(self): return self._pyobject_data @data.setter def data(self, new_data_dict): self._pyobject_data = new_data_dict if self.auto_flush: self.flush() def flush(self): """ Write buffered data to disk. """ self._remove_pyobject_node() self._write_pyobject_node() @classmethod def add_to_h5file(cls, h5, node_path, data=None, **kwargs): """Add dict node to an H5 file at the specified path. Parameters ---------- h5 : H5File The H5 file where the dictionary data will be stored. node_path : str Path to node where data is stored (e.g. '/path/to/my_dict') data : dict Data for initialization, if desired. """ h5.create_group(node_path) group = h5[node_path] cls._create_pyobject_node(h5._h5, node_path, data=data) return cls(group, **kwargs) @classmethod def is_dict_node(cls, pytables_node): """Return True if PyTables node looks like an H5DictNode. NOTE: That this returns False if the node is an `H5DictNode` instance, since the input node should be a normal PyTables Group node. """ # Import here to prevent circular imports from .file import H5Group if isinstance(pytables_node, H5Group): pytables_node = cls._get_pyt_group(pytables_node) if not isinstance(pytables_node, PyTablesGroup): return False return cls._pyobject_data_node in pytables_node._v_children # -------------------------------------------------------------------------- # Private interface # -------------------------------------------------------------------------- def _f_remove(self): """This is called by H5File whenever a node is removed. All nodes in `_h5_group` will be removed. """ for name in self._h5_group._v_children.keys(): if name != self._pyobject_data_node: self._h5_group.__getattr__(name)._f_remove() # Remove the dict node self._remove_pyobject_node() # Remove the group node self._h5_group._f_remove() def _object_hook(self, dct): """This gets passed object dictionaries by `json.load(s)` and if it finds `ARRAY_PROXY_KEY` in the object description it returns the proxied array object. """ if ARRAY_PROXY_KEY in dct: node_name = dct[NODE_KEY] return getattr(self._h5_group, node_name)[:] return dct def _remove_pyobject_node(self): node = getattr(self._h5_group, self._pyobject_data_node) node._f_remove() def _write_pyobject_node(self): pyt_file = self._h5_group._v_file node_path = self._h5_group._v_pathname self._create_pyobject_node(pyt_file, node_path, self.data) @classmethod def _create_pyobject_node(cls, pyt_file, node_path, data=None): if data is None: data = {} # Stash the array values in their own h5 nodes and return a dictionary # which is appropriate for JSON serialization. out_data = cls._handle_array_values(pyt_file, node_path, data) kwargs = dict(where=node_path, name=cls._pyobject_data_node) with closing(filenode.new_node(pyt_file, **kwargs)) as f: f.write(json.dumps(out_data).encode("ascii")) @classmethod def _get_pyt_group(self, group): if hasattr(group, "_h5_group"): group = group._h5_group return group @classmethod def _array_proxy(cls, pyt_file, group, key, array): """Stores an array as a normal H5 node and returns the proxy object which will be serialized to JSON. `ARRAY_PROXY_KEY` marks the object dictionary as an array proxy so that `_object_hook` can recognize it. `NODE_KEY` stores the node name of the array so that `_object_hook` can load the array data when the dict node is deserialized. """ if key in group: pyt_file.remove_node(group, key) pyt_file.create_array(group, key, array) return {ARRAY_PROXY_KEY: True, NODE_KEY: key} @classmethod def _handle_array_values(cls, pyt_file, group_path, data): group = pyt_file.get_node(group_path) # Convert numpy array values to H5 array nodes. out_data = {} for key in data.keys(): value = data[key] if isinstance(value, ndarray): out_data[key] = cls._array_proxy(pyt_file, group, key, value) else: out_data[key] = value # Remove stored arrays which are no longer in the data dictionary. pyt_children = group._v_children nodes_to_remove = [] for key in pyt_children.keys(): if key not in data: nodes_to_remove.append(key) for key in nodes_to_remove: pyt_file.remove_node(group, key) return out_data apptools-5.1.0/apptools/io/tests/0000755000076500000240000000000013777643025017415 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/io/tests/__init__.py0000644000076500000240000000062713777642667021546 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/io/tests/test_file.py0000644000076500000240000001324413777642667021764 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 file operations. """ # Standard library imports. import os import shutil import stat import unittest # Enthought library imports. from apptools.io import File class FileTestCase(unittest.TestCase): """ Tests file operations on a local file system. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ shutil.rmtree("data", ignore_errors=True) os.mkdir("data") def tearDown(self): """ Called immediately after each test method has been called. """ shutil.rmtree("data") ########################################################################### # Tests. ########################################################################### def test_properties(self): """ file properties """ # Properties of a non-existent file. f = File("data/bogus.xx") self.assertIn(os.path.abspath(os.path.curdir), f.absolute_path) self.assertIsNone(f.children) self.assertEqual(f.ext, ".xx") self.assertFalse(f.exists) self.assertFalse(f.is_file) self.assertFalse(f.is_folder) self.assertFalse(f.is_package) self.assertFalse(f.is_readonly) self.assertEqual(f.mime_type, "content/unknown") self.assertEqual(f.name, "bogus") self.assertEqual(f.parent.path, "data") self.assertEqual(f.path, "data/bogus.xx") self.assertIn(os.path.abspath(os.path.curdir), f.url) self.assertEqual(str(f), "File(%s)" % f.path) # Properties of an existing file. f = File("data/foo.txt") f.create_file() self.assertIn(os.path.abspath(os.path.curdir), f.absolute_path) self.assertIsNone(f.children) self.assertEqual(f.ext, ".txt") self.assertTrue(f.exists) self.assertTrue(f.is_file) self.assertFalse(f.is_folder) self.assertFalse(f.is_package) self.assertFalse(f.is_readonly) self.assertEqual(f.mime_type, "text/plain") self.assertEqual(f.name, "foo") self.assertEqual(f.parent.path, "data") self.assertEqual(f.path, "data/foo.txt") self.assertIn(os.path.abspath(os.path.curdir), f.url) # Make it readonly. os.chmod(f.path, stat.S_IRUSR) self.assertTrue(f.is_readonly) # And then make it NOT readonly so that we can delete it at the end of # the test! os.chmod(f.path, stat.S_IRUSR | stat.S_IWUSR) self.assertFalse(f.is_readonly) def test_copy(self): """ file copy """ content = 'print("Hello World!")\n' f = File("data/foo.txt") self.assertFalse(f.exists) # Create the file. f.create_file(content) self.assertTrue(f.exists) self.assertRaises(ValueError, f.create_file, content) self.assertIsNone(f.children) self.assertEqual(f.ext, ".txt") self.assertTrue(f.is_file) self.assertFalse(f.is_folder) self.assertEqual(f.mime_type, "text/plain") self.assertEqual(f.name, "foo") self.assertEqual(f.path, "data/foo.txt") # Copy the file. g = File("data/bar.txt") self.assertFalse(g.exists) f.copy(g) self.assertTrue(g.exists) self.assertIsNone(g.children) self.assertEqual(g.ext, ".txt") self.assertTrue(g.is_file) self.assertFalse(g.is_folder) self.assertEqual(g.mime_type, "text/plain") self.assertEqual(g.name, "bar") self.assertEqual(g.path, "data/bar.txt") # Attempt to copy a non-existent file (should do nothing). f = File("data/bogus.xx") self.assertFalse(f.exists) g = File("data/bogus_copy.txt") self.assertFalse(g.exists) f.copy(g) self.assertFalse(g.exists) def test_create_file(self): """ file creation """ content = 'print("Hello World!")\n' f = File("data/foo.txt") self.assertFalse(f.exists) # Create the file. f.create_file(content) self.assertTrue(f.exists) with open(f.path) as file: self.assertEqual(file.read(), content) # Try to create it again. self.assertRaises(ValueError, f.create_file, content) def test_delete(self): """ file deletion """ content = 'print("Hello World!")\n' f = File("data/foo.txt") self.assertFalse(f.exists) # Create the file. f.create_file(content) self.assertTrue(f.exists) self.assertRaises(ValueError, f.create_file, content) self.assertIsNone(f.children) self.assertEqual(f.ext, ".txt") self.assertTrue(f.is_file) self.assertFalse(f.is_folder) self.assertEqual(f.mime_type, "text/plain") self.assertEqual(f.name, "foo") self.assertEqual(f.path, "data/foo.txt") # Delete it. f.delete() self.assertFalse(f.exists) # Attempt to delete a non-existet file (should do nothing). f = File("data/bogus.txt") self.assertFalse(f.exists) f.delete() self.assertFalse(f.exists) apptools-5.1.0/apptools/io/tests/test_folder.py0000644000076500000240000001564613777642667022330 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 folder operations. """ # Standard library imports. import os import shutil import stat import unittest from os.path import join # Enthought library imports. from apptools.io import File class FolderTestCase(unittest.TestCase): """ Tests folder operations on a local file system. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ shutil.rmtree("data", ignore_errors=True) os.mkdir("data") def tearDown(self): """ Called immediately after each test method has been called. """ shutil.rmtree("data") ########################################################################### # Tests. ########################################################################### def test_properties(self): """ folder properties """ # Properties of a non-existent folder. f = File("data/bogus") self.assertIn(os.path.abspath(os.path.curdir), f.absolute_path) self.assertIsNone(f.children) self.assertEqual(f.ext, "") self.assertFalse(f.exists) self.assertFalse(f.is_file) self.assertFalse(f.is_folder) self.assertFalse(f.is_package) self.assertFalse(f.is_readonly) self.assertEqual(f.mime_type, "content/unknown") self.assertEqual(f.name, "bogus") self.assertEqual(f.parent.path, "data") self.assertEqual(f.path, "data/bogus") self.assertIn(os.path.abspath(os.path.curdir), f.url) self.assertEqual(str(f), "File(%s)" % f.path) # Properties of an existing folder. f = File("data/sub") f.create_folder() self.assertIn(os.path.abspath(os.path.curdir), f.absolute_path) self.assertEqual(len(f.children), 0) self.assertEqual(f.ext, "") self.assertTrue(f.exists) self.assertFalse(f.is_file) self.assertTrue(f.is_folder) self.assertFalse(f.is_package) self.assertFalse(f.is_readonly) self.assertEqual(f.mime_type, "content/unknown") self.assertEqual(f.name, "sub") self.assertEqual(f.parent.path, "data") self.assertEqual(f.path, "data/sub") self.assertIn(os.path.abspath(os.path.curdir), f.url) # Make it readonly. os.chmod(f.path, stat.S_IRUSR) self.assertTrue(f.is_readonly) # And then make it NOT readonly so that we can delete it at the end of # the test! os.chmod(f.path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) self.assertFalse(f.is_readonly) # Properties of a Python package folder. f = File("data/package") f.create_folder() init = File("data/package/__init__.py") init.create_file() self.assertIn(os.path.abspath(os.path.curdir), f.absolute_path) self.assertEqual(len(f.children), 1) self.assertEqual(f.ext, "") self.assertTrue(f.exists) self.assertFalse(f.is_file) self.assertTrue(f.is_folder) self.assertTrue(f.is_package) self.assertFalse(f.is_readonly) self.assertEqual(f.mime_type, "content/unknown") self.assertEqual(f.name, "package") self.assertEqual(f.parent.path, "data") self.assertEqual(f.path, "data/package") self.assertIn(os.path.abspath(os.path.curdir), f.url) def test_copy(self): """ folder copy """ f = File("data/sub") self.assertFalse(f.exists) # Create the folder. f.create_folder() self.assertTrue(f.exists) self.assertRaises(ValueError, f.create_folder) self.assertEqual(len(f.children), 0) self.assertEqual(f.ext, "") self.assertFalse(f.is_file) self.assertTrue(f.is_folder) self.assertEqual(f.mime_type, "content/unknown") self.assertEqual(f.name, "sub") self.assertEqual(f.path, "data/sub") # Copy the folder. g = File("data/copy") self.assertFalse(g.exists) f.copy(g) self.assertTrue(g.exists) self.assertEqual(len(g.children), 0) self.assertEqual(g.ext, "") self.assertFalse(g.is_file) self.assertTrue(g.is_folder) self.assertEqual(g.mime_type, "content/unknown") self.assertEqual(g.name, "copy") self.assertEqual(g.path, "data/copy") # Attempt to copy a non-existent folder (should do nothing). f = File("data/bogus") self.assertFalse(f.exists) g = File("data/bogus_copy") self.assertFalse(g.exists) f.copy(g) self.assertFalse(g.exists) def test_create_folder(self): """ folder creation """ f = File("data/sub") self.assertFalse(f.exists) # Create the folder. f.create_folder() self.assertTrue(f.exists) parent = File("data") self.assertEqual(len(parent.children), 1) self.assertEqual(parent.children[0].path, join("data", "sub")) # Try to create it again. self.assertRaises(ValueError, f.create_folder) def test_create_folders(self): """ nested folder creation """ f = File("data/sub/foo") self.assertFalse(f.exists) # Attempt to create the folder with 'create_folder' which requires # that all intermediate folders exist. self.assertRaises(OSError, f.create_folder) # Create the folder. f.create_folders() self.assertTrue(f.exists) self.assertTrue(File("data/sub").exists) # Try to create it again. self.assertRaises(ValueError, f.create_folders) def test_delete(self): """ folder deletion """ f = File("data/sub") self.assertFalse(f.exists) # Create the folder. f.create_folder() self.assertTrue(f.exists) self.assertRaises(ValueError, f.create_folder) self.assertEqual(len(f.children), 0) self.assertEqual(f.ext, "") self.assertFalse(f.is_file) self.assertTrue(f.is_folder) self.assertEqual(f.mime_type, "content/unknown") self.assertEqual(f.name, "sub") self.assertEqual(f.path, "data/sub") # Delete it. f.delete() self.assertFalse(f.exists) # Attempt to delete a non-existet folder (should do nothing). f = File("data/bogus") self.assertFalse(f.exists) f.delete() self.assertFalse(f.exists) apptools-5.1.0/apptools/io/__init__.py0000644000076500000240000000107613777642667020403 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Provides an abstraction for files and folders in a file system. Part of the AppTools project of the Enthought Tool Suite. """ from apptools.io.api import File apptools-5.1.0/apptools/io/api.py0000644000076500000240000000075713777642667017422 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for the apptools.io subpackage. - :class:`~.File` """ from .file import File apptools-5.1.0/apptools/io/file.py0000644000076500000240000002210013777642667017552 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 representation of files and folders in a file system. """ # Standard/built-in imports. import mimetypes import os import shutil import stat # Enthought library imports. from traits.api import Bool, HasPrivateTraits, Instance, List, Property from traits.api import Str class File(HasPrivateTraits): """ A representation of files and folders in a file system. """ #### 'File' interface ##################################################### # The absolute path name of this file/folder. absolute_path = Property(Str) # The folder's children (for files this is always None). children = Property(List("File")) # The file extension (for folders this is always the empty string). # # fixme: Currently the extension includes the '.' (ie. we have '.py' and # not 'py'). This is because things like 'os.path.splitext' leave the '.' # on, but I'm not sure that this is a good idea! ext = Property(Str) # Does the file/folder exist? exists = Property(Bool) # Is this an existing file? is_file = Property(Bool) # Is this an existing folder? is_folder = Property(Bool) # Is this a Python package (ie. a folder contaning an '__init__.py' file. is_package = Property(Bool) # Is the file/folder readonly? is_readonly = Property(Bool) # The MIME type of the file (for a folder this will always be # 'context/unknown' (is that what it should be?)). mime_type = Property(Str) # The last component of the path without the extension. name = Property(Str) # The parent of this file/folder (None if it has no parent). parent = Property(Instance("File")) # The path name of this file/folder. path = Str # A URL reference to the file. url = Property(Str) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, path, **traits): """ Creates a new representation of the specified path. """ super(File, self).__init__(path=path, **traits) def __str__(self): """ Returns an 'informal' string representation of the object. """ return "File(%s)" % self.path ########################################################################### # 'File' interface. ########################################################################### #### Properties ########################################################### def _get_absolute_path(self): """ Returns the absolute path of this file/folder. """ return os.path.abspath(self.path) def _get_children(self): """Returns the folder's children. Returns None if the path does not exist or is not a folder. """ if self.is_folder: children = [] for name in os.listdir(self.path): children.append(File(os.path.join(self.path, name))) else: children = None return children def _get_exists(self): """ Returns True if the file exists, otherwise False. """ return os.path.exists(self.path) def _get_ext(self): """ Returns the file extension. """ name, ext = os.path.splitext(self.path) return ext def _get_is_file(self): """ Returns True if the path exists and is a file. """ return self.exists and os.path.isfile(self.path) def _get_is_folder(self): """ Returns True if the path exists and is a folder. """ return self.exists and os.path.isdir(self.path) def _get_is_package(self): """ Returns True if the path exists and is a Python package. """ return self.is_folder and "__init__.py" in os.listdir(self.path) def _get_is_readonly(self): """ Returns True if the file/folder is readonly, otherwise False. """ # If the File object is a folder, os.access cannot be used because it # returns True for both read-only and writable folders on Windows # systems. if self.is_folder: # Mask for the write-permission bits on the folder. If these bits # are set to zero, the folder is read-only. WRITE_MASK = 0x92 permissions = os.stat(self.path)[0] if permissions & WRITE_MASK == 0: readonly = True else: readonly = False elif self.is_file: readonly = not os.access(self.path, os.W_OK) else: readonly = False return readonly def _get_mime_type(self): """ Returns the mime-type of this file/folder. """ mime_type, encoding = mimetypes.guess_type(self.path) if mime_type is None: mime_type = "content/unknown" return mime_type def _get_name(self): """ Returns the last component of the path without the extension. """ basename = os.path.basename(self.path) name, ext = os.path.splitext(basename) return name def _get_parent(self): """ Returns the parent of this file/folder. """ return File(os.path.dirname(self.path)) def _get_url(self): """ Returns the path as a URL. """ # Strip out the leading slash on POSIX systems. return "file:///%s" % self.absolute_path.lstrip("/") #### Methods ############################################################## def copy(self, destination): """ Copies this file/folder. """ # Allow the destination to be a string. if not isinstance(destination, File): destination = File(destination) if self.is_folder: shutil.copytree(self.path, destination.path) elif self.is_file: shutil.copyfile(self.path, destination.path) def create_file(self, contents=""): """ Creates a file at this path. """ if self.exists: raise ValueError("file %s already exists" % self.path) f = open(self.path, "w") f.write(contents) f.close() def create_folder(self): """Creates a folder at this path. All intermediate folders MUST already exist. """ if self.exists: raise ValueError("folder %s already exists" % self.path) os.mkdir(self.path) def create_folders(self): """Creates a folder at this path. This will attempt to create any missing intermediate folders. """ if self.exists: raise ValueError("folder %s already exists" % self.path) os.makedirs(self.path) def create_package(self): """Creates a package at this path. All intermediate folders/packages MUST already exist. """ if self.exists: raise ValueError("package %s already exists" % self.path) os.mkdir(self.path) # Create the '__init__.py' file that actually turns the folder into a # package! init = File(os.path.join(self.path, "__init__.py")) init.create_file() def delete(self): """Deletes this file/folder. Does nothing if the file/folder does not exist. """ if self.is_folder: # Try to make sure that everything in the folder is writeable. self.make_writeable() # Delete it! shutil.rmtree(self.path) elif self.is_file: # Try to make sure that the file is writeable. self.make_writeable() # Delete it! os.remove(self.path) def make_writeable(self): """ Attempt to make the file/folder writeable. """ if self.is_folder: # Try to make sure that everything in the folder is writeable # (i.e., can be deleted!). This comes in especially handy when # deleting '.svn' directories. for path, dirnames, filenames in os.walk(self.path): for name in dirnames + filenames: filename = os.path.join(path, name) if not os.access(filename, os.W_OK): os.chmod(filename, stat.S_IWUSR) elif self.is_file: # Try to make sure that the file is writeable (i.e., can be # deleted!). if not os.access(self.path, os.W_OK): os.chmod(self.path, stat.S_IWUSR) def move(self, destination): """ Moves this file/folder. """ # Allow the destination to be a string. if not isinstance(destination, File): destination = File(destination) # Try to make sure that everything in the directory is writeable. self.make_writeable() # Move it! shutil.move(self.path, destination.path) apptools-5.1.0/apptools/__init__.py0000644000076500000240000000115213777642667017767 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! try: from apptools.version import version as __version__ except ImportError: # If we get here, we're using a source tree that hasn't been created via # the setup script. __version__ = "unknown" apptools-5.1.0/apptools/_testing/0000755000076500000240000000000013777643025017460 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/_testing/__init__.py0000644000076500000240000000070613777642667021607 0ustar aayresstaff00000000000000# (C) Copyright 2006-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 apptools internal tests. """ apptools-5.1.0/apptools/_testing/optional_dependencies.py0000644000076500000240000000250413777642667024401 0ustar aayresstaff00000000000000# (C) Copyright 2006-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 handling optional dependencies so that tests for optional features can be skipped gracefully. """ import importlib import unittest def optional_import(name): """ Optionally import a module, returning None if that module is unavailable. Parameters ---------- name : Str The name of the module being imported. Returns ------- None or module None if the module is not available, and the module otherwise. """ try: module = importlib.import_module(name) except ImportError: return None else: return module numpy = optional_import("numpy") requires_numpy = unittest.skipIf(numpy is None, "NumPy not available") pandas = optional_import("pandas") requires_pandas = unittest.skipIf(pandas is None, "Pandas not available") tables = optional_import("tables") requires_tables = unittest.skipIf(tables is None, "PyTables not available") apptools-5.1.0/apptools/undo/0000755000076500000240000000000013777643025016611 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/undo/abstract_command.py0000644000076500000240000000130513777642667022476 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.api import AbstractCommand apptools-5.1.0/apptools/undo/command_stack.py0000644000076500000240000000140313777642667021777 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.api import CommandStack from pyface.undo.command_stack import _MacroCommand, _StackEntry apptools-5.1.0/apptools/undo/__init__.py0000644000076500000240000000134613777642667020741 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Supports undoing and scripting application commands. Part of the AppTools project of the Enthought Tool Suite. """ import warnings warnings.warn( ("apptools.undo is deprecated and will be removed in a future release. The" " functionality is now available via pyface.undo"), DeprecationWarning, stacklevel=2 ) apptools-5.1.0/apptools/undo/i_undo_manager.py0000644000076500000240000000130213777642667022141 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.api import IUndoManager apptools-5.1.0/apptools/undo/i_command.py0000644000076500000240000000127613777642667021132 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.api import ICommand apptools-5.1.0/apptools/undo/api.py0000644000076500000240000000200313777642667017742 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ """ API for apptools.undo subpackage. - :class:`~.AbstractCommand` - :class:`~.CommandStack` - :class:`~.UndoManager` Interfaces ---------- - :class:`~.ICommand` - :class:`~.ICommandStack` - :class:`~.IUndoManager` """ from pyface.undo.api import ( AbstractCommand, CommandStack, ICommand, ICommandStack, IUndoManager, UndoManager, ) apptools-5.1.0/apptools/undo/action/0000755000076500000240000000000013777643025020066 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/undo/action/command_action.py0000644000076500000240000000131213777642667023423 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.action.api import CommandAction apptools-5.1.0/apptools/undo/action/undo_action.py0000644000076500000240000000130713777642667022756 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.action.api import UndoAction apptools-5.1.0/apptools/undo/action/__init__.py0000644000076500000240000000115413777642667022213 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 warnings warnings.warn( ("apptools.undo is deprecated and will be removed in a future release. The" " functionality is now available via pyface.undo"), DeprecationWarning, stacklevel=2 ) apptools-5.1.0/apptools/undo/action/redo_action.py0000644000076500000240000000130713777642667022742 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.action.api import RedoAction apptools-5.1.0/apptools/undo/action/api.py0000644000076500000240000000156113777642667021227 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ """ API for apptools.undo.action subpackage. - :class:`~.CommandAction` - :class:`~.RedoAction` - :class:`~.UndoAction` """ from pyface.undo.action.api import ( CommandAction, RedoAction, UndoAction, ) apptools-5.1.0/apptools/undo/action/abstract_command_stack_action.py0000644000076500000240000000137113777642667026500 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.action.abstract_command_stack_action import ( AbstractCommandStackAction ) apptools-5.1.0/apptools/undo/undo_manager.py0000644000076500000240000000130113777642667021630 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.api import UndoManager apptools-5.1.0/apptools/undo/i_command_stack.py0000644000076500000240000000130313777642667022306 0ustar aayresstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ from pyface.undo.api import ICommandStack apptools-5.1.0/apptools/naming/0000755000076500000240000000000013777643025017115 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/naming/referenceable.py0000644000076500000240000000163313777642667022267 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base class for classes that can produce a reference to themselves. """ # Enthought library imports. from traits.api import HasPrivateTraits, Instance # Local imports. from .reference import Reference class Referenceable(HasPrivateTraits): """ Base class for classes that can produce a reference to themselves. """ #### 'Referenceable' interface ############################################ # The object's reference suitable for binding in a naming context. reference = Instance(Reference) apptools-5.1.0/apptools/naming/dir_context.py0000644000076500000240000001302213777642667022022 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for all directory contexts. """ # Enthought library imports. from traits.api import Dict # Local imports. from .context import Context from .exception import NameNotFoundError class DirContext(Context): """ The base class for all directory contexts. """ # The attributes of every object in the context. The attributes for the # context itself have the empty string as the key. # # {str name : dict attributes} _attributes = Dict ########################################################################### # 'DirContext' interface. ########################################################################### def get_attributes(self, name): """ Returns the attributes associated with a named object. """ # If the name is empty then we return the attributes of this context. if len(name) == 0: attributes = self._get_attributes(name) else: # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual get. attributes = self._get_attributes(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) attributes = next_context.get_attributes( "/".join(components[1:]) ) return attributes def set_attributes(self, name, attributes): """ Sets the attributes associated with a named object. """ # If the name is empty then we set the attributes of this context. if len(name) == 0: attributes = self._set_attributes(name, attributes) else: # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual set. self._set_attributes(atom, attributes) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.set_attributes( "/".join(components[1:]), attributes ) # fixme: Non-JNDI def find_bindings(self, visitor): """Find bindings with attributes matching criteria in visitor. Visitor is a function that is passed the bindings for each level of the heirarchy and the attribute dictionary for those bindings. The visitor examines the bindings and dictionary and returns the bindings it is interested in. """ bindings = visitor(self.list_bindings(), self._attributes) # recursively check other sub contexts. for binding in self.list_bindings(): obj = binding.obj if isinstance(obj, DirContext): bindings.extend(obj.find_bindings(visitor)) return bindings ########################################################################### # Protected 'DirContext' interface. ########################################################################### def _get_attributes(self, name): """ Returns the attributes of an object in this context. """ attributes = self._attributes.setdefault(name, {}) return attributes.copy() def _set_attributes(self, name, attributes): """ Sets the attributes of an object in this context. """ self._attributes[name] = attributes ########################################################################### # Protected 'Context' interface. ########################################################################### def _unbind(self, name): """ Unbinds a name from this context. """ super(DirContext, self)._unbind(name) if name in self._attributes: del self._attributes[name] def _rename(self, old_name, new_name): """ Renames an object in this context. """ super(DirContext, self)._rename(old_name, new_name) if old_name in self._attributes: self._attributes[new_name] = self._attributes[old_name] del self._attributes[old_name] def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ super(DirContext, self)._destroy_subcontext(name) if name in self._attributes: del self._attributes[name] apptools-5.1.0/apptools/naming/address.py0000644000076500000240000000145513777642667021134 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 address of a commuications endpoint. """ # Enthought library imports. from traits.api import Any, HasTraits, Str class Address(HasTraits): """The address of a communications end-point. It contains a type that describes the communication mechanism, and the actual address content. """ # The type of the address. type = Str # The actual content. content = Any apptools-5.1.0/apptools/naming/exception.py0000644000076500000240000000271713777642667021507 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Naming exceptions. """ class NamingError(Exception): """Base class for all naming exceptions.""" class InvalidNameError(NamingError): """Invalid name. This exception is thrown when the name passed to a naming operation does not conform to the syntax of the naming system (or is empty etc). """ class NameAlreadyBoundError(NamingError): """Name already bound. This exception is thrown when an attempt is made to bind a name that is already bound in the current context. """ class NameNotFoundError(NamingError): """Name not found. This exception is thrown when a component of a name cannot be resolved because it is not bound in the current context. """ class NotContextError(NamingError): """Not a context. This exception is thrown when a naming operation has reached a point where a context is required to continue the operation, but the resolved object is not a context. """ class OperationNotSupportedError(NamingError): """The context does support the requested operation.""" apptools-5.1.0/apptools/naming/unique_name.py0000644000076500000240000000172713777642667022017 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 re-usable method for calculating a unique name given a list of existing names. """ def make_unique_name(base, existing=[], format="%s_%s"): """ Return a name, unique within a context, based on the specified name. base: the desired base name of the generated unique name. existing: a sequence of the existing names to avoid returning. format: a formatting specification for how the name is made unique. """ count = 2 name = base while name in existing: name = format % (base, count) count += 1 return name apptools-5.1.0/apptools/naming/referenceable_state_factory.py0000644000076500000240000000223213777642667025212 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ State factory for referenceable objects. """ # Local imports. from .referenceable import Referenceable from .state_factory import StateFactory class ReferenceableStateFactory(StateFactory): """ State factory for referenceable objects. """ ########################################################################### # 'StateFactory' interface. ########################################################################### def get_state_to_bind(self, obj, name, context): """ Returns the state of an object for binding. """ state = None # If the object knows how to create a reference to it then let it # do so. if isinstance(obj, Referenceable): state = obj.reference return state apptools-5.1.0/apptools/naming/pyfs_initial_context_factory.py0000644000076500000240000000340613777642667025472 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 initial context factory for Python file system contexts. """ # Local imports. from .context import Context from .initial_context_factory import InitialContextFactory from .object_serializer import ObjectSerializer from .pyfs_context import PyFSContext from .pyfs_context_factory import PyFSContextFactory from .pyfs_object_factory import PyFSObjectFactory from .pyfs_state_factory import PyFSStateFactory class PyFSInitialContextFactory(InitialContextFactory): """ The initial context factory for Python file system contexts. """ ########################################################################### # 'InitialContextFactory' interface. ########################################################################### def get_initial_context(self, environment): """ Creates an initial context for beginning name resolution. """ # Object factories. object_factories = [PyFSObjectFactory(), PyFSContextFactory()] environment[Context.OBJECT_FACTORIES] = object_factories # State factories. state_factories = [PyFSStateFactory()] environment[Context.STATE_FACTORIES] = state_factories # Object serializers. object_serializers = [ObjectSerializer()] environment[PyFSContext.OBJECT_SERIALIZERS] = object_serializers return PyFSContext(path=r"", environment=environment) apptools-5.1.0/apptools/naming/object_serializer.py0000644000076500000240000000501013777642667023175 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for all object serializers. """ # Standard library imports. import logging from traceback import print_exc from os.path import splitext import pickle # Enthought library imports. from apptools.persistence.versioned_unpickler import VersionedUnpickler from traits.api import HasTraits, Str # Setup a logger for this module. logger = logging.getLogger(__name__) class ObjectSerializer(HasTraits): """ The base class for all object serializers. """ #### 'ObjectSerializer' interface ######################################### # The file extension recognized by this serializer. ext = Str(".pickle") ########################################################################### # 'ObjectSerializer' interface. ########################################################################### def can_load(self, path): """ Returns True if the serializer can load a file. """ rest, ext = splitext(path) return ext == self.ext def load(self, path): """ Loads an object from a file. """ # Unpickle the object. f = open(path, "rb") try: try: obj = VersionedUnpickler(f).load() except Exception as ex: print_exc() logger.exception( "Failed to load pickle file: %s, %s" % (path, ex) ) raise finally: f.close() return obj def can_save(self, obj): """ Returns True if the serializer can save an object. """ return True def save(self, path, obj): """ Saves an object to a file. """ if not path.endswith(self.ext): actual_path = path + self.ext else: actual_path = path # Pickle the object. f = open(actual_path, "wb") try: pickle.dump(obj, f, 1) except Exception as ex: logger.exception( "Failed to pickle into file: %s, %s, object:%s" % (path, ex, obj) ) print_exc() f.close() return actual_path apptools-5.1.0/apptools/naming/trait_defs/0000755000076500000240000000000013777643025021241 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/naming/trait_defs/__init__.py0000644000076500000240000000116113777642667023364 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Imports: # ------------------------------------------------------------------------------ from .naming_traits import NamingInstance apptools-5.1.0/apptools/naming/trait_defs/api.py0000644000076500000240000000103113777642667022372 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for the apptools.naming.trait_defs subpackage. - :attr:`~.NamingInstance` """ from .naming_traits import NamingInstance apptools-5.1.0/apptools/naming/trait_defs/naming_traits.py0000644000076500000240000001360513777642667024472 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------------- import sys from traits.api import Trait, TraitHandler, TraitFactory from traits.trait_base import class_of, get_module_name from apptools.naming.api import Binding # ------------------------------------------------------------------------------- # 'NamingInstance' trait factory: # ------------------------------------------------------------------------------- def NamingInstance(klass=None, value="", allow_none=False, **metadata): metadata.setdefault("copy", "deep") return Trait( value, NamingTraitHandler( klass, or_none=allow_none, module=get_module_name() ), **metadata ) NamingInstance = TraitFactory(NamingInstance) # ------------------------------------------------------------------------------- # 'NamingTraitHandler' class: # ------------------------------------------------------------------------------- class NamingTraitHandler(TraitHandler): # --------------------------------------------------------------------------- # Initializes the object: # --------------------------------------------------------------------------- def __init__(self, aClass, or_none, module): """Initializes the object.""" self.or_none = or_none is not False self.module = module self.aClass = aClass if (aClass is not None) and ( not isinstance(aClass, (str, type)) ): self.aClass = aClass.__class__ def validate(self, object, name, value): if isinstance(value, str): if value == "": if self.or_none: return "" else: self.validate_failed(object, name, value) try: value = self._get_binding_for(value) except: # noqa: E722 self.validate_failed(object, name, value) if isinstance(self.aClass, str): self.resolve_class(object, name, value) if isinstance(value, Binding) and ( (self.aClass is None) or isinstance(value.obj, self.aClass) ): return value.namespace_name self.validate_failed(object, name, value) def info(self): aClass = self.aClass if aClass is None: result = "path" else: if type(aClass) is not str: aClass = aClass.__name__ result = "path to an instance of " + class_of(aClass) if self.or_none is None: return result + " or an empty string" return result def validate_failed(self, object, name, value): if not isinstance(value, type): msg = "class %s" % value.__class__.__name__ else: msg = "%s (i.e. %s)" % (str(type(value))[1:-1], repr(value)) self.error(object, name, msg) def get_editor(self, trait): if self.editor is None: from traitsui.api import DropEditor self.editor = DropEditor( klass=self.aClass, binding=True, readonly=False ) return self.editor def post_setattr(self, object, name, value): other = None if value != "": other = self._get_binding_for(value).obj object.__dict__[name + "_"] = other def _get_binding_for(self, value): result = None # FIXME: The following code makes this whole component have a # dependency on envisage, and worse, assumes the use of a particular # project plugin! This is horrible and should be refactored out, # possibly to a custom sub-class of whoever needs this behavior. try: from envisage import get_application workspace = get_application().service_registry.get_service( "envisage.project.IWorkspace" ) result = workspace.lookup_binding(value) except ImportError: pass return result def resolve_class(self, object, name, value): aClass = self.find_class() if aClass is None: self.validate_failed(object, name, value) self.aClass = aClass # fixme: The following is quite ugly, because it wants to try and fix # the trait referencing this handler to use the 'fast path' now that # the actual class has been resolved. The problem is finding the trait, # especially in the case of List(Instance('foo')), where the # object.base_trait(...) value is the List trait, not the Instance # trait, so we need to check for this and pull out the List # 'item_trait'. Obviously this does not extend well to other traits # containing nested trait references (Dict?)... trait = object.base_trait(name) handler = trait.handler if (handler is not self) and hasattr(handler, "item_trait"): trait = handler.item_trait trait.validate(self.fast_validate) def find_class(self): module = self.module aClass = self.aClass col = aClass.rfind(".") if col >= 0: module = aClass[:col] aClass = aClass[col + 1:] theClass = getattr(sys.modules.get(module), aClass, None) if (theClass is None) and (col >= 0): try: theClass = getattr(__import__(module), aClass, None) except Exception: pass return theClass apptools-5.1.0/apptools/naming/tests/0000755000076500000240000000000013777643025020257 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/naming/tests/test_py_context.py0000644000076500000240000000175013777642667024102 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 the Python namespace context. """ # Enthought library imports. from apptools.naming.api import PyContext # Local imports. from .test_context import ContextTestCase class PyContextTestCase(ContextTestCase): """ Tests the Python namespace context. """ ########################################################################### # 'ContextTestCase' interface. ########################################################################### def create_context(self): """ Creates the context that we are testing. """ return PyContext(namespace={}) apptools-5.1.0/apptools/naming/tests/test_dir_context.py0000644000076500000240000000754413777642667024237 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 the default directory context. """ # Enthought library imports. from apptools.naming.api import ( DirContext, NameNotFoundError, NotContextError, ) # Local imports. from .test_context import ContextTestCase class DirContextTestCase(ContextTestCase): """ Tests the default directory context. """ ########################################################################### # 'ContextTestCase' interface. ########################################################################### def create_context(self): """ Creates the context that we are testing. """ return DirContext() ########################################################################### # Tests. ########################################################################### def test_get_attributes(self): """ get attributes """ # Convenience. context = self.context sub = self.context.lookup("sub") self.assertIsInstance(sub, DirContext) #### Generic name resolution tests #### # Non-existent name. self.assertRaises(NameNotFoundError, context.get_attributes, "x") # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.get_attributes, "x/a") # Attempt to resolve via an existing name that is not a context. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) self.assertRaises(NotContextError, context.get_attributes, "sub/a/x") #### Operation specific tests #### # Attributes of the root context. attributes = self.context.get_attributes("") self.assertEqual(len(attributes), 0) # Attributes of a sub-context. attributes = context.get_attributes("sub") self.assertEqual(len(attributes), 0) def test_set_get_attributes(self): """ get and set attributes """ defaults = {"colour": "blue"} # Convenience. context = self.context sub = self.context.lookup("sub") self.assertIsInstance(sub, DirContext) #### Generic name resolution tests #### # Non-existent name. self.assertRaises( NameNotFoundError, context.set_attributes, "x", defaults ) # Attempt to resolve via a non-existent context. self.assertRaises( NameNotFoundError, context.set_attributes, "x/a", defaults ) # Attempt to resolve via an existing name that is not a context. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) self.assertRaises( NotContextError, context.set_attributes, "sub/a/xx", defaults ) #### Operation specific tests #### # Attributes of the root context. attributes = self.context.get_attributes("") self.assertEqual(len(attributes), 0) # Set the attributes. context.set_attributes("", defaults) attributes = context.get_attributes("") self.assertEqual(len(attributes), 1) self.assertEqual(attributes["colour"], "blue") # Attributes of a sub-context. attributes = context.get_attributes("sub") self.assertEqual(len(attributes), 0) # Set the attributes. context.set_attributes("sub", defaults) attributes = context.get_attributes("sub") self.assertEqual(len(attributes), 1) self.assertEqual(attributes["colour"], "blue") apptools-5.1.0/apptools/naming/tests/__init__.py0000644000076500000240000000062713777642667022410 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/naming/tests/test_pyfs_context.py0000644000076500000240000002660213777642667024436 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 naming operations on PyFS contexts. """ # Standard library imports. import os import shutil import unittest # Enthought library imports. from apptools.io import File from apptools.naming.api import ( DirContext, InvalidNameError, NameAlreadyBoundError, NameNotFoundError, NotContextError, PyFSContext, ) class PyFSContextTestCase(unittest.TestCase): """ Tests naming operations on PyFS contexts. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ if os.path.exists("data"): shutil.rmtree("data", ignore_errors=True) if os.path.exists("other"): shutil.rmtree("other", ignore_errors=True) os.mkdir("data") os.mkdir("other") self.context = PyFSContext(path="data") self.context.create_subcontext("sub") self.context.bind("x", 123) self.context.bind("y", 321) def tearDown(self): """ Called immediately after each test method has been called. """ self.context = None shutil.rmtree("data") shutil.rmtree("other") ########################################################################### # Tests. ########################################################################### def test_initialization(self): """ initialization of an existing context """ context = PyFSContext(path="data") self.assertEqual(len(context.list_bindings("")), 3) def test_initialization_with_empty_environment(self): """ initialization with empty environmentt """ context = PyFSContext(path="other", environment={}) self.assertEqual(len(context.list_names("")), 0) def test_bind(self): """ pyfs context bind """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.bind, "", 1) # Bind a local file object. f = File(os.path.join(sub.path, "foo.py")) # f.create_file('print("foo!")\n') context.bind("sub/foo.py", f) self.assertEqual(len(sub.list_bindings("")), 1) # Bind a reference to a non-local file. f = File("/tmp") context.bind("sub/tmp", f) self.assertEqual(len(sub.list_bindings("")), 2) self.assertEqual(context.lookup("sub/tmp").path, f.path) # Bind a reference to a non-local context. f = PyFSContext(path="other") context.bind("sub/other", f) self.assertEqual(len(sub.list_bindings("")), 3) self.assertIn(f.path, context.lookup("sub/other").path) # Bind a Python object. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 4) # Try to bind it again. self.assertRaises(NameAlreadyBoundError, context.bind, "sub/a", 1) def test_rebind(self): """ pyfs context rebind """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.rebind, "", 1) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Rebind it. context.rebind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) def test_unbind(self): """ pyfs context unbind """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.unbind, "") # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Unbind it. context.unbind("sub/a") self.assertEqual(len(sub.list_bindings("")), 0) # Try to unbind a non-existent name. self.assertRaises(NameNotFoundError, context.unbind, "sub/b") def test_rename(self): """ multi-context rename """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.rename, "", "x") self.assertRaises(InvalidNameError, context.rename, "x", "") # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Rename it. context.rename("sub/a", "sub/b") self.assertEqual(len(sub.list_bindings("")), 1) # Lookup using the new name. self.assertEqual(context.lookup("sub/b"), 1) # Lookup using the old name. self.assertRaises(NameNotFoundError, context.lookup, "sub/a") def test_lookup(self): """ pyfs context lookup """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Bind a file object. f = File(os.path.join(sub.path, "foo.py")) # f.create_file('print("foo!")\n') context.bind("sub/foo.py", f) self.assertEqual(len(sub.list_bindings("")), 1) # Look it up. self.assertEqual(context.lookup("sub/foo.py").path, f.path) # Bind a Python object. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 2) # Look it up. self.assertEqual(context.lookup("sub/a"), 1) # Looking up the Empty name returns the context itself. self.assertEqual(context.lookup(""), context) # Non-existent name. self.assertRaises(NameNotFoundError, context.lookup, "sub/b") def test_create_subcontext(self): """ pyfs context create sub-context """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.create_subcontext, "") # Create a sub-context. context.create_subcontext("sub/a") self.assertEqual(len(sub.list_bindings("")), 1) self.assertTrue(os.path.isdir(os.path.join(sub.path, "a"))) # Try to bind it again. self.assertRaises( NameAlreadyBoundError, context.create_subcontext, "sub/a" ) def test_destroy_subcontext(self): """ single context destroy sub-context """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.destroy_subcontext, "") # Create a sub-context. context.create_subcontext("sub/a") self.assertEqual(len(sub.list_bindings("")), 1) # Destroy it. context.destroy_subcontext("sub/a") self.assertEqual(len(sub.list_bindings("")), 0) self.assertTrue(not os.path.isdir(os.path.join(sub.path, "a"))) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Try to destroy it. self.assertRaises(NotContextError, context.destroy_subcontext, "sub/a") # Try to destroy a non-existent name. self.assertRaises( NameNotFoundError, context.destroy_subcontext, "sub/b" ) def test_get_attributes(self): """ get attributes """ # Convenience. context = self.context sub = self.context.lookup("sub") self.assertIsInstance(sub, DirContext) #### Generic name resolution tests #### # Non-existent name. self.assertRaises(NameNotFoundError, context.get_attributes, "xx") # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.get_attributes, "xx/a") # Attempt to resolve via an existing name that is not a context. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) self.assertRaises(NotContextError, context.get_attributes, "sub/a/x") #### Operation specific tests #### # Attributes of the root context. attributes = context.get_attributes("") self.assertEqual(len(attributes), 0) # Attributes of a sub-context. attributes = context.get_attributes("sub") self.assertEqual(len(attributes), 0) def test_set_get_attributes(self): """ get and set attributes """ defaults = {"colour": "blue"} # Convenience. context = self.context sub = self.context.lookup("sub") self.assertIsInstance(sub, DirContext) #### Generic name resolution tests #### # Non-existent name. self.assertRaises( NameNotFoundError, context.set_attributes, "xx", defaults ) # Attempt to resolve via a non-existent context. self.assertRaises( NameNotFoundError, context.set_attributes, "xx/a", defaults ) # Attempt to resolve via an existing name that is not a context. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) self.assertRaises( NotContextError, context.set_attributes, "sub/a/xx", defaults ) #### Operation specific tests #### # Attributes of the root context. attributes = self.context.get_attributes("") self.assertEqual(len(attributes), 0) # Set the attributes. context.set_attributes("", defaults) attributes = context.get_attributes("") self.assertEqual(len(attributes), 1) self.assertEqual(attributes["colour"], "blue") # Attributes of a sub-context. attributes = context.get_attributes("sub") self.assertEqual(len(attributes), 0) # Set the attributes. context.set_attributes("sub", defaults) attributes = context.get_attributes("sub") self.assertEqual(len(attributes), 1) self.assertEqual(attributes["colour"], "blue") def test_namespace_name(self): """ get the name of a context within its namespace. """ # Convenience. context = self.context sub = self.context.lookup("sub") self.assertIsInstance(sub, DirContext) self.assertEqual(context.namespace_name, "data") self.assertEqual(sub.namespace_name, "data/sub") apptools-5.1.0/apptools/naming/tests/test_context.py0000644000076500000240000004000513777642667023366 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 operations that span contexts. """ # Standard library imports. import unittest # Enthought library imports. from apptools.naming.api import ( Context, InvalidNameError, NameAlreadyBoundError, NameNotFoundError, NotContextError, ObjectFactory, StateFactory, ) class ContextTestCase(unittest.TestCase): """ Tests naming operations that span contexts. """ ########################################################################### # 'TestCase' interface. ########################################################################### def setUp(self): """ Prepares the test fixture before each test method is called. """ self.context = self.create_context() self.context.create_subcontext("sub") def tearDown(self): """ Called immediately after each test method has been called. """ self.context = None ########################################################################### # 'ContextTestCase' interface. ########################################################################### def create_context(self): """ Creates the context that we are testing. """ return Context() ########################################################################### # Tests. ########################################################################### def test_bind(self): """ bind """ # Convenience. context = self.context sub = self.context.lookup("sub") self.assertIsInstance(sub, Context) # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.bind, "", 1) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.bind, "xx/a", 1) self.assertEqual(len(sub.list_bindings("")), 0) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.bind, "sub/a/xx", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Try to bind it again. self.assertRaises(NameAlreadyBoundError, context.bind, "sub/a", 1) def test_bind_with_make_contexts(self): """ bind with make contexts """ # Convenience. context = self.context sub = self.context.lookup("sub") self.assertIsInstance(sub, Context) # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.bind, "", 1, True) # Attempt to resolve via a non-existent context - which should result # in the context being created automatically. context.bind("xx/a", 1, True) self.assertEqual(len(context.list_bindings("xx")), 1) self.assertEqual(1, context.lookup("xx/a")) # Bind an even more 'nested' name. context.bind("xx/foo/bar/baz", 42, True) self.assertEqual(len(context.list_bindings("xx/foo/bar")), 1) self.assertEqual(42, context.lookup("xx/foo/bar/baz")) def test_rebind(self): """ context rebind """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.rebind, "", 1) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.rebind, "xx/a", 1) self.assertEqual(len(sub.list_bindings("")), 0) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Rebind it. context.rebind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.rebind, "sub/a/xx", 1) self.assertEqual(len(sub.list_bindings("")), 1) def test_rebind_with_make_contexts(self): """ rebind with make contexts """ # Convenience. context = self.context sub = self.context.lookup("sub") self.assertIsInstance(sub, Context) # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.rebind, "", 1, True) # Attempt to resolve via a non-existent context - which should result # in the context being created automatically. context.rebind("xx/a", 1, True) self.assertEqual(len(context.list_bindings("xx")), 1) self.assertEqual(1, context.lookup("xx/a")) # Rebind an even more 'nested' name. context.rebind("xx/foo/bar/baz", 42, True) self.assertEqual(len(context.list_bindings("xx/foo/bar")), 1) self.assertEqual(42, context.lookup("xx/foo/bar/baz")) # And do it again... (this is REbind after all). context.rebind("xx/foo/bar/baz", 42, True) self.assertEqual(len(context.list_bindings("xx/foo/bar")), 1) self.assertEqual(42, context.lookup("xx/foo/bar/baz")) def test_unbind(self): """ context unbind """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.unbind, "") # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.unbind, "xx/a") self.assertEqual(len(sub.list_bindings("")), 0) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.unbind, "sub/a/xx") self.assertEqual(len(sub.list_bindings("")), 1) # Unbind it. context.unbind("sub/a") self.assertEqual(len(sub.list_bindings("")), 0) # Try to unbind a non-existent name. self.assertRaises(NameNotFoundError, context.unbind, "sub/b") def test_rename_object(self): """ rename an object """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.rename, "", "x") self.assertRaises(InvalidNameError, context.rename, "x", "") # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.rename, "x/a", "x/b") self.assertEqual(len(sub.list_bindings("")), 0) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.bind, "sub/a/xx", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Rename it. context.rename("sub/a", "sub/b") self.assertEqual(len(sub.list_bindings("")), 1) # Lookup using the new name. self.assertEqual(context.lookup("sub/b"), 1) # Lookup using the old name. self.assertRaises(NameNotFoundError, context.lookup, "sub/a") def test_rename_context(self): """ rename a context """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.rename, "", "x") self.assertRaises(InvalidNameError, context.rename, "x", "") # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.rename, "x/a", "x/b") self.assertEqual(len(sub.list_bindings("")), 0) # Create a context. context.create_subcontext("sub/a") self.assertEqual(len(sub.list_bindings("")), 1) # Rename it. context.rename("sub/a", "sub/b") self.assertEqual(len(sub.list_bindings("")), 1) # Lookup using the new name. context.lookup("sub/b") # Lookup using the old name. self.assertRaises(NameNotFoundError, context.lookup, "sub/a") def test_lookup(self): """ lookup """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.lookup, "xx/a") self.assertEqual(len(sub.list_bindings("")), 0) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.lookup, "sub/a/xx") self.assertEqual(len(sub.list_bindings("")), 1) # Look it up. self.assertEqual(context.lookup("sub/a"), 1) # Looking up the Empty name returns the context itself. self.assertEqual(context.lookup(""), context) # Non-existent name. self.assertRaises(NameNotFoundError, context.lookup, "sub/b") def test_create_subcontext(self): """ create sub-context """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.create_subcontext, "") # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.create_subcontext, "xx/a") self.assertEqual(len(sub.list_bindings("")), 0) # Create a sub-context. context.create_subcontext("sub/a") self.assertEqual(len(sub.list_bindings("")), 1) # Try to bind it again. self.assertRaises( NameAlreadyBoundError, context.create_subcontext, "sub/a" ) # Bind a name. context.bind("sub/b", 1) self.assertEqual(len(sub.list_bindings("")), 2) # Attempt to resolve via an existing name that is not a context. self.assertRaises( NotContextError, context.create_subcontext, "sub/b/xx" ) self.assertEqual(len(sub.list_bindings("")), 2) def test_destroy_subcontext(self): """ single context destroy sub-context """ # Convenience. context = self.context sub = self.context.lookup("sub") # Make sure that the sub-context is empty. self.assertEqual(len(sub.list_bindings("")), 0) # Empty name. self.assertRaises(InvalidNameError, context.destroy_subcontext, "") # Attempt to resolve via a non-existent context. self.assertRaises( NameNotFoundError, context.destroy_subcontext, "xx/a" ) self.assertEqual(len(sub.list_bindings("")), 0) # Create a sub-context. context.create_subcontext("sub/a") self.assertEqual(len(sub.list_bindings("")), 1) # Destroy it. context.destroy_subcontext("sub/a") self.assertEqual(len(sub.list_bindings("")), 0) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Try to destroy it. self.assertRaises(NotContextError, context.destroy_subcontext, "sub/a") # Try to destroy a non-existent name. self.assertRaises( NameNotFoundError, context.destroy_subcontext, "sub/b" ) # Attempt to resolve via an existing name that is not a context. self.assertRaises( NotContextError, context.destroy_subcontext, "sub/a/xx" ) self.assertEqual(len(sub.list_bindings("")), 1) def test_list_bindings(self): """ list bindings """ # Convenience. context = self.context sub = self.context.lookup("sub") # List the bindings in the root. bindings = context.list_bindings("") self.assertEqual(len(bindings), 1) # List the names in the sub-context. bindings = context.list_bindings("sub") self.assertEqual(len(bindings), 0) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.list_bindings, "xx/a") self.assertEqual(len(sub.list_bindings("")), 0) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.list_bindings, "sub/a/xx") self.assertEqual(len(sub.list_bindings("")), 1) def test_list_names(self): """ list names """ # Convenience. context = self.context sub = self.context.lookup("sub") # List the names in the root. names = context.list_names("") self.assertEqual(len(names), 1) # List the names in the sub-context. names = context.list_names("sub") self.assertEqual(len(names), 0) # Attempt to resolve via a non-existent context. self.assertRaises(NameNotFoundError, context.list_names, "xx/a") self.assertEqual(len(sub.list_bindings("")), 0) # Bind a name. context.bind("sub/a", 1) self.assertEqual(len(sub.list_bindings("")), 1) # Attempt to resolve via an existing name that is not a context. self.assertRaises(NotContextError, context.list_names, "sub/a/xx") self.assertEqual(len(sub.list_bindings("")), 1) def test_default_factories(self): """ default object and state factories. """ object_factory = ObjectFactory() self.assertRaises( NotImplementedError, object_factory.get_object_instance, 0, 0, 0 ) state_factory = StateFactory() self.assertRaises( NotImplementedError, state_factory.get_state_to_bind, 0, 0, 0 ) def test_search(self): """ test retrieving the names of bound objects """ # Convenience. context = self.context sub = self.context.lookup("sub") context.create_subcontext("sub sibling") sub_sub = sub.create_subcontext("sub sub") context.bind("one", 1) names = context.search(1) self.assertEqual(len(names), 1) self.assertEqual(names[0], "one") names = sub.search(1) self.assertEqual(len(names), 0) context.bind("sub/two", 2) names = context.search(2) self.assertEqual(len(names), 1) self.assertEqual(names[0], "sub/two") names = sub.search(2) self.assertEqual(len(names), 1) self.assertEqual(names[0], "two") context.bind("sub/sub sub/one", 1) names = context.search(1) self.assertEqual(len(names), 2) self.assertEqual(sorted(names), sorted(["one", "sub/sub sub/one"])) names = sub.search(None) self.assertEqual(len(names), 0) names = context.search(sub_sub) self.assertEqual(len(names), 1) self.assertEqual(names[0], "sub/sub sub") apptools-5.1.0/apptools/naming/tests/test_object_serializer.py0000644000076500000240000000316213777642667025404 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import os import shutil import tempfile import unittest from traits.api import cached_property, HasTraits, Property, Str, Event from apptools.naming.api import ObjectSerializer class FooWithTraits(HasTraits): """Dummy HasTraits class for testing ObjectSerizalizer.""" full_name = Str() last_name = Property(depends_on="full_name") event = Event() @cached_property def _get_last_name(self): return self.full_name.split(" ")[-1] class TestObjectSerializer(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.tmpdir) self.tmp_file = os.path.join(self.tmpdir, "tmp.pickle") def test_save_load_roundtrip(self): # Test HasTraits objects can be serialized and deserialized as expected obj = FooWithTraits(full_name="John Doe") serializer = ObjectSerializer() serializer.save(self.tmp_file, obj) self.assertTrue(serializer.can_load(self.tmp_file)) deserialized = serializer.load(self.tmp_file) self.assertIsInstance(deserialized, FooWithTraits) self.assertEqual(deserialized.full_name, "John Doe") self.assertEqual(deserialized.last_name, "Doe") apptools-5.1.0/apptools/naming/__init__.py0000644000076500000240000000325113777642667021242 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 naming contexts. Supports non-string data types and scoped preferences. Part of the AppTools project of the Enthought Tool Suite. """ from .exception import NamingError, InvalidNameError, NameAlreadyBoundError from .exception import NameNotFoundError, NotContextError from .exception import OperationNotSupportedError from .address import Address from .binding import Binding from .context import Context from .dynamic_context import DynamicContext from .dir_context import DirContext from .initial_context import InitialContext from .initial_context_factory import InitialContextFactory from .naming_event import NamingEvent from .naming_manager import naming_manager from .object_factory import ObjectFactory from .object_serializer import ObjectSerializer from .py_context import PyContext from .py_object_factory import PyObjectFactory from .pyfs_context import PyFSContext from .pyfs_context_factory import PyFSContextFactory from .pyfs_initial_context_factory import PyFSInitialContextFactory from .pyfs_object_factory import PyFSObjectFactory from .pyfs_state_factory import PyFSStateFactory from .reference import Reference from .referenceable import Referenceable from .referenceable_state_factory import ReferenceableStateFactory from .state_factory import StateFactory apptools-5.1.0/apptools/naming/py_object_factory.py0000644000076500000240000000273513777642667023216 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Object factory for Python namespace contexts. """ # Local imports. from .object_factory import ObjectFactory from .reference import Reference class PyObjectFactory(ObjectFactory): """ Object factory for Python namespace contexts. """ ########################################################################### # 'ObjectFactory' interface. ########################################################################### def get_object_instance(self, state, name, context): """ Creates an object using the specified state information. """ obj = None if isinstance(state, Reference): if len(state.addresses) > 0: if state.addresses[0].type == "py_context": namespace = state.addresses[0].content obj = context._context_factory(name, namespace) elif hasattr(state, "__dict__"): from apptools.naming.py_context import PyContext if not isinstance(state, PyContext): obj = context._context_factory(name, state) return obj apptools-5.1.0/apptools/naming/pyfs_context_factory.py0000644000076500000240000000240713777642667023761 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Object factory for Python File System contexts. """ # Local imports. from .object_factory import ObjectFactory from .reference import Reference class PyFSContextFactory(ObjectFactory): """ Object factory for Python File System contexts. """ ########################################################################### # 'ObjectFactory' interface. ########################################################################### def get_object_instance(self, state, name, context): """ Creates an object using the specified state information. """ obj = None if isinstance(state, Reference): if len(state.addresses) > 0: if state.addresses[0].type == "pyfs_context": path = state.addresses[0].content obj = context._context_factory(name, path) return obj apptools-5.1.0/apptools/naming/pyfs_context.py0000644000076500000240000004341413777642667022235 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Python File System context. """ # Standard library imports. import glob import logging import os from os.path import join, splitext import pickle # Enthought library imports. from apptools.io.api import File from traits.api import Any, Dict, Instance, Property, Str # Local imports. from .address import Address from .binding import Binding from .context import Context from .dir_context import DirContext from .naming_event import NamingEvent from .naming_manager import naming_manager from .object_serializer import ObjectSerializer from .pyfs_context_factory import PyFSContextFactory from .pyfs_object_factory import PyFSObjectFactory from .pyfs_state_factory import PyFSStateFactory from .reference import Reference from .referenceable import Referenceable # Setup a logger for this module. logger = logging.getLogger(__name__) # The name of the 'special' file in which we store object attributes. ATTRIBUTES_FILE = "__attributes__" # Constants for environment property keys. FILTERS = "apptools.naming.pyfs.filters" OBJECT_SERIALIZERS = "apptools.naming.pyfs.object.serializers" # The default environment. ENVIRONMENT = { #### 'Context' properties ################################################# # Object factories. Context.OBJECT_FACTORIES: [PyFSObjectFactory(), PyFSContextFactory()], # State factories. Context.STATE_FACTORIES: [PyFSStateFactory()], #### 'PyFSContext' properties ############################################# # Object serializers. OBJECT_SERIALIZERS: [ObjectSerializer()], # List of filename patterns to ignore. These patterns are passed to # 'glob.glob', so things like '*.pyc' will do what you expect. # # fixme: We should have a generalized filter mechanism here, and '.svn' # should be moved elsewhere! FILTERS: [ATTRIBUTES_FILE, ".svn"], } class PyFSContext(DirContext, Referenceable): """A Python File System context. This context represents a directory on a local file system. """ # The name of the 'special' file in which we store object attributes. ATTRIBUTES_FILE = ATTRIBUTES_FILE # Environment property keys. FILTERS = FILTERS OBJECT_SERIALIZERS = OBJECT_SERIALIZERS #### 'Context' interface ################################################## # The naming environment in effect for this context. environment = Dict(ENVIRONMENT) # The name of the context within its own namespace. namespace_name = Property(Str) #### 'PyFSContext' interface ############################################## # The name of the context (the last component of the path). name = Str # The path name of the directory on the local file system. path = Str #### 'Referenceable' interface ############################################ # The object's reference suitable for binding in a naming context. reference = Property(Instance(Reference)) #### Private interface #################################################### # A mapping from bound name to the name of the corresponding file or # directory on the file system. _name_to_filename_map = Dict # (Str, Str) # The attributes of every object in the context. The attributes for the # context itself have the empty string as the key. # # {str name : dict attributes} # # fixme: Don't use 'Dict' here as it causes problems when pickling because # trait dicts have a reference back to the parent object (hence we end up # pickling all kinds of things that we don't need or want to!). _attributes = Any ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Creates a new context. """ # Base class constructor. super(PyFSContext, self).__init__(**traits) # We cache each object as it is looked up so that all accesses to a # serialized Python object return a reference to exactly the same one. self._cache = {} ########################################################################### # 'PyFSContext' interface. ########################################################################### #### Properties ########################################################### def _get_namespace_name(self): """ Returns the name of the context within its own namespace. """ # fixme: clean this up with an initial context API! if "root" in self.environment: root = self.environment["root"] namespace_name = self.path[len(root) + 1:] else: namespace_name = self.path # fixme: This is a bit dodgy 'cos we actually return a name that can # be looked up, and not the file system name... namespace_name = "/".join(namespace_name.split(os.path.sep)) return namespace_name #### methods ############################################################## def refresh(self): """ Refresh the context to reflect changes in the file system. """ # fixme: This needs more work 'cos if we refresh a context then we # will load new copies of serialized Python objects! # This causes the initializer to run again the next time the trait is # accessed. self.reset_traits(["_name_to_filename_map"]) # Clear out the cache. self._cache = {} # fixme: This is a bit hacky since the context in the binding may # not be None! self.context_changed = NamingEvent( new_binding=Binding(name=self.name, obj=self, context=None) ) ########################################################################### # 'Referenceable' interface. ########################################################################### #### Properties ########################################################### def _get_reference(self): """ Returns a reference to this object suitable for binding. """ abspath = os.path.abspath(self.path) reference = Reference( class_name=self.__class__.__name__, addresses=[Address(type="pyfs_context", content=abspath)], ) return reference ########################################################################### # Protected 'Context' interface. ########################################################################### def _is_bound(self, name): """ Is a name bound in this context? """ return name in self._name_to_filename_map def _lookup(self, name): """ Looks up a name in this context. """ if name in self._cache: obj = self._cache[name] else: # Get the full path to the file. path = join(self.path, self._name_to_filename_map[name]) # If the file contains a serialized Python object then load it. for serializer in self._get_object_serializers(): if serializer.can_load(path): try: state = serializer.load(path) # If the load fails then we create a generic file resource # (the idea being that it might be useful to have access to # the file to see what went wrong). except: # noqa: E722 state = File(path) logger.exception("Error loading resource at %s" % path) break # Otherwise, it must just be a file or folder. else: # Directories are contexts. if os.path.isdir(path): state = self._context_factory(name, path) # Files are just files! elif os.path.isfile(path): state = File(path) else: raise ValueError("unrecognized file for %s" % name) # Get the actual object from the naming manager. obj = naming_manager.get_object_instance(state, name, self) # Update the cache. self._cache[name] = obj return obj def _bind(self, name, obj): """ Binds a name to an object in this context. """ # Get the actual state to bind from the naming manager. state = naming_manager.get_state_to_bind(obj, name, self) # If the object is actually an abstract file then we don't have to # do anything. if isinstance(state, File): if not state.exists: state.create_file() filename = name # Otherwise we are binding an arbitrary Python object, so find a # serializer for it. else: for serializer in self._get_object_serializers(): if serializer.can_save(obj): path = serializer.save(join(self.path, name), obj) filename = os.path.basename(path) break else: raise ValueError("cannot serialize object %s" % name) # Update the name to filename map. self._name_to_filename_map[name] = filename # Update the cache. self._cache[name] = obj return state def _rebind(self, name, obj): """ Rebinds a name to an object in this context. """ self._bind(name, obj) def _unbind(self, name): """ Unbinds a name from this context. """ # Get the full path to the file. path = join(self.path, self._name_to_filename_map[name]) # Remove it! f = File(path) f.delete() # Update the name to filename map. del self._name_to_filename_map[name] # Update the cache. if name in self._cache: del self._cache[name] # Remove any attributes. if name in self._attributes: del self._attributes[name] self._save_attributes() def _rename(self, old_name, new_name): """ Renames an object in this context. """ # Get the old filename. old_filename = self._name_to_filename_map[old_name] old_file = File(join(self.path, old_filename)) # Lookup the object bound to the old name. This has the side effect # of adding the object to the cache under the name 'old_name'. obj = self._lookup(old_name) # We are renaming a LOCAL context (ie. a folder)... if old_file.is_folder: # Create the new filename. new_filename = new_name new_file = File(join(self.path, new_filename)) # Move the folder. old_file.move(new_file) # Update the 'Context' object. obj.path = new_file.path # Update the cache. self._cache[new_name] = obj del self._cache[old_name] # Refreshing the context makes sure that all of its contents # reflect the new name (i.e., sub-folders and files have the # correct path). # # fixme: This currently results in new copies of serialized # Python objects! We need to be a bit more judicious in the # refresh. obj.refresh() # We are renaming a file... elif isinstance(obj, File): # Create the new filename. new_filename = new_name new_file = File(join(self.path, new_filename)) # Move the file. old_file.move(new_file) # Update the 'File' object. obj.path = new_file.path # Update the cache. self._cache[new_name] = obj del self._cache[old_name] # We are renaming a serialized Python object... else: # Create the new filename. new_filename = new_name + old_file.ext new_file = File(join(self.path, new_filename)) old_file.delete() # Update the cache. if old_name in self._cache: self._cache[new_name] = self._cache[old_name] del self._cache[old_name] # Force the creation of the new file. # # fixme: I'm not sure that this is really the place for this. We # do it because often the 'name' of the object is actually an # attribute of the object itself, and hence we want the serialized # state to reflect the new name... Hmmm... self._rebind(new_name, obj) # Update the name to filename map. del self._name_to_filename_map[old_name] self._name_to_filename_map[new_name] = new_filename # Move any attributes over to the new name. if old_name in self._attributes: self._attributes[new_name] = self._attributes[old_name] del self._attributes[old_name] self._save_attributes() def _create_subcontext(self, name): """ Creates a sub-context of this context. """ path = join(self.path, name) # Create a directory. os.mkdir(path) # Create a sub-context that represents the directory. sub = self._context_factory(name, path) # Update the name to filename map. self._name_to_filename_map[name] = name # Update the cache. self._cache[name] = sub return sub def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ return self._unbind(name) def _list_names(self): """ Lists the names bound in this context. """ return list(self._name_to_filename_map.keys()) # fixme: YFI this is not part of the protected 'Context' interface so # what is it doing here? def get_unique_name(self, name): ext = splitext(name)[1] # specially handle '.py' files if ext != ".py": return super(PyFSContext, self).get_unique_name(name) body = splitext(name)[0] names = self.list_names() i = 2 unique = name while unique in names: unique = body + "_" + str(i) + ".py" i += 1 return unique ########################################################################### # Protected 'DirContext' interface. ########################################################################### def _get_attributes(self, name): """ Returns the attributes of an object in this context. """ attributes = self._attributes.setdefault(name, {}) return attributes.copy() def _set_attributes(self, name, attributes): """ Sets the attributes of an object in this context. """ self._attributes[name] = attributes self._save_attributes() ########################################################################### # Private interface. ########################################################################### def _get_filters(self): """ Returns the filters for this context. """ return self.environment.get(self.FILTERS, []) def _get_object_serializers(self): """ Returns the object serializers for this context. """ return self.environment.get(self.OBJECT_SERIALIZERS, []) def _context_factory(self, name, path): """ Create a sub-context. """ return self.__class__(path=path, environment=self.environment) def _save_attributes(self): """ Saves all attributes to the attributes file. """ path = join(self.path, self.ATTRIBUTES_FILE) f = open(path, "wb") pickle.dump(self._attributes, f, 1) f.close() #### Trait initializers ################################################### def __name_to_filename_map_default(self): """ Initializes the '_name_to_filename' trait. """ # fixme: We should have a generalized filter mechanism (instead of # just 'glob' patterns we should have filter objects that can be a bit # more flexible in how they do the filtering). patterns = [join(self.path, filter) for filter in self._get_filters()] name_to_filename_map = {} for filename in os.listdir(self.path): path = join(self.path, filename) for pattern in patterns: if path in glob.glob(pattern): break else: for serializer in self._get_object_serializers(): if serializer.can_load(filename): # fixme: We should probably get the name from the # serializer instead of assuming that we can just # drop the file exension. name, ext = os.path.splitext(filename) break else: name = filename name_to_filename_map[name] = filename return name_to_filename_map def __attributes_default(self): """ Initializes the '_attributes' trait. """ attributes_file = File(join(self.path, self.ATTRIBUTES_FILE)) if attributes_file.is_file: f = open(attributes_file.path, "rb") attributes = pickle.load(f) f.close() else: attributes = {} return attributes #### Trait event handlers ################################################# def _path_changed(self): """ Called when the context's path has changed. """ basename = os.path.basename(self.path) self.name, ext = os.path.splitext(basename) apptools-5.1.0/apptools/naming/initial_context_factory.py0000644000076500000240000000203613777642667024427 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for all initial context factories. """ # Enthought library imports. from traits.api import HasTraits # Local imports. from .context import Context class InitialContextFactory(HasTraits): """ The base class for all initial context factories. """ ########################################################################### # 'InitialContextFactory' interface. ########################################################################### def get_initial_context(self, environment): """ Creates an initial context for beginning name resolution. """ return Context(environment=environment) apptools-5.1.0/apptools/naming/naming_manager.py0000644000076500000240000000526713777642667022457 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 naming manager. """ # Enthought library imports. from traits.api import HasTraits class NamingManager(HasTraits): """ The naming manager. """ ########################################################################### # 'NamingManager' interface. ########################################################################### def get_state_to_bind(self, obj, name, context): """Returns the state of an object for binding. The naming manager asks the context for its list of STATE factories and then calls them one by one until it gets a non-None result indicating that the factory recognised the object and created state information for it. If none of the factories recognize the object (or if the context has no factories) then the object itself is returned. """ # Local imports. from .context import Context # We get the state factories from the context's environment. state_factories = context.environment[Context.STATE_FACTORIES] for state_factory in state_factories: state = state_factory.get_state_to_bind(obj, name, context) if state is not None: break else: state = obj return state def get_object_instance(self, info, name, context): """Creates an object using the specified state information. The naming manager asks the context for its list of OBJECT factories and calls them one by one until it gets a non-None result, indicating that the factory recognised the information and created an object. If none of the factories recognize the state information (or if the context has no factories) then the state information itself is returned. """ # Local imports. from .context import Context # We get the object factories from the context's environment. object_factories = context.environment[Context.OBJECT_FACTORIES] for object_factory in object_factories: obj = object_factory.get_object_instance(info, name, context) if obj is not None: break else: obj = info return obj # Singleton instance. naming_manager = NamingManager() apptools-5.1.0/apptools/naming/api.py0000644000076500000240000000461313777642667020257 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for the apptools.naming subpackage. - :class:`~.Address` - :class:`~.Binding` - :class:`~.Context` - :class:`~.DynamicContext` - :class:`~.DirContext` - :func:`~.InitialContext` - :class:`~.InitialContextFactory` - :class:`~.NamingEvent` - :attr:`~.naming_manager` - :class:`~.ObjectFactory` - :class:`~.ObjectSerializer` - :class:`~.PyContext` - :class:`~.PyObjectFactory` - :class:`~.PyFSContext` - :class:`~.PyFSContextFactory` - :class:`~.PyFSInitialContextFactory` - :class:`~.PyFSObjectFactory` - :class:`~.PyFSStateFactory` - :class:`~.Reference` - :class:`~.Referenceable` - :class:`~.ReferenceableStateFactory` - :class:`~.StateFactory` Custom Exceptions ----------------- - :class:`~.NamingError` - :class:`~.InvalidNameError` - :class:`~.NameAlreadyBoundError` - :class:`~.NameNotFoundError` - :class:`~.NotContextError` - :class:`~.OperationNotSupportedError` """ from .exception import NamingError, InvalidNameError, NameAlreadyBoundError from .exception import NameNotFoundError, NotContextError from .exception import OperationNotSupportedError from .address import Address from .binding import Binding from .context import Context from .dynamic_context import DynamicContext from .dir_context import DirContext from .initial_context import InitialContext from .initial_context_factory import InitialContextFactory from .naming_event import NamingEvent from .naming_manager import naming_manager from .object_factory import ObjectFactory from .object_serializer import ObjectSerializer from .py_context import PyContext from .py_object_factory import PyObjectFactory from .pyfs_context import PyFSContext from .pyfs_context_factory import PyFSContextFactory from .pyfs_initial_context_factory import PyFSInitialContextFactory from .pyfs_object_factory import PyFSObjectFactory from .pyfs_state_factory import PyFSStateFactory from .reference import Reference from .referenceable import Referenceable from .referenceable_state_factory import ReferenceableStateFactory from .state_factory import StateFactory apptools-5.1.0/apptools/naming/state_factory.py0000644000076500000240000000232513777642667022353 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for all state factories. """ # Enthought library imports. from traits.api import HasPrivateTraits class StateFactory(HasPrivateTraits): """The base class for all state factories. A state factory accepts an object and returns some data representing the object that is suitable for storing in a particular context. """ ########################################################################### # 'StateFactory' interface. ########################################################################### def get_state_to_bind(self, obj, name, context): """Returns the state of an object for binding. Returns None if the factory cannot create the state (ie. it does not recognise the object passed to it). """ raise NotImplementedError apptools-5.1.0/apptools/naming/context.py0000644000076500000240000005346213777642667021200 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for all naming contexts. """ # Enthought library imports. from traits.api import Any, Dict, Event, HasTraits from traits.api import Property, Str # Local imports. from .binding import Binding from .exception import InvalidNameError, NameAlreadyBoundError from .exception import NameNotFoundError, NotContextError from .naming_event import NamingEvent from .naming_manager import naming_manager from .unique_name import make_unique_name # Constants for environment property keys. INITIAL_CONTEXT_FACTORY = "apptools.naming.factory.initial" OBJECT_FACTORIES = "apptools.naming.factory.object" STATE_FACTORIES = "apptools.naming.factory.state" # The default environment. ENVIRONMENT = { # 'Context' properties. OBJECT_FACTORIES: [], STATE_FACTORIES: [], } class Context(HasTraits): """ The base class for all naming contexts. """ # Keys for environment properties. INITIAL_CONTEXT_FACTORY = INITIAL_CONTEXT_FACTORY OBJECT_FACTORIES = OBJECT_FACTORIES STATE_FACTORIES = STATE_FACTORIES #### 'Context' interface ################################################## # The naming environment in effect for this context. environment = Dict(ENVIRONMENT) # The name of the context within its own namespace. namespace_name = Property(Str) #### Events #### # Fired when an object has been added to the context (either via 'bind' or # 'create_subcontext'). object_added = Event(NamingEvent) # Fired when an object has been changed (via 'rebind'). object_changed = Event(NamingEvent) # Fired when an object has been removed from the context (either via # 'unbind' or 'destroy_subcontext'). object_removed = Event(NamingEvent) # Fired when an object in the context has been renamed (via 'rename'). object_renamed = Event(NamingEvent) # Fired when the contents of the context have changed dramatically. context_changed = Event(NamingEvent) #### Protected 'Context' interface ####################################### # The bindings in the context. _bindings = Dict(Str, Any) ########################################################################### # 'Context' interface. ########################################################################### #### Properties ########################################################### def _get_namespace_name(self): """ Return the name of the context within its own namespace. That is the full-path, through the namespace this context participates in, to get to this context. For example, if the root context of the namespace was called 'Foo', and there was a subcontext of that called 'Bar', and we were within that and called 'Baz', then this should return 'Foo/Bar/Baz'. """ # FIXME: We'd like to raise an exception and force implementors to # decide what to do. However, it appears to be pretty common that # most Context implementations do not override this method -- possibly # because the comments aren't clear on what this is supposed to be? # # Anyway, if we raise an exception then it is impossible to use any # evaluations when building a Traits UI for a Context. That is, the # Traits UI can't include items that have a 'visible_when' or # 'enabled_when' evaluation. This is because the Traits evaluation # code calls the 'get()' method on the Context which attempts to # retrieve the current namespace_name value. # raise OperationNotSupportedError() return "" #### Methods ############################################################## def bind(self, name, obj, make_contexts=False): """Binds a name to an object. If 'make_contexts' is True then any missing intermediate contexts are created automatically. """ if len(name) == 0: raise InvalidNameError("empty name") # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] # Is the name already bound? if self._is_bound(atom): raise NameAlreadyBoundError(name) # Do the actual bind. self._bind(atom, obj) # Trait event notification. self.object_added = NamingEvent( new_binding=Binding(name=name, obj=obj, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): if make_contexts: self._create_subcontext(components[0]) else: raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.bind("/".join(components[1:]), obj, make_contexts) def rebind(self, name, obj, make_contexts=False): """Binds an object to a name that may already be bound. If 'make_contexts' is True then any missing intermediate contexts are created automatically. The object may be a different object but may also be the same object that is already bound to the specified name. The name may or may not be already used. Think of this as a safer version of 'bind' since this one will never raise an exception regarding a name being used. """ if len(name) == 0: raise InvalidNameError("empty name") # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: # Do the actual rebind. self._rebind(components[0], obj) # Trait event notification. self.object_changed = NamingEvent( new_binding=Binding(name=name, obj=obj, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): if make_contexts: self._create_subcontext(components[0]) else: raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.rebind("/".join(components[1:]), obj, make_contexts) def unbind(self, name): """ Unbinds a name. """ if len(name) == 0: raise InvalidNameError("empty name") # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Lookup the object that we are unbinding to use in the event # notification. obj = self._lookup(atom) # Do the actual unbind. self._unbind(atom) # Trait event notification. self.object_removed = NamingEvent( old_binding=Binding(name=name, obj=obj, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.unbind("/".join(components[1:])) def rename(self, old_name, new_name): """ Binds a new name to an object. """ if len(old_name) == 0 or len(new_name) == 0: raise InvalidNameError("empty name") # Parse the names. old_components = self._parse_name(old_name) new_components = self._parse_name(new_name) # If there is axactly one component in BOTH names then the operation # takes place ENTIRELY in this context. if len(old_components) == 1 and len(new_components) == 1: # Is the old name actually bound? if not self._is_bound(old_name): raise NameNotFoundError(old_name) # Is the new name already bound? if self._is_bound(new_name): raise NameAlreadyBoundError(new_name) # Do the actual rename. self._rename(old_name, new_name) # Lookup the object that we are renaming to use in the event # notification. obj = self._lookup(new_name) # Trait event notification. self.object_renamed = NamingEvent( old_binding=Binding(name=old_name, obj=obj, context=self), new_binding=Binding(name=new_name, obj=obj, context=self), ) else: # fixme: This really needs to be transactional in case the bind # succeeds but the unbind fails. To be safe should we just not # support cross-context renaming for now?!?! # # Lookup the object. obj = self.lookup(old_name) # Bind the new name. self.bind(new_name, obj) # Unbind the old one. self.unbind(old_name) def lookup(self, name): """ Resolves a name relative to this context. """ # If the name is empty we return the context itself. if len(name) == 0: # fixme: The JNDI spec. says that this should return a COPY of # the context. return self # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual lookup. obj = self._lookup(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) obj = next_context.lookup("/".join(components[1:])) return obj # fixme: Non-JNDI def lookup_binding(self, name): """ Looks up the binding for a name relative to this context. """ if len(name) == 0: raise InvalidNameError("empty name") # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual lookup. binding = self._lookup_binding(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) binding = next_context.lookup_binding("/".join(components[1:])) return binding # fixme: Non-JNDI def lookup_context(self, name): """Resolves a name relative to this context. The name MUST resolve to a context. """ # If the name is empty we return the context itself. if len(name) == 0: # fixme: The JNDI spec. says that this should return a COPY of # the context. return self # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual lookup. obj = self._get_next_context(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) obj = next_context.lookup("/".join(components[1:])) return obj def create_subcontext(self, name): """ Creates a sub-context. """ if len(name) == 0: raise InvalidNameError("empty name") # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] # Is the name already bound? if self._is_bound(atom): raise NameAlreadyBoundError(name) # Do the actual creation of the sub-context. sub = self._create_subcontext(atom) # Trait event notification. self.object_added = NamingEvent( new_binding=Binding(name=name, obj=sub, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) sub = next_context.create_subcontext("/".join(components[1:])) return sub def destroy_subcontext(self, name): """ Destroys a sub-context. """ if len(name) == 0: raise InvalidNameError("empty name") # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) obj = self._lookup(atom) if not self._is_context(atom): raise NotContextError(name) # Do the actual destruction of the sub-context. self._destroy_subcontext(atom) # Trait event notification. self.object_removed = NamingEvent( old_binding=Binding(name=name, obj=obj, context=self) ) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) next_context.destroy_subcontext("/".join(components[1:])) # fixme: Non-JNDI def get_unique_name(self, prefix): """Returns a name that is unique within the context. The name returned will start with the specified prefix. """ return make_unique_name( prefix, existing=self.list_names(""), format="%s (%d)" ) def list_names(self, name=""): """ Lists the names bound in a context. """ # If the name is empty then the operation takes place in this context. if len(name) == 0: names = self._list_names() # Otherwise, attempt to continue resolution into the next context. else: # Parse the name. components = self._parse_name(name) if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) names = next_context.list_names("/".join(components[1:])) return names def list_bindings(self, name=""): """ Lists the bindings in a context. """ # If the name is empty then the operation takes place in this context. if len(name) == 0: bindings = self._list_bindings() # Otherwise, attempt to continue resolution into the next context. else: # Parse the name. components = self._parse_name(name) if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) bindings = next_context.list_bindings("/".join(components[1:])) return bindings # fixme: Non-JNDI def is_context(self, name): """ Returns True if the name is bound to a context. """ # If the name is empty then it refers to this context. if len(name) == 0: is_context = True else: # Parse the name. components = self._parse_name(name) # If there is exactly one component in the name then the operation # takes place in this context. if len(components) == 1: atom = components[0] if not self._is_bound(atom): raise NameNotFoundError(name) # Do the actual check. is_context = self._is_context(atom) # Otherwise, attempt to continue resolution into the next context. else: if not self._is_bound(components[0]): raise NameNotFoundError(components[0]) next_context = self._get_next_context(components[0]) is_context = next_context.is_context("/".join(components[1:])) return is_context # fixme: Non-JNDI def search(self, obj): """ Returns a list of namespace names that are bound to obj. """ # don't look for None if obj is None: return [] # Obj is bound to these names relative to this context names = [] # path contain the name components down to the current context path = [] self._search(obj, names, path, {}) return names ########################################################################### # Protected 'Context' interface. ########################################################################### def _parse_name(self, name): """Parse a name into a list of components. e.g. 'foo/bar/baz' -> ['foo', 'bar', 'baz'] """ return name.split("/") def _is_bound(self, name): """ Is a name bound in this context? """ return name in self._bindings def _lookup(self, name): """ Looks up a name in this context. """ obj = self._bindings[name] return naming_manager.get_object_instance(obj, name, self) def _lookup_binding(self, name): """ Looks up the binding for a name in this context. """ return Binding(name=name, obj=self._lookup(name), context=self) def _bind(self, name, obj): """ Binds a name to an object in this context. """ state = naming_manager.get_state_to_bind(obj, name, self) self._bindings[name] = state def _rebind(self, name, obj): """ Rebinds a name to an object in this context. """ self._bind(name, obj) def _unbind(self, name): """ Unbinds a name from this context. """ del self._bindings[name] def _rename(self, old_name, new_name): """ Renames an object in this context. """ # Bind the new name. self._bindings[new_name] = self._bindings[old_name] # Unbind the old one. del self._bindings[old_name] def _create_subcontext(self, name): """ Creates a sub-context of this context. """ sub = self.__class__(environment=self.environment) self._bindings[name] = sub return sub def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ del self._bindings[name] def _list_bindings(self): """ Lists the bindings in this context. """ bindings = [] for name in self._list_names(): bindings.append( Binding(name=name, obj=self._lookup(name), context=self) ) return bindings def _list_names(self): """ Lists the names bound in this context. """ return list(self._bindings.keys()) def _is_context(self, name): """ Returns True if a name is bound to a context. """ return self._get_next_context(name) is not None def _get_next_context(self, name): """ Returns the next context. """ obj = self._lookup(name) # If the object is a context then everything is just dandy. if isinstance(obj, Context): next_context = obj else: raise NotContextError(name) return next_context def _search(self, obj, names, path, searched): """Append to names any name bound to obj. Join path and name with '/' to for a complete name from the top context. """ # Check the bindings recursively. for binding in self.list_bindings(): if binding.obj is obj: path.append(binding.name) names.append("/".join(path)) path.pop() if ( isinstance(binding.obj, Context) and binding.obj not in searched ): path.append(binding.name) searched[binding.obj] = True binding.obj._search(obj, names, path, searched) path.pop() apptools-5.1.0/apptools/naming/py_context.py0000644000076500000240000001317413777642667021704 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 naming context for a Python namespace. """ # Enthought library imports. from traits.api import Any, Dict, Instance, Property # Local imports. from .address import Address from .binding import Binding from .context import Context from .naming_manager import naming_manager from .py_object_factory import PyObjectFactory from .reference import Reference from .referenceable import Referenceable from .referenceable_state_factory import ReferenceableStateFactory # The default environment. ENVIRONMENT = { # 'Context' properties. Context.OBJECT_FACTORIES: [PyObjectFactory()], Context.STATE_FACTORIES: [ReferenceableStateFactory()], } class PyContext(Context, Referenceable): """ A naming context for a Python namespace. """ #### 'Context' interface ################################################## # The naming environment in effect for this context. environment = Dict(ENVIRONMENT) #### 'PyContext' interface ################################################ # The Python namespace that we represent. namespace = Any # If the namespace is actual a Python object that has a '__dict__' # attribute, then this will be that object (the namespace will be the # object's '__dict__'. obj = Any #### 'Referenceable' interface ############################################ # The object's reference suitable for binding in a naming context. reference = Property(Instance(Reference)) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Creates a new context. """ # Base class constructor. super(PyContext, self).__init__(**traits) if type(self.namespace) is not dict: if hasattr(self.namespace, "__dict__"): self.obj = self.namespace self.namespace = self.namespace.__dict__ else: raise ValueError("Need a dictionary or a __dict__ attribute") ########################################################################### # 'Referenceable' interface. ########################################################################### #### Properties ########################################################### def _get_reference(self): """ Returns a reference to this object suitable for binding. """ reference = Reference( class_name=self.__class__.__name__, addresses=[Address(type="py_context", content=self.namespace)], ) return reference ########################################################################### # Protected 'Context' interface. ########################################################################### def _is_bound(self, name): """ Is a name bound in this context? """ return name in self.namespace def _lookup(self, name): """ Looks up a name in this context. """ obj = self.namespace[name] return naming_manager.get_object_instance(obj, name, self) def _bind(self, name, obj): """ Binds a name to an object in this context. """ state = naming_manager.get_state_to_bind(obj, name, self) self.namespace[name] = state def _rebind(self, name, obj): """ Rebinds a name to a object in this context. """ self._bind(name, obj) def _unbind(self, name): """ Unbinds a name from this context. """ del self.namespace[name] # Trait event notification. self.trait_property_changed("context_changed", None, None) def _rename(self, old_name, new_name): """ Renames an object in this context. """ state = self.namespace[old_name] # Bind the new name. self.namespace[new_name] = state # Unbind the old one. del self.namespace[old_name] # Trait event notification. self.context_changed = True def _create_subcontext(self, name): """ Creates a sub-context of this context. """ sub = self._context_factory(name, {}) self.namespace[name] = sub # Trait event notification. self.trait_property_changed("context_changed", None, None) return sub def _destroy_subcontext(self, name): """ Destroys a sub-context of this context. """ del self.namespace[name] # Trait event notification. self.trait_property_changed("context_changed", None, None) def _list_bindings(self): """ Lists the bindings in this context. """ bindings = [] for name, value in self.namespace.items(): bindings.append( Binding(name=name, obj=self._lookup(name), context=self) ) return bindings def _list_names(self): """ Lists the names bound in this context. """ return list(self.namespace.keys()) ########################################################################### # Private interface. ########################################################################### def _context_factory(self, name, namespace): """ Create a sub-context. """ return self.__class__(namespace=namespace) apptools-5.1.0/apptools/naming/pyfs_object_factory.py0000644000076500000240000000235013777642667023540 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Object factory for Python File System contexts. """ # Enthought library imports. from apptools.io.api import File # Local imports. from .object_factory import ObjectFactory from .reference import Reference class PyFSObjectFactory(ObjectFactory): """ Object factory for Python File System contexts. """ ########################################################################### # 'ObjectFactory' interface. ########################################################################### def get_object_instance(self, state, name, context): """ Creates an object using the specified state information. """ obj = None if isinstance(state, Reference): if state.class_name == "File" and len(state.addresses) > 0: obj = File(state.addresses[0].content) return obj apptools-5.1.0/apptools/naming/pyfs_state_factory.py0000644000076500000240000000273613777642667023422 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ State factory for Python File System contexts. """ # Enthought library imports. from apptools.io.api import File # Local imports. from .address import Address from .reference import Reference from .state_factory import StateFactory class PyFSStateFactory(StateFactory): """ State factory for Python File System contexts. """ ########################################################################### # 'StateFactory' interface. ########################################################################### def get_state_to_bind(self, obj, name, context): """ Returns the state of an object for binding. """ state = None if isinstance(obj, File): # If the file is not actually in the directory represented by the # context then we create and bind a reference to it. if obj.parent.path != context.path: state = Reference( class_name=obj.__class__.__name__, addresses=[Address(type="file", content=obj.path)], ) return state apptools-5.1.0/apptools/naming/reference.py0000644000076500000240000000307213777642667021442 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 reference to an object that lives outside of the naming system. """ # Enthought library imports. from traits.api import HasPrivateTraits, List, Str # Local imports. from .address import Address class Reference(HasPrivateTraits): """A reference to an object that lives outside of the naming system. References provide a way to store the address(s) of objects that live outside of the naming system. A reference consists of a list of addresses that represent a communications endpoint for the object being referenced. A reference also contains information to assist in the creation of an instance of the object to which it refers. It contains the name of the class that will be created and the class name and location of a factory that will be used to do the actual instance creation. """ #### 'Reference' interface ################################################ # The list of addresses that can be used to 'contact' the object. addresses = List(Address) # The class name of the object that this reference refers to. class_name = Str # The class name of the object factory. factory_class_name = Str apptools-5.1.0/apptools/naming/initial_context.py0000644000076500000240000000352313777642667022702 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 starting point for performing naming operations. """ # Local imports. from .context import Context def InitialContext(environment): """ Creates an initial context for beginning name resolution. """ # Get the class name of the factory that will produce the initial context. klass_name = environment.get(Context.INITIAL_CONTEXT_FACTORY) if klass_name is None: raise ValueError("No initial context factory specified") # Import the factory class. klass = _import_symbol(klass_name) # Create the factory. factory = klass() # Ask the factory for a context implementation instance. return factory.get_initial_context(environment) # fixme: This is the same code as in the Envisage import manager but we don't # want naming to be dependent on Envisage, so we need some other package # for useful 'Python' tools etc. def _import_symbol(symbol_path): """Imports the symbol defined by 'symbol_path'. 'symbol_path' is a string in the form 'foo.bar.baz' which is turned into an import statement 'from foo.bar import baz' (ie. the last component of the name is the symbol name, the rest is the package/ module path to load it from). """ components = symbol_path.split(".") module_name = ".".join(components[:-1]) symbol_name = components[-1] module = __import__(module_name, globals(), locals(), [symbol_name]) symbol = getattr(module, symbol_name) return symbol apptools-5.1.0/apptools/naming/object_factory.py0000644000076500000240000000233513777642667022502 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for all object factories. """ # Enthought library imports. from traits.api import HasTraits class ObjectFactory(HasTraits): """The base class for all object factories. An object factory accepts some information about how to create an object (such as a reference) and returns an instance of that object. """ ########################################################################### # 'ObjectFactory' interface. ########################################################################### def get_object_instance(self, state, name, context): """Creates an object using the specified state information. Returns None if the factory cannot create the object (ie. it does not recognise the state passed to it). """ raise NotImplementedError apptools-5.1.0/apptools/naming/naming_event.py0000644000076500000240000000146113777642667022156 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 event fired by the tree model when it changes. """ # Enthought library imports. from traits.api import HasTraits, Instance # Local imports. from .binding import Binding # Classes for event traits. class NamingEvent(HasTraits): """ Information about tree model changes. """ # The old binding. old_binding = Instance(Binding) # The new binding. new_binding = Instance(Binding) apptools-5.1.0/apptools/naming/binding.py0000644000076500000240000000617413777642667021124 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 representation of a name-to-object binding in a context. """ # Enthought libary imports. from traits.api import Any, HasTraits, Property, Str class Binding(HasTraits): """ The representation of a name-to-object binding in a context. """ #### 'Binding' interface ################################################## # The class name of the object bound to the name in the binding. class_name = Property(Str) # The name. name = Str # The object bound to the name in the binding. obj = Any #### Experimental 'Binding' interface ##################################### # fixme: These is not part of the JNDI spec, but they do seem startlingly # useful! # # fixme: And, errr, startlingly broken! If the context that the binding # is in is required then just look up the name minus the last component! # # The context that the binding came from. context = Any # The name of the bound object within the namespace. namespace_name = Property(Str) #### 'Private' interface ################################################## # Shadow trait for the 'class_name' property. _class_name = Str ########################################################################### # 'object' interface. ########################################################################### def __str__(self): """ Returns an informal string representation of the object. """ return super(Binding, self).__str__() + "(name=%s, obj=%s)" % ( self.name, self.obj, ) ########################################################################### # 'Binding' interface. ########################################################################### #### Properties ########################################################### # class_name def _get_class_name(self): """ Returns the class name of the object. """ if len(self._class_name) == 0: if self.obj is None: class_name = None else: klass = self.obj.__class__ class_name = "%s.%s" % (klass.__module__, klass.__name__) return class_name def _set_class_name(self, class_name): """ Sets the class name of the object. """ self._class_name = class_name # namespace_name def _get_namespace_name(self): """ Returns the name of the context within its own namespace. """ if self.context is not None: base = self.context.namespace_name else: base = "" if len(base) > 0: namespace_name = base + "/" + self.name else: namespace_name = self.name return namespace_name apptools-5.1.0/apptools/naming/dynamic_context.py0000644000076500000240000001471413777642667022701 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Provider of a framework that dynamically determines the contents of a context at the time of interaction with the contents rather than at the time a class is written. This capability is particularly useful when the object acting as a context is part of a plug-in application -- such as Envisage. In general, this capability allows the context to be: - Extendable by contributions from somewhere other than the original code writer - Dynamic in that the elements it is composed of can change each time someone interacts with the contents of the context. It should be noted that this capability is explicitly different from contexts that look at another container to determine their contents, such as a file system context! Users of this framework contribute items to a dynamic context by adding traits to the dynamic context instance. (This addition can happen statically through the use of a Traits Category.) The trait value is the context item's value and the trait definition's metadata determines how the item is treated within the context. The support metadata is: context_name: A non-empty string Represents the name of the item within this context. This must be present for the trait to show up as a context item though the value may change over time as the item gets bound to different names. context_order: A float value Indicates the position for the item within this context. All dynamically contributed context items are sorted by ascending order of this value using the standard list sort function. is_context: A boolean value True if the item is itself a context. """ # Standardlibrary imports import logging # Local imports from .binding import Binding from .context import Context from .exception import OperationNotSupportedError # Setup a logger for this module. logger = logging.getLogger(__name__) class DynamicContext(Context): """A framework that dynamically determines the contents of a context at the time of interaction with the contents rather than at the time a context class is written. It should be noted that this capability is explicitly different from contexts that look at another container to determine their contents, such as a file system context! """ ########################################################################## # 'Context' interface. ########################################################################## ### protected interface ################################################## def _is_bound(self, name): """Is a name bound in this context?""" item = self._get_contributed_context_item(name) result = item != (None, None) return result def _is_context(self, name): """Returns True if a name is bound to a context.""" item = self._get_contributed_context_item(name) if item != (None, None): obj, trait = item result = trait.is_context is True else: result = False return result def _list_bindings(self): """Lists the bindings in this context.""" result = [ Binding(name=n, obj=o, context=self) for n, o, t in self._get_contributed_context_items() ] return result def _list_names(self): """Lists the names bound in this context.""" result = [n for n, o, t in self._get_contributed_context_items()] return result def _lookup(self, name): """Looks up a name in this context.""" item = self._get_contributed_context_item(name) if item != (None, None): obj, trait = item result = obj else: result = None return result def _rename(self, old_name, new_name): """Renames an object in this context.""" item = self._get_contributed_context_item(old_name) if item != (None, None): obj, trait = item trait.context_name = new_name else: raise ValueError('Name "%s" not in context', old_name) def _unbind(self, name): """Unbinds a name from this context.""" # It is an error to try to unbind any contributed context items item = self._get_contributed_context_item(name) if item != (None, None): raise OperationNotSupportedError( "Unable to unbind " + "built-in with name [%s]" % name ) ########################################################################## # 'DynamicContext' interface. ########################################################################## ### protected interface ################################################## def _get_contributed_context_item(self, name): """If the specified name matches a contributed context item then returns a tuple of the item's current value and trait definition (in that order.) Otherwise, returns a tuple of (None, None). """ result = (None, None) for n, o, t in self._get_contributed_context_items(): if n == name: result = (o, t) return result def _get_contributed_context_items(self): """Returns an ordered list of items to be treated as part of our context. Each item in the list is a tuple of its name, object, and trait definition (in that order.) """ # Our traits that get treated as context items are those that declare # themselves via metadata on the trait definition. filter = {"context_name": lambda v: v is not None and len(v) > 0} traits = self.traits(**filter) # Sort the list of context items according to the name of the item. traits = [(t.context_order, n, t) for n, t in traits.items()] traits.sort() # Convert these trait definitions into a list of name and object tuples result = [ (t.context_name, getattr(self, n), t) for order, n, t in traits ] return result apptools-5.1.0/apptools/persistence/0000755000076500000240000000000013777643025020170 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/persistence/tests/0000755000076500000240000000000013777643025021332 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/persistence/tests/test_two_stage_unpickler.py0000644000076500000240000001053413777642667027031 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 was previously a test for the now deleted apptools.sweet_pickle sub package. It is included here to showcase how apptools.persistance can be used to replace sweet_pickle functionality. """ import io import random import re import pickle import unittest from apptools.persistence.versioned_unpickler import VersionedUnpickler ######################################## # Usecase1: generic case class A(object): def __init__(self, b=None): self.x = 0 self.set_b(b) def set_b(self, b): self.b_ref = b if b and hasattr(b, "y"): self.x = b.y def __setstate__(self, state): self.__dict__.update(state) self.set_b(self.b_ref) def __initialize__(self): while self.b_ref is None and self.b_ref.y != 0: yield True self.set_b(self.b_ref) class B(object): def __init__(self, a=None): self.y = 0 self.set_a(a) def __getstate__(self): state = self.__dict__.copy() del state["y"] return state def __setstate__(self, state): self.__dict__.update(state) self.set_a(self.a_ref) def set_a(self, a): self.a_ref = a if a and hasattr(a, "x"): self.y = a.x def __initialize__(self): while self.a_ref is None and self.a_ref.x != 0: yield True self.set_a(self.a_ref) class GenericTestCase(unittest.TestCase): def test_generic(self): a = A() b = B() a.x = random.randint(1, 100) b.set_a(a) a.set_b(b) value = a.x # This will fail, even though we have a __setstate__ method. s = pickle.dumps(a) new_a = pickle.loads(s) # Accessing new_a.x is okay new_a.x # Accessing y directly would fail with self.assertRaisesRegex( AttributeError, "'B' object has no attribute 'y'"): new_a.b_ref.y # This will work! s = pickle.dumps(a) new_a = VersionedUnpickler(io.BytesIO(s)).load() assert new_a.x == new_a.b_ref.y == value ######################################## # Usecase2: Toy Application class StringFinder(object): def __init__(self, source, pattern): self.pattern = pattern self.source = source self.data = [] def __getstate__(self): s = self.__dict__.copy() del s["data"] return s def __initialize__(self): while not self.source.initialized: yield True self.find() def find(self): pattern = self.pattern string = self.source.data self.data = [ (x.start(), x.end()) for x in re.finditer(pattern, string) ] class XMLFileReader(object): def __init__(self, file_name): self.data = "" self.initialized = False self.file_name = file_name self.read() def __getstate__(self): s = self.__dict__.copy() del s["data"] del s["initialized"] return s def __setstate__(self, state): self.__dict__.update(state) self.read() def read(self): # Make up random data from the filename data = [10 * x for x in self.file_name] random.shuffle(data) self.data = " ".join(data) self.initialized = True class Application(object): def __init__(self): self.reader = XMLFileReader("some_test_file.xml") self.finder = StringFinder(self.reader, "e") def get(self): return (self.finder.data, self.reader.data) class ToyAppTestCase(unittest.TestCase): def test_toy_app(self): a = Application() a.finder.find() a.get() s = pickle.dumps(a) b = pickle.loads(s) with self.assertRaisesRegex( AttributeError, "'StringFinder' object has no attribute 'data'"): b.get() # Works fine. c = VersionedUnpickler(io.BytesIO(s)).load() c.get() apptools-5.1.0/apptools/persistence/tests/test_class_mapping.py0000644000076500000240000000513513777642667025602 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 the class mapping functionality of the enthought.pickle framework. """ # Standard library imports. import io import pickle import unittest # Enthought library imports from apptools.persistence.versioned_unpickler import VersionedUnpickler from apptools.persistence.updater import Updater ############################################################################## # Classes to use within the tests ############################################################################## class Foo: pass class Bar: pass class Baz: pass ############################################################################## # class 'ClassMappingTestCase' ############################################################################## class ClassMappingTestCase(unittest.TestCase): """Originally tests for the class mapping functionality of the now deleted apptools.sweet_pickle framework, converted to use apptools.persistence. """ ########################################################################## # 'TestCase' interface ########################################################################## ### public interface ##################################################### def test_unpickled_class_mapping(self): class TestUpdater(Updater): def __init__(self): self.refactorings = { (Foo.__module__, Foo.__name__): (Bar.__module__, Bar.__name__), (Bar.__module__, Bar.__name__): (Baz.__module__, Baz.__name__), } self.setstates = {} # Validate that unpickling the first class gives us an instance of # the second class. start = Foo() test_file = io.BytesIO(pickle.dumps(start, 2)) end = VersionedUnpickler(test_file, updater=TestUpdater()).load() self.assertIsInstance(end, Bar) # Validate that unpickling the second class gives us an instance of # the third class. start = Bar() test_file = io.BytesIO(pickle.dumps(start, 2)) end = VersionedUnpickler(test_file, updater=TestUpdater()).load() self.assertIsInstance(end, Baz) apptools-5.1.0/apptools/persistence/tests/test_version_registry.py0000644000076500000240000001004013777642667026366 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 version registry. """ # Standard library imports. from importlib import reload import unittest # Enthought library imports. from traits.api import HasTraits from apptools._testing.optional_dependencies import ( numpy as np, requires_numpy, ) if np is not None: from apptools.persistence import version_registry, state_pickler class Classic: __version__ = 0 class New(object): __version__ = 0 class TraitClass(HasTraits): __version__ = 0 class Test(New): __version__ = 1 def __init__(self): self.a = Classic() class Handler: def __init__(self): self.calls = [] def upgrade(self, state, version): self.calls.append(("upgrade", state, version)) def upgrade1(self, state, version): self.calls.append(("upgrade1", state, version)) @requires_numpy class TestVersionRegistry(unittest.TestCase): def test_get_version(self): """Test the get_version function.""" extra = [(("object", "builtins"), -1)] c = Classic() v = version_registry.get_version(c) res = extra + [(("Classic", __name__), 0)] self.assertEqual(v, res) state = state_pickler.get_state(c) self.assertEqual(state.__metadata__["version"], res) n = New() v = version_registry.get_version(n) res = extra + [(("New", __name__), 0)] self.assertEqual(v, res) state = state_pickler.get_state(n) self.assertEqual(state.__metadata__["version"], res) t = TraitClass() v = version_registry.get_version(t) res = extra + [ (("CHasTraits", "traits.ctraits"), -1), (("HasTraits", "traits.has_traits"), -1), (("TraitClass", __name__), 0), ] self.assertEqual(v, res) state = state_pickler.get_state(t) self.assertEqual(state.__metadata__["version"], res) def test_reload(self): """Test if the registry is reload safe.""" # A dummy handler. def h(x, y): pass registry = version_registry.registry registry.register("A", __name__, h) self.assertEqual(registry.handlers.get(("A", __name__)), h) reload(version_registry) registry = version_registry.registry self.assertEqual(registry.handlers.get(("A", __name__)), h) del registry.handlers[("A", __name__)] self.assertNotIn(("A", __name__), registry.handlers) def test_update(self): """Test if update method calls the handlers in order.""" registry = version_registry.registry # First an elementary test. c = Classic() state = state_pickler.get_state(c) h = Handler() registry.register("Classic", __name__, h.upgrade) c1 = state_pickler.create_instance(state) state_pickler.set_state(c1, state) self.assertEqual(h.calls, [("upgrade", state, 0)]) # Remove the handler. registry.unregister("Classic", __name__) # Now check to see if this works for inheritance trees. t = Test() state = state_pickler.get_state(t) h = Handler() registry.register("Classic", __name__, h.upgrade) registry.register("New", __name__, h.upgrade) registry.register("Test", __name__, h.upgrade1) t1 = state_pickler.create_instance(state) state_pickler.set_state(t1, state) # This should call New handler, then the Test and then # Classic. self.assertEqual( h.calls, [ ("upgrade", state, 0), ("upgrade1", state, 1), ("upgrade", state.a, 0), ], ) apptools-5.1.0/apptools/persistence/tests/__init__.py0000644000076500000240000000062713777642667023463 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/persistence/tests/state_function_classes.py0000644000076500000240000000314513777642667026464 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Standard library imports import logging # Enthought library imports from traits.api import Bool, Float, HasTraits, Int, Str logger = logging.getLogger(__name__) ############################################################################## # Classes to use within the tests ############################################################################## class Foo(HasTraits): _enthought_pickle_version = Int(1) b1 = Bool(False) f1 = Float(1) i1 = Int(1) s1 = Str("foo") class Bar(HasTraits): _enthought_pickle_version = Int(2) b2 = Bool(True) f2 = Float(2) i2 = Int(2) s2 = Str("bar") class Baz(HasTraits): _enthought_pickle_version = Int(3) b3 = Bool(False) f3 = Float(3) i3 = Int(3) s3 = Str("baz") def __setstate__(self, state): logger.debug("Running Baz's original __setstate__") if state["_enthought_pickle_version"] < 3: info = [("b2", "b3"), ("f2", "f3"), ("i2", "i3"), ("s2", "s3")] for old, new in info: if old in state: state[new] = state[old] del state[old] state["_enthought_pickle_version"] = 3 self.__dict__.update(state) apptools-5.1.0/apptools/persistence/tests/test_file_path.py0000644000076500000240000001033713777642667024715 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 file_path module. """ # Standard library imports. import unittest import os import sys from os.path import abspath, dirname, basename, join from io import BytesIO # 3rd party imports. from importlib_resources import files from apptools._testing.optional_dependencies import ( numpy as np, requires_numpy, ) # Enthought library imports. if np is not None: from apptools.persistence import state_pickler from apptools.persistence import file_path @requires_numpy class Test: def __init__(self): self.f = file_path.FilePath() @requires_numpy class TestFilePath(unittest.TestCase): def setUp(self): # If the cwd is somewhere under /tmp, that confuses the tests below. # Use the directory containing this file, instead. test_cwd = os.fspath(files("apptools.persistence") / "") self.old_cwd = os.getcwd() os.chdir(test_cwd) def tearDown(self): os.chdir(self.old_cwd) def test_relative(self): """Test if relative paths are set correctly.""" fname = "t.vtk" f = file_path.FilePath(fname) cwd = os.getcwd() # Trivial case of both in same dir. f.set_relative(abspath(join(cwd, "t.mv2"))) self.assertEqual(f.rel_pth, fname) # Move one directory deeper. f.set_relative(abspath(join(cwd, "tests", "t.mv2"))) self.assertEqual(f.rel_pth, join(os.pardir, fname)) # Move one directory shallower. f.set_relative(abspath(join(dirname(cwd), "t.mv2"))) diff = basename(cwd) self.assertEqual(f.rel_pth, join(diff, fname)) # Test where the path is relative to the root. f.set(abspath(join("data", fname))) f.set_relative("/tmp/test.mv2") if sys.platform.startswith("win"): expect = os.pardir + abspath(join("data", fname))[2:] else: expect = os.pardir + abspath(join("data", fname)) self.assertEqual(f.rel_pth, expect) def test_absolute(self): """Test if absolute paths are set corectly.""" fname = "t.vtk" f = file_path.FilePath(fname) cwd = os.getcwd() # Easy case of both in same dir. f.set_absolute(join(cwd, "foo", "test", "t.mv2")) self.assertEqual(f.abs_pth, join(cwd, "foo", "test", fname)) # One level lower. fname = join(os.pardir, "t.vtk") f.set(fname) f.set_absolute(join(cwd, "foo", "test", "t.mv2")) self.assertEqual(f.abs_pth, abspath(join(cwd, "foo", "test", fname))) # One level higher. fname = join("test", "t.vtk") f.set(fname) f.set_absolute(join(cwd, "foo", "t.mv2")) self.assertEqual(f.abs_pth, abspath(join(cwd, "foo", fname))) def test_pickle(self): """Test if pickler works correctly with FilePaths.""" t = Test() t.f.set("t.vtk") cwd = os.getcwd() curdir = basename(cwd) # Create a dummy file in the parent dir. s = BytesIO() # Spoof its location. s.name = abspath(join(cwd, os.pardir, "t.mv2")) # Dump into it state_pickler.dump(t, s) # Rewind the stream s.seek(0) # "Move" the file elsewhere s.name = join(cwd, "foo", "test", "t.mv2") state = state_pickler.load_state(s) self.assertEqual( state.f.abs_pth, join(cwd, "foo", "test", curdir, "t.vtk") ) # Create a dummy file in a subdir. s = BytesIO() # Spoof its location. s.name = abspath(join(cwd, "data", "t.mv2")) # Dump into it. state_pickler.dump(t, s) # Rewind the stream s.seek(0) # "Move" the file elsewhere s.name = join(cwd, "foo", "test", "t.mv2") state = state_pickler.load_state(s) self.assertEqual(state.f.abs_pth, join(cwd, "foo", "t.vtk")) apptools-5.1.0/apptools/persistence/tests/test_state_function.py0000644000076500000240000001142613777642667026007 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ These tests were originally for the the state function functionality of the now deleted apptools.sweet_pickle framework. They have been modified here to use apptools.persistence instead. """ # Standard library imports. import io import pickle import unittest import logging # Enthought library imports from apptools.persistence.tests.state_function_classes import Foo, Bar, Baz from apptools.persistence.versioned_unpickler import VersionedUnpickler from apptools.persistence.updater import Updater logger = logging.getLogger(__name__) class TestUpdater(Updater): def __init__(self): self.refactorings = { (Foo.__module__, Foo.__name__): (Bar.__module__, Bar.__name__), (Bar.__module__, Bar.__name__): (Baz.__module__, Baz.__name__), } self.setstates = {} ############################################################################## # class 'StateFunctionTestCase' ############################################################################## class StateFunctionTestCase(unittest.TestCase): """Originally tests for the state function functionality of the now deleted apptools.sweet_pickle framework, converted to use apptools.persistence. """ ########################################################################## # 'TestCase' interface ########################################################################## ### public interface ##################################################### def setUp(self): """Creates the test fixture. Overridden here to ensure each test starts with an empty global registry. """ self.updater = TestUpdater() ########################################################################## # 'StateFunctionTestCase' interface ########################################################################## ### public interface ##################################################### def test_normal_setstate(self): """Validates that only existing setstate methods are called when there are no registered state functions in the class chain. """ # Validate that unpickling the first class gives us an instance of # the second class with the appropriate attribute values. It will have # the default Foo values (because there is no state function to move # them) and also the default Bar values (since they inherit the # trait defaults because nothing overwrote the values.) start = Foo() test_file = io.BytesIO(pickle.dumps(start, 2)) end = VersionedUnpickler(test_file, updater=TestUpdater()).load() self.assertIsInstance(end, Bar) self._assertAttributes(end, 1, (False, 1, 1, "foo")) self._assertAttributes(end, 2, (True, 2, 2, "bar")) self._assertAttributes(end, 3, None) # Validate that unpickling the second class gives us an instance of # the third class with the appropriate attribute values. It will have # only the Baz attributes with the Bar values (since the __setstate__ # on Baz converted the Bar attributes to Baz attributes.) start = Bar() test_file = io.BytesIO(pickle.dumps(start, 2)) end = VersionedUnpickler(test_file, updater=TestUpdater()).load() self.assertIsInstance(end, Baz) self._assertAttributes(end, 2, None) self._assertAttributes(end, 3, (True, 2, 2, "bar")) ### protected interface ################################################## def _assertAttributes(self, obj, suffix, values): """Ensures that the specified object's attributes with the specified suffix have the expected values. If values is None, then the attributes shouldn't exist. """ attributeNames = ["b", "f", "i", "s"] for i in range(len(attributeNames)): name = attributeNames[i] + str(suffix) if values is None: self.assertEqual( False, hasattr(obj, name), "Obj [%s] has attribute [%s]" % (obj, name), ) else: self.assertEqual( values[i], getattr(obj, name), "Obj [%s] attribute [%s] has [%s] instead of [%s]" % (obj, name, values[i], getattr(obj, name)), ) apptools-5.1.0/apptools/persistence/tests/test_state_pickler.py0000644000076500000240000003721613777642667025620 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """Unit tests for the state pickler and unpickler. """ import base64 import pickle import unittest import math import os import tempfile try: import numpy except ImportError: raise unittest.SkipTest("Can't import NumPy: skipping") from traits.api import ( Bool, Int, Array, Float, Complex, Any, Str, Instance, Tuple, List, Dict, HasTraits, ) try: from tvtk.api import tvtk except ImportError: TVTK_AVAILABLE = False else: TVTK_AVAILABLE = True from apptools.persistence import state_pickler # A simple class to test instances. class A(object): def __init__(self): self.a = "a" # NOTE: I think that TVTK specific testing should be moved to the # TVTK package. # A classic class for testing the pickler. class TestClassic: def __init__(self): self.b = False self.i = 7 self.longi = 1234567890123456789 self.f = math.pi self.c = complex(1.01234, 2.3) self.n = None self.s = "String" self.u = "Unicode" self.inst = A() self.tuple = (1, 2, "a", A()) self.list = [1, 1.1, "a", 1j, self.inst] self.pure_list = list(range(5)) self.dict = {"a": 1, "b": 2, "ref": self.inst} self.numeric = numpy.ones((2, 2, 2), "f") self.ref = self.numeric if TVTK_AVAILABLE: self._tvtk = tvtk.Property() # A class with traits for testing the pickler. class TestTraits(HasTraits): b = Bool(False) i = Int(7) longi = Int(12345678901234567890) f = Float(math.pi) c = Complex(complex(1.01234, 2.3)) n = Any s = Str("String") u = Str("Unicode") inst = Instance(A) tuple = Tuple list = List pure_list = List(list(range(5))) dict = Dict numeric = Array(value=numpy.ones((2, 2, 2), "f")) ref = Array if TVTK_AVAILABLE: _tvtk = Instance(tvtk.Property, ()) def __init__(self): self.inst = A() self.tuple = (1, 2, "a", A()) self.list = [1, 1.1, "a", 1j, self.inst] self.dict = {"a": 1, "b": 2, "ref": self.inst} self.ref = self.numeric class TestDictPickler(unittest.TestCase): def set_object(self, obj): """Changes the objects properties to test things.""" obj.b = True obj.i = 8 obj.s = "string" obj.u = "unicode" obj.inst.a = "b" obj.list[0] = 2 obj.tuple[-1].a = "t" obj.dict["a"] = 10 if TVTK_AVAILABLE: obj._tvtk.trait_set( point_size=3, specular_color=(1, 0, 0), representation="w" ) def _check_instance_and_references(self, obj, data): """Asserts that there is one instance and two references in the state. We need this as there isn't a guarantee as to which will be the reference and which will be the instance. """ inst = data["inst"] list_end = data["list"]["data"][-1] dict_ref = data["dict"]["data"]["ref"] all_inst = [inst, list_end, dict_ref] types = [x["type"] for x in all_inst] self.assertEqual(types.count("instance"), 1) self.assertEqual(types.count("reference"), 2) inst_state = all_inst[types.index("instance")] self.assertEqual(inst_state["data"]["data"]["a"], "b") def verify(self, obj, state): data = state["data"] self.assertEqual(state["class_name"], obj.__class__.__name__) data = data["data"] self.assertEqual(data["b"], obj.b) self.assertEqual(data["i"], obj.i) self.assertEqual(data["longi"], obj.longi) self.assertEqual(data["f"], obj.f) self.assertEqual(data["c"], obj.c) self.assertEqual(data["n"], obj.n) self.assertEqual(data["s"], obj.s) self.assertEqual(data["u"], obj.u) tup = data["tuple"]["data"] self.assertEqual(tup[:-1], obj.tuple[:-1]) self.assertEqual(tup[-1]["data"]["data"]["a"], "t") lst = data["list"]["data"] self.assertEqual(lst[:-1], obj.list[:-1]) pure_lst = data["pure_list"]["data"] self.assertEqual(pure_lst, obj.pure_list) dct = data["dict"]["data"] self.assertEqual(dct["a"], obj.dict["a"]) self.assertEqual(dct["b"], obj.dict["b"]) self._check_instance_and_references(obj, data) num_attr = "numeric" if data["numeric"]["type"] == "numeric" else "ref" junk = state_pickler.gunzip_string( base64.decodebytes(data[num_attr]["data"]) ) num = pickle.loads(junk) self.assertEqual(numpy.alltrue(numpy.ravel(num == obj.numeric)), 1) self.assertIn(data["ref"]["type"], ["reference", "numeric"]) if data["ref"]["type"] == "numeric": self.assertEqual(data["numeric"]["type"], "reference") else: self.assertEqual(data["numeric"]["type"], "numeric") self.assertEqual(data["ref"]["id"], data["numeric"]["id"]) def verify_unpickled(self, obj, state): self.assertEqual( state.__metadata__["class_name"], obj.__class__.__name__ ) self.assertEqual(state.b, obj.b) self.assertEqual(state.i, obj.i) self.assertEqual(state.longi, obj.longi) self.assertEqual(state.f, obj.f) self.assertEqual(state.c, obj.c) self.assertEqual(state.n, obj.n) self.assertEqual(state.s, obj.s) self.assertEqual(state.u, obj.u) self.assertEqual(state.inst.__metadata__["type"], "instance") tup = state.tuple self.assertTrue(state.tuple.has_instance) self.assertEqual(tup[:-1], obj.tuple[:-1]) self.assertEqual(tup[-1].a, "t") lst = state.list self.assertTrue(state.list.has_instance) self.assertEqual(lst[:-1], obj.list[:-1]) # Make sure the reference is the same self.assertEqual(id(state.inst), id(lst[-1])) self.assertEqual(lst[-1].a, "b") pure_lst = state.pure_list self.assertEqual(pure_lst, obj.pure_list) self.assertFalse(state.pure_list.has_instance) dct = state.dict self.assertTrue(dct.has_instance) self.assertEqual(dct["a"], obj.dict["a"]) self.assertEqual(dct["b"], obj.dict["b"]) self.assertEqual(dct["ref"].__metadata__["type"], "instance") num = state.numeric self.assertEqual(numpy.alltrue(numpy.ravel(num == obj.numeric)), 1) self.assertEqual(id(state.ref), id(num)) if TVTK_AVAILABLE: _tvtk = state._tvtk self.assertEqual(_tvtk.representation, obj._tvtk.representation) self.assertEqual(_tvtk.specular_color, obj._tvtk.specular_color) self.assertEqual(_tvtk.point_size, obj._tvtk.point_size) def verify_state(self, state1, state): self.assertEqual(state.__metadata__, state1.__metadata__) self.assertEqual(state.b, state1.b) self.assertEqual(state.i, state1.i) self.assertEqual(state.longi, state1.longi) self.assertEqual(state.f, state1.f) self.assertEqual(state.c, state1.c) self.assertEqual(state.n, state1.n) self.assertEqual(state.s, state1.s) self.assertEqual(state.u, state1.u) # The ID's need not be identical so we equate them here so the # tests pass. Note that the ID's only need be consistent not # identical! if TVTK_AVAILABLE: instances = ("inst", "_tvtk") else: instances = ("inst",) for attr in instances: getattr(state1, attr).__metadata__["id"] = getattr( state, attr ).__metadata__["id"] if TVTK_AVAILABLE: self.assertEqual(state1._tvtk, state._tvtk) state1.tuple[-1].__metadata__["id"] = state.tuple[-1].__metadata__[ "id" ] self.assertEqual(state.inst.__metadata__, state1.inst.__metadata__) self.assertEqual(state.tuple, state1.tuple) self.assertEqual(state.list, state1.list) self.assertEqual(state.pure_list, state1.pure_list) self.assertEqual(state.dict, state1.dict) self.assertTrue((state1.numeric == state.numeric).all()) self.assertEqual(id(state.ref), id(state.numeric)) self.assertEqual(id(state1.ref), id(state1.numeric)) def test_has_instance(self): """Test to check has_instance correctness.""" a = A() r = state_pickler.get_state(a) self.assertTrue(r.__metadata__["has_instance"]) lst = [1, a] r = state_pickler.get_state(lst) self.assertTrue(r.has_instance) self.assertTrue(r[1].__metadata__["has_instance"]) d = {"a": lst, "b": 1} r = state_pickler.get_state(d) self.assertTrue(r.has_instance) self.assertTrue(r["a"].has_instance) self.assertTrue(r["a"][1].__metadata__["has_instance"]) class B: def __init__(self): self.a = [1, A()] b = B() r = state_pickler.get_state(b) self.assertTrue(r.__metadata__["has_instance"]) self.assertTrue(r.a.has_instance) self.assertTrue(r.a[1].__metadata__["has_instance"]) def test_pickle_classic(self): """Test if classic classes can be pickled.""" t = TestClassic() self.set_object(t) # Generate the dict that is actually pickled. state = state_pickler.StatePickler().dump_state(t) # First check if all the attributes are handled. keys = sorted(state["data"]["data"].keys()) expect = [x for x in t.__dict__.keys() if "__" not in x] expect.sort() self.assertEqual(keys, expect) # Check each attribute. self.verify(t, state) def test_unpickle_classic(self): """Test if classic classes can be unpickled.""" t = TestClassic() self.set_object(t) # Get the pickled state. res = state_pickler.get_state(t) # Check each attribute. self.verify_unpickled(t, res) def test_state_setter_classic(self): """Test if classic classes' state can be set.""" t = TestClassic() self.set_object(t) # Get the pickled state. res = state_pickler.get_state(t) # Now create a new instance and set its state. t1 = state_pickler.create_instance(res) state_pickler.set_state(t1, res) # Check each attribute. self.verify_unpickled(t1, res) def test_state_setter(self): """Test some of the features of the set_state method.""" t = TestClassic() self.set_object(t) # Get the saved state. res = state_pickler.get_state(t) # Now create a new instance and test the setter. t1 = state_pickler.create_instance(res) keys = [ "c", "b", "f", "i", "tuple", "list", "longi", "numeric", "n", "s", "u", "pure_list", "inst", "ref", "dict", ] ignore = list(keys) ignore.remove("b") first = ["b"] last = [] state_pickler.set_state(t1, res, ignore=ignore, first=first, last=last) # Only 'b' should have been set. self.assertTrue(t1.b) # Rest are unchanged. self.assertEqual(t1.i, 7) self.assertEqual(t1.s, "String") self.assertEqual(t1.u, "Unicode") self.assertEqual(t1.inst.a, "a") self.assertEqual(t1.list[0], 1) self.assertEqual(t1.tuple[-1].a, "a") self.assertEqual(t1.dict["a"], 1) # Check if last works. last = ignore ignore = [] first = [] state_pickler.set_state(t1, res, ignore=ignore, first=first, last=last) # Check everything. self.verify_unpickled(t1, res) def test_pickle_traits(self): """Test if traited classes can be pickled.""" t = TestTraits() self.set_object(t) # Generate the dict that is actually pickled. state = state_pickler.StatePickler().dump_state(t) # First check if all the attributes are handled. keys = sorted(state["data"]["data"].keys()) expect = [x for x in t.__dict__.keys() if "__" not in x] expect.sort() self.assertEqual(keys, expect) # Check each attribute. self.verify(t, state) def test_unpickle_traits(self): """Test if traited classes can be unpickled.""" t = TestTraits() self.set_object(t) # Get the pickled state. res = state_pickler.get_state(t) # Check each attribute. self.verify_unpickled(t, res) def test_state_setter_traits(self): """Test if traited classes' state can be set.""" t = TestTraits() self.set_object(t) # Get the saved state. res = state_pickler.get_state(t) # Now create a new instance and set its state. t1 = state_pickler.create_instance(res) state_pickler.set_state(t1, res) # Check each attribute. self.verify_unpickled(t1, res) def test_reference_cycle(self): """Test if reference cycles are handled when setting the state.""" class A: pass class B: pass a = A() b = B() a.a = b b.b = a state = state_pickler.get_state(a) z = A() z.a = B() z.a.b = z state_pickler.set_state(z, state) def test_get_state_on_tuple_with_numeric_references(self): num = numpy.zeros(10, float) data = (num, num) # If this just completes without error, we are good. state = state_pickler.get_state(data) # The two should be the same object. self.assertIs(state[0], state[1]) numpy.testing.assert_allclose(state[0], num) def test_state_is_saveable(self): """Test if the state can be saved like the object itself.""" t = TestClassic() self.set_object(t) state = state_pickler.get_state(t) # Now get the state of the state itself. state1 = state_pickler.get_state(state) self.verify_state(state1, state) # Same thing for the traited class. t = TestTraits() self.set_object(t) state = state_pickler.get_state(t) # Now get the state of the state itself. state1 = state_pickler.get_state(state) self.verify_state(state1, state) def test_get_pure_state(self): """Test if get_pure_state is called first.""" class B: def __init__(self): self.a = "dict" def __get_pure_state__(self): return {"a": "get_pure_state"} def __getstate__(self): return {"a": "getstate"} b = B() s = state_pickler.get_state(b) self.assertEqual(s.a, "get_pure_state") del B.__get_pure_state__ s = state_pickler.get_state(b) self.assertEqual(s.a, "getstate") del B.__getstate__ s = state_pickler.get_state(b) self.assertEqual(s.a, "dict") def test_dump_to_file_str(self): """Test if dump can take a str as file""" obj = A() filepath = os.path.join(tempfile.gettempdir(), "tmp.file") try: state_pickler.dump(obj, filepath) finally: os.remove(filepath) apptools-5.1.0/apptools/persistence/__init__.py0000644000076500000240000000107013777642667022312 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Supports flexible pickling and unpickling of the state of a Python object to a dictionary. Part of the AppTools project of the Enthought Tool Suite. """ apptools-5.1.0/apptools/persistence/updater.py0000644000076500000240000000277613777642667022235 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! def __replacement_setstate__(self, state): """""" state = self.__updater__(state) self.__dict__.update(state) class Updater: """An abstract class to provide functionality common to the updaters.""" def get_latest(self, module, name): """The refactorings dictionary contains mappings between old and new module names. Since we only bump the version number one increment there is only one possible answer. """ if hasattr(self, "refactorings"): module = self.strip(module) name = self.strip(name) # returns the new module and name if it exists otherwise defaults # to using the original module and name module, name = self.refactorings.get( (module, name), (module, name) ) return module, name def strip(self, string): # Who would have thought that pickle would pass us # names with \013 on the end? Is this after the files have # manually edited? if ord(string[-1:]) == 13: return string[:-1] return string apptools-5.1.0/apptools/persistence/state_pickler.py0000644000076500000240000010372113777642667023412 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 code that allows one to pickle the state of a Python object to a dictionary. The motivation for this is simple. The standard Python pickler/unpickler is best used to pickle simple objects and does not work too well for complex code. Specifically, there are two major problems (1) the pickle file format is not easy to edit with a text editor and (2) when a pickle is unpickled, it creates all the necessary objects and sets the state of these objects. Issue (2) might not appear to be a problem. However, often, the determination of the entire 'state' of an application requires the knowledge of the state of many objects that are not really in the users concern. The user would ideally like to pickle just what he thinks is relevant. Now, given that the user is not going to save the entire state of the application, the use of pickle is insufficient since the state is no longer completely known (or worth knowing). The default `Unpickler` recreates the objects and the typical implementation of `__setstate__` is usually to simply update the object's `__dict__` attribute. This is inadequate because the pickled information is taken out of the real context when it was saved. The `StatePickler` basically pickles the 'state' of an object into a large dictionary. This pickled data may be easily unpickled and modified on the interpreter or edited with a text editor (`pprint.saferepr` is a friend). The second problem is also eliminated. When this state is unpickled using `StateUnpickler`, what you get is a special dictionary (a `State` instance). This allows one to navigate the state just like the original object. Its up to the user to create any new objects and set their states using this information. This allows for a lot of flexibility while allowing one to save and set the state of (almost) any Python object. The `StateSetter` class helps set the state of a known instance. When setting the state of an instance it checks to see if there is a `__set_pure_state__` method that in turn calls `StateSetter.set` appropriately. Additionally, there is support for versioning. The class' version is obtain from the `__version__` class attribute. This version along with the versions of the bases of a class is embedded into the metadata of the state and stored. By using `version_registry.py` a user may register a handler for a particular class and module. When the state of an object is set using `StateSetter.set_state`, then these handlers are called in reverse order of their MRO. This gives the handler an opportunity to upgrade the state depending on its version. Builtin classes are not scanned for versions. If a class has no version, then by default it is assumed to be -1. Example:: >>> class A: ... def __init__(self): ... self.a = 'a' ... >>> a = A() >>> a.a = 100 >>> import state_pickler >>> s = state_pickler.dumps(a) # Dump the state of `a`. >>> state = state_pickler.loads_state(s) # Get the state back. >>> b = state_pickler.create_instance(state) # Create the object. >>> state_pickler.set_state(b, state) # Set the object's state. >>> assert b.a == 100 Features -------- - The output is a plain old dictionary so is easy to parse, edit etc. - Handles references to avoid duplication. - Gzips Numeric arrays when dumping them. - Support for versioning. Caveats ------- - Does not pickle a whole bunch of stuff including code objects and functions. - The output is a pure dictionary and does not contain instances. So using this *as it is* in `__setstate__` will not work. Instead define a `__set_pure_state__` and use the `StateSetter` class or the `set_state` function provided by this module. Notes ----- Browsing the code from XMarshaL_ and pickle.py proved useful for ideas. None of the code is taken from there though. .. _XMarshaL: http://www.dezentral.de/soft/XMarshaL """ # Author: Prabhu Ramachandran # Copyright (c) 2005-2015, Enthought, Inc. # License: BSD Style. # Standard library imports. import base64 import sys import pickle import gzip from io import BytesIO, StringIO import numpy # Local imports. from . import version_registry from .file_path import FilePath NumpyArrayType = type(numpy.array([])) def gzip_string(data): """Given a string (`data`) this gzips the string and returns it.""" s = BytesIO() writer = gzip.GzipFile(mode="wb", fileobj=s) writer.write(data) writer.close() s.seek(0) return s.read() def gunzip_string(data): """Given a gzipped string (`data`) this unzips the string and returns it. """ if type(data) is bytes: s = BytesIO(data) else: s = StringIO(data) writer = gzip.GzipFile(mode="rb", fileobj=s) data = writer.read() writer.close() return data class StatePicklerError(Exception): pass class StateUnpicklerError(Exception): pass class StateSetterError(Exception): pass ###################################################################### # `State` class ###################################################################### class State(dict): """Used to encapsulate the state of an instance in a very convenient form. The '__metadata__' attribute/key is a dictionary that has class specific details like the class name, module name etc. """ def __init__(self, **kw): dict.__init__(self, **kw) self.__dict__ = self ###################################################################### # `StateDict` class ###################################################################### class StateDict(dict): """Used to encapsulate a dictionary stored in a `State` instance. The has_instance attribute specifies if the dict has an instance embedded in it. """ def __init__(self, **kw): dict.__init__(self, **kw) self.has_instance = False ###################################################################### # `StateList` class ###################################################################### class StateList(list): """Used to encapsulate a list stored in a `State` instance. The has_instance attribute specifies if the list has an instance embedded in it. """ def __init__(self, seq=None): if seq: list.__init__(self, seq) else: list.__init__(self) self.has_instance = False ###################################################################### # `StateTuple` class ###################################################################### class StateTuple(tuple): """Used to encapsulate a tuple stored in a `State` instance. The has_instance attribute specifies if the tuple has an instance embedded in it. """ def __new__(cls, seq=None): if seq: obj = super(StateTuple, cls).__new__(cls, tuple(seq)) else: obj = super(StateTuple, cls).__new__(cls) obj.has_instance = False return obj ###################################################################### # `StatePickler` class ###################################################################### class StatePickler: """Pickles the state of an object into a dictionary. The dictionary is itself either saved as a pickled file (`dump`) or pickled string (`dumps`). Alternatively, the `dump_state` method will return the dictionary that is pickled. The format of the state dict is quite strightfoward. Basic types (bool, int, long, float, complex, None, string) are represented as they are. Everything else is stored as a dictionary containing metadata information on the object's type etc. and also the actual object in the 'data' key. For example:: >>> p = StatePickler() >>> p.dump_state(1) 1 >>> l = [1,2.0, None, [1,2,3]] >>> p.dump_state(l) {'data': [1, 2.0, None, {'data': [1, 2, 3], 'type': 'list', 'id': 1}], 'id': 0, 'type': 'list'} Classes are also represented similarly. The state in this case is obtained from the `__getstate__` method or from the `__dict__`. Here is an example:: >>> class A: ... __version__ = 1 # State version ... def __init__(self): ... self.attribute = 1 ... >>> a = A() >>> p = StatePickler() >>> p.dump_state(a) {'class_name': 'A', 'data': {'data': {'attribute': 1}, 'type': 'dict', 'id': 2}, 'id': 0, 'initargs': {'data': (), 'type': 'tuple', 'id': 1}, 'module': '__main__', 'type': 'instance', 'version': [(('A', '__main__'), 1)]} When pickling data, references are taken care of. Numeric arrays can be pickled and are stored as a gzipped base64 encoded string. """ def __init__(self): self._clear() type_map = { bool: self._do_basic_type, complex: self._do_basic_type, float: self._do_basic_type, int: self._do_basic_type, type(None): self._do_basic_type, str: self._do_basic_type, bytes: self._do_basic_type, tuple: self._do_tuple, list: self._do_list, dict: self._do_dict, NumpyArrayType: self._do_numeric, State: self._do_state, } self.type_map = type_map def dump(self, value, file): """Pickles the state of the object (`value`) into the passed file. """ try: # Store the file name we are writing to so we can munge # file paths suitably. self.file_name = file.name except AttributeError: pass pickle.dump(self._do(value), file) def dumps(self, value): """Pickles the state of the object (`value`) and returns a string. """ return pickle.dumps(self._do(value)) def dump_state(self, value): """Returns a dictionary or a basic type representing the complete state of the object (`value`). This value is pickled by the `dump` and `dumps` methods. """ return self._do(value) ###################################################################### # Non-public methods ###################################################################### def _clear(self): # Stores the file name of the file being used to dump the # state. This is used to change any embedded paths relative # to the saved file. self.file_name = "" # Caches id's to handle references. self.obj_cache = {} # Misc cache to cache things that are not persistent. For # example, object.__getstate__()/__getinitargs__() usually # returns a copy of a dict/tuple that could possibly be reused # on another object's __getstate__. Caching these prevents # some wierd problems with the `id` of the object. self._misc_cache = [] def _flush_traits(self, obj): """Checks if the object has traits and ensures that the traits are set in the `__dict__` so we can pickle it. """ # Not needed with Traits3. def _do(self, obj): obj_type = type(obj) key = self._get_id(obj) if key in self.obj_cache: return self._do_reference(obj) elif obj_type in self.type_map: return self.type_map[obj_type](obj) elif isinstance(obj, tuple): # Takes care of StateTuples. return self._do_tuple(obj) elif isinstance(obj, list): # Takes care of TraitListObjects. return self._do_list(obj) elif isinstance(obj, dict): # Takes care of TraitDictObjects. return self._do_dict(obj) elif hasattr(obj, "__dict__"): return self._do_instance(obj) def _get_id(self, value): try: key = hash(value) except TypeError: key = id(value) return key def _register(self, value): key = self._get_id(value) cache = self.obj_cache idx = len(cache) cache[key] = idx return idx def _do_basic_type(self, value): return value def _do_reference(self, value): key = self._get_id(value) idx = self.obj_cache[key] return dict(type="reference", id=idx, data=None) def _do_instance(self, value): # Flush out the traits. self._flush_traits(value) # Setup the relative paths of FilePaths before dumping. if self.file_name and isinstance(value, FilePath): value.set_relative(self.file_name) # Get the initargs. args = () if hasattr(value, "__getinitargs__") and value.__getinitargs__: args = value.__getinitargs__() # Get the object state. if hasattr(value, "__get_pure_state__"): state = value.__get_pure_state__() elif hasattr(value, "__getstate__"): state = value.__getstate__() else: state = value.__dict__ state.pop("__traits_version__", None) # Cache the args and state since they are likely to be gc'd. self._misc_cache.extend([args, state]) # Register and process. idx = self._register(value) args_data = self._do(args) data = self._do(state) # Get the version of the object. version = version_registry.get_version(value) module = value.__class__.__module__ class_name = value.__class__.__name__ return dict( type="instance", module=module, class_name=class_name, version=version, id=idx, initargs=args_data, data=data, ) def _do_state(self, value): metadata = value.__metadata__ args = metadata.get("initargs") state = dict(value) state.pop("__metadata__") self._misc_cache.extend([args, state]) idx = self._register(value) args_data = self._do(args) data = self._do(state) return dict( type="instance", module=metadata["module"], class_name=metadata["class_name"], version=metadata["version"], id=idx, initargs=args_data, data=data, ) def _do_tuple(self, value): idx = self._register(value) data = tuple([self._do(x) for x in value]) return dict(type="tuple", id=idx, data=data) def _do_list(self, value): idx = self._register(value) data = [self._do(x) for x in value] return dict(type="list", id=idx, data=data) def _do_dict(self, value): idx = self._register(value) vals = [self._do(x) for x in value.values()] data = dict(zip(value.keys(), vals)) return dict(type="dict", id=idx, data=data) def _do_numeric(self, value): idx = self._register(value) data = base64.encodebytes(gzip_string(numpy.ndarray.dumps(value))) return dict(type="numeric", id=idx, data=data) ###################################################################### # `StateUnpickler` class ###################################################################### class StateUnpickler: """Unpickles the state of an object saved using StatePickler. Please note that unlike the standard Unpickler, no instances of any user class are created. The data for the state is obtained from the file or string, reference objects are setup to refer to the same state value and this state is returned in the form usually in the form of a dictionary. For example:: >>> class A: ... def __init__(self): ... self.attribute = 1 ... >>> a = A() >>> p = StatePickler() >>> s = p.dumps(a) >>> up = StateUnpickler() >>> state = up.loads_state(s) >>> state.__class__.__name__ 'State' >>> state.attribute 1 >>> state.__metadata__ {'class_name': 'A', 'has_instance': True, 'id': 0, 'initargs': (), 'module': '__main__', 'type': 'instance', 'version': [(('A', '__main__'), -1)]} Note that the state is actually a `State` instance and is navigable just like the original object. The details of the instance are stored in the `__metadata__` attribute. This is highly convenient since it is possible for someone to view and modify the state very easily. """ def __init__(self): self._clear() self.type_map = { "reference": self._do_reference, "instance": self._do_instance, "tuple": self._do_tuple, "list": self._do_list, "dict": self._do_dict, "numeric": self._do_numeric, } def load_state(self, file): """Returns the state of an object loaded from the pickled data in the given file. """ try: self.file_name = file.name except AttributeError: pass data = pickle.load(file) result = self._process(data) return result def loads_state(self, string): """Returns the state of an object loaded from the pickled data in the given string. """ data = pickle.loads(string) result = self._process(data) return result ###################################################################### # Non-public methods ###################################################################### def _clear(self): # The file from which we are being loaded. self.file_name = "" # Cache of the objects. self._obj_cache = {} # Paths to the instances. self._instances = [] # Caches the references. self._refs = {} # Numeric arrays. self._numeric = {} def _set_has_instance(self, obj, value): if isinstance(obj, State): obj.__metadata__["has_instance"] = value elif isinstance(obj, (StateDict, StateList, StateTuple)): obj.has_instance = value def _process(self, data): result = self._do(data) # Setup all the Numeric arrays. Do this first since # references use this. for key, (path, val) in self._numeric.items(): if isinstance(result, StateTuple): result = list(result) exec("result%s = val" % path) result = StateTuple(result) else: exec("result%s = val" % path) # Setup the references so they really are references. for key, paths in self._refs.items(): for path in paths: x = self._obj_cache[key] if isinstance(result, StateTuple): result = list(result) exec("result%s = x" % path) result = StateTuple(result) else: exec("result%s = x" % path) # if the reference is to an instance append its path. if isinstance(x, State): self._instances.append(path) # Now setup the 'has_instance' attribute. If 'has_instance' # is True then the object contains an instance somewhere # inside it. for path in self._instances: pth = path while pth: ns = {"result": result} exec("val = result%s" % pth, ns, ns) self._set_has_instance(ns["val"], True) end = pth.rfind("[") pth = pth[:end] # Now make sure that the first element also has_instance. self._set_has_instance(result, True) return result def _do(self, data, path=""): if type(data) is dict: return self.type_map[data["type"]](data, path) else: return data def _do_reference(self, value, path): id = value["id"] if id in self._refs: self._refs[id].append(path) else: self._refs[id] = [path] return State(__metadata__=value) def _handle_file_path(self, value): if ( (value["class_name"] == "FilePath") and ("file_path" in value["module"]) and self.file_name ): data = value["data"]["data"] fp = FilePath(data["rel_pth"]) fp.set_absolute(self.file_name) data["abs_pth"] = fp.abs_pth def _do_instance(self, value, path): self._instances.append(path) initargs = self._do( value["initargs"], path + '.__metadata__["initargs"]' ) # Handle FilePaths. self._handle_file_path(value) d = self._do(value["data"], path) md = dict( type="instance", module=value["module"], class_name=value["class_name"], version=value["version"], id=value["id"], initargs=initargs, has_instance=True, ) result = State(**d) result.__metadata__ = md self._obj_cache[value["id"]] = result return result def _do_tuple(self, value, path): res = [] for i, x in enumerate(value["data"]): res.append(self._do(x, path + "[%d]" % i)) result = StateTuple(res) self._obj_cache[value["id"]] = result return result def _do_list(self, value, path): result = StateList() for i, x in enumerate(value["data"]): result.append(self._do(x, path + "[%d]" % i)) self._obj_cache[value["id"]] = result return result def _do_dict(self, value, path): result = StateDict() for key, val in value["data"].items(): result[key] = self._do(val, path + '["%s"]' % key) self._obj_cache[value["id"]] = result return result def _do_numeric(self, value, path): data = value["data"] if isinstance(data, str): data = value["data"].encode("utf-8") junk = gunzip_string(base64.decodebytes(data)) result = pickle.loads(junk, encoding="bytes") self._numeric[value["id"]] = (path, result) self._obj_cache[value["id"]] = result return result ###################################################################### # `StateSetter` class ###################################################################### class StateSetter: """This is a convenience class that helps a user set the attributes of an object given its saved state. For instances it checks to see if a `__set_pure_state__` method exists and calls that when it sets the state. """ def __init__(self): # Stores the ids of instances already done. self._instance_ids = [] self.type_map = { State: self._do_instance, StateTuple: self._do_tuple, StateList: self._do_list, StateDict: self._do_dict, } def set(self, obj, state, ignore=None, first=None, last=None): """Sets the state of the object. This is to be used as a means to simplify loading the state of an object from its `__setstate__` method using the dictionary describing its state. Note that before the state is set, the registered handlers for the particular class are called in order to upgrade the version of the state to the latest version. Parameters ---------- - obj : `object` The object whose state is to be set. If this is `None` (default) then the object is created. - state : `dict` The dictionary representing the state of the object. - ignore : `list(str)` The list of attributes specified in this list are ignored and the state of these attributes are not set (this excludes the ones specified in `first` and `last`). If one specifies a '*' then all attributes are ignored except the ones specified in `first` and `last`. - first : `list(str)` The list of attributes specified in this list are set first (in order), before any other attributes are set. - last : `list(str)` The list of attributes specified in this list are set last (in order), after all other attributes are set. """ if (not isinstance(state, State)) and state.__metadata__[ "type" ] != "instance": raise StateSetterError( "Can only set the attributes of an instance." ) # Upgrade the state to the latest using the registry. self._update_and_check_state(obj, state) self._register(obj) # This wierdness is needed since the state's own `keys` might # be set to something else. state_keys = list(dict.keys(state)) state_keys.remove("__metadata__") if first is None: first = [] if last is None: last = [] # Remove all the ignored keys. if ignore: if "*" in ignore: state_keys = first + last else: for name in ignore: try: state_keys.remove(name) except KeyError: pass # Do the `first` attributes. for key in first: state_keys.remove(key) self._do(obj, key, state[key]) # Remove the `last` attributes. for key in last: state_keys.remove(key) # Set the remaining attributes. for key in state_keys: self._do(obj, key, state[key]) # Do the last ones in order. for key in last: self._do(obj, key, state[key]) ###################################################################### # Non-public methods. ###################################################################### def _register(self, obj): idx = id(obj) if idx not in self._instance_ids: self._instance_ids.append(idx) def _is_registered(self, obj): return id(obj) in self._instance_ids def _has_instance(self, value): """Given something (`value`) that is part of the state this returns if the value has an instance embedded in it or not. """ if isinstance(value, State): return True elif isinstance(value, (StateDict, StateList, StateTuple)): return value.has_instance return False def _get_pure(self, value): """Returns the Python representation of the object (usually a list, tuple or dict) that has no instances embedded within it. """ result = value if self._has_instance(value): raise StateSetterError("Value has an instance: %s" % value) if isinstance(value, (StateList, StateTuple)): result = [self._get_pure(x) for x in value] if isinstance(value, StateTuple): result = tuple(result) elif isinstance(value, StateDict): result = {} for k, v in value.items(): result[k] = self._get_pure(v) return result def _update_and_check_state(self, obj, state): """Updates the state from the registry and then checks if the object and state have same class. """ # Upgrade this state object to the latest using the registry. # This is done before testing because updating may change the # class name/module. version_registry.registry.update(state) # Make sure object and state have the same class and module names. metadata = state.__metadata__ cls = obj.__class__ if metadata["class_name"] != cls.__name__: raise StateSetterError( "Instance (%s) and state (%s) do not have the same class" " name!" % (cls.__name__, metadata["class_name"]) ) if metadata["module"] != cls.__module__: raise StateSetterError( "Instance (%s) and state (%s) do not have the same module" " name!" % (cls.__module__, metadata["module"]) ) def _do(self, obj, key, value): try: getattr(obj, key) except AttributeError: raise StateSetterError( "Object %s does not have an attribute called: %s" % (obj, key) ) if isinstance(value, (State, StateDict, StateList, StateTuple)): # Special handlers are needed. if not self._has_instance(value): result = self._get_pure(value) setattr(obj, key, result) elif isinstance(value, StateTuple): setattr(obj, key, self._do_tuple(getattr(obj, key), value)) else: self._do_object(getattr(obj, key), value) else: setattr(obj, key, value) def _do_object(self, obj, state): self.type_map[state.__class__](obj, state) def _do_instance(self, obj, state): if self._is_registered(obj): return else: self._register(obj) metadata = state.__metadata__ if hasattr(obj, "__set_pure_state__"): self._update_and_check_state(obj, state) obj.__set_pure_state__(state) elif "tvtk_classes" in metadata["module"]: self._update_and_check_state(obj, state) tmp = self._get_pure(StateDict(**state)) del tmp["__metadata__"] obj.__setstate__(tmp) else: # No need to update or check since `set` does it for us. self.set(obj, state) def _do_tuple(self, obj, state): if not self._has_instance(state): return self._get_pure(state) else: result = list(obj) self._do_list(result, state) return tuple(result) def _do_list(self, obj, state): if len(obj) == len(state): for i in range(len(obj)): if not self._has_instance(state[i]): obj[i] = self._get_pure(state[i]) elif isinstance(state[i], tuple): obj[i] = self._do_tuple(state[i]) else: self._do_object(obj[i], state[i]) else: raise StateSetterError( "Cannot set state of list of incorrect size." ) def _do_dict(self, obj, state): for key, value in state.items(): if not self._has_instance(value): obj[key] = self._get_pure(value) elif isinstance(value, tuple): obj[key] = self._do_tuple(value) else: self._do_object(obj[key], value) ###################################################################### # Internal Utility functions. ###################################################################### def _get_file_read(f): if hasattr(f, "read"): return f else: return open(f, "rb") def _get_file_write(f): if hasattr(f, "write"): return f else: return open(f, "wb") ###################################################################### # Utility functions. ###################################################################### def dump(value, file): """Pickles the state of the object (`value`) into the passed file (or file name). """ f = _get_file_write(file) try: StatePickler().dump(value, f) finally: f.flush() if f is not file: f.close() def dumps(value): """Pickles the state of the object (`value`) and returns a string.""" return StatePickler().dumps(value) def load_state(file): """Returns the state of an object loaded from the pickled data in the given file (or file name). """ f = _get_file_read(file) try: state = StateUnpickler().load_state(f) finally: if f is not file: f.close() return state def loads_state(string): """Returns the state of an object loaded from the pickled data in the given string. """ return StateUnpickler().loads_state(string) def get_state(obj): """Returns the state of the object (usually as a dictionary). The returned state may be used directy to set the state of the object via `set_state`. """ s = dumps(obj) return loads_state(s) def set_state(obj, state, ignore=None, first=None, last=None): StateSetter().set(obj, state, ignore, first, last) set_state.__doc__ = StateSetter.set.__doc__ def update_state(state): """Given the state of an object, this updates the state to the latest version using the handlers given in the version registry. The state is modified in-place. """ version_registry.registry.update(state) def create_instance(state): """Create an instance from the state if possible.""" if (not isinstance(state, State)) and ( "class_name" not in state.__metadata__ ): raise StateSetterError("No class information in state") metadata = state.__metadata__ class_name = metadata.get("class_name") mod_name = metadata.get("module") if "tvtk_classes" in mod_name: # FIXME: This sort of special-case is probably indicative of something # that needs more thought, plus it makes it tought to decide whether # this component depends on tvtk! from tvtk.api import tvtk return getattr(tvtk, class_name)() initargs = metadata["initargs"] if initargs.has_instance: raise StateUnpicklerError("Cannot unpickle non-trivial initargs") __import__(mod_name, globals(), locals(), class_name) mod = sys.modules[mod_name] cls = getattr(mod, class_name) return cls(*initargs) apptools-5.1.0/apptools/persistence/version_registry.py0000644000076500000240000000726313777642667024202 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 version registry that manages handlers for different state versions. """ # Standard library imports. import sys import inspect import logging logger = logging.getLogger(__name__) ###################################################################### # Utility functions. ###################################################################### def get_version(obj): """Walks the class hierarchy and obtains the versions of the various classes and returns a list of tuples of the form ((class_name, module), version) in reverse order of the MRO. """ res = [] for cls in inspect.getmro(obj.__class__): class_name, module = cls.__name__, cls.__module__ if module in ["__builtin__"]: # No point in versioning builtins. continue try: version = cls.__version__ except AttributeError: version = -1 res.append(((class_name, module), version)) res.reverse() return res ###################################################################### # `HandlerRegistry` class. ###################################################################### class HandlerRegistry: """A simple version conversion handler registry. Classes register handlers in order to convert the state version to the latest version. When an object's state is about to be set, the `update` method of the registy is called. This in turn calls any handlers registered for the class/module and this handler is then called with the state and the version of the state. The state is modified in-place by the handlers. """ def __init__(self): # The version conversion handlers. # Key: (class_name, module), value: handler self.handlers = {} def register(self, class_name, module, handler): """Register `handler` that handles versioning for class having class name (`class_name`) and module name (`module`). The handler function will be passed the state and its version to fix. """ key = (class_name, module) if key in self.handlers: msg = "Overwriting version handler for (%s, %s)" % (key[0], key[1]) logger.warn(msg) self.handlers[(class_name, module)] = handler def unregister(self, class_name, module): """Unregisters any handlers for a class and module.""" self.handlers.pop((class_name, module)) def update(self, state): """Updates the given state using the handlers. Note that the state is modified in-place. """ if (not self.handlers) or (not hasattr(state, "__metadata__")): return versions = state.__metadata__["version"] for ver in versions: key = ver[0] try: self.handlers[key](state, ver[1]) except KeyError: pass def _create_registry(): """Creates a reload safe, singleton registry.""" registry = None for key in sys.modules.keys(): if "version_registry" in key: mod = sys.modules[key] if hasattr(mod, "registry"): registry = mod.registry break if not registry: registry = HandlerRegistry() return registry # The singleton registry. registry = _create_registry() apptools-5.1.0/apptools/persistence/versioned_unpickler.py0000644000076500000240000001731313777642667024634 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pickle import _Unpickler as Unpickler from pickle import UnpicklingError, BUILD import logging from types import GeneratorType # Enthought library imports from apptools.persistence.updater import __replacement_setstate__ logger = logging.getLogger(__name__) ############################################################################## # class 'NewUnpickler' ############################################################################## class NewUnpickler(Unpickler): """An unpickler that implements a two-stage pickling process to make it possible to unpickle complicated Python object hierarchies where the unserialized state of an object depends on the state of other objects in the same pickle. """ def load(self, max_pass=-1): """Read a pickled object representation from the open file. Return the reconstituted object hierarchy specified in the file. """ # List of objects to be unpickled. self.objects = [] # We overload the load_build method. dispatch = self.dispatch dispatch[BUILD[0]] = NewUnpickler.load_build # call the super class' method. ret = Unpickler.load(self) self.initialize(max_pass) self.objects = [] # Reset the Unpickler's dispatch table. dispatch[BUILD[0]] = Unpickler.load_build return ret def initialize(self, max_pass): # List of (object, generator) tuples that initialize objects. generators = [] # Execute object's initialize to setup the generators. for obj in self.objects: if hasattr(obj, "__initialize__") and callable(obj.__initialize__): ret = obj.__initialize__() if isinstance(ret, GeneratorType): generators.append((obj, ret)) elif ret is not None: raise UnpicklingError( "Unexpected return value from " "__initialize__. %s returned %s" % (obj, ret) ) # Ensure a maximum number of passes if max_pass < 0: max_pass = len(generators) # Now run the generators. count = 0 while len(generators) > 0: count += 1 if count > max_pass: not_done = [x[0] for x in generators] msg = """Reached maximum pass count %s. You may have a deadlock! The following objects are uninitialized: %s""" % ( max_pass, not_done, ) raise UnpicklingError(msg) for o, g in generators[:]: try: next(g) except StopIteration: generators.remove((o, g)) # Make this a class method since dispatch is a class variable. # Otherwise, supposing the initial VersionedUnpickler.load call (which # would have overloaded the load_build method) makes a pickle.load call at # some point, we would have the dispatch still pointing to # NewPickler.load_build whereas the object being passed in will be an # Unpickler instance, causing a TypeError. def load_build(cls, obj): # Just save the instance in the list of objects. if isinstance(obj, NewUnpickler): obj.objects.append(obj.stack[-2]) Unpickler.load_build(obj) load_build = classmethod(load_build) class VersionedUnpickler(NewUnpickler): """This class reads in a pickled file created at revision version 'n' and then applies the transforms specified in the updater class to generate a new set of objects which are at revision version 'n+1'. I decided to keep the loading of the updater out of this generic class because we will want updaters to be generated for each plugin's type of project. This ensures that the VersionedUnpickler can remain ignorant about the actual version numbers - all it needs to do is upgrade one release. """ def __init__(self, file, updater=None): Unpickler.__init__(self, file) self.updater = updater def find_class(self, module, name): """Overridden method from Unpickler. NB __setstate__ is not called until later. """ if self.updater: # check to see if this class needs to be mapped to a new class # or module name original_module, original_name = module, name module, name = self.updater.get_latest(module, name) # load the class... klass = self.import_name(module, name) # add the updater.... TODO - why the old name? self.add_updater(original_module, original_name, klass) else: # there is no updater so we will be reading in an up to date # version of the file... try: klass = Unpickler.find_class(self, module, name) except Exception: logger.error("Looking for [%s] [%s]" % (module, name)) logger.exception( "Problem using default unpickle functionality" ) # restore the original __setstate__ if necessary fn = getattr(klass, "__setstate_original__", False) if fn: setattr(klass, "__setstate__", fn) return klass def add_updater(self, module, name, klass): """If there is an updater defined for this class we will add it to the class as the __setstate__ method. """ fn = self.updater.setstates.get((module, name), False) if fn: # move the existing __setstate__ out of the way self.backup_setstate(module, klass) # add the updater into the class setattr(klass, "__updater__", fn) # hook up our __setstate__ which updates self.__dict__ setattr(klass, "__setstate__", __replacement_setstate__) else: pass def backup_setstate(self, module, klass): """If the class has a user defined __setstate__ we back it up.""" if getattr(klass, "__setstate__", False): if getattr(klass, "__setstate_original__", False): # don't overwrite the original __setstate__ name = "__setstate__%s" % self.updater.__class__ else: # backup the original __setstate__ which we will restore # and run later when we have finished updating the class name = "__setstate_original__" method = getattr(klass, "__setstate__") setattr(klass, name, method) else: # the class has no __setstate__ method so do nothing pass def import_name(self, module, name): """ If the class is needed for the latest version of the application then it should presumably exist. If the class no longer exists then we should perhaps return a proxy of the class. If the persisted file is at v1 say and the application is at v3 then objects that are required for v1 and v2 do not have to exist they only need to be placeholders for the state during an upgrade. """ module = __import__(module, globals(), locals(), [name]) return vars(module)[name] apptools-5.1.0/apptools/persistence/file_path.py0000644000076500000240000000557413777642667022523 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """Simple class to support file path objects that work well in the context of persistent storage with the state_pickler. """ # Standard library imports. import os from os.path import abspath, normpath, dirname, join class FilePath(object): """This class stores two paths to the file. A relative path and an absolute one. The absolute path is used by the end user. When this object is pickled the state_pickler sets the relative path relative to the file that is being generated. When unpickled, the stored relative path is used to set the absolute path correctly based on the path of the saved file. """ def __init__(self, value=""): self.set(value) def __str__(self): return self.abs_pth def __repr__(self): return self.abs_pth.__repr__() def get(self): """Get the path.""" return self.abs_pth def set(self, value): """Sets the value of the path.""" self.rel_pth = value if value: self.abs_pth = normpath(abspath(value)) else: self.abs_pth = "" def set_relative(self, base_f_name): """Sets the path relative to `base_f_name`. Note that `base_f_name` and self.rel_pth should be valid file names correct on the current os. The set name is a file name that has a POSIX path. """ # Get normalized paths. _src = abspath(base_f_name) _dst = self.abs_pth # Now strip out any common prefix between the two paths. for part in _src.split(os.sep): if _dst.startswith(part + os.sep): length = len(part) + 1 _src = _src[length:] _dst = _dst[length:] else: break # For each directory in the source, we need to add a reference to # the parent directory to the destination. ret = (_src.count(os.sep) * (".." + os.sep)) + _dst # Make it posix style. if os.sep != "/": ret.replace(os.sep, "/") # Store it. self.rel_pth = ret def set_absolute(self, base_f_name): """Sets the absolute file name for the current relative file name with respect to the given `base_f_name`. """ base_f_name = normpath(abspath(base_f_name)) rel_file_name = normpath(self.rel_pth) file_name = join(dirname(base_f_name), rel_file_name) file_name = os.path.normpath(file_name) self.abs_pth = file_name apptools-5.1.0/apptools/persistence/project_loader.py0000644000076500000240000000770213777642667023557 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys import pickle import logging # Enthought library imports from apptools.persistence.versioned_unpickler import VersionedUnpickler logger = logging.getLogger(__name__) def load_project( pickle_filename, updater_path, application_version, protocol, max_pass=-1 ): """Reads a project from a pickle file and if necessary will update it to the latest version of the application. """ latest_file = pickle_filename # Read the pickled project's metadata. f = open(latest_file, "rb") metadata = VersionedUnpickler(f).load(max_pass) f.close() project_version = metadata.get("version", False) if not project_version: raise ValueError("Could not read version number from the project file") logger.debug( "Project version: %d, Application version: %d" % (project_version, application_version) ) # here you can temporarily force an upgrade each time for testing .... # project_version = 0 latest_file = upgrade_project( pickle_filename, updater_path, project_version, application_version, protocol, max_pass, ) # Finally we can import the project ... logger.info("loading %s" % latest_file) i_f = open(latest_file, "rb") project = VersionedUnpickler(i_f).load(max_pass) i_f.close() return project def upgrade_project( pickle_filename, updater_path, project_version, application_version, protocol, max_pass=-1, ): """Repeatedly read and write the project to disk updating it one version at a time. Example the p5.project is at version 0 The application is at version 3 p5.project --- Update1 ---> p5.project.v1 p5.project.v1 --- Update2 ---> p5.project.v2 p5.project.v2 --- Update3 ---> p5.project.v3 p5.project.v3 ---> loaded into app The user then has the option to save the updated project as p5.project """ first_time = True latest_file = pickle_filename # update the project until it's version matches the application's while project_version < application_version: next_version = project_version + 1 if first_time: i_f = open(pickle_filename, "rb") data = i_f.read() open("%s.bak" % pickle_filename, "wb").write(data) i_f.seek(0) # rewind the file to the start else: name = "%s.v%d" % (pickle_filename, project_version) i_f = open(name, "rb") latest_file = name logger.info("converting %s" % latest_file) # find this version's updater ... updater_name = "%s.update%d" % (updater_path, next_version) __import__(updater_name) mod = sys.modules[updater_name] klass = getattr(mod, "Update%d" % next_version) updater = klass() # load and update this version of the project project = VersionedUnpickler(i_f, updater).load(max_pass) i_f.close() # set the project version to be the same as the updater we just # ran on the unpickled files ... project.metadata["version"] = next_version # Persist the updated project ... name = "%s.v%d" % (pickle_filename, next_version) latest_file = name o_f = open(name, "wb") pickle.dump(project.metadata, o_f, protocol=protocol) pickle.dump(project, o_f, protocol=protocol) o_f.close() # Bump up the version number of the pickled project... project_version += 1 first_time = False return latest_file apptools-5.1.0/apptools/selection/0000755000076500000240000000000013777643025017631 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/selection/i_selection_provider.py0000644000076500000240000000317613777642667024434 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import Event, Interface, Str class ISelectionProvider(Interface): """ Source of selections. """ #: Unique ID identifying the provider. provider_id = Str() #: Event triggered when the selection changes. #: The content of the event is an :class:`~.ISelection` instance. selection = Event def get_selection(self): """Return the current selection. Returns: selection -- ISelection Object representing the current selection. """ def set_selection(self, items, ignore_missing=False): """Set the current selection to the given items. If ``ignore_missing`` is ``True``, items that are not available in the selection provider are silently ignored. If it is ``False`` (default), an :class:`~.ValueError` should be raised. Arguments: items -- list List of items to be selected. ignore_missing -- bool If ``False`` (default), the provider raises an exception if any of the items in ``items`` is not available to be selected. Otherwise, missing elements are silently ignored, and the rest is selected. """ apptools-5.1.0/apptools/selection/tests/0000755000076500000240000000000013777643025020773 5ustar aayresstaff00000000000000apptools-5.1.0/apptools/selection/tests/test_list_selection.py0000644000076500000240000000431113777642667025436 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from apptools._testing.optional_dependencies import ( numpy, requires_numpy, ) if numpy is not None: from apptools.selection.api import ListSelection @requires_numpy class TestListSelection(unittest.TestCase): def test_list_selection(self): all_items = ["a", "b", "c", "d"] selected = ["d", "b"] list_selection = ListSelection.from_available_items( provider_id="foo", selected=selected, all_items=all_items ) self.assertEqual(list_selection.items, selected) self.assertEqual(list_selection.indices, [3, 1]) def test_list_selection_of_sequence_items(self): all_items = [["a", "b"], ["c", "d"], ["e", "f"]] selected = [all_items[2], all_items[1]] list_selection = ListSelection.from_available_items( provider_id="foo", selected=selected, all_items=all_items ) self.assertEqual(list_selection.items, selected) self.assertEqual(list_selection.indices, [2, 1]) def test_list_selection_of_numpy_array_items(self): data = numpy.arange(10) all_items = [data, data + 10, data + 30] selected = [all_items[0], all_items[2]] list_selection = ListSelection.from_available_items( provider_id="foo", selected=selected, all_items=all_items ) self.assertEqual(list_selection.items, selected) self.assertEqual(list_selection.indices, [0, 2]) def test_list_selection_with_invalid_selected_items(self): data = numpy.arange(10) all_items = [data, data + 10, data + 30] selected = [ data - 10, ] with self.assertRaises(ValueError): ListSelection.from_available_items( provider_id="foo", selected=selected, all_items=all_items ) apptools-5.1.0/apptools/selection/tests/__init__.py0000644000076500000240000000062713777642667023124 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/selection/tests/test_selection_service.py0000644000076500000240000002335013777642667026127 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import Any, Event, HasTraits, List, provides, Str from apptools.selection.api import ( IDConflictError, ISelection, ISelectionProvider, ListenerNotConnectedError, ListSelection, ProviderNotRegisteredError, SelectionService, ) @provides(ISelection) class BogusSelection(HasTraits): provider_id = Str # Some content to check that two selections are the same content = Any def is_empty(self): """ Is the selection empty? """ return False @provides(ISelectionProvider) class BogusSelectionProvider(HasTraits): #### 'ISelectionProvider' protocol ######################################## provider_id = Str selection = Event def get_selection(self): return BogusSelection( provider_id=self.provider_id, content="get_selection" ) def set_selection(self, items, ignore_missing=False): pass #### 'BogusSelectionProvider' protocol #################################### def trigger_selection(self, content): self.selection = BogusSelection( provider_id=self.provider_id, content=content ) class BogusListener(HasTraits): selections = List def on_selection_changed(self, selection): self.selections.append(selection) @provides(ISelectionProvider) class SimpleListProvider(HasTraits): #### 'ISelectionProvider' protocol ######################################## provider_id = Str("test.simple_list_provider") selection = Event def get_selection(self): selection = ListSelection.from_available_items( provider_id=self.provider_id, selected=self._selected, all_items=self.items, ) return selection def set_selection(self, items, ignore_missing=False): selected = [x for x in items if x in self.items] if not ignore_missing and len(selected) < len(items): raise ValueError() self._selected = selected #### 'SimpleListProvider' protocol ######################################## items = List _selected = List class TestSelectionService(unittest.TestCase): def test_add_selection_provider(self): service = SelectionService() provider = BogusSelectionProvider() service.add_selection_provider(provider) self.assertTrue(service.has_selection_provider(provider.provider_id)) def test_add_selection_id_conflict(self): service = SelectionService() provider_id = "Foo" provider = BogusSelectionProvider(provider_id=provider_id) another_provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) with self.assertRaises(IDConflictError): service.add_selection_provider(another_provider) def test_remove_selection_provider(self): service = SelectionService() provider = BogusSelectionProvider(provider_id="Bogus") service.add_selection_provider(provider) service.remove_selection_provider(provider) self.assertFalse(service.has_selection_provider(provider.provider_id)) with self.assertRaises(ProviderNotRegisteredError): service.remove_selection_provider(provider) def test_get_selection(self): service = SelectionService() provider_id = "Bogus" provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) selection = service.get_selection(provider_id) self.assertIsInstance(selection, ISelection) self.assertEqual(selection.provider_id, provider.provider_id) def test_get_selection_id_not_registered(self): service = SelectionService() with self.assertRaises(ProviderNotRegisteredError): service.get_selection("not-registered") def test_connect_listener(self): service = SelectionService() provider_id = "Bogus" provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) listener = BogusListener() service.connect_selection_listener( provider_id, listener.on_selection_changed ) content = [1, 2, 3] provider.trigger_selection(content) selections = listener.selections self.assertEqual(len(selections), 1) self.assertEqual(selections[0].provider_id, provider.provider_id) self.assertEqual(selections[0].content, content) def test_connect_listener_then_add_remove_provider(self): service = SelectionService() provider_id = "Bogus" # Connect listener before provider is registered. listener = BogusListener() service.connect_selection_listener( provider_id, listener.on_selection_changed ) # When the provider is first added, the listener should receive the # initial selection (as returned by provider.get_selection) provider = BogusSelectionProvider(provider_id=provider_id) expected = provider.get_selection() service.add_selection_provider(provider) selections = listener.selections self.assertEqual(len(selections), 1) self.assertEqual(selections[-1].content, expected.content) # When the provider changes the selection, the event arrive as usual. content = [1, 2, 3] provider.trigger_selection(content) self.assertEqual(len(selections), 2) self.assertEqual(selections[-1].content, content) # When we un-register the provider, a change in selection does not # generate a callback. service.remove_selection_provider(provider) provider.trigger_selection(content) self.assertEqual(len(selections), 2) # Finally, we register again and get the current selection. service.add_selection_provider(provider) self.assertEqual(len(selections), 3) self.assertEqual(selections[-1].content, expected.content) def test_disconnect_listener(self): service = SelectionService() provider_id = "Bogus" provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) listener = BogusListener() service.connect_selection_listener( provider_id, listener.on_selection_changed ) service.disconnect_selection_listener( provider_id, listener.on_selection_changed ) provider.trigger_selection([1, 2, 3]) self.assertEqual(len(listener.selections), 0) def test_disconnect_unknown_listener(self): service = SelectionService() provider_id = "Bogus" provider = BogusSelectionProvider(provider_id=provider_id) service.add_selection_provider(provider) # First case: there are listeners to a provider, but not the one we # pass to the disconnect method listener_1 = BogusListener() service.connect_selection_listener( provider_id, listener_1.on_selection_changed ) listener_2 = BogusListener() with self.assertRaises(ListenerNotConnectedError): service.disconnect_selection_listener( provider_id, listener_2.on_selection_changed ) # Second case: there is no listener connected to the ID with self.assertRaises(ListenerNotConnectedError): service.disconnect_selection_listener( "does-not-exists", listener_2.on_selection_changed ) def test_set_selection(self): service = SelectionService() provider = SimpleListProvider(items=list(range(10))) service.add_selection_provider(provider) provider_id = provider.provider_id selection = service.get_selection(provider_id) self.assertTrue(selection.is_empty()) new_selection = [5, 6, 3] service.set_selection(provider_id, new_selection) selection = service.get_selection(provider_id) self.assertFalse(selection.is_empty()) # We can't assume that the order of the items in the selection we set # remains stable. self.assertEqual(selection.items, new_selection) self.assertEqual(selection.indices, selection.items) def test_selection_id_not_registered(self): service = SelectionService() with self.assertRaises(ProviderNotRegisteredError): service.set_selection(provider_id="not-existent", items=[]) def test_ignore_missing(self): # What we are really testing here is that the selection service # passes the keyword argument to the selection provider. It's the # selection provider responsibility to ignore missing elements, or # raise an exception. service = SelectionService() provider = SimpleListProvider(items=list(range(10))) service.add_selection_provider(provider) new_selection = [0, 11, 1] provider_id = provider.provider_id service.set_selection(provider_id, new_selection, ignore_missing=True) selection = service.get_selection(provider_id) self.assertFalse(selection.is_empty()) self.assertEqual(selection.items, [0, 1]) new_selection = [0, 11, 1] with self.assertRaises(ValueError): service.set_selection( provider_id, new_selection, ignore_missing=False ) apptools-5.1.0/apptools/selection/__init__.py0000644000076500000240000000062713777642667021762 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! apptools-5.1.0/apptools/selection/api.py0000644000076500000240000000206113777642667020766 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for the apptools.selection subpackage. - :class:`~.ListSelection` - :class:`~.SelectionService` Interfaces ---------- - :class:`~.ISelection` - :class:`~.IListSelection` - :class:`~.ISelectionProvider` Custom Exceptions ----------------- - :class:`~.IDConflictError` - :class:`~.ListenerNotConnectedError` - :class:`~.ProviderNotRegisteredError` """ from .errors import ( IDConflictError, ListenerNotConnectedError, ProviderNotRegisteredError, ) from .i_selection import ISelection, IListSelection from .i_selection_provider import ISelectionProvider from .list_selection import ListSelection from .selection_service import SelectionService apptools-5.1.0/apptools/selection/errors.py0000644000076500000240000000270613777642667021537 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ProviderNotRegisteredError(Exception): """ Raised when a provider is requested by ID and not found. """ def __init__(self, provider_id): self.provider_id = provider_id def __str__(self): msg = "Selection provider with ID '{id}' not found." return msg.format(id=self.provider_id) class IDConflictError(Exception): """ Raised when a provider is added and its ID is already registered. """ def __init__(self, provider_id): self.provider_id = provider_id def __str__(self): msg = "A selection provider with ID '{id}' is already registered." return msg.format(id=self.provider_id) class ListenerNotConnectedError(Exception): """ Raised when a listener that was never connected is disconnected. """ def __init__(self, provider_id, listener): self.provider_id = provider_id self.listener = listener def __str__(self): msg = "Selection listener {lr} is not connected to provider '{id}'." return msg.format(lr=self.listener, id=self.provider_id) apptools-5.1.0/apptools/selection/selection_service.py0000644000076500000240000001615413777642667023732 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import Dict, HasTraits from apptools.selection.errors import ( ProviderNotRegisteredError, IDConflictError, ListenerNotConnectedError, ) class SelectionService(HasTraits): """The selection service connects selection providers and listeners. The selection service is a register of selection providers, i.e., objects that publish their current selection. Selections can be requested actively, by explicitly requesting the current selection in a provider (:meth:`get_selection(id)`), or passively by connecting selection listeners. """ #### 'SelectionService' protocol ########################################## def add_selection_provider(self, provider): """Add a selection provider. The provider is identified by its ID. If a provider with the same ID has been already registered, an :class:`~.IDConflictError` is raised. Arguments: provider -- ISelectionProvider The selection provider added to the internal registry. """ provider_id = provider.provider_id if self.has_selection_provider(provider_id): raise IDConflictError(provider_id=provider_id) self._providers[provider_id] = provider if provider_id in self._listeners: self._connect_all_listeners(provider_id) def has_selection_provider(self, provider_id): """ Has a provider with the given ID been registered? """ return provider_id in self._providers def remove_selection_provider(self, provider): """Remove a selection provider. If the provider has not been registered, a :class:`~.ProviderNotRegisteredError` is raised. Arguments: provider -- ISelectionProvider The selection provider added to the internal registry. """ provider_id = provider.provider_id self._raise_if_not_registered(provider_id) if provider_id in self._listeners: self._disconnect_all_listeners(provider_id) del self._providers[provider_id] def get_selection(self, provider_id): """Return the current selection of the provider with the given ID. If a provider with that ID has not been registered, a :class:`~.ProviderNotRegisteredError` is raised. Arguments: provider_id -- str The selection provider ID. Returns: selection -- ISelection The current selection of the provider. """ self._raise_if_not_registered(provider_id) provider = self._providers[provider_id] return provider.get_selection() def set_selection(self, provider_id, items, ignore_missing=False): """Set the current selection in a provider to the given items. If a provider with the given ID has not been registered, a :class:`~.ProviderNotRegisteredError` is raised. If ``ignore_missing`` is ``True``, items that are not available in the selection provider are silently ignored. If it is ``False`` (default), a :class:`ValueError` should be raised. Arguments: provider_id -- str The selection provider ID. items -- list List of items to be selected. ignore_missing -- bool If ``False`` (default), the provider raises an exception if any of the items in ``items`` is not available to be selected. Otherwise, missing elements are silently ignored, and the rest is selected. """ self._raise_if_not_registered(provider_id) provider = self._providers[provider_id] return provider.set_selection(items, ignore_missing=ignore_missing) def connect_selection_listener(self, provider_id, func): """Connect a listener to selection events from a specific provider. The signature if the listener callback is ``func(i_selection)``. The listener is called: 1) When a provider with the given ID is registered, with its initial selection as argument, or 2) whenever the provider fires a selection event. It is perfectly valid to connect a listener before a provider with the given ID is registered. The listener will remain connected even if the provider is repeatedly connected and disconnected. Arguments: provider_id -- str The selection provider ID. func -- callable(i_selection) A callable object that is notified when the selection changes. """ self._listeners.setdefault(provider_id, []) self._listeners[provider_id].append(func) if self.has_selection_provider(provider_id): self._toggle_listener(provider_id, func, remove=False) def disconnect_selection_listener(self, provider_id, func): """Disconnect a listener from a specific provider. Arguments: provider_id -- str The selection provider ID. func -- callable(provider_id, i_selection) A callable object that is notified when the selection changes. """ if self.has_selection_provider(provider_id): self._toggle_listener(provider_id, func, remove=True) try: self._listeners[provider_id].remove(func) except (ValueError, KeyError): raise ListenerNotConnectedError( provider_id=provider_id, listener=func ) #### Private protocol ##################################################### _listeners = Dict() _providers = Dict() def _toggle_listener(self, provider_id, func, remove): provider = self._providers[provider_id] provider.on_trait_change(func, "selection", remove=remove) def _connect_all_listeners(self, provider_id): """Connect all listeners connected to a provider. As soon as they are connected, they receive the initial selection. """ provider = self._providers[provider_id] selection = provider.get_selection() for func in self._listeners[provider_id]: self._toggle_listener(provider_id, func, remove=False) # FIXME: make this robust to notifications that raise exceptions. # Can we send the error to the traits exception hook? func(selection) def _disconnect_all_listeners(self, provider_id): for func in self._listeners[provider_id]: self._toggle_listener(provider_id, func, remove=True) def _raise_if_not_registered(self, provider_id): if not self.has_selection_provider(provider_id): raise ProviderNotRegisteredError(provider_id=provider_id) apptools-5.1.0/apptools/selection/i_selection.py0000644000076500000240000000160713777642667022517 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import Interface, List, Str class ISelection(Interface): """ Collection of selected items. """ #: ID of the selection provider that created this selection object. provider_id = Str def is_empty(self): """ Is the selection empty? """ class IListSelection(ISelection): """ Selection for ordered sequences of items. """ #: Selected objects. items = List #: Indices of the selected objects in the selection provider. indices = List apptools-5.1.0/apptools/selection/list_selection.py0000644000076500000240000000435013777642667023240 0ustar aayresstaff00000000000000# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasTraits, List, provides, Str from apptools.selection.i_selection import IListSelection @provides(IListSelection) class ListSelection(HasTraits): """Selection for ordered sequences of items. This is the default implementation of the :class:`~.IListSelection` interface. """ #### 'ISelection' protocol ################################################ #: ID of the selection provider that created this selection object. provider_id = Str def is_empty(self): """ Is the selection empty? """ return len(self.items) == 0 #### 'IListSelection' protocol ############################################ #: Selected objects. items = List #: Indices of the selected objects in the selection provider. indices = List #### 'ListSelection' class protocol ####################################### @classmethod def from_available_items(cls, provider_id, selected, all_items): """Create a list selection given a list of all available items. Fills in the required information (in particular, the indices) based on a list of selected items and a list of all available items. .. note:: - The list of available items must not contain any duplicate items. - It is expected that ``selected`` is populated by items in ``all_items``. """ number_of_items = len(all_items) indices = [] for item in selected: for index in range(number_of_items): if all_items[index] is item: indices.append(index) break else: msg = "Selected item: {!r}, could not be found" raise ValueError(msg.format(item)) return cls(provider_id=provider_id, items=selected, indices=indices)