├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── ci ├── appveyor-bootstrap.ps1 ├── appveyor-with-compiler.cmd ├── bootstrap.py └── templates │ ├── .travis.yml │ ├── appveyor.yml │ └── tox.ini ├── docs ├── authors.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── index.rst ├── installation.rst ├── readme.rst ├── reference │ ├── index.rst │ └── pytraits.rst ├── requirements.txt ├── spelling_wordlist.txt └── usage.rst ├── examples ├── class_is_composed_from_cherrypicked_method_in_class.py ├── class_is_composed_from_cherrypicked_method_in_instance.py ├── class_is_composed_from_cherrypicked_methods_with_rename.py ├── class_is_composed_from_cherrypicked_property_in_class.py ├── class_is_composed_from_cherrypicked_property_in_instance.py ├── class_is_composed_from_other_class.py ├── class_is_composed_from_other_instance.py ├── composition_in_alternative_syntax.py ├── extendable_function_class_vs_instance.py ├── function_is_composed_as_a_part_of_class.py ├── function_is_composed_as_a_part_of_instance.py ├── instance_is_composed_from_cherrypicked_method_in_class.py ├── instance_is_composed_from_cherrypicked_methods_with_rename.py ├── instance_is_composed_from_cherrypicked_property_in_class.py ├── instance_is_composed_from_cherrypicked_property_in_instance.py ├── instance_is_composed_from_other_class.py ├── instance_is_composed_from_other_instance.py ├── multiple_traits_composed_into_new_class.py ├── property_is_created_into_instance.py ├── pyqt_builtins_composed_into_class.py └── pyqt_builtins_composed_into_instance.py ├── setup.cfg ├── setup.py ├── src └── pytraits │ ├── __init__.py │ ├── combiner.py │ ├── core │ ├── __init__.py │ ├── base │ │ ├── __init__.py │ │ └── inspectors.py │ ├── composing │ │ ├── __init__.py │ │ ├── compiler.py │ │ ├── composer.py │ │ ├── resolutions.py │ │ └── traits.py │ └── primitives │ │ ├── __init__.py │ │ ├── class_object.py │ │ ├── instance_object.py │ │ ├── property_object.py │ │ ├── routine_object.py │ │ ├── trait_object.py │ │ └── unidentified_object.py │ ├── extendable.py │ ├── setproperty.py │ ├── support │ ├── __init__.py │ ├── errors.py │ ├── factory.py │ ├── inspector.py │ ├── magic.py │ ├── singleton.py │ └── utils.py │ └── trait_composer.py ├── tests ├── testdata.py ├── unittest_classobject.py ├── unittest_compiler.py ├── unittest_factory.py ├── unittest_inspector.py ├── unittest_singleton.py ├── unittest_traits.py ├── unittest_typeconverted.py ├── unittest_typesafe.py └── utils.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = src 3 | 4 | [run] 5 | branch = True 6 | source = src 7 | parallel = true 8 | 9 | [report] 10 | show_missing = true 11 | precision = 2 12 | omit = *migrations* 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | .coverage 28 | .coverage.* 29 | nosetests.xml 30 | htmlcov 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | .idea 40 | 41 | # Complexity 42 | output/*.html 43 | output/*/index.html 44 | 45 | # Sphinx 46 | docs/_build 47 | 48 | .DS_Store 49 | *~ 50 | .*.sw[po] 51 | .build 52 | .ve 53 | .bootstrap 54 | *.bak 55 | *.pypirc 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.4 3 | sudo: false 4 | env: 5 | global: 6 | LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | matrix: 8 | - TOXENV=check 9 | - TOXENV=3.3,coveralls 10 | - TOXENV=3.3-nocover 11 | - TOXENV=3.4,coveralls 12 | - TOXENV=3.4-nocover 13 | before_install: 14 | - python --version 15 | - virtualenv --version 16 | - pip --version 17 | - uname -a 18 | - lsb_release -a 19 | install: 20 | - pip install tox 21 | script: 22 | - tox -v 23 | notifications: 24 | email: 25 | on_success: never 26 | on_failure: always 27 | 28 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Authors 3 | ======= 4 | 5 | * Teppo Perä - https://github.com/Debith/py3traits 6 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | Changelog 3 | ========= 4 | 5 | 1.2.1 (2015-07-08) 6 | ------------------ 7 | - Added "Motivation" section to documentation to help to discover use cases. 8 | 9 | 1.2.0 (2015-07-08) 10 | ------------------ 11 | - New feature: Precompiled (builtin) functions can be used with properties 12 | - New feature: Precompiled (builtin) functions can be used as traits 13 | - New feature: @validation decorator for validating arguments by value 14 | - New feature: Factory class for object creation 15 | - Improving feature: @type_safe and @type_converted wraps functions properly 16 | - Fixed homepage link which was pointing to Python 2 version 17 | - Added back the missing github link in documentation 18 | - Done a major overhaul for the core to better support adding new features 19 | - Many other bigger or smaller improvements 20 | 21 | 1.1.0 (2015-06-13) 22 | ------------------ 23 | - Improving feature: setproperty does not require all property functions 24 | - Improving feature: added name as more convenient way to name the property 25 | - Improving example: examples/property_is_created_into_instance.py 26 | - Changing version numbering. 27 | 28 | 1.0.1 (2015-06-12) 29 | ------------------ 30 | - New feature: Added setproperty convenience function 31 | - New example: examples/property_is_created_into_instance.py 32 | - Added documentation 33 | - Some refactoring for testability 34 | - Added new test utility to parametrize tests 35 | - Added unit tests 36 | 37 | 1.0.0 (2015-05-25) 38 | ------------------ 39 | - First official release 40 | 41 | 0.15.0 (2015-05-23) 42 | ------------------- 43 | - New feature: Alternative syntax added to add_traits function 44 | - New example: examples/composition_in_alternative_syntax.py 45 | - New example: examples/multiple_traits_composed_into_new_class.py 46 | - Addes unit tests 47 | 48 | 0.14.0 (2015-05-19) 49 | ------------------- 50 | - New feature: Setter and Deleter for properties are now supported 51 | - New example: examples/instance_is_composed_from_cherrypicked_property_in_class.py 52 | - New example: examples/instance_is_composed_from_cherrypicked_property_in_instance.py 53 | - Updated example: examples/class_is_composed_from_cherrypicked_property_in_class.py 54 | - Updated example: examples/class_is_composed_from_cherrypicked_property_in_instance.py 55 | 56 | 0.13.0 (2015-04-25) 57 | ------------------- 58 | - New feature: Decorator type_safe to check function arguments 59 | - New feature: combine_class function takes name for new class as first argument 60 | - Refactoring magic.py to look less like black magic 61 | - Improving errors.py exception class creation to accept custom messages 62 | - Adding unit tests 63 | 64 | 0.12.0 (2015-04-22) 65 | ------------------- 66 | - New feature: Rename of composed traits 67 | - Cleaning up parts belonging to py2traits 68 | 69 | 0.11.0 (2015-04-18) 70 | ------------------- 71 | - PEP8 fixes 72 | - General cleaning for all files 73 | - Removed unused parts 74 | - Removed Python 2 code 75 | 76 | 0.10.0 (2015-03-30) 77 | ------------------- 78 | - Splitting into two projects: py2traits and py3traits 79 | - Taking new project template to use from cookiecutter. 80 | 81 | 0.9.0 Bringing back compatibility to Python 2.x 82 | ----------------------------------------------- 83 | - Some small clean up too 84 | 85 | 0.8.0 Adding support to private class and instance attributes 86 | ------------------------------------------------------------- 87 | - Redone function binding to include recompilation of the function 88 | - Leaving Python 2.x into unsupported state temporarily. 89 | 90 | 0.7.0 Improving usability of the library 91 | ---------------------------------------- 92 | - Introduced new extendable decorator, which adds function to add traits to object 93 | - Introduced new function combine_class to create new classes out of traits 94 | - Fixed module imports through out the library 95 | - Improved documentation in examples 96 | 97 | 0.6.0 Restructuring into library 98 | -------------------------------- 99 | - Added support for py.test 100 | - Preparing to support tox 101 | - Improved multiple examples and renamed them to make more sense 102 | - Removed the need of having two separate code branches for different Python versions 103 | 104 | 0.5.0 Instances can now be extended with traits in Python 3.x 105 | ------------------------------------------------------------- 106 | - Instance support now similar to classes 107 | - Added more examples 108 | 109 | 0.4.0 Completed function binding with examples in Python 2.x 110 | ------------------------------------------------------------ 111 | - Separate functions can now be bound to classes 112 | - Functions with 'self' as a first parameter will be acting as a method 113 | - Functions with 'cls' as a first parameter will be acting as classmethod 114 | - Other functions will be static methods. 115 | - Fixed an issue with binding functions 116 | 117 | 0.3.0 Trait extension support without conflicts for Python 2.x 118 | -------------------------------------------------------------- 119 | - Classes can be extended 120 | - Instances can be extended 121 | - Python 2.x supported 122 | 123 | 0.2.0 Apache License Updated 124 | ---------------------------- 125 | - Added apache 2.0 license to all files 126 | - Set the character set as utf-8 for all files 127 | 128 | 0.1.0 Initial Version 129 | --------------------- 130 | - prepared files for Python 2.x 131 | - prepared files for Python 3.x 132 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Bug reports 9 | =========== 10 | 11 | When `reporting a bug `_ please include: 12 | 13 | * Your operating system name, version and python version. 14 | * Failing test case created similar manner as in py3traits/examples. 15 | 16 | Documentation improvements 17 | ========================== 18 | 19 | py3traits could always use more documentation, whether as part of the 20 | official py3traits docs, in docstrings, or even on the web in blog posts, 21 | articles, and such. 22 | 23 | Feature requests and feedback 24 | ============================= 25 | 26 | The best way to send feedback is to file an issue at https://github.com/Debith/py3traits/issues. 27 | 28 | If you are proposing a feature: 29 | 30 | * Explain in detail how it would work or even better, create a failing test case similar manner as in py3traits/examples 31 | * Keep the scope as narrow as possible, to make it easier to implement. 32 | * Remember that this is a volunteer-driven project, and that contributions are welcome :) 33 | 34 | Development 35 | =========== 36 | 37 | To set up `py3traits` for local development: 38 | 39 | 1. `Fork py3traits on GitHub `_. 40 | 2. Clone your fork locally:: 41 | 42 | git clone git@github.com:your_name_here/py3traits.git 43 | 44 | 3. Create a branch for local development:: 45 | 46 | git checkout -b name-of-your-bugfix-or-feature 47 | 48 | Now you can make your changes locally. 49 | 50 | 4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: 51 | 52 | tox 53 | 54 | 5. Commit your changes and push your branch to GitHub:: 55 | 56 | git add . 57 | git commit -m "Your detailed description of your changes." 58 | git push origin name-of-your-bugfix-or-feature 59 | 60 | 6. Submit a pull request through the GitHub website. 61 | 62 | Pull Request Guidelines 63 | ----------------------- 64 | 65 | If you need some code review or feedback while you're developing the code just make the pull request. 66 | 67 | For merging, you should: 68 | 69 | 1. Include passing tests (run ``tox``) [1]_. 70 | 2. Update documentation when there's new API, functionality etc. 71 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 72 | 4. Add yourself to ``AUTHORS.rst``. 73 | 74 | .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will 75 | `run the tests `_ for each change you add in the pull request. 76 | 77 | It will be slower though ... 78 | 79 | Tips 80 | ---- 81 | 82 | To run a subset of tests:: 83 | 84 | tox -e envname -- py.test -k test_myfeature 85 | 86 | To run all the test environments in *parallel* (you need to ``pip install detox``):: 87 | 88 | detox -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft examples 3 | graft src 4 | graft ci 5 | graft tests 6 | 7 | include *.komodoproject 8 | include .bumpversion.cfg 9 | include .coveragerc 10 | include .isort.cfg 11 | include .pylintrc 12 | 13 | include AUTHORS.rst 14 | include CHANGELOG.rst 15 | include CONTRIBUTING.rst 16 | include LICENSE 17 | include README.rst 18 | 19 | include tox.ini .travis.yml appveyor.yml 20 | 21 | global-exclude *.py[co] __pycache__ *.so *.pyd 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | py3traits 3 | =============================== 4 | 5 | | |docs| |travis| |downloads| |wheel| |pyversions| 6 | 7 | .. |docs| image:: https://readthedocs.org/projects/py3traits/badge/ 8 | :target: https://readthedocs.org/projects/py3traits 9 | :alt: Documentation Status 10 | 11 | .. |travis| image:: http://img.shields.io/travis/Debith/py3traits/master.png 12 | :alt: Travis-CI Build Status 13 | :target: https://travis-ci.org/Debith/py3traits 14 | 15 | .. |downloads| image:: http://img.shields.io/pypi/dm/py3traits.png 16 | :alt: PyPI Package monthly downloads 17 | :target: https://pypi.python.org/pypi/py3traits 18 | 19 | .. |wheel| image:: https://img.shields.io/pypi/format/py3traits.svg 20 | :alt: PyPI Wheel 21 | :target: https://pypi.python.org/pypi/py3traits 22 | 23 | .. |pyversions| image:: https://img.shields.io/pypi/pyversions/py3traits.svg 24 | 25 | Trait support for Python 3 26 | 27 | * Free software: Apache license 28 | 29 | Installation 30 | ============ 31 | 32 | :: 33 | 34 | pip install py3traits 35 | 36 | Documentation 37 | ============= 38 | 39 | https://py3traits.readthedocs.org/ 40 | 41 | Development 42 | =========== 43 | 44 | To run the all tests run:: 45 | 46 | tox 47 | 48 | About Traits 49 | ============ 50 | 51 | Traits are classes which contain methods that can be used to extend 52 | other classes, similar to mixins, with exception that traits do not use 53 | inheritance. Instead, traits are composed into other classes. That is; 54 | methods, properties and internal state are copied to master object. 55 | 56 | The point is to improve code reusability by dividing code into simple 57 | building blocks that can be then combined into actual classes. 58 | 59 | Read more from wikipedia: http://en.wikipedia.org/wiki/Traits_class 60 | 61 | Look for examples from examples folder. 62 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | build: off 3 | environment: 4 | global: 5 | WITH_COMPILER: "cmd /E:ON /V:ON /C .\\ci\\appveyor-with-compiler.cmd" 6 | matrix: 7 | - TOXENV: "3.3" 8 | TOXPYTHON: "C:\\Python33\\python.exe" 9 | WINDOWS_SDK_VERSION: "v7.1" 10 | PYTHON_HOME: "C:\\Python33" 11 | PYTHON_VERSION: "3.3" 12 | PYTHON_ARCH: "32" 13 | - TOXENV: "3.3" 14 | TOXPYTHON: "C:\\Python33-x64\\python.exe" 15 | WINDOWS_SDK_VERSION: "v7.1" 16 | PYTHON_HOME: "C:\\Python33-x64" 17 | PYTHON_VERSION: "3.3" 18 | PYTHON_ARCH: "64" 19 | - TOXENV: "3.3-nocover" 20 | TOXPYTHON: "C:\\Python33\\python.exe" 21 | WINDOWS_SDK_VERSION: "v7.1" 22 | PYTHON_HOME: "C:\\Python33" 23 | PYTHON_VERSION: "3.3" 24 | PYTHON_ARCH: "32" 25 | - TOXENV: "3.3-nocover" 26 | TOXPYTHON: "C:\\Python33-x64\\python.exe" 27 | WINDOWS_SDK_VERSION: "v7.1" 28 | PYTHON_HOME: "C:\\Python33-x64" 29 | PYTHON_VERSION: "3.3" 30 | PYTHON_ARCH: "64" 31 | - TOXENV: "3.4" 32 | TOXPYTHON: "C:\\Python34\\python.exe" 33 | WINDOWS_SDK_VERSION: "v7.1" 34 | PYTHON_HOME: "C:\\Python34" 35 | PYTHON_VERSION: "3.4" 36 | PYTHON_ARCH: "32" 37 | - TOXENV: "3.4" 38 | TOXPYTHON: "C:\\Python34-x64\\python.exe" 39 | WINDOWS_SDK_VERSION: "v7.1" 40 | PYTHON_HOME: "C:\\Python34-x64" 41 | PYTHON_VERSION: "3.4" 42 | PYTHON_ARCH: "64" 43 | - TOXENV: "3.4-nocover" 44 | TOXPYTHON: "C:\\Python34\\python.exe" 45 | WINDOWS_SDK_VERSION: "v7.1" 46 | PYTHON_HOME: "C:\\Python34" 47 | PYTHON_VERSION: "3.4" 48 | PYTHON_ARCH: "32" 49 | - TOXENV: "3.4-nocover" 50 | TOXPYTHON: "C:\\Python34-x64\\python.exe" 51 | WINDOWS_SDK_VERSION: "v7.1" 52 | PYTHON_HOME: "C:\\Python34-x64" 53 | PYTHON_VERSION: "3.4" 54 | PYTHON_ARCH: "64" 55 | init: 56 | - "ECHO %TOXENV%" 57 | - ps: "ls C:\\Python*" 58 | install: 59 | - "powershell ci\\appveyor-bootstrap.ps1" 60 | test_script: 61 | - "%PYTHON_HOME%\\Scripts\\tox --version" 62 | - "%PYTHON_HOME%\\Scripts\\virtualenv --version" 63 | - "%PYTHON_HOME%\\Scripts\\pip --version" 64 | - "%WITH_COMPILER% %PYTHON_HOME%\\Scripts\\tox" 65 | after_test: 66 | - "IF \"%TOXENV:~-8,8%\" == \"-nocover\" %WITH_COMPILER% %TOXPYTHON% setup.py bdist_wheel" 67 | artifacts: 68 | - path: dist\* 69 | 70 | -------------------------------------------------------------------------------- /ci/appveyor-bootstrap.ps1: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/pypa/python-packaging-user-guide/blob/master/source/code/install.ps1 2 | # Sample script to install Python and pip under Windows 3 | # Authors: Olivier Grisel and Kyle Kastner 4 | # License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | $BASE_URL = "https://www.python.org/ftp/python/" 7 | $GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" 8 | $GET_PIP_PATH = "C:\get-pip.py" 9 | 10 | 11 | function DownloadPython ($python_version, $platform_suffix) { 12 | $webclient = New-Object System.Net.WebClient 13 | $filename = "python-" + $python_version + $platform_suffix + ".msi" 14 | $url = $BASE_URL + $python_version + "/" + $filename 15 | 16 | $basedir = $pwd.Path + "\" 17 | $filepath = $basedir + $filename 18 | if (Test-Path $filename) { 19 | Write-Host "Reusing" $filepath 20 | return $filepath 21 | } 22 | 23 | # Download and retry up to 5 times in case of network transient errors. 24 | Write-Host "Downloading" $filename "from" $url 25 | $retry_attempts = 3 26 | for($i=0; $i -lt $retry_attempts; $i++){ 27 | try { 28 | $webclient.DownloadFile($url, $filepath) 29 | break 30 | } 31 | Catch [Exception]{ 32 | Start-Sleep 1 33 | } 34 | } 35 | Write-Host "File saved at" $filepath 36 | return $filepath 37 | } 38 | 39 | 40 | function InstallPython ($python_version, $architecture, $python_home) { 41 | Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home 42 | if (Test-Path $python_home) { 43 | Write-Host $python_home "already exists, skipping." 44 | return $false 45 | } 46 | if ($architecture -eq "32") { 47 | $platform_suffix = "" 48 | } else { 49 | $platform_suffix = ".amd64" 50 | } 51 | $filepath = DownloadPython $python_version $platform_suffix 52 | Write-Host "Installing" $filepath "to" $python_home 53 | $args = "/qn /i $filepath TARGETDIR=$python_home" 54 | Write-Host "msiexec.exe" $args 55 | Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -Passthru 56 | Write-Host "Python $python_version ($architecture) installation complete" 57 | return $true 58 | } 59 | 60 | 61 | function InstallPip ($python_home) { 62 | $pip_path = $python_home + "/Scripts/pip.exe" 63 | $python_path = $python_home + "/python.exe" 64 | if (-not(Test-Path $pip_path)) { 65 | Write-Host "Installing pip..." 66 | $webclient = New-Object System.Net.WebClient 67 | $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) 68 | Write-Host "Executing:" $python_path $GET_PIP_PATH 69 | Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru 70 | } else { 71 | Write-Host "pip already installed." 72 | } 73 | } 74 | 75 | function InstallPackage ($python_home, $pkg) { 76 | $pip_path = $python_home + "/Scripts/pip.exe" 77 | & $pip_path install $pkg 78 | } 79 | 80 | function main () { 81 | InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON_HOME 82 | InstallPip $env:PYTHON_HOME 83 | InstallPackage $env:PYTHON_HOME setuptools 84 | InstallPackage $env:PYTHON_HOME wheel 85 | InstallPackage $env:PYTHON_HOME tox 86 | } 87 | 88 | main 89 | -------------------------------------------------------------------------------- /ci/appveyor-with-compiler.cmd: -------------------------------------------------------------------------------- 1 | :: To build extensions for 64 bit Python 3, we need to configure environment 2 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 3 | :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) 4 | :: 5 | :: To build extensions for 64 bit Python 2, we need to configure environment 6 | :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: 7 | :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) 8 | :: 9 | :: 32 bit builds do not require specific environment configurations. 10 | :: 11 | :: Note: this script needs to be run with the /E:ON and /V:ON flags for the 12 | :: cmd interpreter, at least for (SDK v7.0) 13 | :: 14 | :: More details at: 15 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 16 | :: http://stackoverflow.com/a/13751649/163740 17 | :: 18 | :: Author: Olivier Grisel 19 | :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 20 | @ECHO OFF 21 | 22 | SET COMMAND_TO_RUN=%* 23 | SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows 24 | 25 | IF "%PYTHON_ARCH%"=="64" ( 26 | ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% 27 | SET DISTUTILS_USE_SDK=1 28 | SET MSSdk=1 29 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% 30 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release 31 | ECHO Executing: %COMMAND_TO_RUN% 32 | call %COMMAND_TO_RUN% || EXIT 1 33 | ) ELSE ( 34 | ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% 35 | ECHO Executing: %COMMAND_TO_RUN% 36 | call %COMMAND_TO_RUN% || EXIT 1 37 | ) 38 | -------------------------------------------------------------------------------- /ci/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import os 6 | import sys 7 | from os.path import exists 8 | from os.path import join 9 | 10 | 11 | if __name__ == "__main__": 12 | base_path = join(".tox", "configure") 13 | if sys.platform == "win32": 14 | bin_path = join(base_path, "Scripts") 15 | else: 16 | bin_path = join(base_path, "bin") 17 | if not exists(base_path): 18 | import subprocess 19 | print("Bootstrapping ...") 20 | try: 21 | subprocess.check_call(["virtualenv", base_path]) 22 | except Exception: 23 | subprocess.check_call([sys.executable, "-m", "virtualenv", base_path]) 24 | print("Installing `jinja2` and `matrix` into bootstrap environment ...") 25 | subprocess.check_call([join(bin_path, "pip"), "install", "jinja2", "matrix"]) 26 | activate = join(bin_path, "activate_this.py") 27 | exec(compile(open(activate, "rb").read(), activate, "exec"), dict(__file__=activate)) 28 | 29 | import jinja2 30 | import matrix 31 | 32 | jinja = jinja2.Environment( 33 | loader=jinja2.FileSystemLoader(join("ci", "templates")), 34 | trim_blocks=True, 35 | lstrip_blocks=True, 36 | keep_trailing_newline=True 37 | ) 38 | tox_environments = {} 39 | for (alias, conf) in matrix.from_file("setup.cfg").items(): 40 | python = conf["python_versions"] 41 | deps = conf["dependencies"] 42 | if "coverage_flags" in conf: 43 | cover = {"false": False, "true": True}[conf["coverage_flags"].lower()] 44 | if "environment_variables" in conf: 45 | env_vars = conf["environment_variables"] 46 | 47 | tox_environments[alias] = { 48 | "python": "python" + python if "py" not in python else python, 49 | "deps": deps.split(), 50 | } 51 | if "coverage_flags" in conf: 52 | tox_environments[alias].update(cover=cover) 53 | if "environment_variables" in conf: 54 | tox_environments[alias].update(env_vars=env_vars.split()) 55 | 56 | for name in os.listdir(join("ci", "templates")): 57 | with open(name, "w") as fh: 58 | fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) 59 | print("Wrote {}".format(name)) 60 | print("DONE.") 61 | -------------------------------------------------------------------------------- /ci/templates/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.4 3 | sudo: false 4 | env: 5 | global: 6 | LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | matrix: 8 | - TOXENV=check 9 | {% for env, config in tox_environments|dictsort %} 10 | - TOXENV={{ env }}{% if config.cover %},coveralls{% endif %} 11 | 12 | {% endfor %} 13 | before_install: 14 | - python --version 15 | - virtualenv --version 16 | - pip --version 17 | - uname -a 18 | - lsb_release -a 19 | install: 20 | - pip install tox 21 | script: 22 | - tox -v 23 | notifications: 24 | email: 25 | on_success: never 26 | on_failure: always 27 | 28 | -------------------------------------------------------------------------------- /ci/templates/appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | build: off 3 | environment: 4 | global: 5 | WITH_COMPILER: "cmd /E:ON /V:ON /C .\\ci\\appveyor-with-compiler.cmd" 6 | matrix: 7 | {% for env, config in tox_environments|dictsort %}{% if env.startswith('2.7') or env.startswith('3.4') or env.startswith('3.3') %} 8 | - TOXENV: "{{ env }}" 9 | TOXPYTHON: "C:\\Python{{ env[:3].replace('.', '') }}\\python.exe" 10 | WINDOWS_SDK_VERSION: "v7.{{ '1' if env[0] == '3' else '0' }}" 11 | PYTHON_HOME: "C:\\Python{{ env[:3].replace('.', '') }}" 12 | PYTHON_VERSION: "{{ env[:3] }}" 13 | PYTHON_ARCH: "32" 14 | - TOXENV: "{{ env }}" 15 | TOXPYTHON: "C:\\Python{{ env[:3].replace('.', '') }}-x64\\python.exe" 16 | WINDOWS_SDK_VERSION: "v7.{{ '1' if env[0] == '3' else '0' }}" 17 | PYTHON_HOME: "C:\\Python{{ env[:3].replace('.', '') }}-x64" 18 | PYTHON_VERSION: "{{ env[:3] }}" 19 | PYTHON_ARCH: "64" 20 | {% endif %}{% endfor %} 21 | init: 22 | - "ECHO %TOXENV%" 23 | - ps: "ls C:\\Python*" 24 | install: 25 | - "powershell ci\\appveyor-bootstrap.ps1" 26 | test_script: 27 | - "%PYTHON_HOME%\\Scripts\\tox --version" 28 | - "%PYTHON_HOME%\\Scripts\\virtualenv --version" 29 | - "%PYTHON_HOME%\\Scripts\\pip --version" 30 | - "%WITH_COMPILER% %PYTHON_HOME%\\Scripts\\tox" 31 | after_test: 32 | - "IF \"%TOXENV:~-8,8%\" == \"-nocover\" %WITH_COMPILER% %TOXPYTHON% setup.py bdist_wheel" 33 | artifacts: 34 | - path: dist\* 35 | 36 | -------------------------------------------------------------------------------- /ci/templates/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | clean, 4 | check, 5 | {% for env in tox_environments|sort %} 6 | {{ env }}, 7 | {% endfor %} 8 | report, 9 | docs 10 | 11 | [testenv] 12 | setenv = 13 | PYTHONPATH={toxinidir}/tests 14 | PYTHONUNBUFFERED=yes 15 | deps = 16 | pytest 17 | pytest-capturelog 18 | commands = 19 | {posargs:py.test -vv --ignore=src} 20 | 21 | [testenv:spell] 22 | setenv = 23 | SPELLCHECK = 1 24 | commands = 25 | sphinx-build -b spelling docs dist/docs 26 | usedevelop = true 27 | deps = 28 | -r{toxinidir}/docs/requirements.txt 29 | sphinxcontrib-spelling 30 | pyenchant 31 | 32 | [testenv:docs] 33 | whitelist_externals = 34 | rm 35 | commands = 36 | rm -rf dist/docs || rmdir /S /Q dist\docs 37 | sphinx-build -b html docs dist/docs 38 | sphinx-build -b linkcheck docs dist/docs 39 | usedevelop = true 40 | deps = 41 | -r{toxinidir}/docs/requirements.txt 42 | 43 | [testenv:configure] 44 | deps = 45 | jinja2 46 | matrix 47 | usedevelop = true 48 | commands = 49 | python bootstrap.py 50 | 51 | [testenv:check] 52 | basepython = python3.4 53 | deps = 54 | docutils 55 | check-manifest 56 | flake8 57 | readme 58 | pygments 59 | usedevelop = true 60 | commands = 61 | python setup.py check --strict --metadata --restructuredtext 62 | check-manifest {toxinidir} 63 | flake8 src 64 | 65 | [testenv:coveralls] 66 | deps = 67 | coveralls 68 | usedevelop = true 69 | commands = 70 | coverage combine 71 | coverage report 72 | coveralls 73 | 74 | [testenv:report] 75 | basepython = python3.4 76 | commands = 77 | coverage combine 78 | coverage report 79 | usedevelop = true 80 | deps = coverage 81 | 82 | [testenv:clean] 83 | commands = coverage erase 84 | usedevelop = true 85 | deps = coverage 86 | 87 | {% for env, config in tox_environments|dictsort %} 88 | [testenv:{{ env }}] 89 | basepython = {{ config.python }} 90 | {% if config.cover or config.env_vars %} 91 | setenv = 92 | {[testenv]setenv} 93 | {% endif %} 94 | {% for var in config.env_vars %} 95 | {{ var }} 96 | {% endfor %} 97 | {% if config.cover %} 98 | WITH_COVERAGE=yes 99 | usedevelop = true 100 | commands = 101 | {posargs:py.test --cov=src --cov-report=term-missing -vv} 102 | {% endif %} 103 | {% if config.cover or config.deps %} 104 | deps = 105 | {[testenv]deps} 106 | {% endif %} 107 | {% if config.cover %} 108 | pytest-cov 109 | {% endif %} 110 | {% for dep in config.deps %} 111 | {{ dep }} 112 | {% endfor %} 113 | 114 | {% endfor %} 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | 7 | extensions = [ 8 | 'sphinx.ext.autodoc', 9 | 'sphinx.ext.autosummary', 10 | 'sphinx.ext.todo', 11 | 'sphinx.ext.coverage', 12 | 'sphinx.ext.ifconfig', 13 | 'sphinx.ext.viewcode', 14 | 'sphinxcontrib.napoleon' 15 | ] 16 | if os.getenv('SPELLCHECK'): 17 | extensions += 'sphinxcontrib.spelling', 18 | spelling_show_suggestions = True 19 | spelling_lang = 'en_US' 20 | 21 | source_suffix = '.rst' 22 | source_encoding = 'utf-8' 23 | master_doc = 'index' 24 | project = 'py3traits' 25 | year = '2015' 26 | author = 'Teppo Perä' 27 | copyright = '{0}, {1}'.format(year, author) 28 | version = release = '1.2.0' 29 | 30 | import sphinx_rtd_theme 31 | html_theme = "sphinx_rtd_theme" 32 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 33 | 34 | pygments_style = 'trac' 35 | templates_path = ['.'] 36 | html_use_smartypants = True 37 | html_last_updated_fmt = '%b %d, %Y' 38 | html_split_index = True 39 | html_sidebars = { 40 | '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], 41 | } 42 | html_short_title = '%s-%s' % (project, version) 43 | html_theme_options = {} 44 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to py3traits's documentation! 2 | ====================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | readme 10 | installation 11 | usage 12 | reference/index 13 | contributing 14 | authors 15 | changelog 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | pip install py3traits 8 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | Python Traits 2 | ============= 3 | 4 | Project can be found from GitHub_. 5 | 6 | About Traits 7 | ------------ 8 | 9 | Traits are classes which contain methods that can be used to extend 10 | other classes, similar to mixins, with exception that traits do not use 11 | inheritance. Instead, traits are composed into other classes. That is; 12 | methods, properties and internal state are copied to master object. 13 | 14 | The point is to improve code reusability by dividing code into simple 15 | building blocks that can be then combined into actual classes. 16 | 17 | There is also a wikipedia article about Traits_. 18 | 19 | Motivation 20 | ---------- 21 | 22 | Traits are meant to be small pieces of behavior (functions or classes) used to 23 | extend other objects in a flexible, dynamic manner. Being small and independent 24 | entities, they are easy to understand, maintain and test. Traits also give an 25 | alternative approach in Python to handle diamond inheritance cases due to fact 26 | that no inheritance is happening at all (not saying multiple inheritance is an 27 | issue in Python). 28 | 29 | The dynamic nature of traits enables some interesting use cases that are 30 | unreachable for conventional inheritance; Any changes made to class or instance 31 | are applied immediately, and they affect whole application. In practice, this 32 | means it is possible to add new functionality to any class or instance and it 33 | can be from your own module, some 3rd party module (e.g Django) or even Python's 34 | own internal classes (e.g. collections.OrderedDict). 35 | 36 | For example, there is feature you would need from framework someone else has 37 | written. Only thing to do is to write traits for those classes that needs to 38 | be updated and extend them. After extending the classes, framework will behave 39 | based on those extended classes. Or if there is need to alter the behavior only 40 | some specific situation (or you just want to be careful), instances of classes 41 | can be extended only. 42 | 43 | Other example would be a situation, where you discover a bug in 3rd party 44 | framework. Now you can create own solution safely, while waiting for the official 45 | patch to appear. Updating the framework code won't override your extensions 46 | as they are applied dynamically. Your changes are only removed when you don't 47 | need them anymore. 48 | 49 | Basics 50 | ------ 51 | 52 | In the simplest form, traits are very similar to any class, that is inherited 53 | by some other class. That is a good way to approach traits in general; If you 54 | can inherit some class, then you can also use it as a trait. Let's look an 55 | example:: 56 | 57 | .. code:: python 58 | from pytraits import extendable 59 | 60 | class Parent: 61 | def parent_function(self): 62 | return "Hello World" 63 | 64 | # Traditional inheritance 65 | class TraditionalChild(Parent): 66 | pass 67 | 68 | @extendable 69 | class ExceptionalChild: 70 | pass 71 | 72 | # Composing as trait 73 | ExceptionalChild.add_traits(Parent) 74 | 75 | In above example both TraditionalChild and Exceptional child have parent_function 76 | method. Only small difference is that ExceptionalChild is inherited from object, 77 | not Parent. 78 | 79 | Effective use 80 | ------------- 81 | 82 | To be effective with traits, one must have some knowledge about how to 83 | write code that can be reused effectively through out the system. It also 84 | helps to know something about good coding practices, for example: 85 | 86 | * `SOLID principles`_ 87 | * `Law of Demeter`_ 88 | 89 | Especially in Law of Demeter, the interfaces tend to bloat because many small 90 | and specific functions needs to be implemented for classes. Traits can help to 91 | keep interfaces more manageable since one trait would contain methods only for 92 | some specific situation. 93 | 94 | Vertical and Horizontal program architecture 95 | -------------------------------------------- 96 | 97 | Traits can really shine, when the application is layered both vertically and 98 | horizontally. Vertical layer basically means different components of the system, 99 | such as: `User`, `Address`, `Account`, `Wallet`, `Car`, `Computer`, etc. 100 | Horinzontal layers would contain: `Security`, `Serialization`, `Rendering`, etc. 101 | One approach with traits for above layering would be to create modules for 102 | horizontal parts and then create trait for each type object needing that 103 | behavior. Finally, in your main module, you would combine traits into classes. 104 | 105 | Example: 106 | `core/account.py` 107 | 108 | .. code:: python 109 | 110 | from pytraits import extendable 111 | 112 | # Very simple address class 113 | @extendable 114 | class Address: 115 | def __init__(self, street, number): 116 | self.__street = street 117 | self.__number = number 118 | 119 | `core/wallet.py` 120 | 121 | .. code:: python 122 | 123 | from pytraits import extendable 124 | 125 | # Very simple wallet class 126 | @extendable 127 | class Wallet: 128 | def __init__(self, money=0): 129 | self.__money = money 130 | 131 | `horizontal/html_rendering.py` 132 | 133 | .. code:: python 134 | 135 | # This is a trait for address rendering 136 | class Address: 137 | def render(self): 138 | data = dict(street=self.__street, number=self.__number) 139 | return "

Address: {street} {number}

".format(**data) 140 | 141 | class Wallet: 142 | def render(self): 143 | # It is extremely straight-forward to render money situation. 144 | return "

Money: 0€

" 145 | 146 | `__main__.py` 147 | 148 | .. code:: python 149 | 150 | from core import Address, Wallet 151 | from horizontal import html_rendering 152 | 153 | Address.add_traits(html_rendering.Address) 154 | Wallet.add_traits(html_rendering.Wallet) 155 | 156 | With this approach, if there becomes a need to support other rendering mechanisms 157 | then just add new module and write rendering specific code there. 158 | 159 | .. _Traits: http://en.wikipedia.org/wiki/Traits_class 160 | .. _SOLID principles: https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) 161 | .. _Law of Demeter: https://en.wikipedia.org/wiki/Law_of_Demeter 162 | .. _GitHub: https://github.com/Debith/py3traits 163 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | pytraits* 8 | -------------------------------------------------------------------------------- /docs/reference/pytraits.rst: -------------------------------------------------------------------------------- 1 | pytraits 2 | ============================= 3 | 4 | .. automodule:: pytraits 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinxcontrib-napoleon 3 | sphinx-py3doc-enhanced-theme 4 | -e . 5 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | builtin 2 | builtins 3 | classmethod 4 | staticmethod 5 | classmethods 6 | staticmethods 7 | args 8 | kwargs 9 | callstack 10 | Changelog 11 | Indices 12 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Composing traits 5 | ---------------- 6 | 7 | Combining classes 8 | ----------------- 9 | 10 | Adding properties dynamically 11 | ----------------------------- 12 | 13 | Properties can be very handy in some situations. Unfortunately, it is not 14 | that straightforward to add new properties to instances, thus pytraits has 15 | a small convenience function named `setproperty`. Using the function should 16 | be as simple as possible as it is quite flexible with ways to use it. 17 | Here is example of the simplest case:: 18 | 19 | from pytraits import setproperty 20 | 21 | class Account: 22 | def __init__(self, money): 23 | self.__money = money 24 | 25 | def money(self): 26 | return self.__money 27 | 28 | def set_money(self, new_money): 29 | self.__money = new_money 30 | 31 | my_account = Account(0) 32 | setproperty(my_account, "money", "set_money") 33 | 34 | 35 | There are more examples found in ``examples/property_is_created_into_instance.py`` 36 | -------------------------------------------------------------------------------- /examples/class_is_composed_from_cherrypicked_method_in_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | @staticmethod 27 | def static_method(): 28 | return 1, 2, 3 29 | 30 | @classmethod 31 | def class_method(cls): 32 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 33 | 34 | def instance_method(self): 35 | return self.public, self._hidden, self.__private 36 | 37 | 38 | # Then, here we do the actual composition, where we cherry-pick each method from 39 | # ExampleTrait and compose them into ExampleClass. 40 | ExampleClass.add_traits(ExampleTrait.instance_method, 41 | ExampleTrait.class_method, 42 | ExampleTrait.static_method) 43 | 44 | 45 | # Here are the proofs that composed methods work as part of new class. 46 | # Also we show that there is no inheritance done for ExampleClass. 47 | assert ExampleClass.__bases__ == (object, ), "Inheritance has occurred!" 48 | assert ExampleClass.static_method() == (1, 2, 3),\ 49 | "Class composition fails with classmethod in class!" 50 | assert ExampleClass.class_method() == (24, 25, 26),\ 51 | "Class composition fails with class method in class!" 52 | assert ExampleClass().class_method() == (24, 25, 26),\ 53 | "Class composition fails with class method in instance!" 54 | assert ExampleClass().instance_method() == (42, 43, 44),\ 55 | "Class composition fails with instance method!" 56 | -------------------------------------------------------------------------------- /examples/class_is_composed_from_cherrypicked_method_in_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | @staticmethod 27 | def static_method(): 28 | return 1, 2, 3 29 | 30 | @classmethod 31 | def class_method(cls): 32 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 33 | 34 | def instance_method(self): 35 | return self.public, self._hidden, self.__private 36 | 37 | 38 | # Then, here we do the actual composition, where we cherry-pick each method from 39 | # ExampleTrait instance and compose them into ExampleClass. 40 | my_trait_instance = ExampleTrait() 41 | ExampleClass.add_traits(my_trait_instance.instance_method, 42 | my_trait_instance.class_method, 43 | my_trait_instance.static_method) 44 | 45 | 46 | # Here are the proofs that composed methods work as part of new class. 47 | # Also we show that there is no inheritance done for ExampleClass. 48 | assert ExampleClass.__bases__ == (object, ), "Inheritance has occurred!" 49 | assert ExampleClass.static_method() == (1, 2, 3),\ 50 | "Class composition fails with classmethod in class!" 51 | assert ExampleClass.class_method() == (24, 25, 26),\ 52 | "Class composition fails with classmethod in class!" 53 | assert ExampleClass().class_method() == (24, 25, 26),\ 54 | "Class composition fails with classmethod in instance!" 55 | assert ExampleClass().instance_method() == (42, 43, 44),\ 56 | "Class composition fails with instance method!" 57 | -------------------------------------------------------------------------------- /examples/class_is_composed_from_cherrypicked_methods_with_rename.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | @staticmethod 27 | def static_method(): 28 | return 1, 2, 3 29 | 30 | @classmethod 31 | def class_method(cls): 32 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 33 | 34 | def instance_method(self): 35 | return self.public, self._hidden, self.__private 36 | 37 | 38 | # Then, here we do the actual composition, where we cherry-pick each method from 39 | # ExampleTrait and compose them into ExampleClass. While we do that, we also 40 | # specify function names as keyword parameters to change the name of the 41 | # functions. 42 | ExampleClass.add_traits(ExampleTrait.instance_method, 43 | ExampleTrait.class_method, 44 | ExampleTrait.static_method, 45 | instance_method="renamed_instance_method", 46 | class_method="renamed_class_method", 47 | static_method="renamed_static_method") 48 | 49 | # Here are the proofs that composed methods work as part of new class. 50 | assert ExampleClass.renamed_static_method() == (1, 2, 3),\ 51 | "Class composition fails with classmethod in class!" 52 | assert ExampleClass.renamed_class_method() == (24, 25, 26),\ 53 | "Class composition fails with class method in class!" 54 | assert ExampleClass().renamed_class_method() == (24, 25, 26), \ 55 | "Class composition fails with class method in instance!" 56 | assert ExampleClass().renamed_instance_method() == (42, 43, 44),\ 57 | "Class composition fails with instance method!" 58 | -------------------------------------------------------------------------------- /examples/class_is_composed_from_cherrypicked_property_in_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # only instance variables. Composed property will have access to all 8 | # these variables. 9 | @extendable 10 | class ExampleClass: 11 | def __init__(self): 12 | self.public = 42 13 | self._hidden = 43 14 | self.__private = 44 15 | 16 | 17 | # Then we create a class which contains different types of methods that will be 18 | # transferred as a part of the class above. Note that ExampleTrait requires target 19 | # object to contain instance variables, thus it won't work as a stand-alone object. 20 | class ExampleTrait: 21 | @property 22 | def trait_property(self): 23 | return self.public, self._hidden, self.__private 24 | 25 | @trait_property.setter 26 | def trait_property(self, new_value): 27 | self.public, self._hidden, self.__private = new_value 28 | 29 | @trait_property.deleter 30 | def trait_property(self): 31 | self.public, self._hidden, self.__private = (42, 43, 44) 32 | 33 | 34 | # Then add the property as a part of our new class simply by referring it. 35 | ExampleClass.add_traits(ExampleTrait.trait_property) 36 | 37 | 38 | # Here are the proofs that composed property works as part of new class. 39 | # Also we show that there is no inheritance done for ExampleClass instance. 40 | example_instance = ExampleClass() 41 | assert ExampleClass.__bases__ == (object, ), "Inheritance has occurred!" 42 | assert example_instance.trait_property == (42, 43, 44),\ 43 | "Cherry-picked property not working in new class!" 44 | 45 | # We also demonstrate that we can alter the values through the property 46 | example_instance.trait_property = (142, 143, 144) 47 | assert example_instance.trait_property == (142, 143, 144),\ 48 | "Cherry-picked property's setter not working in new class!" 49 | 50 | # Finally, we can delete property's content 51 | del example_instance.trait_property 52 | assert example_instance.trait_property == (42, 43, 44),\ 53 | "Cherry-picked property's deleter not working in new class!" 54 | -------------------------------------------------------------------------------- /examples/class_is_composed_from_cherrypicked_property_in_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # only instance variables. Composed property will have access to all 8 | # these variables. 9 | @extendable 10 | class ExampleClass: 11 | def __init__(self): 12 | self.public = 42 13 | self._hidden = 43 14 | self.__private = 44 15 | 16 | 17 | # Then we create a class which contains different types of methods that will be 18 | # transferred as a part of the class above. Note that ExampleTrait requires target 19 | # object to contain instance variables, thus it won't work as a stand-alone object. 20 | class ExampleTrait: 21 | @property 22 | def trait_property(self): 23 | return self.public, self._hidden, self.__private 24 | 25 | @trait_property.setter 26 | def trait_property(self, new_value): 27 | self.public, self._hidden, self.__private = new_value 28 | 29 | @trait_property.deleter 30 | def trait_property(self): 31 | self.public, self._hidden, self.__private = (42, 43, 44) 32 | 33 | 34 | # Create instance out of the trait class. Now we need to notice that we need 35 | # to refer to instance's class to get the property and transfer it to new 36 | # location. Using directly my_trait_instance.trait_property would naturally 37 | # invoke retrieval of the values (which in this case would not even exist and 38 | # and would raise an error). 39 | my_trait_instance = ExampleTrait() 40 | ExampleClass.add_traits(my_trait_instance.__class__.trait_property) 41 | 42 | 43 | # Here are the proofs that composed property works as part of new class. 44 | # Also we show that there is no inheritance done for ExampleClass instance. 45 | example_instance = ExampleClass() 46 | assert ExampleClass.__bases__ == (object, ), "Inheritance has occurred!" 47 | assert example_instance.trait_property == (42, 43, 44),\ 48 | "Cherry-picked property not working in new class!" 49 | 50 | # We also demonstrate that we can alter the values through the property 51 | example_instance.trait_property = (142, 143, 144) 52 | assert example_instance.trait_property == (142, 143, 144),\ 53 | "Cherry-picked property's setter not working in new class!" 54 | 55 | # Finally, we can delete property's content 56 | del example_instance.trait_property 57 | assert example_instance.trait_property == (42, 43, 44),\ 58 | "Cherry-picked property's deleter not working in new class!" 59 | -------------------------------------------------------------------------------- /examples/class_is_composed_from_other_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | TEST_DATA = 123 27 | 28 | @staticmethod 29 | def static_method(): 30 | return 1, 2, 3 31 | 32 | @classmethod 33 | def class_method(cls): 34 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 35 | 36 | def instance_method(self): 37 | return self.public, self._hidden, self.__private 38 | 39 | @property 40 | def value(self): 41 | return self.public, self._hidden, self.__private 42 | 43 | 44 | # Compose new methods and property from ExampleTrait into ExampleClass. 45 | ExampleClass.add_traits(ExampleTrait) 46 | 47 | 48 | # Here are the proofs that composed methods work as part of new class. Also we show 49 | # that there is no inheritance done for ExampleClass. 50 | assert ExampleClass.__bases__ == (object, ), "Inheritance has occurred!" 51 | assert ExampleClass.static_method() == (1, 2, 3),\ 52 | "Class composition fails with static method!" 53 | assert ExampleClass.class_method() == (24, 25, 26),\ 54 | "Class composition fails with classmethod!" 55 | assert ExampleClass().class_method() == (24, 25, 26),\ 56 | "Class composition fails with classmethod in instance!" 57 | assert ExampleClass().instance_method() == (42, 43, 44),\ 58 | "Class composition fails with instance method!" 59 | assert ExampleClass().value == (42, 43, 44),\ 60 | "Class composition fails with property!" 61 | -------------------------------------------------------------------------------- /examples/class_is_composed_from_other_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | @staticmethod 27 | def static_method(): 28 | return 1, 2, 3 29 | 30 | @classmethod 31 | def class_method(cls): 32 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 33 | 34 | def instance_method(self): 35 | return self.public, self._hidden, self.__private 36 | 37 | @property 38 | def value(self): 39 | return self.public, self._hidden, self.__private 40 | 41 | 42 | # Compose new methods and property from ExampleTrait instance into ExampleClass. 43 | ExampleClass.add_traits(ExampleTrait()) 44 | 45 | 46 | # Here are the proofs that composed methods work as part of new class. Also we show 47 | # that there is no inheritance done for ExampleClass. 48 | assert ExampleClass.__bases__ == (object, ), "Inheritance has occurred!" 49 | assert ExampleClass.static_method() == (1, 2, 3),\ 50 | "Class composition fails with static method!" 51 | assert ExampleClass.class_method() == (24, 25, 26),\ 52 | "Class composition fails with classmethod!" 53 | assert ExampleClass().class_method() == (24, 25, 26),\ 54 | "Class composition fails with classmethod in instance!" 55 | assert ExampleClass().instance_method() == (42, 43, 44),\ 56 | "Class composition fails with instance method!" 57 | assert ExampleClass().value == (42, 43, 44),\ 58 | "Class composition fails with property!" 59 | -------------------------------------------------------------------------------- /examples/composition_in_alternative_syntax.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | @staticmethod 27 | def static_method(): 28 | return 1, 2, 3 29 | 30 | @classmethod 31 | def class_method(cls): 32 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 33 | 34 | def instance_method(self): 35 | return self.public, self._hidden, self.__private 36 | 37 | @property 38 | def value(self): 39 | return self.public, self._hidden, self.__private 40 | 41 | 42 | def modify_instance(self, public, hidden, private): 43 | self.public, self._hidden, self.__private = public, hidden, private 44 | 45 | 46 | # Instead of giving directly the function or class object, we can also give 47 | # refer to objects by their name. 48 | example_instance1 = ExampleClass() 49 | example_instance1.add_traits(ExampleTrait, "value", "class_method", "instance_method") 50 | example_instance1.add_traits(modify_instance) 51 | 52 | # Modify the content for both instances 53 | example_instance1.modify_instance(10, 20, 30) 54 | 55 | # Here are the proofs that composed methods work as part of new instances. 56 | assert hasattr(example_instance1, "value"),\ 57 | "Failed to copy 'value' property" 58 | assert hasattr(example_instance1, "class_method"),\ 59 | "Failed to copy 'class_method' function" 60 | assert hasattr(example_instance1, "instance_method"),\ 61 | "Failed to copy 'class_method' function" 62 | assert example_instance1.instance_method() == (10, 20, 30) 63 | -------------------------------------------------------------------------------- /examples/extendable_function_class_vs_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Using decorator to add trait support for objects is the recommended way 7 | # to do. Here we add add_traits function to our example object. 8 | @extendable 9 | class MyExample: 10 | pass 11 | 12 | assert hasattr(MyExample, 'add_traits'), "Failed to add 'add_traits' function to MyExample" 13 | 14 | 15 | # Here we define a trait class, that we use to cherry-pick some functions 16 | # to our new object. 17 | class Traits: 18 | def for_all_classes(self): 19 | pass 20 | 21 | def for_single_instance(self): 22 | pass 23 | 24 | 25 | # Here we add the method into MyExample CLASS. 26 | MyExample.add_traits(Traits.for_all_classes) 27 | 28 | # Here is the proof that new method is there. 29 | assert hasattr(MyExample, 'for_all_classes') 30 | 31 | 32 | # In this second case, we add new method into INSTANCE of MyExample class. 33 | instance = MyExample() 34 | instance.add_traits(Traits.for_single_instance) 35 | 36 | # Here is the proofs that method is found from the instance and not from class. 37 | assert hasattr(instance, 'for_single_instance') 38 | assert not hasattr(MyExample, 'for_single_instance') 39 | -------------------------------------------------------------------------------- /examples/function_is_composed_as_a_part_of_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed functions will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | def new_method(self): 22 | return self.public, self._hidden, self.__private 23 | 24 | 25 | def new_class_function(cls): 26 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 27 | 28 | 29 | def new_static_function(): 30 | return 1, 2, 3 31 | 32 | 33 | # Compose cherry-picked functions into ExampleClass. 34 | ExampleClass.add_traits(new_method, new_class_function, new_static_function) 35 | 36 | 37 | # Here are the proofs that composed functions work as part of new class. 38 | assert ExampleClass.new_static_function() == (1, 2, 3),\ 39 | "Class composition fails with class method in class!" 40 | assert ExampleClass.new_class_function() == (24, 25, 26),\ 41 | "Class composition fails with classmethod in class!" 42 | assert ExampleClass().new_class_function() == (24, 25, 26),\ 43 | "Class composition fails with classmethod in instance!" 44 | assert ExampleClass().new_method() == (42, 43, 44),\ 45 | "Class composition fails with instance method!" 46 | -------------------------------------------------------------------------------- /examples/function_is_composed_as_a_part_of_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed functions will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | def new_method(self): 22 | return self.public, self._hidden, self.__private 23 | 24 | 25 | def new_class_function(cls): 26 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 27 | 28 | 29 | def new_static_function(): 30 | return 1, 2, 3 31 | 32 | 33 | # Create instance of ExampleClass and compose cherry-picked functions into it. 34 | example_instance = ExampleClass() 35 | example_instance.add_traits(new_method, new_class_function, new_static_function) 36 | 37 | 38 | # Here are the proofs that composed functions work as part of new instance. Also 39 | # we demonstrate that original class is still untouched. 40 | assert example_instance.new_static_function() == (1, 2, 3),\ 41 | "Instance composition fails with static method in instance!" 42 | assert example_instance.new_class_function() == (24, 25, 26),\ 43 | "Instance composition fails with class method in instance!" 44 | assert example_instance.new_method() == (42, 43, 44),\ 45 | "Instance composition fails with instance method!" 46 | assert not hasattr(ExampleClass, "new_static_function"),\ 47 | "Instance composition fails due to class has changed!" 48 | assert not hasattr(ExampleClass, "new_class_function"),\ 49 | "Instance composition fails due to class has changed!" 50 | assert not hasattr(ExampleClass, "new_method"),\ 51 | "Instance composition fails due to class has changed!" 52 | -------------------------------------------------------------------------------- /examples/instance_is_composed_from_cherrypicked_method_in_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | @staticmethod 27 | def static_method(): 28 | return 1, 2, 3 29 | 30 | @classmethod 31 | def class_method(cls): 32 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 33 | 34 | def instance_method(self): 35 | return self.public, self._hidden, self.__private 36 | 37 | 38 | # Create instance of target class and cherry-pick methods from ExampleTrait class. 39 | example_instance = ExampleClass() 40 | example_instance.add_traits(ExampleTrait.instance_method, 41 | ExampleTrait.class_method, 42 | ExampleTrait.static_method) 43 | 44 | 45 | # Here are the proofs that composed methods work as part of new class. 46 | assert example_instance.instance_method() == (42, 43, 44),\ 47 | "Instance composition fails with instance method!" 48 | assert example_instance.class_method() == (24, 25, 26),\ 49 | "Instance composition fails with class method in instance!" 50 | assert example_instance.static_method() == (1, 2, 3),\ 51 | "Instance composition fails with class method in instance!" 52 | assert not hasattr(ExampleClass, "new_static_function"),\ 53 | "Instance composition fails due to class has changed!" 54 | assert not hasattr(ExampleClass, "new_class_function"),\ 55 | "Instance composition fails due to class has changed!" 56 | assert not hasattr(ExampleClass, "new_method"),\ 57 | "Instance composition fails due to class has changed!" 58 | -------------------------------------------------------------------------------- /examples/instance_is_composed_from_cherrypicked_methods_with_rename.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | @staticmethod 27 | def static_method(): 28 | return 1, 2, 3 29 | 30 | @classmethod 31 | def class_method(cls): 32 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 33 | 34 | def instance_method(self): 35 | return self.public, self._hidden, self.__private 36 | 37 | 38 | # Then, before composition, we create instance out of the class. After that 39 | # we do the actual composition, where we cherry-pick each method from 40 | # ExampleTrait and compose them into ExampleClass. While we do that, we also 41 | # specify function names as keyword parameters to change the name of the 42 | # functions. 43 | example_instance = ExampleClass() 44 | example_instance.add_traits(ExampleTrait.instance_method, 45 | ExampleTrait.class_method, 46 | ExampleTrait.static_method, 47 | instance_method="renamed_instance_method", 48 | class_method="renamed_class_method", 49 | static_method="renamed_static_method") 50 | 51 | # Here are the proofs that composed methods work as part of new instance. 52 | assert example_instance.renamed_static_method() == (1, 2, 3),\ 53 | "Class composition fails with classmethod in class!" 54 | assert example_instance.renamed_class_method() == (24, 25, 26),\ 55 | "Class composition fails with class method in class!" 56 | assert example_instance.renamed_instance_method() == (42, 43, 44),\ 57 | "Class composition fails with instance method!" 58 | -------------------------------------------------------------------------------- /examples/instance_is_composed_from_cherrypicked_property_in_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # only instance variables. Composed property will have access to all 8 | # these variables. 9 | @extendable 10 | class ExampleClass: 11 | def __init__(self): 12 | self.public = 42 13 | self._hidden = 43 14 | self.__private = 44 15 | 16 | 17 | # Then we create a class which contains different types of methods that will be 18 | # transferred as a part of the class above. Note that ExampleTrait requires target 19 | # object to contain instance variables, thus it won't work as a stand-alone object. 20 | class ExampleTrait: 21 | @property 22 | def trait_property(self): 23 | return self.public, self._hidden, self.__private 24 | 25 | @trait_property.setter 26 | def trait_property(self, new_value): 27 | self.public, self._hidden, self.__private = new_value 28 | 29 | @trait_property.deleter 30 | def trait_property(self): 31 | self.public, self._hidden, self.__private = (42, 43, 44) 32 | 33 | 34 | # Then add the property as a part of our new class simply by referring it. 35 | example_instance = ExampleClass() 36 | example_instance.add_traits(ExampleTrait.trait_property) 37 | 38 | 39 | # Here are the proofs that composed property works as part of new class. 40 | assert example_instance.trait_property == (42, 43, 44),\ 41 | "Cherry-picked property not working in instance!" 42 | 43 | # We also demonstrate that we can alter the values through the property 44 | example_instance.trait_property = (142, 143, 144) 45 | assert example_instance.trait_property == (142, 143, 144),\ 46 | "Cherry-picked property's setter not working in instance!" 47 | 48 | # Finally, we can delete property's content 49 | del example_instance.trait_property 50 | assert example_instance.trait_property == (42, 43, 44),\ 51 | "Cherry-picked property's deleter not working in instance!" 52 | 53 | # And finally, original class is still unaffected. 54 | assert not hasattr(ExampleClass, "trait_property"),\ 55 | "Cherry-picked property has leaked into class!" 56 | -------------------------------------------------------------------------------- /examples/instance_is_composed_from_cherrypicked_property_in_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # only instance variables. Composed property will have access to all 8 | # these variables. 9 | @extendable 10 | class ExampleClass: 11 | def __init__(self): 12 | self.public = 42 13 | self._hidden = 43 14 | self.__private = 44 15 | 16 | 17 | # Then we create a class which contains different types of methods that will be 18 | # transferred as a part of the class above. Note that ExampleTrait requires target 19 | # object to contain instance variables, thus it won't work as a stand-alone object. 20 | class ExampleTrait: 21 | @property 22 | def trait_property(self): 23 | return self.public, self._hidden, self.__private 24 | 25 | @trait_property.setter 26 | def trait_property(self, new_value): 27 | self.public, self._hidden, self.__private = new_value 28 | 29 | @trait_property.deleter 30 | def trait_property(self): 31 | self.public, self._hidden, self.__private = (42, 43, 44) 32 | 33 | 34 | # Create instance out of the trait class. Now we need to notice that we need 35 | # to refer to instance's class to get the property and transfer it to new 36 | # location. Using directly my_trait_instance.trait_property would naturally 37 | # invoke retrieval of the values (which in this case would not even exist and 38 | # and would raise an error). 39 | example_instance = ExampleClass() 40 | my_trait_instance = ExampleTrait() 41 | example_instance.add_traits(my_trait_instance.__class__.trait_property) 42 | 43 | 44 | # Here are the proofs that composed property works as part of new class. 45 | # Also we show that there is no inheritance done for ExampleClass instance. 46 | assert example_instance.trait_property == (42, 43, 44),\ 47 | "Cherry-picked property not working in new class!" 48 | 49 | # We also demonstrate that we can alter the values through the property 50 | example_instance.trait_property = (142, 143, 144) 51 | assert example_instance.trait_property == (142, 143, 144),\ 52 | "Cherry-picked property's setter not working in new class!" 53 | 54 | # Finally, we can delete property's content 55 | del example_instance.trait_property 56 | assert example_instance.trait_property == (42, 43, 44),\ 57 | "Cherry-picked property's deleter not working in new class!" 58 | 59 | # And finally, original class is still unaffected. 60 | assert not hasattr(ExampleClass, "trait_property"),\ 61 | "Cherry-picked property has leaked into class!" 62 | -------------------------------------------------------------------------------- /examples/instance_is_composed_from_other_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | @staticmethod 27 | def static_method(): 28 | return 1, 2, 3 29 | 30 | @classmethod 31 | def class_method(cls): 32 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 33 | 34 | def instance_method(self): 35 | return self.public, self._hidden, self.__private 36 | 37 | 38 | # Create composition from instance of ExampleClass and ExampleTrait class. 39 | example_instance = ExampleClass() 40 | example_instance.add_traits(ExampleTrait) 41 | 42 | 43 | # Here are the proofs that composed methods work as part of new instance. Also 44 | # we demonstrate that original class is still untouched. 45 | assert example_instance.static_method() == (1, 2, 3),\ 46 | "Class composition fails with static method!" 47 | assert example_instance.class_method() == (24, 25, 26),\ 48 | "Class composition fails with class method!" 49 | assert example_instance.instance_method() == (42, 43, 44),\ 50 | "Class composition fails with instance method!" 51 | assert not hasattr(ExampleClass, "new_static_function"),\ 52 | "Instance composition fails due to class has changed!" 53 | assert not hasattr(ExampleClass, "new_class_function"),\ 54 | "Instance composition fails due to class has changed!" 55 | assert not hasattr(ExampleClass, "new_method"),\ 56 | "Instance composition fails due to class has changed!" 57 | -------------------------------------------------------------------------------- /examples/instance_is_composed_from_other_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import extendable 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # class variables and instance variables. Composed methods will have 8 | # access to all these variables. 9 | @extendable 10 | class ExampleClass: 11 | PUBLIC = 24 12 | _HIDDEN = 25 13 | __PRIVATE = 26 14 | 15 | def __init__(self): 16 | self.public = 42 17 | self._hidden = 43 18 | self.__private = 44 19 | 20 | 21 | # Then we create a class which contains different types of methods that will be 22 | # transferred as a part of the class above. Note that ExampleTrait requires target 23 | # object to contain class variables and instance variables, thus it won't work as a 24 | # stand-alone object. 25 | class ExampleTrait: 26 | @staticmethod 27 | def static_method(): 28 | return 1, 2, 3 29 | 30 | @classmethod 31 | def class_method(cls): 32 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 33 | 34 | def instance_method(self): 35 | return self.public, self._hidden, self.__private 36 | 37 | 38 | # Create composition from instance of ExampleClass and instance of ExampleTrait. 39 | example_instance = ExampleClass() 40 | my_trait_instance = ExampleTrait() 41 | example_instance.add_traits(my_trait_instance) 42 | 43 | 44 | # Here are the proofs that composed methods work as part of new instance. Also 45 | # we demonstrate that original class is still untouched. 46 | assert example_instance.static_method() == (1, 2, 3),\ 47 | "Class composition fails with static method!" 48 | assert example_instance.class_method() == (24, 25, 26),\ 49 | "Class composition fails with class method!" 50 | assert example_instance.instance_method() == (42, 43, 44),\ 51 | "Class composition fails with instance method!" 52 | assert not hasattr(ExampleClass, "new_static_function"),\ 53 | "Instance composition fails due to class has changed!" 54 | assert not hasattr(ExampleClass, "new_class_function"),\ 55 | "Instance composition fails due to class has changed!" 56 | assert not hasattr(ExampleClass, "new_method"),\ 57 | "Instance composition fails due to class has changed!" 58 | -------------------------------------------------------------------------------- /examples/multiple_traits_composed_into_new_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import combine_class 4 | 5 | 6 | # In the beginning, we don't have own object even, we just have traits. 7 | class FirstExampleTrait: 8 | @staticmethod 9 | def static_method(): 10 | return 1, 2, 3 11 | 12 | 13 | class SecondExampleTrait: 14 | @classmethod 15 | def class_method(cls): 16 | return cls.PUBLIC, cls._HIDDEN, cls.__PRIVATE 17 | 18 | def instance_method(self): 19 | return self.public, self._hidden, self.__private 20 | 21 | 22 | class ThirdExampleTrait: 23 | @property 24 | def value(self): 25 | return self.public, self._hidden, self.__private 26 | 27 | @value.setter 28 | def value(self, new_values): 29 | self.public, self._hidden, self.__private = new_values 30 | 31 | 32 | # Now, we create completely new class out of the traits. 33 | ExampleClass = combine_class("ExampleClass", FirstExampleTrait, 34 | SecondExampleTrait, 35 | ThirdExampleTrait) 36 | 37 | # Create new instance and update its values 38 | example_instance = ExampleClass() 39 | example_instance.value = (42, 43, 44) 40 | 41 | # Also fill in the class variables. 42 | ExampleClass.PUBLIC = 24 43 | ExampleClass._HIDDEN = 25 44 | ExampleClass._ExampleClass__PRIVATE = 26 45 | 46 | 47 | # Here are the proofs that composed methods work as part of new class. Also we show 48 | # that there is no inheritance done for ExampleClass. 49 | assert ExampleClass.__bases__ == (object, ), "Inheritance has occurred!" 50 | assert ExampleClass.__name__ == "ExampleClass" 51 | assert ExampleClass.static_method() == (1, 2, 3),\ 52 | "Class composition fails with static method!" 53 | assert ExampleClass.class_method() == (24, 25, 26),\ 54 | "Class composition fails with classmethod!" 55 | assert example_instance.class_method() == (24, 25, 26),\ 56 | "Class composition fails with classmethod in instance!" 57 | assert example_instance.instance_method() == (42, 43, 44),\ 58 | "Class composition fails with instance method!" 59 | assert example_instance.value == (42, 43, 44),\ 60 | "Class composition fails with property!" 61 | -------------------------------------------------------------------------------- /examples/property_is_created_into_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | from pytraits import setproperty 4 | 5 | 6 | # Let's start by creating a simple class with some values. It contains 7 | # only instance variables. Added property will have access to all 8 | # these variables. 9 | class ExampleClass: 10 | def __init__(self): 11 | self.public = 42 12 | self._hidden = 43 13 | self.__private = 44 14 | 15 | def set_all(self, values): 16 | self.public, self._hidden, self.__private = values 17 | 18 | def get_all(self): 19 | return self.public, self._hidden, self.__private 20 | 21 | def del_all(self): 22 | self.public, self._hidden, self.__private = (42, 43, 44) 23 | 24 | # Create new instances for each situation 25 | example1 = ExampleClass() 26 | example2 = ExampleClass() 27 | example3 = ExampleClass() 28 | example4 = ExampleClass() 29 | 30 | # Use functions from class 31 | setproperty(example1, ExampleClass.get_all, 32 | ExampleClass.set_all, 33 | ExampleClass.del_all, name="all") 34 | 35 | # Create property using functions from other instance 36 | setproperty(example2, example1.get_all, 37 | example1.set_all, 38 | example1.del_all, name="all") 39 | 40 | # Create property for current instance 41 | setproperty(example3, "get_all", "set_all", name="all") 42 | 43 | # Create property referring functions in other instance 44 | setproperty(example4, "get_all", "set_all", "del_all", example1, name="all") 45 | 46 | 47 | # All instances have their own independent properties 48 | example1.all = 1, 2, 3 49 | example2.all = 10, 20, 30 50 | example3.all = 100, 200, 300 51 | example4.all = 1000, 2000, 3000 52 | 53 | # Demonstrate that each instance is modified properly 54 | assert example1.all == (1, 2, 3), "Values were %d, %d, %d" % example1.all 55 | assert example2.all == (10, 20, 30), "Values were %d, %d, %d" % example2.all 56 | assert example3.all == (100, 200, 300), "Values were %d, %d, %d" % example3.all 57 | assert example4.all == (1000, 2000, 3000), "Values were %d, %d, %d" % example4.all 58 | 59 | # And deleting works also for properties independently 60 | del example1.all 61 | del example4.all 62 | 63 | # Verify that only touched instances are handled and rest remains intact. 64 | assert example1.all == (42, 43, 44), "Values were %d, %d, %d" % example1.all 65 | assert example2.all == (10, 20, 30), "Values were %d, %d, %d" % example2.all 66 | assert example3.all == (100, 200, 300), "Values were %d, %d, %d" % example3.all 67 | assert example4.all == (42, 43, 44), "Values were %d, %d, %d" % example4.all 68 | 69 | 70 | # Acknowledge the fact that type of instances do change because of property assignment. 71 | # This is an unfortunate tradeof of making properties instance specific. The limitation 72 | # comes from the fact that descriptors work only in classes and when doing instance specific 73 | # property, we need clone the class the instance is using. Result is that we have little 74 | # bit different class of same name for some instances. 75 | assert example1.__class__.__name__ == "ExampleClass", "Class names should always match!" 76 | assert isinstance(example1, ExampleClass), "Instance must still be of class ExampleClass!" 77 | assert issubclass(example1.__class__, ExampleClass), "Unexpectedly not a subclass of original class!" 78 | assert not hasattr(ExampleClass, "trait_property"), "New property has leaked into class!" 79 | 80 | # It is good to understand that the instance's class and original class are not same anymore. 81 | assert example1.__class__ != ExampleClass, "Unexpectedly classes are matching!" 82 | -------------------------------------------------------------------------------- /examples/pyqt_builtins_composed_into_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | from pytraits import add_traits, setproperty 7 | 8 | try: 9 | from PyQt5.QtWidgets import QSpinBox, QApplication, QLineEdit 10 | 11 | # We need an application to get things done. 12 | app = QApplication(sys.argv) 13 | 14 | # Define a controller that encapsulates handling of two UI components that 15 | # are closely linked together. 16 | class UiController: 17 | pass 18 | 19 | # Create two UI widgets 20 | spinner = QSpinBox() 21 | line_edit = QLineEdit() 22 | 23 | # Update controller by adding functions and property from builtin components. 24 | add_traits(UiController, line_edit.text, line_edit.setText) 25 | setproperty(UiController, spinner.value, spinner.setValue, name="answer") 26 | 27 | # Create controller 28 | ctrl = UiController() 29 | ctrl.answer = 42 30 | ctrl.setText("Life, Universe...") 31 | 32 | # And here's the proof that properties and functions are working and they 33 | # modify correct widgets. 34 | assert ctrl.answer == 42, "Spinner property is unaware of the answer!" 35 | assert ctrl.text() == "Life, Universe...", "Text property is unaware of nature of the answer!" 36 | assert ctrl.answer == spinner.value(), "UiController and QSpinBox widget are not communicating!" 37 | assert ctrl.text() == line_edit.text(), "UiController and QLineEdit widget are not communicating!" 38 | except ImportError: 39 | pass # Skipping since PyQt is not available 40 | -------------------------------------------------------------------------------- /examples/pyqt_builtins_composed_into_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | from pytraits import add_traits, setproperty 7 | 8 | try: 9 | from PyQt5.QtWidgets import QSpinBox, QApplication, QLineEdit 10 | 11 | # We need an application to get things done. 12 | app = QApplication(sys.argv) 13 | 14 | # Define a controller that encapsulates handling of two UI components that 15 | # are closely linked together. 16 | class UiController: 17 | pass 18 | 19 | # Create two UI widgets 20 | spinner = QSpinBox() 21 | line_edit = QLineEdit() 22 | 23 | # Create controller and update the instance with new property and functions. 24 | ctrl = UiController() 25 | add_traits(ctrl, line_edit.text, line_edit.setText) 26 | setproperty(ctrl, spinner.value, spinner.setValue, name="answer") 27 | 28 | # Then modify the widgets through the controller. 29 | ctrl.answer = 42 30 | ctrl.setText("Life, Universe...") 31 | 32 | # And here's the proof that properties and functions are working and they 33 | # modify correct widgets. 34 | assert ctrl.answer == 42, "Spinner property is unaware of the answer!" 35 | assert ctrl.text() == "Life, Universe...", "Text property is unaware of nature of the answer!" 36 | assert ctrl.answer == spinner.value(), "UiController and QSpinBox widget are not communicating!" 37 | assert ctrl.text() == line_edit.text(), "UiController and QLineEdit widget are not communicating!" 38 | except ImportError: 39 | pass # Skipping since PyQt is not available 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [aliases] 5 | release = register clean --all sdist bdist_wheel upload 6 | 7 | [flake8] 8 | max-line-length = 140 9 | exclude = examples/*,tests/*,*/migrations/*,*/south_migrations/* 10 | 11 | [bumpversion] 12 | current_version = 0.1.0 13 | files = setup.py docs/conf.py src/pytraits/__init__.py 14 | commit = True 15 | tag = True 16 | 17 | [pytest] 18 | norecursedirs = 19 | .git 20 | .tox 21 | dist 22 | build 23 | south_migrations 24 | migrations 25 | python_files = 26 | examples/*.py 27 | tests/*.py 28 | addopts = 29 | -rxEfs 30 | --strict 31 | --ignore=docs/conf.py 32 | --ignore=setup.py 33 | --ignore=ci 34 | --doctest-modules 35 | --doctest-glob=\*.rst 36 | --tb=short 37 | 38 | [isort] 39 | force_single_line=True 40 | line_length=120 41 | known_first_party=pytraits 42 | default_section=THIRDPARTY 43 | forced_separate=test_pytraits 44 | 45 | [matrix] 46 | 47 | python_versions = 48 | 3.3 49 | 3.4 50 | 51 | dependencies = 52 | coverage_flags = 53 | : true 54 | nocover: false 55 | 56 | environment_variables = 57 | - 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | import io 4 | import os 5 | import re 6 | from glob import glob 7 | from os.path import basename 8 | from os.path import dirname 9 | from os.path import join 10 | from os.path import relpath 11 | from os.path import splitext 12 | 13 | from setuptools import find_packages 14 | from setuptools import setup 15 | 16 | 17 | def read(*names, **kwargs): 18 | return io.open( 19 | join(dirname(__file__), *names), 20 | encoding=kwargs.get('encoding', 'utf8') 21 | ).read() 22 | 23 | 24 | setup( 25 | name='py3traits', 26 | version='1.2.1', 27 | license='Apache License 2', 28 | description='Trait support for Python 3', 29 | long_description='%s\n%s' % (read('README.rst'), re.sub(':obj:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), 30 | author='Teppo Per\xc3\xa4', 31 | author_email='debith-dev@outlook.com', 32 | url='https://github.com/Debith/py3traits', 33 | packages=find_packages('src'), 34 | package_dir={'': 'src'}, 35 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 36 | include_package_data=True, 37 | zip_safe=False, 38 | classifiers=[ 39 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 40 | 'Development Status :: 5 - Production/Stable', 41 | 'Intended Audience :: Developers', 42 | 'License :: OSI Approved :: BSD License', 43 | 'Operating System :: Unix', 44 | 'Operating System :: Microsoft :: Windows', 45 | 'Programming Language :: Python', 46 | 'Programming Language :: Python :: 3', 47 | 'Programming Language :: Python :: 3.3', 48 | 'Programming Language :: Python :: 3.4', 49 | 'Topic :: Utilities', 50 | ], 51 | keywords=['traits'], 52 | install_requires=[], 53 | extras_require={}, 54 | entry_points={}, 55 | ) 56 | -------------------------------------------------------------------------------- /src/pytraits/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.support import Singleton, Factory, type_safe, type_converted 20 | from pytraits.combiner import combine_class 21 | from pytraits.extendable import extendable 22 | from pytraits.setproperty import setproperty 23 | from pytraits.trait_composer import add_traits 24 | 25 | __version__ = "1.2.1" 26 | __all__ = ["Singleton", "Factory", "combine_class", "extendable", "add_traits", 27 | "type_safe", "type_converted", "setproperty"] 28 | -------------------------------------------------------------------------------- /src/pytraits/combiner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.trait_composer import add_traits 20 | 21 | 22 | def combine_class(class_name: str, *traits, **resolved_conflicts): 23 | """ This function composes new class out of any number of traits. 24 | 25 | Args: 26 | class_name: Name of the new class. 27 | traits: Collection of traits, such as functions, classes or instances. 28 | 29 | Keyword Args: 30 | name of trait (str): new name 31 | 32 | Example of combining multiple classes to one: 33 | 34 | >>> class One: 35 | ... def first(self): return 1 36 | ... 37 | >>> class Two: 38 | ... def second(self): return 2 39 | ... 40 | >>> class Three: 41 | ... def third(self): return 3 42 | ... 43 | >>> Combination = combine_class("Combination", One, Two, Three) 44 | >>> instance = Combination() 45 | >>> instance.first(), instance.second(), instance.third() 46 | (1, 2, 3) 47 | >>> instance.__class__.__name__ 48 | 'Combination' 49 | """ 50 | NewClass = type(class_name, (object,), {}) 51 | add_traits(NewClass, *traits) 52 | return NewClass 53 | 54 | 55 | if __name__ == '__main__': 56 | import doctest 57 | doctest.testmod() 58 | -------------------------------------------------------------------------------- /src/pytraits/core/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | 4 | from .base import TraitFactory 5 | from .primitives.trait_object import TraitObject 6 | import pytraits.core.composing # NOQA 7 | 8 | __all__ = ["TraitObject", "TraitFactory"] 9 | -------------------------------------------------------------------------------- /src/pytraits/core/base/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.support import Factory 20 | from .inspectors import TraitSourceInspector, TraitTargetInspector 21 | 22 | 23 | class TraitFactory(Factory): 24 | """ Factory for core trait objects. """ 25 | 26 | # TODO: Don't leave this permanent 27 | TraitFactory(override_duplicates=True) 28 | TraitFactory.register(TraitSourceInspector, TraitTargetInspector) 29 | 30 | __all__ = ["TraitFactory"] 31 | -------------------------------------------------------------------------------- /src/pytraits/core/base/inspectors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.support import Inspector, Singleton 20 | 21 | 22 | class TraitInspector(metaclass=Singleton): 23 | """ Trait specific implementation of Inspector. 24 | 25 | This class is extension to general purpose Inspector. While original 26 | inspector gives string as a result of inspection, this class provides 27 | whole class (a.k.a Primitive) to caller. These primitives then provide 28 | special functionality for given object type. 29 | 30 | This class acts also as a singleton. 31 | """ 32 | def __init__(self): 33 | self.__inspector = Inspector() 34 | 35 | def __call__(self, object): 36 | return self.__inspector.inspect(object) 37 | 38 | @classmethod 39 | def add_hook(cls, name, hook): 40 | cls().__inspector.add_hook(name, hook) 41 | 42 | @classmethod 43 | def add_default_hook(cls, hook): 44 | cls().__inspector.add_default_hook(hook) 45 | 46 | 47 | class TraitTargetInspector(TraitInspector): 48 | """ Inspector used to identify target objects for trait composition. """ 49 | TYPE = "target" 50 | 51 | 52 | class TraitSourceInspector(TraitInspector): 53 | """ Inspector used to identify source objects for trait composition """ 54 | TYPE = "source" 55 | 56 | 57 | if __name__ == "__main__": 58 | import doctest 59 | doctest.testmod() 60 | -------------------------------------------------------------------------------- /src/pytraits/core/composing/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | import pytraits.core.composing.compiler # NOQA 20 | import pytraits.core.composing.resolutions # NOQA 21 | import pytraits.core.composing.traits # NOQA 22 | import pytraits.core.composing.composer # NOQA 23 | -------------------------------------------------------------------------------- /src/pytraits/core/composing/compiler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | import types 20 | import collections 21 | import sys 22 | 23 | from pytraits.support import is_sysname 24 | from pytraits.core import TraitFactory 25 | 26 | 27 | @TraitFactory.register 28 | class Compiler: 29 | """ 30 | Compiler to transfer the function to other class or instance. 31 | 32 | This class works as a heart of the whole system. To make function to be fully 33 | part of other class or instance, it needs to be recompiled on top of target 34 | object, as if it was written there in the first place. This is because internals 35 | of the function are read-only and we need to change them in order to access 36 | private attributes. 37 | """ 38 | def _clone_function(self, function): 39 | trait = collections.OrderedDict() 40 | 41 | trait["co_argcount"] = function.__code__.co_argcount 42 | trait["co_kwonlyargcount"] = function.__code__.co_kwonlyargcount 43 | if sys.version_info[:2] >= (3, 8) : 44 | trait["co_posonlyargcount"] = function.__code__.co_posonlyargcount 45 | trait["co_nlocals"] = function.__code__.co_nlocals 46 | trait["co_stacksize"] = function.__code__.co_stacksize 47 | trait["co_flags"] = function.__code__.co_flags 48 | trait["co_code"] = function.__code__.co_code 49 | trait["co_consts"] = function.__code__.co_consts 50 | trait["co_names"] = function.__code__.co_names 51 | trait["co_varnames"] = function.__code__.co_varnames 52 | trait["co_filename"] = function.__code__.co_filename 53 | trait["co_name"] = function.__code__.co_name 54 | trait["co_firstlineno"] = function.__code__.co_firstlineno 55 | trait["co_lnotab"] = function.__code__.co_lnotab 56 | 57 | return trait 58 | 59 | def _transfer_names(self, trait, clazz): 60 | items = [] 61 | for name in trait["co_names"]: 62 | if "__" not in name or is_sysname(name): 63 | items.append(name) 64 | else: 65 | items.append("_%s%s" % (clazz.__name__, name[name.index('__'):])) 66 | trait["co_names"] = tuple(items) 67 | 68 | def _compile_trait(self, trait, globs): 69 | return types.FunctionType(types.CodeType(*trait.values()), globs) 70 | 71 | def recompile(self, function, target, name: str=""): 72 | """ 73 | Recompile function on target object. 74 | 75 | @param function: Function to be recompiled 76 | @param target: Target class or instance 77 | @param {str} name: New name for the target 78 | """ 79 | trait = self._clone_function(function) 80 | self._transfer_names(trait, target) 81 | trait["co_name"] = name or trait["co_name"].strip('<>') 82 | 83 | return self._compile_trait(trait, function.__globals__) 84 | 85 | 86 | if __name__ == "__main__": 87 | import doctest 88 | doctest.testmod() 89 | -------------------------------------------------------------------------------- /src/pytraits/core/composing/composer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.support import type_safe 20 | from pytraits.core import TraitFactory, TraitObject 21 | 22 | 23 | @TraitFactory.register 24 | class Composer: 25 | """ Factory object for composers. 26 | 27 | This factory object is called for each pair of object that is being composed 28 | together. Composer object is created for identified and supported pairs of 29 | target and source objects. Some combinations can require special behavior 30 | which these composer objects need to address. 31 | 32 | Composing itself is a process, where source object is first recompiled 33 | and then bound against the new target. Compiling is required to alter the 34 | content of code object to match target object and binding is required to 35 | be able to call the function. 36 | """ 37 | # Dictionary of registered composers. Each composer is identified by 38 | # two strings (target type names) as a key and all composers are added here 39 | # by ComposerMeta metaclass. 40 | __COMPOSERS = dict() 41 | 42 | @classmethod 43 | def register(cls, key, composer): 44 | """ Stores composer with given key for the future use. """ 45 | cls.__COMPOSERS[key] = composer 46 | 47 | @type_safe 48 | def __call__(self, target: TraitObject, source: TraitObject): 49 | """ Factory method that selects correct composer for target and source. """ 50 | # Each TraitObject has a string representation that can be used to 51 | # select correct composer. 52 | joined = str(target), str(source) 53 | try: 54 | return self.__COMPOSERS[joined](target, source) 55 | except KeyError: 56 | msg = "{target} '{targetqname}' and {source} '{sourceqname}' is not supported combination!" 57 | msg = msg.format(target=str(target), 58 | targetqname=target.qualname, 59 | source=str(source), 60 | sourceqname=source.qualname) 61 | raise TypeError(msg) 62 | 63 | 64 | class ComposerMeta(type): 65 | """ Automatically registers all composers to Composer factory class. """ 66 | def __init__(cls, name, bases, attrs): 67 | """ Handles registering of classes to Composer factory class. 68 | 69 | This function is called when initializing the class object. Any class 70 | that is identified as a composer (=has defined CAN_COMPOSE attribute) 71 | is registered to factory. 72 | """ 73 | for support in getattr(cls, "CAN_COMPOSE", ()): 74 | Composer.register(support, cls) 75 | 76 | @type_safe 77 | def __call__(cls, target: TraitObject, source: TraitObject): 78 | """ Initializes class instance. 79 | 80 | This function is roughly equivalent to class.__init__. Here we 81 | initialize the composer object. 82 | """ 83 | instance = super().__call__() 84 | instance.target = target 85 | instance.source = source 86 | return instance 87 | 88 | 89 | class BasicComposer(metaclass=ComposerMeta): 90 | """ Basic composer for simple types. 91 | 92 | This class handles composition of most of the target - source pairs. 93 | """ 94 | CAN_COMPOSE = [('class', 'method'), ('instance', 'method'), 95 | ('class', 'classmethod'), ('instance', 'classmethod'), 96 | ('class', 'staticmethod'), ('instance', 'staticmethod'), 97 | ('class', 'builtin'), ('instance', 'builtin'), 98 | ('class', 'property')] 99 | 100 | def compose(self, resolutions): 101 | """ Composes trait to target object. 102 | 103 | General flow of composition is: 104 | - Resolve the name of the trait in target object. 105 | - Compile trait against the target (as if it was written to it.) 106 | - Bind the compiled trait to target. 107 | """ 108 | name = resolutions.resolve(self.source.name) 109 | compiled = self.source.recompile(self.target, name) 110 | bound = self.source.rebind(self.target, compiled) 111 | self.target[name] = bound 112 | 113 | 114 | class Property2Instance(metaclass=ComposerMeta): 115 | """ Special handling for composing properties to instances. """ 116 | CAN_COMPOSE = [('instance', 'property')] 117 | 118 | def compose(self, resolutions): 119 | """ Composes property trait to instance target. 120 | 121 | Composing properties to instances is bit trickier business, since 122 | properties are descriptors by their nature and they work only on class 123 | level. If we assign the property to instance's dictionary (instance.__dict__), 124 | it won't work at all. If we assign the property to instance's class' 125 | dictionary (instance.__class__.__dict__), it will work, but the property 126 | will go to any other instance of that class too. That's why, we create 127 | a clone of the class and set it to instance. 128 | """ 129 | # Modify target instance so that changing its class content won't 130 | # affect other classes. 131 | self.target.forge() 132 | 133 | # Resolve the name and recompile 134 | name = resolutions.resolve(self.source.name) 135 | compiled = self.source.recompile(self.target, name) 136 | 137 | # Assing property to instance's class. 138 | # TODO: Figure out pretty way to do this by calling target's function. 139 | setattr(self.target.compile_target, name, compiled) 140 | -------------------------------------------------------------------------------- /src/pytraits/core/composing/resolutions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.core import TraitFactory 20 | 21 | 22 | @TraitFactory.register 23 | class Resolutions: 24 | """ Container of resolutions for possible conflicts during composition. 25 | 26 | During composition process, function or property names can collide and 27 | every situation should be handled by user at the same time the traits 28 | are composed. That will guarantee that class or instance will behave 29 | as expected in every situation. 30 | """ 31 | def __init__(self, resolutions): 32 | self.__resolutions = resolutions 33 | 34 | def resolve(self, name): 35 | """ Resolves name that shall be used for trait being composed. 36 | 37 | NOTE: Currently it is possible to rename traits but collisions are 38 | not checked. 39 | 40 | Returns: 41 | (string) name of the trait 42 | (NoneType) nothing is returned in case trait should be ignored 43 | 44 | Raises: 45 | Error if there is a conflict but no resolution. 46 | """ 47 | return self.__resolutions.get(name, name) 48 | 49 | if __name__ == "__main__": 50 | import doctest 51 | doctest.testmod() 52 | -------------------------------------------------------------------------------- /src/pytraits/core/composing/traits.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | import inspect 20 | 21 | from pytraits.support import flatten, type_converted 22 | from pytraits.support.errors import (FirstTraitArgumentError, 23 | TraitArgumentTypeError) 24 | from pytraits.core import TraitFactory 25 | 26 | TraitSource = TraitFactory["TraitSourceInspector"] 27 | Resolutions = TraitFactory["Resolutions"] 28 | 29 | 30 | @TraitFactory.register 31 | class Traits: 32 | """ This class encapsulates handling of multiple traits. """ 33 | def __init__(self, traits): 34 | self.__traits = traits 35 | 36 | @classmethod 37 | def create(cls, traits): 38 | instance = cls(traits) 39 | 40 | # In case object and strings are given, we need to do some extra work 41 | # to get desired traits out. 42 | if instance.needs_preprocessing(): 43 | instance.preprocess() 44 | return instance 45 | 46 | def needs_preprocessing(self): 47 | """ Identifies need to resolve attributes for string arguments within trait source. 48 | 49 | In order to support following syntax: 50 | add_traits(Target, Source, "all", "its", "required", "attributes") 51 | we need to turn those strings to objects. This function is to used to 52 | identify that need. 53 | """ 54 | # Calculate number of string arguments so that we can give bit more 55 | # detailed error messages in case of some weird combinations are found. 56 | string_arg_count = 0 57 | for arg in self.__traits: 58 | if isinstance(arg, str): 59 | string_arg_count += 1 60 | 61 | # No string arguments means that all of the traits are objects, who 62 | # can be composed directly. 63 | if not string_arg_count: 64 | return False 65 | 66 | # First trait argument must be an object (instance or class), otherwise 67 | # none of this makes any sense. 68 | if isinstance(self.__traits[0], str) or inspect.isroutine(self.__traits[0]): 69 | raise FirstTraitArgumentError() 70 | 71 | # In case string arguments are provided, all of them have to be strings. 72 | if len(self.__traits[1:]) != string_arg_count: 73 | raise TraitArgumentTypeError() 74 | 75 | return True 76 | 77 | def preprocess(self): 78 | obj = self.__traits[0] 79 | names = self.__traits[1:] 80 | self.__traits = [getattr(obj, name) for name in names] 81 | 82 | def __iter__(self): 83 | """ Walk through each given trait. 84 | 85 | Any class source is walked through for its contents. 86 | """ 87 | for trait in flatten(map(TraitSource, self.__traits)): 88 | yield trait 89 | 90 | @type_converted 91 | def compose(self, target, resolutions: Resolutions): 92 | """ Compose trait sources to target using composer. """ 93 | for source in self: 94 | TraitFactory["Composer"](target, source).compose(resolutions) 95 | -------------------------------------------------------------------------------- /src/pytraits/core/primitives/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | import pkgutil 20 | import importlib 21 | from os.path import dirname 22 | from pytraits.support import is_sysname 23 | from ..base.inspectors import TraitSourceInspector, TraitTargetInspector 24 | 25 | # Import each module and register their TraitObject based classes to 26 | # corresponsing inspectors. This mechanism allows us to add new modules and 27 | # classes without need to do any other steps to get them registered into 28 | # inspectors. 29 | for _, module_name, _ in pkgutil.iter_modules([dirname(__file__)]): 30 | module = importlib.import_module("{}.{}".format(__package__, module_name)) 31 | 32 | for object_name in dir(module): 33 | if is_sysname(object_name): 34 | continue 35 | 36 | object = getattr(module, object_name) 37 | 38 | try: 39 | object.hook_into(TraitSourceInspector) 40 | object.hook_into(TraitTargetInspector) 41 | except AttributeError: 42 | pass 43 | 44 | # Let's remove the option of modifying the singletons after we are done with 45 | # this. 46 | TraitTargetInspector.add_hook = None 47 | TraitTargetInspector.add_default_hook = None 48 | TraitSourceInspector.add_hook = None 49 | TraitSourceInspector.add_default_hook = None 50 | -------------------------------------------------------------------------------- /src/pytraits/core/primitives/class_object.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | 20 | from pytraits.support import is_sysname 21 | from .trait_object import TraitObject 22 | from ..base import TraitFactory 23 | 24 | 25 | class ClassObject(TraitObject): 26 | INSPECTORS = ('source', 'target') 27 | 28 | def __iter__(self): 29 | """ Yields each element in the class. """ 30 | for name, object in self.items(): 31 | sub = TraitFactory["TraitSourceInspector"](object) 32 | if sub: 33 | yield sub 34 | 35 | def __dir__(self): 36 | return [i[0] for i in self.items()] 37 | 38 | def __getitem__(self, key): 39 | return self._object.__dict__[key] 40 | 41 | def __setitem__(self, key, value): 42 | setattr(self._object, key, value) 43 | 44 | def items(self): 45 | return ((k, v) for (k, v) in self._object.__dict__.items() if not is_sysname(k)) 46 | 47 | @property 48 | def compile_target(self): 49 | return self._object 50 | 51 | @property 52 | def bind_target(self): 53 | return self._object 54 | -------------------------------------------------------------------------------- /src/pytraits/core/primitives/instance_object.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.support import is_sysname 20 | from .trait_object import TraitObject 21 | from ..base import TraitFactory 22 | 23 | 24 | class InstanceObject(TraitObject): 25 | INSPECTORS = ('source', 'target') 26 | 27 | def __iter__(self): 28 | """ Yields each element in the class. """ 29 | for name, object in self.items(): 30 | sub = TraitFactory["TraitSourceInspector"](object) 31 | if sub: 32 | yield sub 33 | 34 | def __dir__(self): 35 | return [i[0] for i in self.items()] 36 | 37 | def __getitem__(self, name): 38 | try: 39 | return self._object.__class__.__dict__[name] 40 | except KeyError: 41 | return self._object.__dict__[name] 42 | 43 | def __setitem__(self, key, value): 44 | self._object.__dict__[key] = value 45 | 46 | @property 47 | def compile_target(self): 48 | return self._object.__class__ 49 | 50 | @property 51 | def bind_target(self): 52 | return self._object 53 | 54 | def items(self): 55 | items = dict() 56 | items.update(self._object.__class__.__dict__) 57 | items.update(self._object.__dict__) # Makes sure that instance values override class values. 58 | return ((k, v) for (k, v) in items.items() if not is_sysname(k)) 59 | 60 | def forge(self): 61 | """ Modifies instance's class to be unique. 62 | 63 | This method creates a clone of instance's class and replaces the 64 | original class with the clone. By doing this, it allows modifying 65 | instance in a manner that no changes are reflected to other instances 66 | of the same class. 67 | 68 | This is mainly needed to make properties and other descriptors work 69 | so that they can be instance specific. They normally work only on classes 70 | 71 | """ 72 | # In case the object's class is already forged, no need to do it again. 73 | if not hasattr(self._object, '__instance_forged'): 74 | # Retrieve the class of the object and create new class inherited 75 | # from it. It can be used on this instance again. 76 | original_class = self._object.__class__ 77 | new_class = type(original_class.__name__, (original_class, ), {}) 78 | new_class.__instance_forged = True 79 | 80 | # Replace the class with forged class. 81 | self._object.__class__ = new_class 82 | -------------------------------------------------------------------------------- /src/pytraits/core/primitives/property_object.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from .trait_object import TraitObject 20 | 21 | 22 | class PropertyObject(TraitObject): 23 | INSPECTORS = ('source',) 24 | 25 | def __init__(self, property, name=None): 26 | self.__property = property 27 | self.__name = name 28 | self.__compiler = self.FACTORY["Compiler"]() 29 | self.__inspector = self.FACTORY["TraitSourceInspector"] 30 | 31 | def get_func(self, func_name): 32 | func = getattr(self.__property, func_name, None) 33 | if func: 34 | return self.__inspector(func) 35 | 36 | @property 37 | def name(self): 38 | return self.__name or self.get_func('fget').name 39 | 40 | def __recompile_func(self, func_name, target, new_name): 41 | func = self.get_func(func_name) 42 | if func: 43 | return func.recompile(target, new_name) 44 | 45 | def recompile(self, target, name): 46 | getter = self.__recompile_func('fget', target, name) 47 | setter = self.__recompile_func('fset', target, name) 48 | deleter = self.__recompile_func('fdel', target, name) 49 | 50 | return property(getter, setter, deleter) 51 | 52 | def rebind(self, target, compiled_property): 53 | return compiled_property 54 | -------------------------------------------------------------------------------- /src/pytraits/core/primitives/routine_object.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.support import get_func_name 20 | from .trait_object import TraitObject 21 | 22 | 23 | class FunctionObject(TraitObject): 24 | """ This class encapsulates handling of function objects. 25 | 26 | Generally, there's no functions in terms of traits. Functions are turned to 27 | something else since they behave differently depending on the context, like 28 | pure functions for class should be staticmethods since they are not likely to 29 | do any modifications to class (or instance) itself. 30 | """ 31 | @classmethod 32 | def hook_into(cls, inspector): 33 | if inspector.TYPE == 'source': 34 | inspector.add_hook('function', cls.check) 35 | 36 | @staticmethod 37 | def check(object): 38 | args = object.__code__.co_varnames 39 | 40 | # Functions without arguments are considered to be static 41 | # methods. 42 | if len(args) == 0: 43 | return StaticMethodObject(object) 44 | 45 | # Function, that has first argument 'self', wants to be method. 46 | elif args[0] == 'self': 47 | return MethodObject(object) 48 | 49 | # Function, that has first argument 'cls', wants to be classmethod. 50 | elif args[0] == 'cls': 51 | return ClassMethodObject(object) 52 | 53 | # Other functions are to be static methods. 54 | else: 55 | return StaticMethodObject(object) 56 | 57 | 58 | class RoutineObject(TraitObject): 59 | @property 60 | def name(self): 61 | return get_func_name(self._object, False) 62 | 63 | @property 64 | def compile_target(self): 65 | return self._object 66 | 67 | def recompile(self, target, name): 68 | return self._compiler.recompile(self.compile_target, target.compile_target, name) 69 | 70 | 71 | class MethodObject(RoutineObject): 72 | """ This class encapsulates handling of methods. 73 | """ 74 | INSPECTORS = ('source',) 75 | 76 | def rebind(self, target, source): 77 | # TODO: Can this be made prettier? 78 | if target.compile_target == target.bind_target: 79 | return source.__get__(None, target.compile_target) 80 | return source.__get__(target.bind_target, target.compile_target) 81 | 82 | 83 | class ClassMethodObject(RoutineObject): 84 | """ This class encapsulates handling of classmethods. 85 | 86 | This class is able handle functions that are decorated with classmethod 87 | and pure functions that have 'cls' as a first argument. 88 | """ 89 | INSPECTORS = ('source',) 90 | 91 | @property 92 | def compile_target(self): 93 | return getattr(self._object, '__func__', self._object) 94 | 95 | def rebind(self, target, source): 96 | return source.__get__(target.bind_target, target.bind_target) 97 | 98 | 99 | class StaticMethodObject(RoutineObject): 100 | """ This class encapsulates handling of staticmethods. 101 | """ 102 | INSPECTORS = ('source',) 103 | 104 | @property 105 | def compile_target(self): 106 | return getattr(self._object, '__func__', self._object) 107 | 108 | def rebind(self, target, source): 109 | return source.__get__(None, target.compile_target) 110 | 111 | 112 | # TODO: where to go? 113 | def wrap_builtin(builtin_func): 114 | def wrapper(self, *args, **kwargs): 115 | return builtin_func(*args, **kwargs) 116 | return wrapper 117 | 118 | 119 | class BuiltinObject(MethodObject): 120 | """ This class encapsulates handling of builtin functions. """ 121 | INSPECTORS = ('source',) 122 | 123 | def recompile(self, target, name): 124 | return lambda *args, **kwargs: self._object(*args[1:], **kwargs) 125 | -------------------------------------------------------------------------------- /src/pytraits/core/primitives/trait_object.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from ..base import TraitFactory 20 | 21 | 22 | class TraitObject: 23 | FACTORY = TraitFactory() 24 | 25 | def __init__(self, object): 26 | self._object = object 27 | self._compiler = self.FACTORY["Compiler"]() 28 | 29 | @classmethod 30 | def __str__(cls): 31 | return cls.__name__.lower().replace('object', '') 32 | 33 | @classmethod 34 | def hook_into(cls, inspector): 35 | if inspector.TYPE in cls.INSPECTORS: 36 | inspector.add_hook(cls.__str__(), cls) 37 | 38 | @property 39 | def object(self): 40 | return self._object 41 | 42 | @property 43 | def qualname(self): 44 | try: 45 | return self._object.__qualname__ 46 | except AttributeError: 47 | return type(self._object).__name__ 48 | -------------------------------------------------------------------------------- /src/pytraits/core/primitives/unidentified_object.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from .trait_object import TraitObject 20 | 21 | 22 | class UnidentifiedObject(TraitObject): 23 | @classmethod 24 | def hook_into(cls, inspector): 25 | inspector.set_default_hook(cls) 26 | 27 | def __str__(self): 28 | return "Unidentified object" 29 | 30 | def __bool__(self): 31 | return False 32 | -------------------------------------------------------------------------------- /src/pytraits/extendable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.trait_composer import add_traits 20 | 21 | 22 | def extendable(target): 23 | """ 24 | Decorator that adds function for object to be extended using traits. 25 | 26 | NOTE: The 'add_traits' function this extendable decorator adds contains 27 | behavior that differs from usual function behavior. This method 28 | alters its behavior depending is the function called on a class 29 | or on an instance. If the function is invoked on class, then the 30 | class gets updated by the traits, affecting all new instances 31 | created from the class. On the other hand, if the function is invoked 32 | on an instance, only that instance gets the update, NOT whole class. 33 | 34 | See complete example from: 35 | pytraits/examples/extendable_function_class_vs_instance.py 36 | 37 | >>> @extendable 38 | ... class ExampleClass: 39 | ... pass 40 | ... 41 | >>> hasattr(ExampleClass, 'add_traits') 42 | True 43 | 44 | >>> class InstanceExample: 45 | ... pass 46 | ... 47 | >>> instance_example = InstanceExample() 48 | >>> _ = extendable(instance_example) 49 | >>> hasattr(instance_example, 'add_traits') 50 | True 51 | """ 52 | class TypeFunction: 53 | def __init__(self): 54 | self._target_object = None 55 | 56 | def __call__(self, *args, **kwargs): 57 | add_traits(self._target_object, *args, **kwargs) 58 | 59 | def __get__(self, instance, clazz): 60 | self._target_object = instance or clazz 61 | return self 62 | 63 | target.add_traits = TypeFunction() 64 | return target 65 | 66 | 67 | if __name__ == '__main__': 68 | import doctest 69 | doctest.testmod() 70 | -------------------------------------------------------------------------------- /src/pytraits/setproperty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | import inspect 20 | 21 | from pytraits.trait_composer import add_traits 22 | 23 | __all__ = ["setproperty"] 24 | 25 | 26 | def setproperty(target, fget=None, fset=None, fdel=None, source=None, name=None): 27 | """ 28 | Convinience function that dynamically creates property to an object. 29 | (If you have property created, just use 'add_traits') 30 | 31 | This function has different behavior depending on the target object, 32 | whether it is an instance or a class. If target is an instance the 33 | property is being set only for that instance. In case the object is 34 | a class, the property will be added to it normally to class. 35 | 36 | Args: 37 | target (object or type): Target object, which can be any instance or class. 38 | fget (str or function): Getter function or its name 39 | fset (str or function): Setter function or its name 40 | fdel (str or function): Deleter function or its name 41 | source (object or type): Source object in case fget, fset and fdel are strings. 42 | 43 | Keyword args: 44 | name (str): Name of the property 45 | name of fget (str): Name of the property 46 | 47 | Example, where new property is added dynamically into instance: 48 | 49 | >>> class Example: 50 | ... def __init__(self): 51 | ... self.__value = 42 52 | ... 53 | ... def set_value(self, new_value): 54 | ... self.__value = new_value 55 | ... 56 | ... def value(self): 57 | ... return self.__value 58 | ... 59 | ... def del_value(self): 60 | ... self.__value = 42 61 | ... 62 | >>> instance = Example() 63 | >>> setproperty(instance, "value", "set_value", "del_value", name="my_property") 64 | >>> instance.my_property 65 | 42 66 | """ 67 | resolutions = {} 68 | 69 | # If some arguments are left out, skip them from test. 70 | args = [arg for arg in (fget, fset, fdel) if arg] 71 | 72 | # There must be at least one argument 73 | if not args: 74 | raise TypeError("Property needs to have at least one function.") 75 | 76 | # Handle case, when all provided arguments are strings. 77 | elif all(isinstance(arg, str) for arg in args): 78 | owner = source or target 79 | resolutions[fget] = name 80 | 81 | new_property = property(getattr(owner, fget or "", None), 82 | getattr(owner, fset or "", None), 83 | getattr(owner, fdel or "", None)) 84 | 85 | # It is also possible to provide functions. 86 | elif all(inspect.isroutine(arg) for arg in args): 87 | resolutions[fget.__name__] = name 88 | new_property = property(fget, fset, fdel) 89 | 90 | # Other conditions are not supported. 91 | else: 92 | raise TypeError("Unsupported setup for property functions!") 93 | 94 | add_traits(target, new_property, **resolutions) 95 | 96 | 97 | if __name__ == '__main__': 98 | import doctest 99 | doctest.testmod() 100 | -------------------------------------------------------------------------------- /src/pytraits/support/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from .singleton import Singleton 20 | from .inspector import Inspector 21 | from .factory import Factory 22 | from .magic import type_safe, type_converted 23 | from .utils import flatten, is_sysname, get_func_name 24 | 25 | __all__ = ["Singleton", "Inspector", "Factory", "flatten", "type_safe", 26 | "type_converted", "is_sysname", "errors", "get_func_name"] 27 | -------------------------------------------------------------------------------- /src/pytraits/support/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | # TODO: This is probably in wrong place 20 | # Exceptions 21 | UnextendableObjectError = "Target context can be only class or instance of class" 22 | FactoryError = "Factory is abstract, inherit your own!" 23 | FactoryClassMissingError = "Class is missing!" 24 | FactoryRegisterError = "Already registered!" 25 | SingletonError = 'Singletons are immutable!' 26 | SingletonInstanceError = 'Singleton with arguments has already been created!' 27 | BuiltinSourceError = 'Built-in objects can not used as traits!' 28 | PropertySourceError = 'Properties can not be extended!' 29 | TypeConversionError = 'Conversion impossible!' 30 | ArgumentValueError = 'Unexpected value!' 31 | FirstTraitArgumentError = 'First argument must not be string!' 32 | TraitArgumentTypeError = "Expected list of trait names for given source object!" 33 | 34 | 35 | # Convert strings to exception objects 36 | for exception, message in dict(globals()).items(): 37 | if not exception.endswith('Error'): 38 | continue 39 | 40 | bases = (Exception,) 41 | attrs = {'__default_msg': message, 42 | '__init__': lambda self, msg=None: setattr(self, '__msg', msg), 43 | '__str__': lambda self: self.__msg or self.__default_msg} 44 | globals()[exception] = type(exception, bases, attrs) 45 | -------------------------------------------------------------------------------- /src/pytraits/support/factory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | import inspect 20 | 21 | from pytraits.support import Singleton 22 | from pytraits.support.errors import (FactoryError, 23 | FactoryRegisterError, 24 | FactoryClassMissingError) 25 | 26 | __all__ = ["Factory"] 27 | 28 | 29 | class FactoryType(Singleton): 30 | """ Convenience type for factory to allow dictionary type access to objects.""" 31 | def __getitem__(cls, name): 32 | return cls()[name] 33 | 34 | def __call__(cls, *args, **kwargs): 35 | if cls is Factory: 36 | raise FactoryError() 37 | return super().__call__(*args, **kwargs) 38 | 39 | 40 | class Factory(metaclass=FactoryType): 41 | """ Simple factory to register and create objects. 42 | 43 | This class contains multiple ways to hold and create instances of classes. 44 | This class also works as a container for all those classes that are 45 | registered in and can those classes can be accessed from anywhere by simply 46 | importing that factory. 47 | 48 | The main mechanism in python to create and initialize objects are __new__ 49 | and __init__ functions. It is also a good habit to avoid any conditional 50 | logic inside class constructor, thus writing own create classmethod is 51 | recommended and also supported by this factory. By using own class method 52 | for creating the object, it makes far more easier to setup and test classes 53 | you write since the __init__ method is left for simple assignments. 54 | 55 | NOTE: This factory is abstract thus anyone using it must inherit own 56 | version before instantiating it. 57 | 58 | >>> class ExampleFactory(Factory): 59 | ... pass 60 | ... 61 | >>> @ExampleFactory.register 62 | ... class ExampleObject: 63 | ... def __init__(self, name, **kwargs): 64 | ... self.name = name 65 | ... 66 | ... @classmethod 67 | ... def create(cls, *args, **kwargs): 68 | ... return cls(*args, **kwargs) 69 | ... 70 | >>> example_instance = ExampleFactory["ExampleObject"]("MyObject") 71 | >>> example_instance.name 72 | 'MyObject' 73 | """ 74 | def __init__(self, override_duplicates=False): 75 | self.__methods = {} 76 | self.__classes = {} 77 | self.__override_duplicates = override_duplicates 78 | 79 | @classmethod 80 | def register(cls, *classes, override=False, autoinit=True): 81 | """ Decorator function to register classes to this factory. """ 82 | assert classes, "No classes provided!" 83 | 84 | # This is singleton, so we can get the singleton instance directly 85 | # here and start filling it. 86 | self = cls() 87 | for clazz in classes: 88 | self.__register(clazz, override=override, autoinit=autoinit) 89 | 90 | # When single class is registered, return it too so that 91 | # this function can act as a class decorator. 92 | if len(classes) == 1: 93 | return classes[0] 94 | 95 | def __register(self, clazz, override, autoinit): 96 | assert inspect.isclass(clazz) 97 | 98 | # Make sure duplicates are not registered. By default, raise error 99 | # in order to avoid weird debugging errors. Duplicates, if tolerated, 100 | # can be 101 | override |= self.__override_duplicates 102 | if self.exists(clazz) and not override: 103 | # TODO: Record traceback for each registered object. 104 | msg = "Name '{}' already found from factory" 105 | raise FactoryRegisterError(msg.format(clazz.__name__)) 106 | 107 | # Keep a list of classes in case there is a need to override them. 108 | self.__classes[clazz.__name__] = clazz 109 | 110 | # In case the clazz defines __call__ function, it is considered 111 | # as subfactory, which means we try to initialize the clazz 112 | # and use it's instance as a factory method. Setting autoinit to 113 | # False will of course prevent that behavior. 114 | if "__call__" in dir(clazz) and autoinit: 115 | self.__methods[clazz.__name__] = getattr(clazz, 'create', clazz)() 116 | else: 117 | self.__methods[clazz.__name__] = getattr(clazz, 'create', clazz) 118 | 119 | return clazz 120 | 121 | def __access(self, collection, name): 122 | try: 123 | return collection[name] 124 | except KeyError: 125 | msg = "Name '{}' is not in registered list: {}" 126 | msg = msg.format(name, self.registered_classes) 127 | raise FactoryClassMissingError(msg) 128 | 129 | def __getitem__(self, name): 130 | """ Returns factory method of registered object. 131 | 132 | @see constructor 133 | """ 134 | return self.__access(self.__methods, name) 135 | 136 | def exists(self, clazz): 137 | """ Convenience function to check if class is already exists. """ 138 | return clazz.__name__ in self.__classes 139 | 140 | def original_class(self, name): 141 | """ Retrieves the original registered class. """ 142 | return self.__access(self.__classes, name) 143 | 144 | @classmethod 145 | def reset(cls): 146 | """ Removes all registered classes. """ 147 | cls().__methods.clear() 148 | cls().__classes.clear() 149 | 150 | @property 151 | def registered_classes(self): 152 | return list(self.__classes.keys()) 153 | 154 | 155 | if __name__ == "__main__": 156 | import doctest 157 | doctest.testmod() 158 | -------------------------------------------------------------------------------- /src/pytraits/support/inspector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | import inspect 20 | from collections import OrderedDict as odict 21 | 22 | from pytraits.support.magic import type_safe 23 | 24 | __all__ = ["Inspector"] 25 | 26 | 27 | def isproperty(object): 28 | """ Convinience method to check if object is property. """ 29 | return isinstance(object, property) 30 | 31 | 32 | def isbuiltin(object): 33 | """ Convinience method to check if object is builtin. """ 34 | if inspect.isbuiltin(object): 35 | return True 36 | 37 | return getattr(object, '__module__', None) == 'builtins' 38 | 39 | 40 | def isclass(object): 41 | """ Convinience method to check if object is class. """ 42 | if not inspect.isclass(object): 43 | return False 44 | if isbuiltin(object): 45 | return False 46 | return type not in inspect.getmro(object) 47 | 48 | 49 | def ismetaclass(object): 50 | """ Convinience method to check if object is meta class. """ 51 | if not inspect.isclass(object): 52 | return False 53 | if isbuiltin(object): 54 | return False 55 | return type in inspect.getmro(object) 56 | 57 | 58 | def _get_dict_function(object): 59 | try: 60 | return object.__self__.__dict__[object.__name__] 61 | except (AttributeError, KeyError): 62 | return None 63 | 64 | 65 | def isclassmethod(object): 66 | """ Convinience method to check if object is class method. """ 67 | if isinstance(object, classmethod): 68 | return True 69 | 70 | # Let's not give up quite yet. 71 | original = _get_dict_function(object) 72 | return isinstance(original, classmethod) 73 | 74 | 75 | def isdatatype(object): 76 | """ Convinience method to check if object is data type. """ 77 | return isinstance(object, (str, int, bool, float, type(None))) 78 | 79 | 80 | def isstaticmethod(object): 81 | """ Convinience method to check if object is static method. """ 82 | # TODO: This can only identify those static methods that 83 | # are directly taken from object's dict. Like 84 | # Class.__dict__[staticmethodname] 85 | if isinstance(object, staticmethod): 86 | return True 87 | 88 | if not inspect.isfunction(object): 89 | return False 90 | 91 | # Module level functions are disqualified here. 92 | if "." not in getattr(object, "__qualname__", ""): 93 | return False 94 | 95 | # It is either method (accessed as Class.method) or staticfunction 96 | # TODO: Is this really the only way? 97 | args = object.__code__.co_varnames 98 | if len(args) == 0: 99 | return True 100 | 101 | return args[0] != 'self' 102 | 103 | 104 | def isclassinstance(object): 105 | """ Convinience method to check if object is class instance. """ 106 | if not hasattr(object, "__class__"): 107 | return False 108 | if isbuiltin(object.__class__): 109 | return False 110 | return True 111 | 112 | 113 | class Inspector: 114 | """ Class for inspecting and hooking types. """ 115 | TYPES = odict([('builtin', isbuiltin), 116 | ('module', inspect.ismodule), 117 | ('property', isproperty), 118 | ('code', inspect.iscode), 119 | ('generator', inspect.isgenerator), 120 | ('traceback', inspect.istraceback), 121 | ('frame', inspect.isframe), 122 | ('staticmethod', isstaticmethod), 123 | ('classmethod', isclassmethod), 124 | ('method', inspect.ismethod), 125 | ('function', inspect.isfunction), 126 | ('routine', inspect.isroutine), 127 | ('methoddescriptor', inspect.ismethoddescriptor), 128 | ('generatorfunction', inspect.isgeneratorfunction), 129 | ('datadescriptor', inspect.isdatadescriptor), 130 | ('memberdescriptor', inspect.ismemberdescriptor), 131 | ('getsetdescriptor', inspect.isgetsetdescriptor), 132 | ('descriptor', isclassmethod), 133 | ('metaclass', ismetaclass), 134 | ('class', isclass), 135 | ('data', isdatatype), 136 | ('instance', isclassinstance)]) 137 | TYPENAMES = tuple(TYPES.keys()) 138 | 139 | def __init__(self, custom_types: odict=None): 140 | self.__custom_types = custom_types or odict() 141 | self.__hooks = odict() 142 | self.__default_hook = None 143 | 144 | def __iter__(self): 145 | # Favor custom types. It is possible to override default behavior. 146 | yield from self.__custom_types.items() 147 | yield from self.TYPES.items() 148 | 149 | @type_safe 150 | def __getitem__(self, typename: str): 151 | """ Get check function for typename 152 | 153 | >>> Inspector()["class"].__name__ 154 | 'isclass' 155 | """ 156 | try: 157 | return self.TYPES[typename] 158 | except KeyError: 159 | return self.__custom_types[typename] 160 | 161 | @type_safe 162 | def inspect_many(self, *objects, all: bool=False): 163 | """ Identify all arguments to certain type. 164 | 165 | This function identifies all arguments to certain type and for those 166 | types that have a registered hook, will be called with given object for 167 | any special handling needed for that type. 168 | 169 | Returns: 170 | List of identified objects. 171 | """ 172 | inspected = [] 173 | for object in objects: 174 | inspected.append(self.__inspect_arg(object)) 175 | 176 | return inspected 177 | 178 | @type_safe 179 | def inspect(self, object, hooked_only: bool=True): 180 | """ Identifies type of single object. 181 | 182 | Loops over every type check defined in Inspector.TYPES dictionary and 183 | returns type for the first check that qualifies the object. 184 | 185 | Args: 186 | object (anything): Object needs to be identified. 187 | hooked_only (bool): Switch to decide whether all types are checked 188 | or only hooks. If no hooks are defined then 189 | check is done against all types. 190 | Default is only hooked. 191 | 192 | Return: 193 | If no hook found, then name of object type. 194 | If hook is found, then any object returned by hook. 195 | """ 196 | for typename, check in self: 197 | # Skip checks if it is not required for this type. 198 | if hooked_only and len(self.__hooks) and typename not in self.__hooks: 199 | continue 200 | 201 | # Keep going if object is not matching. 202 | if not check(object): 203 | continue 204 | 205 | if typename in self.__hooks: 206 | return self.__hooks[typename](object) 207 | elif self.__default_hook: 208 | return self.__default_hook(object) 209 | else: 210 | return typename 211 | 212 | # Situation that occurs when receiving a type checks are not covering. 213 | if self.__default_hook: 214 | return self.__default_hook(object) 215 | return None 216 | 217 | @type_safe 218 | def add_typecheck(self, name: str, callable=None): 219 | """ Adds typecheck for given name. 220 | 221 | This method allows adding custom typechecks. It's possible to either 222 | add completely new checks or override existing ones. 223 | 224 | Args: 225 | name: Name of the type check. If the name is found from the 226 | Inspector.TYPES list, it will be overridden as new check for 227 | that type. If name is completely new one, then it will be 228 | added as a custom typecheck. 229 | callable: Any callable object taking single argument as parameter 230 | and returns True or False as an answer. If None, existing 231 | type check is promoted to be custom. This changes priority 232 | of checks so that desired checks are run earlier. 233 | 234 | Raises: 235 | ValueError when there already is a custom type check for given name. 236 | """ 237 | if name in self.__custom_types: 238 | raise ValueError("Type '{}' already exists".format(name)) 239 | self.__custom_types[name] = callable or self.TYPES[name] 240 | 241 | @type_safe 242 | def del_typecheck(self, name: str): 243 | """ Removes custom type checks by name. """ 244 | try: 245 | del self.__custom_types[name] 246 | except KeyError: 247 | pass 248 | 249 | @type_safe 250 | def add_hook(self, name: str, callable): 251 | """ Add hook that is called for given type. 252 | 253 | Args: 254 | name: Type name. 255 | callable: Any callable taking object as an argument. 256 | """ 257 | assert name in self.typenames, "'{}' not in '{}'".format(name, self.typenames) 258 | self.__hooks[name] = callable 259 | 260 | @type_safe 261 | def del_hook(self, name: str): 262 | """ Removes a hook by name. """ 263 | try: 264 | del self.__hooks[name] 265 | except KeyError: 266 | pass 267 | 268 | def set_default_hook(self, callable): 269 | self.__default_hook = callable 270 | 271 | def del_default_hook(self): 272 | self.__default_hook = None 273 | 274 | def clear(self): 275 | """ Removes all the hooks. """ 276 | self.__hooks = odict() 277 | 278 | @property 279 | def hooks(self): 280 | """ Tuple of registered hooks. """ 281 | return tuple(self.__hooks.keys()) 282 | 283 | @property 284 | def typenames(self): 285 | """ Tuple of supported types """ 286 | return tuple((item[0] for item in self)) 287 | 288 | 289 | if __name__ == "__main__": 290 | import doctest 291 | doctest.testmod() 292 | -------------------------------------------------------------------------------- /src/pytraits/support/magic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | import inspect 20 | import itertools 21 | import functools 22 | 23 | from pytraits.support.errors import TypeConversionError, ArgumentValueError 24 | from pytraits.support.utils import get_func_name 25 | from pytraits.support.utils import get_signature 26 | 27 | 28 | __all__ = ["type_safe", "type_converted"] 29 | 30 | 31 | class ErrorMessage: 32 | """ 33 | Encapsulates building of error message. 34 | """ 35 | def __init__(self, main_msg, repeat_msg, get_func_name): 36 | self.__errors = [] 37 | self.__get_func_name = get_func_name 38 | self.__main_msg = main_msg 39 | self.__repeat_msg = repeat_msg 40 | 41 | def __bool__(self): 42 | return bool(self.__errors) 43 | 44 | def __str__(self): 45 | msg = [self.__main_msg.format(self.__get_func_name)] 46 | for error in self.__errors: 47 | msg.append(" - " + self.__repeat_msg.format(**error)) 48 | return "\n".join(msg) 49 | 50 | def set_main_messsage(self, msg): 51 | self.__main_msg = msg 52 | 53 | def set_repeat_message(self, msg): 54 | self.__repeat_msg = msg 55 | 56 | def add(self, **kwargs): 57 | self.__errors.append(kwargs) 58 | 59 | def reset(self): 60 | self.__errors = [] 61 | 62 | 63 | class type_safe: 64 | """ 65 | Decorator to enforce type safety. It certainly kills some ducks 66 | but allows us also to fail fast. 67 | 68 | >>> @type_safe 69 | ... def check(value: int, answer: bool, anything): 70 | ... return value, answer, anything 71 | ... 72 | 73 | >>> check("12", "false", True) 74 | Traceback (most recent call last): 75 | ... 76 | TypeError: While calling check(value:int, answer:bool, anything): 77 | - parameter 'value' had value '12' of type 'str' 78 | - parameter 'answer' had value 'false' of type 'str' 79 | 80 | >>> check(1000, True) 81 | Traceback (most recent call last): 82 | ... 83 | TypeError: check() missing 1 required positional argument: 'anything' 84 | """ 85 | def __init__(self, function): 86 | self._function = function 87 | functools.update_wrapper(self, function) 88 | self.__signature = inspect.signature(function) 89 | self._specs = inspect.getfullargspec(self._function) 90 | self._self = None 91 | self._errors = ErrorMessage( 92 | 'While calling {}:', 93 | "parameter '{name}' had value '{value}' of type '{typename}'", 94 | get_signature(function)) 95 | 96 | def __get__(self, instance, clazz): 97 | """ 98 | Stores calling instances and returns this decorator object as function. 99 | """ 100 | # In Python, every function is a property. Before Python invokes function, 101 | # it must access the function using __get__, where it can deliver the calling 102 | # object. After the __get__, function is ready for being invoked by __call__. 103 | self._self = instance 104 | return self 105 | 106 | def iter_positional_args(self, args): 107 | """ 108 | Yields type, name, value combination of function arguments. 109 | """ 110 | # specs.args contains all arguments of the function. Loop here all 111 | # argument names and their values putting them together. If there 112 | # are arguments missing values, fill them with None. 113 | for name, val in itertools.zip_longest(self._specs.args, args, fillvalue=None): 114 | # __annotations__ is a dictionary of argument name and annotation. 115 | # We accept empty annotations, in which case the argument has no 116 | # type requirement. 117 | yield self._function.__annotations__.get(name, None), name, val 118 | 119 | def _analyze_args(self, args): 120 | """ 121 | Invoked by __call__ in order to work with positional arguments. 122 | 123 | This function does the actual work of evaluating arguments against 124 | their annotations. Any deriving class can override this function 125 | to do different kind of handling for the arguments. Overriding function 126 | must return list of arguments that will be used to call the decorated 127 | function. 128 | 129 | @param args: Arguments given for the function. 130 | @return same list of arguments given in parameter. 131 | """ 132 | # TODO: inspect.Signature does quite lot of similar things. Figure 133 | # out, how to take advantage of that. 134 | for arg_type, arg_name, arg_value in self.iter_positional_args(args): 135 | if not arg_type or isinstance(arg_value, arg_type): 136 | continue 137 | 138 | self._errors.add( 139 | typename=type(arg_value).__name__, 140 | name=arg_name, 141 | value=arg_value) 142 | 143 | if self._errors: 144 | raise TypeError(str(self._errors)) 145 | 146 | return args 147 | 148 | def __match_arg_count(self, args): 149 | """ 150 | Verifies that proper number of arguments are given to function. 151 | """ 152 | # With default values this verification is bit tricky. In case 153 | # given arguments match with number of arguments in function signature, 154 | # we can proceed. 155 | if len(args) == len(self._specs.args): 156 | return True 157 | 158 | # It's possible to have less arguments given than defined in function 159 | # signature in case any default values exist. 160 | if len(args) - len(self._specs.defaults or []) == len(self._specs.args): 161 | return True 162 | 163 | # When exceeding number of args, also check if function accepts 164 | # indefinite number of positional arguments. 165 | if len(args) > len(self._specs.args) and self._specs.varargs: 166 | return True 167 | 168 | # We got either too many arguments or too few. 169 | return False 170 | 171 | def __call__(self, *args, **kwargs): 172 | """ 173 | Converts annotated types into proper type and calls original function. 174 | """ 175 | self._errors.reset() 176 | 177 | # Methods require instance of the class to be first argument. We 178 | # stored it in __get__ and now add it to argument list so that 179 | # function can be invoked correctly. 180 | if self._self: 181 | args = (self._self, ) + args 182 | 183 | # Before doing any type checks, make sure argument count matches. 184 | if self.__match_arg_count(args): 185 | args = self._analyze_args(args) 186 | 187 | return self._function(*args, **kwargs) 188 | 189 | 190 | class type_converted(type_safe): 191 | """ 192 | Decorator to enforce types and do auto conversion to values. 193 | 194 | >>> @type_converted 195 | ... def convert(value: int, answer: bool, anything): 196 | ... return value, answer, anything 197 | ... 198 | >>> convert("12", "false", None) 199 | (12, False, None) 200 | 201 | >>> class Example: 202 | ... @type_converted 203 | ... def convert(self, value: int, answer: bool, anything): 204 | ... return value, answer, anything 205 | ... 206 | >>> Example().convert("12", 0, "some value") 207 | (12, False, 'some value') 208 | 209 | >>> Example().convert(None, None, None) 210 | Traceback (most recent call last): 211 | ... 212 | pytraits.support.errors.TypeConversionError: While calling Example.convert(self, value:int, answer:bool, anything): 213 | - got arg 'value' as 'None' of type 'NoneType' which cannot be converted to 'int' 214 | - got arg 'answer' as 'None' of type 'NoneType' which cannot be converted to 'bool' 215 | """ 216 | def __init__(self, function): 217 | super().__init__(function) 218 | self.__converters = {bool: self.boolean_conversion} 219 | self._errors = ErrorMessage( 220 | 'While calling {}:', 221 | "got arg '{name}' as '{value}' of type '{typename}' " 222 | "which cannot be converted to '{expectedtype}'", 223 | get_signature(function)) 224 | 225 | def convert(self, arg_type, arg_name, arg_value): 226 | """ 227 | Converts argument to given type. 228 | """ 229 | # If no type required, return value as is. 230 | if arg_type is None: 231 | return arg_value 232 | 233 | try: 234 | return self.__converters[arg_type](arg_value) 235 | except KeyError: 236 | return arg_type(arg_value) 237 | 238 | def boolean_conversion(self, value): 239 | """ 240 | Convert given value to boolean. 241 | 242 | >>> conv = type_converted(lambda self: None) 243 | >>> conv.boolean_conversion("True"), conv.boolean_conversion("false") 244 | (True, False) 245 | 246 | >>> conv.boolean_conversion(1), conv.boolean_conversion(0) 247 | (True, False) 248 | """ 249 | if isinstance(value, bool): 250 | return value 251 | 252 | elif isinstance(value, str): 253 | if value.lower() == "true": 254 | return True 255 | if value.lower() == "false": 256 | return False 257 | 258 | elif isinstance(value, int): 259 | if not value: 260 | return False 261 | if value == 1: 262 | return True 263 | 264 | raise TypeConversionError() # This will be caught by convert method. 265 | 266 | def _analyze_args(self, args): 267 | """ 268 | Converts annotated types into proper type and calls original function. 269 | """ 270 | self._errors.reset() 271 | new_args = [] 272 | 273 | for arg_type, arg_name, arg_value in self.iter_positional_args(args): 274 | try: 275 | new_args.append(self.convert(arg_type, arg_name, arg_value)) 276 | except (TypeConversionError, TypeError): 277 | self._errors.add( 278 | name=arg_name, 279 | value=arg_value, 280 | typename=type(arg_value).__name__, 281 | expectedtype=arg_type.__name__) 282 | 283 | if self._errors: 284 | raise TypeConversionError(str(self._errors)) 285 | 286 | return new_args 287 | 288 | 289 | class validation(type_safe): 290 | """ Class to validate attributes against given values 291 | 292 | >>> @validation 293 | ... def show_number(number: (1, 2, 3, 5)): 294 | ... return number 295 | ... 296 | >>> show_number(1), show_number(2), show_number(3), show_number(5) 297 | (1, 2, 3, 5) 298 | 299 | >>> show_number(4) 300 | Traceback (most recent call last): 301 | ... 302 | pytraits.support.errors.ArgumentValueError: While calling 'show_number': 303 | - got arg 'number' as '4' of type 'int' which is not any of these values: (1, 2, 3, 5) 304 | 305 | >>> show_number("5") 306 | Traceback (most recent call last): 307 | ... 308 | pytraits.support.errors.ArgumentValueError: While calling 'show_number': 309 | - got arg 'number' as '5' of type 'str' which is not any of these values: (1, 2, 3, 5) 310 | """ 311 | def __init__(self, function): 312 | super().__init__(function) 313 | self.__function = function 314 | self._errors = ErrorMessage( 315 | "While calling '{}':", 316 | "got arg '{name}' as '{value}' of type '{typename}' " 317 | "which is not any of these values: {values}", 318 | get_func_name(function)) 319 | 320 | def _analyze_args(self, args): 321 | self._errors.reset() 322 | 323 | for arg_values, arg_name, arg_value in self.iter_positional_args(args): 324 | if not arg_values: 325 | continue 326 | 327 | arg_values = tuple([arg_values]) if not isinstance(arg_values, tuple) else arg_values 328 | if arg_value not in arg_values: 329 | self._errors.add(name=arg_name, 330 | value=arg_value, 331 | typename=type(arg_value).__name__, 332 | values=arg_values) 333 | 334 | if self._errors: 335 | raise ArgumentValueError(str(self._errors)) 336 | 337 | return args 338 | 339 | 340 | if __name__ == "__main__": 341 | import doctest 342 | doctest.testmod() 343 | -------------------------------------------------------------------------------- /src/pytraits/support/singleton.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.support.errors import SingletonError 20 | 21 | 22 | class Singleton(type): 23 | """ Turn the class to immutable singleton. 24 | 25 | >>> class Example(object, metaclass=Singleton): 26 | ... pass 27 | ... 28 | >>> a = Example() 29 | >>> b = Example() 30 | >>> id(a) == id(b) 31 | True 32 | 33 | Having your instance as a singleton is faster than creating from scratch 34 | 35 | >>> import timeit 36 | >>> class MySingleton(object, metaclass=Singleton): 37 | ... def __init__(self): 38 | ... self._store = dict(one=1, two=2, three=3, four=4) 39 | ... 40 | >>> class NonSingleton: 41 | ... def __init__(self): 42 | ... self._store = dict(one=1, two=2, three=3, four=4) 43 | ... 44 | >>> #timeit.timeit(NonSingleton) > timeit.timeit(MySingleton) 45 | 46 | After creating a singleton, data it is holding should not be changed. 47 | There is a small enforcement done for these singletons to prevent modifying 48 | the contents. With little effort it is doable, but don't do it. :) 49 | 50 | >>> MySingleton().new_item = False 51 | Traceback (most recent call last): 52 | ... 53 | pytraits.support.errors.SingletonError: Singletons are immutable! 54 | """ 55 | def __call__(self, *args, **kwargs): 56 | try: 57 | return self.__instance 58 | except AttributeError: 59 | def immutable_object(*args): 60 | raise SingletonError() 61 | 62 | self.__instance = super().__call__(*args, **kwargs) 63 | self.__setitem__ = immutable_object 64 | self.__setattr__ = immutable_object 65 | return self.__instance 66 | 67 | if __name__ == "__main__": 68 | import doctest 69 | doctest.testmod() 70 | -------------------------------------------------------------------------------- /src/pytraits/support/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | import inspect 20 | 21 | 22 | def is_sysname(name: str): 23 | """ Quick check if name is system specific. 24 | 25 | When doing lot of introspection with functions and attributes, very often 26 | arises a situation to check, if attribute name is following pattern used 27 | by Python for system attributes. This simple function allows to identify 28 | them in simple fashion without need to write same pattern everywhere. 29 | 30 | >>> is_sysname("test") 31 | False 32 | >>> is_sysname("__test") 33 | False 34 | >>> is_sysname("__test__") 35 | True 36 | >>> is_sysname("_test_") 37 | False 38 | """ 39 | return name.startswith('__') and name.endswith('__') 40 | 41 | 42 | def is_container(obj): 43 | """ Checks whether the object is container or not. 44 | 45 | Container is considered an object, which includes other objects, 46 | thus string is not qualified, even it implments iterator protocol. 47 | 48 | >>> is_container("text") 49 | False 50 | 51 | >>> is_container(tuple()) 52 | True 53 | """ 54 | if isinstance(obj, str): 55 | return False 56 | 57 | return hasattr(obj, '__iter__') 58 | 59 | 60 | def has_dict_protocol(obj): 61 | """ Checks whether object supports dict protocol. """ 62 | return hasattr(obj, "__getitem__") and hasattr(obj, "__setitem__") 63 | 64 | 65 | def flatten(items): 66 | """ Flatten the nested arrays into single one. 67 | 68 | Example about list of lists. 69 | >>> list(flatten([[1, 2], [3, 4]])) 70 | [1, 2, 3, 4] 71 | 72 | Example of deeply nested irregular list: 73 | >>> list(flatten([[[1, 2]], [[[3]]], 4, 5, [[6, [7, 8]]]])) 74 | [1, 2, 3, 4, 5, 6, 7, 8] 75 | 76 | List of strings is handled properly too 77 | >>> list(flatten(["one", "two", ["three", "four"]])) 78 | ['one', 'two', 'three', 'four'] 79 | """ 80 | for subitem in items: 81 | if is_container(subitem): 82 | for item in flatten(subitem): 83 | yield item 84 | else: 85 | yield subitem 86 | 87 | 88 | def get_signature(function): 89 | """ Constructs signature of function. 90 | 91 | >>> def test(arg, *args, **kwargs): pass 92 | >>> get_signature(test) 93 | 'test(arg, *args, **kwargs)' 94 | 95 | >>> class Test: 96 | ... def test(self, arg, *args, **kwargs): pass 97 | ... 98 | >>> get_signature(Test.test) 99 | 'Test.test(self, arg, *args, **kwargs)' 100 | """ 101 | sig = inspect.signature(function) 102 | return "{}{}".format(get_func_name(function), str(sig)) 103 | 104 | 105 | def get_func_name(routine, fullname=True): 106 | """ Returns name of the function as a string. 107 | 108 | Full name examples, where we get the __qualname__. 109 | >>> class Test: 110 | ... def test_method(self): pass 111 | ... 112 | ... @classmethod 113 | ... def test_classmethod(cls): pass 114 | ... 115 | ... @staticmethod 116 | ... def test_staticmethod(): pass 117 | ... 118 | >>> get_func_name(Test.test_method) 119 | 'Test.test_method' 120 | >>> get_func_name(Test.test_classmethod) 121 | 'Test.test_classmethod' 122 | >>> get_func_name(Test.test_staticmethod) 123 | 'Test.test_staticmethod' 124 | 125 | Examples where we want to have just __name__. 126 | >>> get_func_name(Test.test_method, fullname=False) 127 | 'test_method' 128 | >>> get_func_name(Test.test_classmethod, fullname=False) 129 | 'test_classmethod' 130 | >>> get_func_name(Test.test_staticmethod, fullname=False) 131 | 'test_staticmethod' 132 | 133 | >>> def test_function(): pass 134 | >>> get_func_name(test_function) 135 | 'test_function' 136 | >>> get_func_name(test_function, fullname=False) 137 | 'test_function' 138 | """ 139 | name_attr = '__qualname__' if fullname else '__name__' 140 | try: 141 | return getattr(routine, name_attr) 142 | except AttributeError: 143 | function = getattr(routine, '__func__', routine) 144 | return function.__name__ 145 | 146 | if __name__ == "__main__": 147 | import doctest 148 | doctest.testmod() 149 | -------------------------------------------------------------------------------- /src/pytraits/trait_composer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Copyright 2014-2015 Teppo Perä 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | from pytraits.core import TraitFactory 20 | from pytraits.support import type_converted 21 | 22 | 23 | TraitTarget = TraitFactory["TraitTargetInspector"] 24 | Traits = TraitFactory["Traits"] 25 | 26 | 27 | @type_converted 28 | def add_traits(target: TraitTarget, *traits, **resolutions): 29 | """ Bind new traits to given object. 30 | 31 | Args: 32 | target: Object of any type that is going to be extended with traits 33 | traits: Tuple of traits as object and strings or callables or functions. 34 | resolutions: dictionary of conflict resolutions to solve situations 35 | where multiple methods or properties of same name are 36 | encountered in traits. 37 | 38 | >>> class ExampleClass: 39 | ... def example_method(self): 40 | ... return None 41 | ... 42 | >>> class ExampleTrait: 43 | ... def other_method(self): 44 | ... return 42 45 | ... 46 | >>> add_traits(ExampleClass, ExampleTrait) 47 | >>> ExampleClass().other_method() 48 | 42 49 | """ 50 | # Return immediately, if no traits provided. 51 | if not len(traits): 52 | return 53 | 54 | # Just prepare object to start the work and get done with it. 55 | traits = Traits(traits) 56 | 57 | # This call puts all gears moving. Each trait in turn is being added 58 | # to target object. Resolutions are used to solve any conflicts along 59 | # the way. 60 | traits.compose(target, resolutions) 61 | 62 | 63 | if __name__ == '__main__': 64 | import doctest 65 | doctest.testmod() 66 | -------------------------------------------------------------------------------- /tests/testdata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class MetaClass(type): 6 | def __init__(self): 7 | pass 8 | 9 | 10 | class ExampleClass: 11 | TEST_ATTR_PUB = "public" 12 | 13 | def __init__(self): 14 | self.test_attr_pub = "public" 15 | 16 | @property 17 | def test_property(self): 18 | pass 19 | 20 | def test_method(self): 21 | pass 22 | 23 | @classmethod 24 | def test_classmethod(cls): 25 | pass 26 | 27 | @staticmethod 28 | def test_staticmethod(): 29 | pass 30 | 31 | example_instance = ExampleClass() 32 | 33 | 34 | def example_function(): 35 | pass 36 | 37 | 38 | def example_function_self(self): 39 | pass 40 | 41 | 42 | def example_function_cls(cls): 43 | pass 44 | -------------------------------------------------------------------------------- /tests/unittest_classobject.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | from pytraits.support.errors import FirstTraitArgumentError, TraitArgumentTypeError 6 | from pytraits.support import flatten 7 | from pytraits.core import TraitFactory 8 | 9 | from testdata import * 10 | 11 | 12 | inspect = TraitFactory["TraitTargetInspector"] 13 | 14 | 15 | class TestClassObject(unittest.TestCase): 16 | def setUp(self): 17 | self.classtype = inspect(ExampleClass) 18 | self.dir = dir(self.classtype) 19 | self.items = dict(self.classtype.items()) 20 | 21 | def __getitem__(self, key): 22 | return self.items[key] 23 | 24 | def typename(self, key): 25 | return type(self[key]).__name__ 26 | 27 | @property 28 | def iterated(self): 29 | return list(self.classtype) 30 | 31 | def test_has_a_custom_dir(self): 32 | self.assertEqual(self.dir, 33 | ['TEST_ATTR_PUB', 'test_classmethod', 'test_method', 34 | 'test_property', 'test_staticmethod']) 35 | 36 | def test_supports_showing_items(self): 37 | self.assertEqual(self.typename("TEST_ATTR_PUB"), "str") 38 | self.assertEqual(self.typename("test_classmethod"), "classmethod") 39 | self.assertEqual(self.typename("test_method"), "function") 40 | self.assertEqual(self.typename("test_property"), "property") 41 | self.assertEqual(self.typename("test_staticmethod"), "staticmethod") 42 | 43 | def test_supports_iteration(self): 44 | iterated = sorted([str(f) for f in self.iterated]) 45 | self.assertEqual(iterated[0], "classmethod") 46 | self.assertEqual(iterated[1], "method") 47 | self.assertEqual(iterated[2], "property") 48 | self.assertEqual(iterated[3], "staticmethod") 49 | 50 | def test_can_be_flattened(self): 51 | flat = flatten(self.classtype) 52 | self.assertEqual(sorted([str(f) for f in list(flat)]), 53 | ['classmethod', 'method', 'property', 'staticmethod']) 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /tests/unittest_compiler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | from utils import for_examples 6 | 7 | from pytraits.support.utils import get_func_name 8 | from pytraits.core.composing.compiler import Compiler 9 | 10 | 11 | def empty_func(self): 12 | pass 13 | 14 | 15 | class TestDummy: 16 | def __private_func(self): pass 17 | def _hidden_func(self): pass 18 | def public_func(self): pass 19 | 20 | 21 | class TestCompiler(unittest.TestCase): 22 | def setUp(self): 23 | self.compiler = Compiler() 24 | self.recompile = self.compiler.recompile 25 | self.test_class = type("TestClass", (), {}) 26 | 27 | @for_examples( 28 | (lambda self: self.__private, ('_TestClass__private', )), 29 | (lambda self: self.__internal__, ('__internal__', )), 30 | (lambda self: self.public, ('public', )), 31 | (lambda self: self.__private(), ('_TestClass__private', )), 32 | (lambda self: self.public(), ('public', )), 33 | (lambda self: (self.a, self._b, self._c, self.__d, self.__e), 34 | ('a', '_b', '_c', '_TestClass__d', '_TestClass__e')), 35 | (empty_func, ())) 36 | def test_supports_converting_attribute_names(self, func, attributes): 37 | compiled = self.recompile(func, self.test_class, "test") 38 | self.assertEqual(compiled.__code__.co_names, attributes) 39 | 40 | @for_examples( 41 | (empty_func, "anything", "anything"), 42 | (empty_func, "", "empty_func"), 43 | (empty_func, None, "empty_func"), 44 | (lambda: None, "anything", "anything"), 45 | (lambda: None, "", "lambda"), 46 | (TestDummy.public_func, "anything", "anything"), 47 | (TestDummy.public_func, "", "public_func"), 48 | (TestDummy._hidden_func, "anything", "anything"), 49 | (TestDummy._hidden_func, "", "_hidden_func"), 50 | (TestDummy._TestDummy__private_func, "anything", "anything"), 51 | (TestDummy._TestDummy__private_func, "", "__private_func")) 52 | def test_supports_renaming_trait(self, func, given_name, expected_name): 53 | compiled = self.recompile(func, self.test_class, given_name) 54 | self.assertEqual(get_func_name(compiled), expected_name) 55 | 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /tests/unittest_factory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | from pytraits import Factory 6 | from pytraits.support.errors import (FactoryError, 7 | FactoryRegisterError, 8 | FactoryClassMissingError) 9 | 10 | from utils import for_examples 11 | 12 | 13 | class TestFactory1(Factory): 14 | pass 15 | 16 | 17 | class TestFactory2(Factory): 18 | pass 19 | 20 | 21 | class TestFactory(unittest.TestCase): 22 | def setUp(self): 23 | @TestFactory1.register 24 | class TestClass1: 25 | pass 26 | 27 | @TestFactory2.register 28 | class TestClass2: 29 | pass 30 | 31 | def tearDown(self): 32 | TestFactory1.reset() 33 | TestFactory2.reset() 34 | 35 | def test_inherited_factories_are_own_instances(self): 36 | self.assertEqual(TestFactory1().registered_classes, ['TestClass1']) 37 | self.assertEqual(TestFactory2().registered_classes, ['TestClass2']) 38 | 39 | def test_can_create_instances_of_classes_with_create_classmethod(self): 40 | @TestFactory1.register 41 | class TestClass3: 42 | def __init__(self, test_value=None): 43 | self.test_value = test_value 44 | 45 | @classmethod 46 | def create(cls, init_value): 47 | return cls(init_value) 48 | 49 | instance = TestFactory1["TestClass3"](5) 50 | self.assertEqual(instance.test_value, 5) 51 | 52 | def test_classes_with_call_method_are_initialized(self): 53 | @TestFactory2.register 54 | class TestClass4: 55 | def __init__(self, test_value="default"): 56 | self.test_value = test_value 57 | 58 | def __call__(self, argument): 59 | return str(argument) 60 | 61 | sub_factory = TestFactory2["TestClass4"] 62 | answer_to_everything = sub_factory(42) 63 | self.assertEqual(sub_factory.test_value, "default") 64 | self.assertEqual(answer_to_everything, '42') 65 | 66 | def test_classes_with_call_and_create_method_are_properly(self): 67 | @TestFactory2.register 68 | class TestClass5: 69 | def __init__(self, test_value="default"): 70 | self.test_value = test_value 71 | 72 | @classmethod 73 | def create(cls): 74 | return cls("modified") 75 | 76 | def __call__(self, argument): 77 | return str(argument) 78 | 79 | sub_factory = TestFactory2["TestClass5"] 80 | answer_to_everything = sub_factory(100) 81 | self.assertEqual(sub_factory.test_value, "modified") 82 | self.assertEqual(answer_to_everything, '100') 83 | 84 | def test_complains_when_registered_class_exists_in_factory(self): 85 | @TestFactory2.register 86 | class TestClass4: 87 | pass 88 | 89 | with self.assertRaises(FactoryRegisterError): 90 | @TestFactory2.register 91 | class TestClass4: 92 | pass 93 | 94 | def test_factory_is_abstract(self): 95 | with self.assertRaises(FactoryError): 96 | Factory() 97 | 98 | def test_complains_when_class_missing_factory(self): 99 | with self.assertRaisesRegex(FactoryClassMissingError, 'is not in registered'): 100 | TestFactory1()["MissingClass"] 101 | 102 | def test_can_register_multiple_classes_in_single_call(self): 103 | TestClass3 = type('TestClass3', (), {}) 104 | TestClass4 = type('TestClass4', (), {}) 105 | TestFactory1.register(TestClass3, TestClass4) 106 | self.assertIn('TestClass3', TestFactory1().registered_classes) 107 | self.assertIn('TestClass4', TestFactory1().registered_classes) 108 | 109 | def test_knows_when_class_is_registered(self): 110 | TestClass5 = type('TestClass5', (), {}) 111 | self.assertFalse(TestFactory1().exists(TestClass5)) 112 | TestFactory1.register(TestClass5) 113 | self.assertTrue(TestFactory1().exists(TestClass5)) 114 | 115 | def test_register_works_as_decorator(self): 116 | TestClass6 = type('TestClass6', (), {}) 117 | clazz = TestFactory1.register(TestClass6) 118 | self.assertEqual(TestClass6, clazz) 119 | 120 | def test_registering_multiple_classes_returns_nothing(self): 121 | TestClass7 = type('TestClass7', (), {}) 122 | TestClass8 = type('TestClass8', (), {}) 123 | clazz = TestFactory1.register(TestClass7, TestClass8) 124 | self.assertEqual(None, clazz) 125 | 126 | if __name__ == '__main__': 127 | unittest.main() 128 | -------------------------------------------------------------------------------- /tests/unittest_inspector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | from collections import OrderedDict as odict 5 | 6 | from pytraits.support.inspector import Inspector 7 | 8 | from utils import for_examples 9 | from testdata import * 10 | 11 | 12 | class tdict(odict): 13 | def set(self, key, value): 14 | # Create a clone so that we don't mess up other tests. 15 | new_dict = tdict(self) 16 | new_dict[key] = value 17 | return new_dict 18 | 19 | 20 | class TestInspector(unittest.TestCase): 21 | TESTS = tdict([ 22 | (unittest, False), 23 | (MetaClass, False), 24 | (ExampleClass, False), 25 | (example_instance, False), 26 | (for_examples, False), 27 | (ExampleClass.test_method, False), 28 | (ExampleClass.test_property, False), 29 | (ExampleClass.TEST_ATTR_PUB, False), 30 | (ExampleClass.test_classmethod, False), 31 | (ExampleClass.test_staticmethod, False), 32 | (ExampleClass.__dict__["test_staticmethod"], False), 33 | (example_instance.test_method, False), 34 | (example_instance.TEST_ATTR_PUB, False), 35 | (example_instance.test_classmethod, False), 36 | (example_instance.test_staticmethod, False), 37 | (example_function, False), 38 | (int, False), 39 | (int.__add__, False), 40 | (type, False), 41 | (str, False), 42 | (1, False), 43 | ("test", False), 44 | (True, False), 45 | (None, False), 46 | (type(None), False), 47 | (map, False)]) 48 | 49 | def setUp(self): 50 | self.inspector = Inspector() 51 | self.inspect = self.inspector.inspect 52 | 53 | @for_examples( 54 | (unittest, 'module'), 55 | (MetaClass, 'metaclass'), 56 | (ExampleClass, 'class'), 57 | (example_instance, 'instance'), 58 | (odict(), 'instance'), 59 | (for_examples, 'function'), 60 | (ExampleClass.test_method, 'function'), 61 | (ExampleClass.test_property, 'property'), 62 | (ExampleClass.TEST_ATTR_PUB, 'data'), 63 | (ExampleClass.test_classmethod, 'classmethod'), 64 | (ExampleClass.test_staticmethod, 'staticmethod'), 65 | (ExampleClass.__dict__["test_staticmethod"], 'staticmethod'), 66 | (example_instance.test_method, 'method'), 67 | (example_instance.TEST_ATTR_PUB, 'data'), 68 | (example_instance.test_classmethod, 'classmethod'), 69 | (example_instance.test_staticmethod, 'staticmethod'), 70 | (example_function, 'function'), 71 | (int, 'builtin'), 72 | (int.__add__, 'routine'), 73 | (type, 'builtin'), 74 | (str, 'builtin'), 75 | (1, 'data'), 76 | ("test", 'data'), 77 | (True, 'data'), 78 | (None, 'data'), 79 | (type(None), 'builtin'), 80 | (map, 'builtin')) 81 | def test_check_all_types_when_hooks_are_not_defined(self, object, typename): 82 | self.assertEqual(self.inspect(object), typename) 83 | 84 | @for_examples(*TESTS.set(example_instance, True).items()) 85 | def test_isinstance_checks_identifies_types_properly(self, object, expected_result): 86 | self.assertEqual(self.inspector['instance'](object), expected_result) 87 | 88 | @for_examples(*TESTS.set(MetaClass, True).items()) 89 | def test_ismetaclass_checks_identifies_types_properly(self, object, expected_result): 90 | self.assertEqual(self.inspector['metaclass'](object), expected_result) 91 | 92 | @for_examples(*TESTS.set(ExampleClass, True).items()) 93 | def test_isclass_checks_identifies_types_properly(self, object, expected_result): 94 | self.assertEqual(self.inspector['class'](object), expected_result) 95 | 96 | @for_examples(*TESTS.set(ExampleClass.test_staticmethod, True) 97 | .set(ExampleClass.__dict__["test_staticmethod"], True).items()) 98 | def test_isclass_checks_identifies_types_properly(self, object, expected_result): 99 | self.assertEqual(self.inspector['staticmethod'](object), expected_result) 100 | 101 | def test_allows_promoting_type_checks(self): 102 | self.assertEqual(self.inspector.typenames[0], 'builtin') 103 | self.inspector.add_typecheck('data') 104 | self.assertEqual(self.inspector.typenames[0], 'data') 105 | self.assertEqual(self.inspector.inspect(2), 'data') 106 | 107 | if __name__ == '__main__': 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /tests/unittest_singleton.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | from pytraits import Singleton 6 | from pytraits.support.errors import SingletonError, SingletonInstanceError 7 | 8 | 9 | class TestSingleton(unittest.TestCase): 10 | def test_there_can_only_be_one(self): 11 | class One(metaclass=Singleton): 12 | pass 13 | 14 | first = One() 15 | second = One() 16 | 17 | self.assertEqual(id(first), id(second)) 18 | 19 | def test_second_initialization_with_arguments_are_ignored(self): 20 | class OneWithArgs(metaclass=Singleton): 21 | def __init__(self, one, two): 22 | self.one = one 23 | self.two = two 24 | 25 | this = OneWithArgs(1, 2) 26 | other = OneWithArgs(3, 4) 27 | 28 | self.assertEqual(this.one, 1) 29 | self.assertEqual(this.two, 2) 30 | self.assertEqual(other.one, 1) 31 | self.assertEqual(other.two, 2) 32 | self.assertEqual(id(this), id(other)) 33 | 34 | def test_second_initialization_with_kwarguments_are_ignored(self): 35 | class OneWithKwArgs(metaclass=Singleton): 36 | def __init__(self, **kwargs): 37 | self.one = kwargs['one'] 38 | self.two = kwargs['two'] 39 | 40 | this = OneWithKwArgs(one=1, two=2) 41 | other = OneWithKwArgs(three=3, four=4) 42 | 43 | self.assertEqual(this.one, 1) 44 | self.assertEqual(this.two, 2) 45 | self.assertEqual(other.one, 1) 46 | self.assertEqual(other.two, 2) 47 | self.assertEqual(id(this), id(other)) 48 | 49 | def test_enforces_immutability(self): 50 | class NoWrite(metaclass=Singleton): 51 | def __init__(self, begin): 52 | self.begin = begin 53 | 54 | no_write = NoWrite(3) 55 | with self.assertRaises(SingletonError): 56 | no_write.test = 5 57 | with self.assertRaises(SingletonError): 58 | no_write.begin = 1 59 | self.assertEqual(no_write.begin, 3) 60 | 61 | if __name__ == '__main__': 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /tests/unittest_traits.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | from pytraits.support.errors import FirstTraitArgumentError, TraitArgumentTypeError 6 | from pytraits.core.composing.traits import Traits 7 | 8 | 9 | class TestTraits(unittest.TestCase): 10 | def test_does_not_need_preprosessing_for_single_trait_object(self): 11 | traits = Traits(({}, )) 12 | self.assertFalse(traits.needs_preprocessing()) 13 | 14 | def test_does_not_need_preprosessing_for_multiple_trait_objects(self): 15 | traits = Traits(({}, {})) 16 | self.assertFalse(traits.needs_preprocessing()) 17 | 18 | def test_does_not_need_preprosessing_for_single_trait_function(self): 19 | traits = Traits((lambda: None, )) 20 | self.assertFalse(traits.needs_preprocessing()) 21 | 22 | def test_does_not_need_preprosessing_for_multiple_trait_functions(self): 23 | traits = Traits((lambda: None, lambda: None)) 24 | self.assertFalse(traits.needs_preprocessing()) 25 | 26 | def test_does_not_need_preprosessing_for_multiple_trait_functions(self): 27 | traits = Traits((lambda: None, lambda: None)) 28 | self.assertFalse(traits.needs_preprocessing()) 29 | 30 | def test_raises_error_when_first_argument_is_string(self): 31 | traits = Traits(("", "")) 32 | with self.assertRaises(FirstTraitArgumentError): 33 | traits.needs_preprocessing() 34 | 35 | def test_raises_error_when_arguments_mixed_with_objects_and_strings(self): 36 | traits = Traits(({}, "", lambda: None)) 37 | with self.assertRaises(TraitArgumentTypeError): 38 | traits.needs_preprocessing() 39 | 40 | def test_raises_error_when_arguments_mixed_with_function_and_strings(self): 41 | traits = Traits((lambda: None, "", "")) 42 | with self.assertRaises(FirstTraitArgumentError): 43 | traits.needs_preprocessing() 44 | 45 | def test_needs_preprosessing_when_object_and_strings_are_given(self): 46 | traits = Traits(({}, "")) 47 | self.assertTrue(traits.needs_preprocessing()) 48 | 49 | if __name__ == '__main__': 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /tests/unittest_typeconverted.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | from pytraits import type_converted 6 | from pytraits.support.errors import TypeConversionError 7 | 8 | 9 | class TestTypeConverted(unittest.TestCase): 10 | def test_shows_unassigned_arguments_error_for_omitted_arguments(self): 11 | # We need to make sure that when user misses argument from the 12 | # function call, we show proper error message. 13 | @type_converted 14 | def converted(existing, missing): 15 | pass 16 | 17 | with self.assertRaisesRegex(TypeError, ".*missing 1 required.*"): 18 | converted(True) 19 | 20 | def test_shows_unassigned_arguments_error_for_ommitted_arguments_with_type(self): 21 | # Even if argument has any arguments with annotated type, we still 22 | # need to give proper error message, when that argument has been 23 | # omitted. 24 | @type_converted 25 | def converted(existing, missing: int): 26 | pass 27 | 28 | with self.assertRaisesRegex(TypeError, ".*missing 1 required.*"): 29 | converted(True) 30 | 31 | def test_uses_default_value_for_omitted_arguments(self): 32 | # Missing arguments with default values should be properly used when 33 | # arguments are omitted. 34 | @type_converted 35 | def converted(existing, missing_with_default=42): 36 | return missing_with_default 37 | 38 | self.assertEqual(converted(True), 42) 39 | 40 | def test_uses_default_value_for_omitted_arguments_with_type(self): 41 | # Missing arguments with default values should be properly used when 42 | # arguments are omitted even when there are annotated arguments. 43 | @type_converted 44 | def converted(existing, missing_with_default: int=42): 45 | return missing_with_default 46 | 47 | self.assertEqual(converted(True), 42) 48 | 49 | def test_ignores_default_value_when_argument_given_with_type(self): 50 | # Missing arguments with default values should be properly used when 51 | # arguments are omitted even when there are annotated arguments. 52 | @type_converted 53 | def converted(existing, missing_with_default: int=42): 54 | return missing_with_default 55 | 56 | self.assertEqual(converted(True, "52"), 52) 57 | 58 | def test_handles_properly_tuple_arguments(self): 59 | @type_converted 60 | def converted(existing, *remainder): 61 | return existing 62 | 63 | self.assertEqual(converted(True), True) 64 | 65 | def test_handles_properly_tuple_arguments_with_type(self): 66 | @type_converted 67 | def converted(existing: bool, *remainder): 68 | return existing 69 | 70 | self.assertEqual(converted(True), True) 71 | 72 | def test_handles_properly_tuple_arguments_with_type(self): 73 | @type_converted 74 | def converted(existing: bool, *remainder): 75 | return existing 76 | 77 | with self.assertRaisesRegex(TypeConversionError, "While calling.*"): 78 | converted(2, "tuple", "args") 79 | 80 | def test_shows_proper_error_when_too_many_args_given(self): 81 | @type_converted 82 | def converted(existing): 83 | return missing_with_default 84 | 85 | with self.assertRaisesRegex(TypeError, ".*takes 1 positional.*"): 86 | self.assertEqual(converted(True, 52), 52) 87 | 88 | def test_shows_proper_error_when_too_many_args_given_with_type(self): 89 | @type_converted 90 | def converted(existing: bool): 91 | return missing_with_default 92 | 93 | with self.assertRaisesRegex(TypeError, ".*takes 1 positional.*"): 94 | self.assertEqual(converted(True, 52), 52) 95 | 96 | def test_shows_proper_error_when_too_many_args_given_with_default(self): 97 | @type_converted 98 | def converted(existing=False): 99 | return missing_with_default 100 | 101 | with self.assertRaisesRegex(TypeError, ".*takes from 0 to 1 positional.*"): 102 | self.assertEqual(converted(True, 52), 52) 103 | 104 | def test_shows_proper_error_when_too_many_args_given_with_type_and_default(self): 105 | @type_converted 106 | def converted(existing: bool=False): 107 | return missing_with_default 108 | 109 | with self.assertRaisesRegex(TypeError, ".*takes from 0 to 1 positional.*"): 110 | self.assertEqual(converted(True, 52), 52) 111 | 112 | if __name__ == '__main__': 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /tests/unittest_typesafe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | from pytraits import type_safe 6 | 7 | 8 | class TestTypeSafe(unittest.TestCase): 9 | def test_shows_unassigned_arguments_error_for_omitted_arguments(self): 10 | # We need to make sure that when user misses argument from the 11 | # function call, we show proper error message. 12 | @type_safe 13 | def checked(existing, missing): 14 | pass 15 | 16 | with self.assertRaisesRegex(TypeError, ".*missing 1 required.*"): 17 | checked(True) 18 | 19 | def test_shows_unassigned_arguments_error_for_ommitted_arguments_with_type(self): 20 | # Even if argument has any arguments with annotated type, we still 21 | # need to give proper error message, when that argument has been 22 | # omitted. 23 | @type_safe 24 | def checked(existing, missing: int): 25 | pass 26 | 27 | with self.assertRaisesRegex(TypeError, ".*missing 1 required.*"): 28 | checked(True) 29 | 30 | def test_uses_default_value_for_omitted_arguments(self): 31 | # Missing arguments with default values should be properly used when 32 | # arguments are omitted. 33 | @type_safe 34 | def checked(existing, missing_with_default=42): 35 | return missing_with_default 36 | 37 | self.assertEqual(checked(True), 42) 38 | 39 | def test_uses_default_value_for_omitted_arguments_with_type(self): 40 | # Missing arguments with default values should be properly used when 41 | # arguments are omitted even when there are annotated arguments. 42 | @type_safe 43 | def checked(existing, missing_with_default: int=42): 44 | return missing_with_default 45 | 46 | self.assertEqual(checked(True), 42) 47 | 48 | def test_ignores_default_value_when_argument_given_with_type(self): 49 | # Missing arguments with default values should be properly used when 50 | # arguments are omitted even when there are annotated arguments. 51 | @type_safe 52 | def checked(existing, missing_with_default: int=42): 53 | return missing_with_default 54 | 55 | self.assertEqual(checked(True, 52), 52) 56 | 57 | def test_handles_properly_tuple_arguments(self): 58 | @type_safe 59 | def checked(existing, *remainder): 60 | return existing 61 | 62 | self.assertEqual(checked(True), True) 63 | 64 | def test_handles_properly_tuple_arguments_with_type(self): 65 | @type_safe 66 | def checked(existing: bool, *remainder): 67 | return existing 68 | 69 | self.assertEqual(checked(True), True) 70 | 71 | def test_handles_properly_tuple_arguments_with_type(self): 72 | @type_safe 73 | def checked(existing: bool, *remainder): 74 | return existing 75 | 76 | with self.assertRaisesRegex(TypeError, "While calling.*"): 77 | checked(2, "tuple", "args") 78 | 79 | def test_shows_proper_error_when_too_many_args_given(self): 80 | @type_safe 81 | def checked(existing): 82 | return missing_with_default 83 | 84 | with self.assertRaisesRegex(TypeError, ".*takes 1 positional.*"): 85 | self.assertEqual(checked(True, 52), 52) 86 | 87 | def test_shows_proper_error_when_too_many_args_given_with_type(self): 88 | @type_safe 89 | def checked(existing: bool): 90 | return missing_with_default 91 | 92 | with self.assertRaisesRegex(TypeError, ".*takes 1 positional.*"): 93 | self.assertEqual(checked(True, 52), 52) 94 | 95 | def test_shows_proper_error_when_too_many_args_given_with_default(self): 96 | @type_safe 97 | def checked(existing=False): 98 | return missing_with_default 99 | 100 | with self.assertRaisesRegex(TypeError, ".*takes from 0 to 1 positional.*"): 101 | self.assertEqual(checked(True, 52), 52) 102 | 103 | def test_shows_proper_error_when_too_many_args_given_with_type_and_default(self): 104 | @type_safe 105 | def checked(existing: bool=False): 106 | return missing_with_default 107 | 108 | with self.assertRaisesRegex(TypeError, ".*takes from 0 to 1 positional.*"): 109 | self.assertEqual(checked(True, 52), 52) 110 | 111 | 112 | if __name__ == '__main__': 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | 7 | def for_examples(*parameters): 8 | """ 9 | From StackOverflow: 10 | http://stackoverflow.com/questions/2798956/python-unittest-generate-multiple-tests-programmatically 11 | """ 12 | def tuplify(x): 13 | if not isinstance(x, tuple): 14 | return (x,) 15 | return x 16 | 17 | def decorator(method, parameters=parameters): 18 | for parameter in (tuplify(x) for x in parameters): 19 | def method_for_parameter(self, method=method, parameter=parameter): 20 | method(self, *parameter) 21 | args_for_parameter = ",".join(repr(v) for v in parameter) 22 | name_for_parameter = method.__name__ + "(" + args_for_parameter + ")" 23 | frame = sys._getframe(1) 24 | frame.f_locals[name_for_parameter] = method_for_parameter 25 | return None 26 | return decorator 27 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | clean, 4 | check, 5 | 3.3, 6 | 3.3-nocover, 7 | 3.4, 8 | 3.4-nocover, 9 | report, 10 | docs 11 | 12 | [testenv] 13 | setenv = 14 | PYTHONPATH={toxinidir}/tests 15 | PYTHONUNBUFFERED=yes 16 | deps = 17 | pytest 18 | pytest-capturelog 19 | commands = 20 | {posargs:py.test -vv --ignore=src} 21 | 22 | [testenv:spell] 23 | setenv = 24 | SPELLCHECK = 1 25 | commands = 26 | sphinx-build -b spelling docs dist/docs 27 | usedevelop = true 28 | deps = 29 | -r{toxinidir}/docs/requirements.txt 30 | sphinxcontrib-spelling 31 | pyenchant 32 | 33 | [testenv:docs] 34 | whitelist_externals = 35 | rm 36 | commands = 37 | rm -rf dist/docs || rmdir /S /Q dist\docs 38 | sphinx-build -b html docs dist/docs 39 | sphinx-build -b linkcheck docs dist/docs 40 | usedevelop = true 41 | deps = 42 | -r{toxinidir}/docs/requirements.txt 43 | 44 | [testenv:configure] 45 | deps = 46 | jinja2 47 | matrix 48 | usedevelop = true 49 | commands = 50 | python bootstrap.py 51 | 52 | [testenv:check] 53 | basepython = python3.4 54 | deps = 55 | docutils 56 | check-manifest 57 | flake8 58 | readme 59 | pygments 60 | usedevelop = true 61 | commands = 62 | python setup.py check --strict --metadata --restructuredtext 63 | check-manifest {toxinidir} 64 | flake8 src 65 | 66 | [testenv:coveralls] 67 | deps = 68 | coveralls 69 | usedevelop = true 70 | commands = 71 | coverage combine 72 | coverage report 73 | coveralls 74 | 75 | [testenv:report] 76 | basepython = python3.4 77 | commands = 78 | coverage combine 79 | coverage report 80 | usedevelop = true 81 | deps = coverage 82 | 83 | [testenv:clean] 84 | commands = coverage erase 85 | usedevelop = true 86 | deps = coverage 87 | 88 | [testenv:3.3] 89 | basepython = python3.3 90 | setenv = 91 | {[testenv]setenv} 92 | WITH_COVERAGE=yes 93 | usedevelop = true 94 | commands = 95 | {posargs:py.test --cov=src --cov-report=term-missing -vv} 96 | deps = 97 | {[testenv]deps} 98 | pytest-cov 99 | 100 | [testenv:3.3-nocover] 101 | basepython = python3.3 102 | 103 | [testenv:3.4] 104 | basepython = python3.4 105 | setenv = 106 | {[testenv]setenv} 107 | WITH_COVERAGE=yes 108 | usedevelop = true 109 | commands = 110 | {posargs:py.test --cov=src --cov-report=term-missing -vv} 111 | deps = 112 | {[testenv]deps} 113 | pytest-cov 114 | 115 | [testenv:3.4-nocover] 116 | basepython = python3.4 117 | 118 | 119 | 120 | --------------------------------------------------------------------------------