├── .coveragerc ├── .gitignore ├── .landscape.yaml ├── .noserc ├── .travis.yml ├── CHANGES ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── LICENSE.txt ├── README.md ├── README.txt ├── appveyor.yml ├── docs ├── Makefile ├── conf.py ├── index.rst ├── logo_small.png ├── make.bat ├── make_html.bat └── requirements.txt ├── pyblish ├── __init__.py ├── __main__.py ├── api.py ├── cli.py ├── compat.py ├── error.py ├── icons │ ├── logo-32x32.svg │ └── logo-64x64.svg ├── lib.py ├── logic.py ├── main.py ├── plugin.py ├── plugins │ ├── collect_current_date.py │ ├── collect_current_user.py │ └── collect_current_working_directory.py ├── util.py ├── vendor │ ├── __init__.py │ ├── click │ │ ├── __init__.py │ │ ├── _bashcomplete.py │ │ ├── _compat.py │ │ ├── _termui_impl.py │ │ ├── _textwrap.py │ │ ├── core.py │ │ ├── decorators.py │ │ ├── exceptions.py │ │ ├── formatting.py │ │ ├── parser.py │ │ ├── termui.py │ │ ├── testing.py │ │ ├── types.py │ │ └── utils.py │ ├── iscompatible.py │ ├── mock.py │ └── six.py └── version.py ├── run_coverage.py ├── run_testsuite.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── lib.py ├── plugins.py ├── plugins ├── missing_extension │ └── myCollector ├── missing_host │ └── missing_host.py └── private │ └── _start_with_underscore.py ├── pre11 ├── __init__.py ├── lib.py ├── plugins │ ├── conform_instances.py │ ├── custom │ │ └── validate_custom_instance.py │ ├── duplicate │ │ ├── copy1 │ │ │ └── select_duplicate_instances.py │ │ └── copy2 │ │ │ └── select_duplicate_instances.py │ ├── echo │ │ └── select_echo.py │ ├── extract_documents.py │ ├── extract_instances.py │ ├── failing │ │ ├── conform_instances_fail.py │ │ ├── extract_instances_fail.py │ │ ├── select_instances_fail.py │ │ └── validate_instances_fail.py │ ├── failing_cli │ │ ├── extract_cli_instances.py │ │ └── select_cli_instances.py │ ├── full │ │ ├── conform_instances.py │ │ ├── extract_instances.py │ │ ├── select_instances.py │ │ └── validate_instances.py │ ├── invalid │ │ ├── select_missing_hosts.py │ │ └── validate_missing_families.py │ ├── select_instances.py │ ├── validate_instances.py │ ├── validate_other_instance.py │ └── wildcards │ │ ├── select_instances.py │ │ └── validate_instances.py ├── test_cli.py ├── test_logic.py └── test_plugins.py ├── pre13 ├── __init__.py ├── test_di.py ├── test_logic.py └── test_plugin.py ├── test_cli.py ├── test_context.py ├── test_events.py ├── test_logic.py ├── test_misc.py ├── test_plugin.py ├── test_simple.py └── test_util.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = pyblish 3 | 4 | [report] 5 | include = *pyblish* 6 | omit = 7 | */vendor* 8 | */tests* 9 | */run_* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swatches 2 | .idea 3 | **/published 4 | *.mb 5 | *.egg-info 6 | dist 7 | *.pyc 8 | cover 9 | .coverage 10 | docs/_* 11 | build 12 | playground.py 13 | *.sublime-* 14 | __pycache__ 15 | *.p4env 16 | *pyrightconfig.json 17 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | doc-warnings: true 2 | max-line-length: 80 3 | ignore-paths: 4 | - docs 5 | - pyblish/vendor -------------------------------------------------------------------------------- /.noserc: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=2 3 | with-doctest=1 4 | with-coverage=1 5 | cover-html=1 6 | cover-erase=1 7 | exclude=vendor 8 | cover-tests=1 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.5 5 | - 3.7 6 | - 3.8 7 | install: 8 | - pip install coveralls 9 | script: 10 | - nosetests -c .noserc 11 | - pip install git+git://github.com/pyblish/pyblish-base.git 12 | after_success: 13 | - coveralls 14 | deploy: 15 | provider: pypi 16 | user: mottosso 17 | distributions: "sdist bdist_wheel" 18 | password: 19 | secure: fwXIOGKn38gJFNkzlpvholQBhSBzNorOnMvs04JT3+Fdq6ys2TiUV6tlXHDwo6DkMY++USI19oD9NWlvg8gQaRJq4g2V/taazn8XDv1XnyKLzb6DnAT1ALilJtbIgotH/0QNOvkDP40a8UDwbelE0aR4AptNO1Ts7n1eERygENA= 20 | on: 21 | tags: true 22 | all_branches: true 23 | python: 2.7 24 | sudo: false 25 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | pyblish Changelog 2 | ================= 3 | 4 | This contains all major version changes between pyblish releases. 5 | 6 | Version 1.4.2 7 | ------------- 8 | 9 | - Bugfix: `plugins` argument of CVEI convenience functions did not have any effect (#286) 10 | - Feature: CVEI-friendly convenience functions (#288) 11 | - BACKWARD INCOMPATIBLE: Due to #288, the behavior of util.collect() and friends are different and will need refactoring in your code if you depend on them. 12 | 13 | Version 1.4.1 14 | ------------- 15 | 16 | - Feature: register_gui (#279) 17 | 18 | Version 1.4.0 - Steel 19 | --------------------- 20 | 21 | - Enhancement: All major objects now boasts unique IDs for increased stability (#266) 22 | - Enhancement: Stability improvements; Context and Instance are now much closer related (88478e9) 23 | - Bugfix: InstancePlugin collector will error out util.publish() with multiple instances (#268) 24 | - Enhancement: Simplified Iterator (#264) 25 | 26 | Version 1.3.1 27 | ------------- 28 | 29 | - Enhancement: pyblish.api.deregister_callback now throws an error on missing callback 30 | 31 | Version 1.3.0 32 | ------------- 33 | 34 | - Explicit plug-ins (#242) 35 | - Simplified CLI 36 | - CLI --data now handles inproperly formatted data (by treating it as strings) 37 | - DEPRECATED: Services 38 | - DEPRECATED: pyblish.logic.process 39 | - BACKWARD INCOMPATIBLE: Vendored packages nose, yaml and coverage no longer included 40 | - BACKWARD INCOMPATIBLE: CLI side-car configuration files no longer picked up. 41 | - BACKWARD INCOMPATIBLE: CLI side-car data files no longer picked up. 42 | 43 | Version 1.2.3 44 | ------------- 45 | 46 | - Callbacks (#238) 47 | - Iterator (#239) 48 | 49 | Version 1.2.2 50 | ------------- 51 | 52 | - Added icons for integrations under /icons 53 | - Added support for instances with multiple families (#231) 54 | 55 | Version 1.2.1 56 | ------------- 57 | 58 | - Feature: Actions (see #142) 59 | - Enhancement: All instances are now given a default family, where none is provided, called "default". 60 | - Enhancement: Added deprecation warnings; quiet by default, run Python with -Wdefault to see them. 61 | - Enhancement: In favour of maintainability, the dict-like property of Context was simplified; no behavioural difference. 62 | - Enhancement: Multiple instances of the same name now officially supported. 63 | - DEPRECATED: Context/Instance `.data` is now a pure dictionary. 64 | - DEPRECATED: Context/Instance `.add` and `.remove` 65 | 66 | Version 1.2.0 67 | ------------- 68 | 69 | - BACKWARD INCOMPATIBLE: Directly registered plug-ins are now also checked for duplicates; before, they overwrite those discovered on disk. 70 | - BACKWARD INCOMPATIBLE: `register_plugin` now checks the proposed registered plug-in for correctness, and throws an exception otherwise. 71 | - BACKWARD INCOMPATIBLE: `regex` has been deprecated from `discover()` and does nothing. 72 | - BACKWARD INCOMPATIBLE: `type` has been deprecated from `discover()` and does nothing. 73 | - Enhancement: Implementation of discover() has been greatly simplified, made more robust and reusable. 74 | 75 | Version 1.1.6 76 | ------------- 77 | 78 | - Bugfix: Duplicate instances was allowed (#219) 79 | - Added pyblish.plugin.plugins_from_module 80 | 81 | Version 1.1.5 82 | ------------- 83 | 84 | - Bugfix: Deprecated function pyblish.api.current_host fixed (#215) 85 | 86 | Version 1.1.4 87 | ------------- 88 | 89 | - Feature: Added support for `"MyInstance" in context` 90 | - Enhancement: Removing the need for @pyblish.api.log (#213) 91 | - Bugfix: Negative collectors (#210) 92 | 93 | Version 1.1.3 94 | ------------- 95 | 96 | - Bugfix: Decrementing order actually works now. (#206) 97 | - Feature: Dict-like getting of instances from Context (#208) 98 | - Feature: Host registration (#177) 99 | - `collect` and `integrate` now available via pyblish.util 100 | - "asset" available in context.data("result") for forwards compatibility 101 | 102 | Version 1.1.2 103 | ------------- 104 | 105 | - Logic: Excluding SimplePlugin and Selectors from Default test (See #198) 106 | - BACKWARDS INCOMPATIBLE order now supports being decremented (see #199) 107 | 108 | Version 1.1.1 109 | ------------- 110 | 111 | - Enhancement: Hosts limit, not allow (see #194) 112 | - Enhancement: CLI outputs less, more concise information 113 | - Enhancement: Lowering logging level for plug-ins skipped during discovery to DEBUG 114 | - Enhancement: Underscore-prefixed plug-ins are hidden from discovery (see #196) 115 | - Bugfix: Discover ignores non-Python files (see #192) 116 | 117 | Version 1.1.0 118 | ------------- 119 | 120 | - Feature: Dependency Injection (see #127) 121 | - Feature: SimplePlugin (see #186) 122 | - Feature: In-memory plug-ins (see #140) 123 | - Feature: Custom test (see #183) 124 | - Feature: create_instance(name, **kwargs) (see #187) 125 | - Preview: Asset (see #188) 126 | - Enhancement: Logic unified between pyblish.util and pyblish.cli 127 | - Bugfix: Order now works with pyblish.util and pyblish.cli (see #178) 128 | - Standardised time format (see #181) 129 | - pyblish.util minified. For data visualisation, refer to pyblish-qml 130 | - Bugfix: True singleton configuration (see #182) 131 | - Added pyblish.lib.ItemList 132 | - API: Added Plugin.label 133 | - API: Added Plugin.active 134 | - API: Added pyblish.api.register_test() 135 | - API: Added pyblish.api.deregister_test() 136 | - API: Added pyblish.api.registered_test() 137 | - API: Added pyblish.api.plugins_by_instance() 138 | - API: New defaults for `hosts` and `families` of plug-ins. (see #176) 139 | - API: Added pyblish.api.register_plugin() 140 | - API: Added pyblish.api.deregister_plugin() 141 | - API: Added pyblish.api.registered_plugins() 142 | - API: Added pyblish.api.deregister_all_plugins() 143 | - API: Added pyblish.api.register_service() 144 | - API: Added pyblish.api.deregister_service() 145 | - API: Added pyblish.api.registered_services() 146 | - API: Added pyblish.api.deregister_all_services() 147 | - API: Renamed pyblish.api.sort() to pyblish.api.sort_plugins(), original deprecated 148 | - API: Renamed pyblish.api.deregister_all -> deregister_all_paths, original deprecated 149 | - BACKWARD INCOMPATIBLE You can no longer use both process_instance and process_context in the same plug-in; process_instance takes precedence. 150 | - BACKWARD INCOMPATIBLE Removed Extractor.compute_commit_dir 151 | - BACKWARD INCOMPATIBLE Removed Extractor.commit 152 | - BACKWARD INCOMPATIBLE Removed validate_naming_convention plug-in 153 | - Known issue: pyblish.logic.process.next_plugin yields wrong result 154 | 155 | Version 1.0.16 156 | -------------- 157 | 158 | - Feature: The Pyblish CLI is back! 159 | - API: Added pyblish.api.sort() 160 | - API: Added pyblish.api.current_host() 161 | - API: Plug-in paths can no longer be modified by altering 162 | the results returned from pyblish.api.plugin_paths() 163 | - API: Paths are no longer made absolute during registration. 164 | 165 | Version 1.0.15 166 | -------------- 167 | 168 | - API: Plugin.repair_* documented and implemented by default 169 | - API: Added lib.where() 170 | - API: Added `.id` attribute to instances and plug-ins 171 | 172 | Version 1.0.14 173 | -------------- 174 | 175 | - Added pyblish.api.version 176 | - Matched verbosity level from processing context as the processing of instances. 177 | - Processing logic change; processing of plug-in *without* compatible instances will now *not* yield anything; previously it yielded a pair of (None, None). 178 | 179 | Version 1.0.13 180 | -------------- 181 | 182 | - Added pyblish.api.sort_plugins 183 | - Added ordered output to pyblish.api.discover 184 | - pyblish.api.plugins_by_family now yields correct results 185 | for plug-ins with wildcard families. 186 | - Refactored main.py into util.py 187 | 188 | Version 1.0.12 189 | -------------- 190 | 191 | - plugin.py reloadable without loosing currently loaded plug-ins 192 | - Basic plug-in manager "Manager" 193 | - Simplified configuration (no more user or custom configuration) 194 | - Simplifying discover() (no more indirection) 195 | - Adding default logger (good bye "No logger found..") 196 | - Temporarily removig CLI 197 | - Context is no longer a Singleton 198 | - Added forwards compatibility for Collector plug-in 199 | 200 | Version 1.0.11 201 | -------------- 202 | 203 | - Added ability to process individual instances from within context 204 | 205 | Version 1.0.10 206 | -------------- 207 | 208 | - Fixing PyPI installation 209 | 210 | Version 1.0.9 211 | ------------- 212 | 213 | - Requires. Plug-ins may now specify a version with which they 214 | are compatible with, using [iscompatible][] which is a 215 | requirements.txt-like syntax for dependency specifications. 216 | - Improved logging, including visualisation of which instance 217 | is currently being processed by each plug-in. 218 | - iscompatible is now included in /vendor 219 | 220 | [iscompatible]: https://github.com/mottosso/iscompatible 221 | 222 | 223 | Version 1.0.8 224 | ------------- 225 | 226 | - Nice name for Plug-ins. A plug-in can now carry an 227 | optional `name` attribute that will replace the 228 | default class-name used when visualising a plug-in 229 | name. 230 | - Configurable configuration location. Users can now specify 231 | where Pyblish will look for configuration files, via the 232 | PYBLISHCONFIGPATH variable. 233 | - Registered path no longer have to exist. To align better with 234 | paths added via environment variables or configuration, the 235 | registration of paths is now okay even though the path may 236 | not exist. A non-existing path will simply be discarded upon 237 | discovery (and log a warning message). 238 | - Auto-repair. pyblish.main.publish() now takes a auto_repair flag 239 | with which the Plugin.repair_instance method is called. If a 240 | repair fails, a message is logged, otherwise the validation is 241 | considered a success and publishing resumes. 242 | - Plug-ins for other hosts than the currently running host are 243 | discarded upon discovery. This means that they will be invisible 244 | to any incompatible host. 245 | - Optional plug-ins. A plug-in may now be marked "optional" and thus 246 | be ignored during processing by user-request using 247 | pyblish.main.publish(include_optional=False) 248 | 249 | Version 1.0.7 250 | ------------- 251 | 252 | - Improved logging of pyblish.main.publish() 253 | - Refactored backend/frontend; frontends now in their own 254 | individual repositories. 255 | - Added landscape.io code-quality badge 256 | 257 | Version 1.0.6 258 | ------------- 259 | 260 | - New API members: plugins_by_family, plugins_by_host and 261 | instances_by_plugin 262 | - Improved print of Context and Instance 263 | Context now prints members, like a regular list 264 | and Instance prints its name and class relation. 265 | - Implemented Context.delete() to simplify working with a Singleton. 266 | 267 | Version 1.0.5 268 | ------------- 269 | 270 | - Redefined and simplified configuration 271 | - Moving API from root package to namespace "api" 272 | - Initial version of command-line interface (cli) 273 | - Initial version of package-control using cli -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing to this project 2 | 3 | To contribute, fork this project and submit a pull-request. 4 | 5 | Your code will be reviewed and merged once it: 6 | 7 | 1. Does something useful 8 | 1. Is up to par with surrounding code 9 | 10 | Development for this project takes place in individual forks. The parent project ever only contains a single branch, a branch containing the latest working version of the project. 11 | 12 | Bugreports must include: 13 | 14 | 1. Description 15 | 2. Expected results 16 | 3. Short reproducible 17 | 18 | Feature requests must include: 19 | 20 | 1. Goal (what the feature aims to solve) 21 | 2. Motivation (why *you* think this is necessary) 22 | 3. Suggested implementation (psuedocode) 23 | 24 | Questions may also be submitted as issues. 25 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | https://github.com/mottosso 2 | https://github.com/ljkart 3 | https://github.com/Byron 4 | https://github.com/madoodia 5 | danielottosson2@gmail.com 6 | https://github.com/davidmartinezanim 7 | https://github.com/tokejepsen 8 | https://github.com/bigroy 9 | https://github.com/mkolar 10 | https://github.com/pscadding -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | 167 | -------------------------------------------------------------------------------- 168 | 169 | SPECIAL EXCEPTION GRANTED BY COPYRIGHT HOLDERS 170 | 171 | As a special exception, copyright holders give you permission to link this 172 | library with independent modules to produce an executable, regardless of 173 | the license terms of these independent modules, and to copy and distribute 174 | the resulting executable under terms of your choice, provided that you also 175 | meet, for each linked independent module, the terms and conditions of 176 | the license of that module. An independent module is a module which is not 177 | derived from or based on this library. If you modify this library, you must 178 | extend this exception to your version of the library. 179 | 180 | Note: this exception relieves you of any obligations under sections 4 and 5 181 | of this license, and section 6 of the GNU General Public License. 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][travis-image]][travis-link] 2 | [![Build status][appveyor-image]](https://ci.appveyor.com/project/mottosso/pyblish) 3 | [![Coverage Status][cover-image]][cover-link] 4 | [![PyPI version][pypi-image]][pypi-link] 5 | [![Code Health][landscape-image]][landscape-repo] 6 | [![Gitter][gitter-image]](https://gitter.im/pyblish/pyblish?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | [![image](https://cloud.githubusercontent.com/assets/2152766/12704326/b6ff015c-c850-11e5-91be-68d824526f13.png)](https://www.youtube.com/watch?v=j5uUTW702-U) 9 | 10 | Test-driven content creation for collaborative, creative projects. 11 | 12 | - [Wiki](../../wiki) 13 | - [Learn by Example](http://learn.pyblish.com) 14 | 15 |
16 |
17 |
18 | 19 | ### Introduction 20 | 21 | Pyblish is a modular framework, consisting of many sub-projects. This project contains the primary API upon which all other projects build. 22 | 23 | You may use this project as-is, or in conjunction with surrounding projects - such as [pyblish-maya][] for integration with Autodesk Maya, [pyblish-qml][] for a visual front-end and [pyblish-starter][] for a starting point your publishing pipeline. 24 | 25 | [pyblish-maya]: https://github.com/pyblish/pyblish-maya 26 | [pyblish-qml]: https://github.com/pyblish/pyblish-qml 27 | [pyblish-starter]: http://pyblish.com/pyblish-starter 28 | 29 | - [Browse All Projects](https://github.com/pyblish) 30 | 31 |
32 |
33 |
34 | 35 | ### Installation 36 | 37 | pyblish-base is available on PyPI. 38 | 39 | ```bash 40 | $ pip install pyblish-base 41 | ``` 42 | 43 | Like all other Pyblish projects, it may also be cloned as-is via Git and added to your PYTHONPATH. 44 | 45 | ```bash 46 | $ git clone https://github.com/pyblish/pyblish-base.git 47 | $ # Windows 48 | $ set PYTHONPATH=%cd%\pyblish-base 49 | $ # Unix 50 | $ export PYTHONPATH=$(pwd)/pyblish-base 51 | ``` 52 | 53 |
54 |
55 |
56 | 57 | ### Usage 58 | 59 | Refer to the [getting started guide](http://learn.pyblish.com) for a gentle introduction to the framework and [the forums](http://forums.pyblish.com) for tips and tricks. 60 | 61 | - [Learn Pyblish By Example](http://learn.pyblish.com) 62 | - [Pyblish Starter - an example pipeline](http://pyblish.com/pyblish-starter) 63 | - [Forums](http://forums.pyblish.com) 64 | 65 | [travis-image]: https://travis-ci.org/pyblish/pyblish-base.svg?branch=master 66 | [travis-link]: https://travis-ci.org/pyblish/pyblish-base 67 | 68 | [appveyor-image]: https://ci.appveyor.com/api/projects/status/github/pyblish/pyblish-base?svg=true 69 | 70 | [cover-image]: https://coveralls.io/repos/pyblish/pyblish-base/badge.svg 71 | [cover-link]: https://coveralls.io/r/pyblish/pyblish-base 72 | [pypi-image]: https://badge.fury.io/py/pyblish-base.svg 73 | [pypi-link]: http://badge.fury.io/py/pyblish 74 | [landscape-image]: https://landscape.io/github/pyblish/pyblish-base/master/landscape.png 75 | [landscape-repo]: https://landscape.io/github/pyblish/pyblish-base/master 76 | [gitter-image]: https://badges.gitter.im/Join%20Chat.svg 77 | 78 |
79 | 80 | ### Upload to PyPI 81 | 82 | To make a new release onto PyPI, you'll need to be mottosso and type this. 83 | 84 | ```bash 85 | cd pyblish-base 86 | python .\setup.py sdist bdist_wheel 87 | python -m twine upload .\dist\* 88 | ``` 89 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Pyblish 2 | ======= 3 | 4 | Plug-in driven automation framework for content. 5 | 6 | Built for platform, software and language agnosticism. Free, community-driven and licensed under LGPLv3. 7 | 8 | `See our website`_ for more information 9 | 10 | .. _`See our website`: http://pyblish.com 11 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - PYTHON: "C:\\Python27" 4 | PYTHON_VERSION: "2.7.8" 5 | PYTHON_ARCH: "32" 6 | 7 | - PYTHON: "C:\\Python27-x64" 8 | PYTHON_VERSION: "2.7.8" 9 | PYTHON_ARCH: "64" 10 | 11 | - PYTHON: "C:\\Python33" 12 | PYTHON_VERSION: "3.3.5" 13 | PYTHON_ARCH: "32" 14 | 15 | - PYTHON: "C:\\Python33-x64" 16 | PYTHON_VERSION: "3.3.5" 17 | PYTHON_ARCH: "64" 18 | 19 | - PYTHON: "C:\\Python34" 20 | PYTHON_VERSION: "3.4.1" 21 | PYTHON_ARCH: "32" 22 | 23 | - PYTHON: "C:\\Python34-x64" 24 | PYTHON_VERSION: "3.4.1" 25 | PYTHON_ARCH: "64" 26 | 27 | - PYTHON: "C:\\Python35" 28 | PYTHON_VERSION: "3.5.1" 29 | PYTHON_ARCH: "32" 30 | 31 | - PYTHON: "C:\\Python35-x64" 32 | PYTHON_VERSION: "3.5.1" 33 | PYTHON_ARCH: "64" 34 | 35 | install: 36 | - SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% 37 | - pip install nose 38 | - pip install coverage 39 | 40 | build_script: 41 | - python run_coverage.py 42 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Publish.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Publish.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Publish" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Publish" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | import sphinx 6 | 7 | src_path = os.path.abspath('..') 8 | if not src_path in sys.path: 9 | sys.path.insert(0, src_path) 10 | 11 | import pyblish 12 | 13 | extensions = [ 14 | 'sphinx.ext.autodoc', 15 | 'sphinx.ext.autosummary', 16 | 'sphinx.ext.viewcode', 17 | 'sphinx.ext.autodoc', 18 | ] 19 | 20 | if sphinx.version_info >= (1, 3): 21 | extensions.append('sphinx.ext.napoleon') 22 | else: 23 | extensions.append('sphinxcontrib.napoleon') 24 | 25 | templates_path = ['_templates'] 26 | source_suffix = '.rst' 27 | master_doc = 'index' 28 | 29 | project = u'Pyblish' 30 | copyright = u'2014, Marcus Ottosson' 31 | version = pyblish.__version__ 32 | release = version 33 | 34 | exclude_patterns = [] 35 | pygments_style = 'sphinx' 36 | 37 | 38 | # -- Options for HTML output ---------------------------------------------- 39 | 40 | if os.environ.get('READTHEDOCS', None) != 'True': 41 | try: 42 | import sphinx_rtd_theme 43 | html_theme = 'sphinx_rtd_theme' 44 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 45 | except ImportError: 46 | pass 47 | 48 | html_static_path = ['_static'] 49 | htmlhelp_basename = 'Pyblishdoc' 50 | 51 | 52 | # -- Options for LaTeX output --------------------------------------------- 53 | 54 | latex_elements = {} 55 | 56 | latex_documents = [ 57 | ('index', 'Pyblish.tex', u'Pyblish Documentation', 58 | u'Marcus Ottosson', 'manual'), 59 | ] 60 | 61 | man_pages = [ 62 | ('index', 'pyblish', u'Pyblish Documentation', 63 | [u'Marcus Ottosson'], 1) 64 | ] 65 | 66 | texinfo_documents = [ 67 | ('index', 'Pyblish', u'Pyblish Documentation', 68 | u'Marcus Ottosson', 'Pyblish', 'Quality Assurance for Content', 69 | 'Miscellaneous'), 70 | ] 71 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: logo_small.png 3 | 4 | API documentation for Pyblish v\ |version|. 5 | 6 | .. module:: pyblish.plugin 7 | 8 | Objects 9 | ======= 10 | 11 | Central objects used throughout Pyblish. 12 | 13 | .. autosummary:: 14 | :nosignatures: 15 | 16 | AbstractEntity 17 | Context 18 | Instance 19 | Plugin 20 | Selector 21 | Validator 22 | Extractor 23 | Conformer 24 | 25 | Functions 26 | ========= 27 | 28 | Helper utilities. 29 | 30 | .. autosummary:: 31 | :nosignatures: 32 | 33 | discover 34 | plugin_paths 35 | registered_paths 36 | configured_paths 37 | environment_paths 38 | register_plugin_path 39 | deregister_plugin_path 40 | deregister_all 41 | plugins_by_family 42 | plugins_by_host 43 | instances_by_plugin 44 | 45 | .. module:: pyblish 46 | 47 | Configuration 48 | ============= 49 | 50 | .. autosummary:: 51 | :nosignatures: 52 | 53 | Config 54 | 55 | .. module:: pyblish.lib 56 | 57 | Library 58 | ======= 59 | 60 | .. autosummary:: 61 | :nosignatures: 62 | 63 | log 64 | format_filename 65 | 66 | .. module:: pyblish.error 67 | 68 | Exceptions 69 | ========== 70 | 71 | Exceptions raised that are specific to Pyblish. 72 | 73 | .. autosummary:: 74 | :nosignatures: 75 | 76 | PyblishError 77 | SelectionError 78 | ValidationError 79 | ExtractionError 80 | ConformError 81 | 82 | .. module:: pyblish.plugin 83 | 84 | 85 | AbstractEntity 86 | -------------- 87 | 88 | Superclass to Context and Instance, providing the data plug-in to plug-in 89 | API via the data member. 90 | 91 | .. autoclass:: AbstractEntity 92 | :members: 93 | :undoc-members: 94 | 95 | Context 96 | ------- 97 | 98 | The context is a container of one or more objects of type :class:`Instance` along with metadata to describe them all; such as the current working directory or logged on user. 99 | 100 | .. autoclass:: Context 101 | :members: 102 | :undoc-members: 103 | 104 | Instance 105 | -------- 106 | 107 | An instance describes one or more items in a working scene; you can think of it as the counter-part of a file on disk - once the file has been loaded, it's an `instance`. 108 | 109 | .. autoclass:: Instance 110 | :members: 111 | :undoc-members: 112 | 113 | 114 | Plugin 115 | ------ 116 | 117 | As a plug-in driven framework, any action is implemented as a plug-in and this is the superclass from which all plug-ins are derived. The superclass defines behaviour common across all plug-ins, such as its internally executed method :meth:`Plugin.process` or it's virtual members :meth:`Plugin.process_instance` and :meth:`Plugin.process_context`. 118 | 119 | Each plug-in MAY define one or more of the following attributes prior to being useful to Pyblish. 120 | 121 | - :attr:`Plugin.hosts` 122 | - :attr:`Plugin.optional` 123 | - :attr:`Plugin.version` 124 | 125 | Some of which are MANDATORY, others which are OPTIONAL. See each corresponding subclass for details. 126 | 127 | - :class:`Selector` 128 | - :class:`Validator` 129 | - :class:`Extractor` 130 | - :class:`Conformer` 131 | 132 | 133 | .. autoclass:: Plugin 134 | :members: 135 | :undoc-members: 136 | 137 | Selector 138 | -------- 139 | 140 | A selector finds instances within a working file. 141 | 142 | .. note:: The following attributes must be present when implementing this plug-in. 143 | 144 | - :attr:`Selector.hosts` 145 | - :attr:`Selector.version` 146 | 147 | .. autoclass:: Selector 148 | :members: 149 | :undoc-members: 150 | 151 | Validator 152 | --------- 153 | 154 | A validator validates selected instances. 155 | 156 | .. note:: The following attributes must be present when implementing this plug-in. 157 | 158 | - :attr:`Plugin.hosts` 159 | - :attr:`Plugin.version` 160 | - :attr:`Validator.families` 161 | 162 | .. autoclass:: Validator 163 | :members: 164 | :undoc-members: 165 | 166 | Extractor 167 | --------- 168 | 169 | Extractors are responsible for serialising selected data into a format suited for persistence on disk. Keep in mind that although an extractor does place file on disk, it isn't responsible for the final destination of files. See :class:`Conformer` for more information. 170 | 171 | .. note:: The following attributes must be present when implementing this plug-in. 172 | 173 | - :attr:`Plugin.hosts` 174 | - :attr:`Plugin.version` 175 | - :attr:`Extractor.families` 176 | 177 | .. autoclass:: Extractor 178 | :members: 179 | :undoc-members: 180 | 181 | Conformer 182 | --------- 183 | 184 | The conformer, also known as `integrator`, integrates data produced by extraction. 185 | 186 | Its responsibilities include: 187 | 188 | 1. Placing files into their final destination 189 | 2. To manage and increment versions, typically involving a third-party versioning library. 190 | 3. To notify artists of events 191 | 4. To provide hooks for out-of-band processes 192 | 193 | .. note:: The following attributes must be present when implementing this plug-in. 194 | 195 | - :attr:`Plugin.hosts` 196 | - :attr:`Plugin.version` 197 | - :attr:`Conformer.families` 198 | 199 | .. autoclass:: Conformer 200 | :members: 201 | :undoc-members: 202 | 203 | 204 | discover 205 | -------- 206 | 207 | .. autofunction:: discover 208 | 209 | plugin_paths 210 | ------------ 211 | 212 | .. autofunction:: plugin_paths 213 | 214 | registered_paths 215 | ---------------- 216 | 217 | .. autofunction:: registered_paths 218 | 219 | configured_paths 220 | ---------------- 221 | 222 | .. autofunction:: configured_paths 223 | 224 | environment_paths 225 | ----------------- 226 | 227 | .. autofunction:: environment_paths 228 | 229 | register_plugin_path 230 | -------------------- 231 | 232 | .. autofunction:: register_plugin_path 233 | 234 | deregister_plugin_path 235 | ---------------------- 236 | 237 | .. autofunction:: deregister_plugin_path 238 | 239 | deregister_all 240 | -------------- 241 | 242 | .. autofunction:: deregister_all 243 | 244 | plugins_by_family 245 | ----------------- 246 | 247 | .. autofunction:: plugins_by_family 248 | 249 | plugins_by_host 250 | ---------------- 251 | 252 | .. autofunction:: plugins_by_host 253 | 254 | instances_by_plugin 255 | ------------------- 256 | 257 | .. autofunction:: instances_by_plugin 258 | 259 | .. module:: pyblish 260 | 261 | Config 262 | ------ 263 | 264 | .. autoclass:: Config 265 | :members: 266 | 267 | .. module:: pyblish.lib 268 | 269 | log 270 | --- 271 | 272 | .. autofunction:: log 273 | 274 | 275 | format_filename 276 | --------------- 277 | 278 | .. autofunction:: format_filename 279 | 280 | 281 | .. module:: pyblish.error 282 | 283 | 284 | PyblishError 285 | ------------ 286 | 287 | .. autoclass:: PyblishError 288 | :members: 289 | :undoc-members: 290 | 291 | SelectionError 292 | -------------------- 293 | 294 | .. autoclass:: SelectionError 295 | :members: 296 | :undoc-members: 297 | 298 | ValidationError 299 | --------------- 300 | 301 | .. autoclass:: ValidationError 302 | :members: 303 | :undoc-members: 304 | 305 | ExtractionError 306 | --------------- 307 | 308 | .. autoclass:: ExtractionError 309 | :members: 310 | :undoc-members: 311 | 312 | ConformError 313 | ------------ 314 | 315 | .. autoclass:: ConformError 316 | :members: 317 | :undoc-members: 318 | -------------------------------------------------------------------------------- /docs/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyblish/pyblish-base/03cda36b26010642bcbdc8dbf2f256f298742f9f/docs/logo_small.png -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Publish.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Publish.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/make_html.bat: -------------------------------------------------------------------------------- 1 | make html -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinxcontrib-napoleon -------------------------------------------------------------------------------- /pyblish/__init__.py: -------------------------------------------------------------------------------- 1 | """Pyblish initialisation 2 | 3 | Attributes: 4 | _registered_paths: Currently registered plug-in paths. 5 | _registered_plugins: Currently registered plug-ins. 6 | 7 | """ 8 | 9 | from .version import version, version_info, __version__ 10 | 11 | 12 | _registered_paths = list() 13 | _registered_callbacks = dict() 14 | _registered_plugins = dict() 15 | _registered_services = dict() 16 | _registered_test = dict() 17 | _registered_hosts = list() 18 | _registered_targets = list() 19 | _registered_gui = list() 20 | _registered_plugin_filters = list() 21 | 22 | 23 | __all__ = [ 24 | "version", 25 | "version_info", 26 | "__version__", 27 | "_registered_paths", 28 | "_registered_callbacks", 29 | "_registered_plugins", 30 | "_registered_services", 31 | "_registered_test", 32 | "_registered_hosts", 33 | "_registered_targets", 34 | "_registered_gui", 35 | "_registered_plugin_filters" 36 | ] 37 | -------------------------------------------------------------------------------- /pyblish/__main__.py: -------------------------------------------------------------------------------- 1 | """Pyblish package interface 2 | 3 | This makes the Pyblish package into an executable, via cli.py 4 | 5 | """ 6 | 7 | from . import cli 8 | 9 | if __name__ == '__main__': 10 | cli.main(obj={}, prog_name="pyblish") 11 | -------------------------------------------------------------------------------- /pyblish/api.py: -------------------------------------------------------------------------------- 1 | """Expose common functionality 2 | 3 | Use this in plugins, integrations and extensions of Pyblish. 4 | This part of the library is what will change the least and 5 | attempt to maintain backwards- and forwards-compatibility. 6 | 7 | This way, we as developers are free to refactor the library 8 | without breaking any of your tools. 9 | 10 | .. note:: Contributors, don't use this in any other module internal 11 | to Pyblish or a cyclic dependency will be created. This is only 12 | to be used by end-users of the library and from 13 | integrations/extensions of Pyblish. 14 | 15 | """ 16 | 17 | from __future__ import absolute_import 18 | 19 | from . import version 20 | import getpass 21 | import os 22 | 23 | from .plugin import ( 24 | Context, 25 | Instance, 26 | 27 | Action, 28 | Category, 29 | Separator, 30 | 31 | # Matching algorithms 32 | Subset, 33 | Intersection, 34 | Exact, 35 | 36 | Asset, 37 | Plugin, 38 | Validator, 39 | Extractor, 40 | Integrator, 41 | Collector, 42 | discover, 43 | 44 | ContextPlugin, 45 | InstancePlugin, 46 | 47 | CollectorOrder, 48 | ValidatorOrder, 49 | ExtractorOrder, 50 | IntegratorOrder, 51 | 52 | register_host, 53 | registered_hosts, 54 | deregister_host, 55 | deregister_all_hosts, 56 | 57 | current_target, 58 | register_target, 59 | registered_targets, 60 | deregister_target, 61 | deregister_all_targets, 62 | 63 | register_plugin, 64 | deregister_plugin, 65 | deregister_all_plugins, 66 | registered_plugins, 67 | 68 | plugin_paths, 69 | register_plugin_path, 70 | deregister_plugin_path, 71 | deregister_all_paths, 72 | 73 | register_service, 74 | deregister_service, 75 | deregister_all_services, 76 | registered_services, 77 | 78 | register_callback, 79 | deregister_callback, 80 | deregister_all_callbacks, 81 | registered_callbacks, 82 | 83 | register_discovery_filter, 84 | deregister_discovery_filter, 85 | deregister_all_discovery_filters, 86 | registered_discovery_filters, 87 | 88 | sort as sort_plugins, 89 | 90 | registered_paths, 91 | environment_paths, 92 | current_host, 93 | ) 94 | 95 | from .lib import ( 96 | log, 97 | time as __time, 98 | emit, 99 | main_package_path as __main_package_path 100 | ) 101 | 102 | from .logic import ( 103 | plugins_by_family, 104 | plugins_by_host, 105 | plugins_by_instance, 106 | plugins_by_targets, 107 | instances_by_plugin, 108 | register_test, 109 | deregister_test, 110 | registered_test, 111 | 112 | register_gui, 113 | registered_guis, 114 | deregister_gui, 115 | 116 | default_test as __default_test, 117 | ) 118 | 119 | from .error import ( 120 | PyblishError, 121 | SelectionError, 122 | ValidationError, 123 | ExtractionError, 124 | ConformError, 125 | NoInstancesError 126 | ) 127 | 128 | from .compat import ( 129 | deregister_all, 130 | sort, 131 | Selector, 132 | Conformer, 133 | format_filename, 134 | ) 135 | 136 | 137 | def __init__(): 138 | """Initialise Pyblish 139 | 140 | This function registered default services, 141 | hosts and tests. It is idempotent and thread-safe. 142 | 143 | """ 144 | 145 | # Register default services 146 | register_service("time", __time) 147 | register_service("user", getpass.getuser()) 148 | register_service("context", None) 149 | register_service("instance", None) 150 | 151 | # Register default host 152 | register_host("python") 153 | 154 | # Register hosts from environment "PYBLISHHOSTS" 155 | for host in os.environ.get("PYBLISH_HOSTS", "").split(os.pathsep): 156 | if not host: 157 | continue 158 | 159 | register_host(host) 160 | 161 | # Register targets for current session 162 | for target in os.environ.get("PYBLISH_TARGETS", "").split(os.pathsep): 163 | if not target: 164 | continue 165 | 166 | register_target(target) 167 | 168 | # Register default path 169 | register_plugin_path(os.path.join(__main_package_path(), "plugins")) 170 | 171 | # Register default test 172 | register_test(__default_test) 173 | 174 | 175 | __init__() 176 | 177 | 178 | __all__ = [ 179 | # Base objects 180 | "Context", 181 | "Instance", 182 | "Asset", 183 | 184 | # Matching algorithms 185 | "Subset", 186 | "Intersection", 187 | "Exact", 188 | 189 | "Plugin", 190 | "Action", 191 | "Category", 192 | "Separator", 193 | 194 | # SVEC plug-ins 195 | "Collector", 196 | "Selector", 197 | "Validator", 198 | "Extractor", 199 | "Conformer", 200 | "Integrator", 201 | 202 | "ContextPlugin", 203 | "InstancePlugin", 204 | 205 | "CollectorOrder", 206 | "ValidatorOrder", 207 | "ExtractorOrder", 208 | "IntegratorOrder", 209 | 210 | # Plug-in utilities 211 | "discover", 212 | 213 | "plugin_paths", 214 | "registered_paths", 215 | "environment_paths", 216 | 217 | "register_host", 218 | "registered_hosts", 219 | "deregister_host", 220 | "deregister_all_hosts", 221 | 222 | "register_plugin", 223 | "deregister_plugin", 224 | "deregister_all_plugins", 225 | "registered_plugins", 226 | 227 | "register_service", 228 | "deregister_service", 229 | "deregister_all_services", 230 | "registered_services", 231 | 232 | "register_callback", 233 | "deregister_callback", 234 | "deregister_all_callbacks", 235 | "registered_callbacks", 236 | 237 | "register_plugin_path", 238 | "deregister_plugin_path", 239 | "deregister_all_paths", 240 | 241 | "register_test", 242 | "deregister_test", 243 | "registered_test", 244 | 245 | "register_gui", 246 | "registered_guis", 247 | "deregister_gui", 248 | 249 | "plugins_by_family", 250 | "plugins_by_host", 251 | "plugins_by_instance", 252 | "instances_by_plugin", 253 | 254 | "current_target", 255 | "register_target", 256 | "registered_targets", 257 | "deregister_target", 258 | "deregister_all_targets", 259 | 260 | "sort_plugins", 261 | "format_filename", 262 | "current_host", 263 | "sort_plugins", 264 | 265 | "version", 266 | 267 | # Utilities 268 | "log", 269 | "emit", 270 | 271 | # Exceptions 272 | "PyblishError", 273 | "SelectionError", 274 | "ValidationError", 275 | "ExtractionError", 276 | "ConformError", 277 | "NoInstancesError", 278 | 279 | # Compatibility 280 | "deregister_all", 281 | "sort", 282 | ] 283 | -------------------------------------------------------------------------------- /pyblish/compat.py: -------------------------------------------------------------------------------- 1 | """Compatibility module""" 2 | 3 | import re 4 | import inspect 5 | import warnings 6 | from . import plugin, lib, logic 7 | from .vendor import six 8 | 9 | if six.PY2: 10 | get_arg_spec = inspect.getargspec 11 | else: 12 | get_arg_spec = inspect.getfullargspec 13 | 14 | # Aliases 15 | Selector = plugin.Collector 16 | Conformer = plugin.Integrator 17 | 18 | _filename_ascii_strip_re = re.compile(r'[^-\w.]') 19 | _windows_device_files = ('CON', 'AUX', 'COM1', 'COM2', 'COM3', 'COM4', 20 | 'LPT1', 'LPT2', 'LPT3', 'PRN', 'NUL') 21 | 22 | 23 | def sort(*args, **kwargs): 24 | warnings.warn("pyblish.api.sort has been deprecated; " 25 | "use pyblish.api.sort_plugins") 26 | return plugin.sort(*args, **kwargs) 27 | 28 | 29 | def deregister_all(*args, **kwargs): 30 | warnings.warn("pyblish.api.deregister_all has been deprecated; " 31 | "use pyblish.api.deregister_all_paths") 32 | return plugin.deregister_all_paths(*args, **kwargs) 33 | 34 | 35 | # AbstractEntity 36 | # 37 | # The below members represent backwards compatibility 38 | # features, kept separate for maintainability as they 39 | # will no longer be updated and eventually discarded. 40 | 41 | 42 | @lib.deprecated 43 | def set_data(self, key, value): 44 | """DEPRECATED - USE .data DICTIONARY DIRECTLY 45 | 46 | Modify/insert data into entity 47 | 48 | Arguments: 49 | key (str): Name of data to add 50 | value (object): Value of data to add 51 | 52 | """ 53 | 54 | self.data[key] = value 55 | 56 | 57 | @lib.deprecated 58 | def remove_data(self, key): 59 | """DEPRECATED - USE .data DICTIONARY DIRECTLY 60 | 61 | Remove data from entity 62 | 63 | Arguments; 64 | key (str): Name of data to remove 65 | 66 | """ 67 | 68 | self.data.pop(key) 69 | 70 | 71 | @lib.deprecated 72 | def has_data(self, key): 73 | """DEPRECATED - USE .data DICTIONARY DIRECTLY 74 | 75 | Check if entity has key 76 | 77 | Arguments: 78 | key (str): Key to check 79 | 80 | Return: 81 | True if it exists, False otherwise 82 | 83 | """ 84 | 85 | return key in self.data 86 | 87 | 88 | @lib.deprecated 89 | def add(self, other): 90 | """DEPRECATED - USE .append 91 | 92 | Add member to self 93 | 94 | This is to mimic the interface of set() 95 | 96 | """ 97 | 98 | return self.append(other) 99 | 100 | 101 | @lib.deprecated 102 | def remove(self, other): 103 | """DEPRECATED - USE .pop 104 | 105 | Remove member from self 106 | 107 | This is to mimic the interface of set() 108 | 109 | """ 110 | 111 | index = self.index(other) 112 | return self.pop(index) 113 | 114 | 115 | plugin.AbstractEntity.add = add 116 | plugin.AbstractEntity.remove = remove 117 | plugin.AbstractEntity.set_data = set_data 118 | plugin.AbstractEntity.remove_data = remove_data 119 | plugin.AbstractEntity.has_data = has_data 120 | 121 | 122 | # Context 123 | 124 | @lib.deprecated 125 | def create_asset(self, *args, **kwargs): 126 | return self.create_instance(*args, **kwargs) 127 | 128 | 129 | @lib.deprecated 130 | def add(self, other): 131 | return super(plugin.Context, self).append(other) 132 | 133 | 134 | plugin.Context.create_asset = create_asset 135 | plugin.Context.add = add 136 | 137 | 138 | @lib.deprecated 139 | def format_filename(filename): 140 | return filename 141 | 142 | 143 | @lib.deprecated 144 | def format_filename2(filename): 145 | return filename 146 | 147 | 148 | lib.format_filename = format_filename 149 | lib.format_filename2 = format_filename2 150 | 151 | 152 | @lib.deprecated 153 | def process(func, plugins, context, test=None): 154 | r"""Primary processing logic 155 | 156 | Takes callables and data as input, and performs 157 | logical operations on them until the currently 158 | registered test fails. 159 | 160 | If `plugins` is a callable, it is called early, before 161 | processing begins. If `context` is a callable, it will 162 | be called once per plug-in. 163 | 164 | Arguments: 165 | func (callable): Callable taking three arguments; 166 | plugin(Plugin), context(Context) and optional 167 | instance(Instance). Each must provide a matching 168 | interface to their corresponding objects. 169 | plugins (list, callable): Plug-ins to process. If a 170 | callable is provided, the return value is used 171 | as plug-ins. It is called with no arguments. 172 | context (Context, callable): Context whose instances 173 | are to be processed. If a callable is provided, 174 | the return value is used as context. It is called 175 | with no arguments. 176 | test (callable, optional): Provide custom test, defaults 177 | to the currently registered test. 178 | 179 | Yields: 180 | A result per complete process. If test fails, 181 | a TestFailed exception is returned, containing the 182 | variables used in the test. Finally, any exception 183 | thrown by `func` is yielded. Note that this is 184 | considered a bug in *your* code as you are the one 185 | supplying it. 186 | 187 | """ 188 | 189 | __plugins = plugins 190 | __context = context 191 | 192 | if test is None: 193 | test = logic.registered_test() 194 | 195 | if hasattr(__plugins, "__call__"): 196 | plugins = __plugins() 197 | 198 | def gen(plugin, instances): 199 | if plugin.__instanceEnabled__ and len(instances) > 0: 200 | for instance in instances: 201 | yield instance 202 | else: 203 | yield None 204 | 205 | vars = { 206 | "nextOrder": None, 207 | "ordersWithError": list() 208 | } 209 | 210 | # Clear introspection values 211 | # TODO(marcus): Return *next* pair, this currently 212 | # returns the current pair. 213 | self = process 214 | self.next_plugin = None 215 | self.next_instance = None 216 | 217 | for Plugin in plugins: 218 | self.next_plugin = Plugin 219 | vars["nextOrder"] = Plugin.order 220 | 221 | if not test(**vars): 222 | if hasattr(__context, "__call__"): 223 | context = __context() 224 | 225 | args = get_arg_spec(Plugin.process).args 226 | 227 | # Backwards compatibility with `asset` 228 | if "asset" in args: 229 | args.append("instance") 230 | 231 | instances = logic.instances_by_plugin(context, Plugin) 232 | 233 | # Limit processing to plug-ins with an available instance 234 | if not instances and "*" not in Plugin.families: 235 | continue 236 | 237 | for instance in gen(Plugin, instances): 238 | if instance is None and "instance" in args: 239 | continue 240 | 241 | # Provide introspection 242 | self.next_instance = instance 243 | 244 | try: 245 | result = func(Plugin, context, instance) 246 | 247 | except Exception as exc: 248 | # Any exception occuring within the function 249 | # you pass is yielded, you are expected to 250 | # handle it. 251 | yield exc 252 | 253 | else: 254 | # Make note of the order at which 255 | # the potential error error occured. 256 | if result["error"]: 257 | if Plugin.order not in vars["ordersWithError"]: 258 | vars["ordersWithError"].append(Plugin.order) 259 | yield result 260 | 261 | # Clear current 262 | self.next_instance = None 263 | 264 | else: 265 | yield logic.TestFailed(test(**vars), vars) 266 | break 267 | 268 | 269 | process.next_plugin = None 270 | process.next_instance = None 271 | 272 | logic.process = process 273 | -------------------------------------------------------------------------------- /pyblish/error.py: -------------------------------------------------------------------------------- 1 | class PyblishError(Exception): 2 | """Baseclass for all Pyblish exceptions""" 3 | 4 | 5 | class ValidationError(PyblishError): 6 | """Baseclass for validation errors""" 7 | 8 | 9 | class SelectionError(PyblishError): 10 | """Baseclass for selection errors""" 11 | 12 | 13 | class ExtractionError(PyblishError): 14 | """Baseclass for extraction errors""" 15 | 16 | 17 | class ConformError(PyblishError): 18 | """Baseclass for conforming errors""" 19 | 20 | 21 | class NoInstancesError(Exception): 22 | """Raised if no instances could be found""" 23 | -------------------------------------------------------------------------------- /pyblish/icons/logo-32x32.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /pyblish/icons/logo-64x64.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /pyblish/lib.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import datetime 5 | import warnings 6 | import traceback 7 | import functools 8 | 9 | from . import _registered_callbacks 10 | from .vendor import six 11 | 12 | 13 | def inrange(number, base, offset=0.5): 14 | r"""Evaluate whether `number` is within `base` +- `offset` 15 | 16 | Lower bound is *included* whereas upper bound is *excluded* 17 | so as to allow for ranges to be stacked up against each other. 18 | For example, an offset of 0.5 and a base of 1 evenly stacks 19 | up against a base of 2 with identical offset. 20 | 21 | Arguments: 22 | number (float): Number to consider 23 | base (float): Center of range 24 | offset (float, optional): Amount of offset from base 25 | 26 | Usage: 27 | >>> inrange(0, base=1, offset=0.5) 28 | False 29 | >>> inrange(0.4, base=1, offset=0.5) 30 | False 31 | >>> inrange(1.4, base=1, offset=0.5) 32 | True 33 | >>> # Lower bound is included 34 | >>> inrange(0.5, base=1, offset=0.5) 35 | True 36 | >>> # Upper bound is excluded 37 | >>> inrange(1.5, base=1, offset=0.5) 38 | False 39 | 40 | """ 41 | 42 | return (base - offset) <= number < (base + offset) 43 | 44 | 45 | class MessageHandler(logging.Handler): 46 | def __init__(self, records, *args, **kwargs): 47 | # Not using super(), for compatibility with Python 2.6 48 | logging.Handler.__init__(self, *args, **kwargs) 49 | self.records = records 50 | 51 | def emit(self, record): 52 | if record.name.startswith("pyblish"): 53 | self.records.append(record) 54 | 55 | 56 | def extract_traceback(exception, fname=None): 57 | """Inject current traceback and store in exception.traceback. 58 | 59 | Also storing the formatted traceback on exception.formtatted_traceback. 60 | 61 | Arguments: 62 | exception (Exception): Exception object 63 | fname (str): Optionally provide a file name for the exception. 64 | This is necessary to inject the correct file path in the traceback. 65 | If plugins are registered through `api.plugin.discover`, they only 66 | show "" instead of the actual source file. 67 | """ 68 | exc_type, exc_value, exc_traceback = sys.exc_info() 69 | exception.traceback = traceback.extract_tb(exc_traceback)[-1] 70 | 71 | formatted_traceback = ''.join(traceback.format_exception( 72 | exc_type, exc_value, exc_traceback)) 73 | if 'File "", line' in formatted_traceback and fname is not None: 74 | _, lineno, func, msg = exception.traceback 75 | fname = os.path.abspath(fname) 76 | exception.traceback = (fname, lineno, func, msg) 77 | formatted_traceback = formatted_traceback.replace( 78 | 'File "", line', 79 | 'File "{0}", line'.format(fname)) 80 | exception.formatted_traceback = formatted_traceback 81 | 82 | del(exc_type, exc_value, exc_traceback) 83 | 84 | 85 | def time(): 86 | """Return ISO formatted string representation of current UTC time.""" 87 | return '%sZ' % datetime.datetime.utcnow().isoformat() 88 | 89 | 90 | class ItemList(list): 91 | """List with keys 92 | 93 | Raises: 94 | KeyError is item is not in list 95 | 96 | Example: 97 | >>> Obj = type("Object", (object,), {}) 98 | >>> obj = Obj() 99 | >>> obj.name = "Test" 100 | >>> l = ItemList(key="name") 101 | >>> l.append(obj) 102 | >>> l[0] == obj 103 | True 104 | >>> l["Test"] == obj 105 | True 106 | >>> try: 107 | ... l["NotInList"] 108 | ... except KeyError: 109 | ... print(True) 110 | True 111 | >>> obj == l.get("Test") 112 | True 113 | >>> l.get("NotInList") == None 114 | True 115 | 116 | """ 117 | 118 | def __init__(self, key, object=list()): 119 | super(ItemList, self).__init__(object) 120 | self.key = key 121 | 122 | def __getitem__(self, index): 123 | if isinstance(index, int): 124 | return super(ItemList, self).__getitem__(index) 125 | 126 | for item in self: 127 | if getattr(item, self.key) == index: 128 | return item 129 | 130 | raise KeyError("%s not in list" % index) 131 | 132 | def get(self, key, default=None): 133 | try: 134 | return self.__getitem__(key) 135 | except KeyError: 136 | return default 137 | 138 | 139 | class classproperty(object): 140 | def __init__(self, getter): 141 | self.getter = getter 142 | 143 | def __get__(self, instance, owner): 144 | return self.getter(owner) 145 | 146 | 147 | def log(cls): 148 | """Decorator for attaching a logger to the class `cls` 149 | 150 | Loggers inherit the syntax {module}.{submodule} 151 | 152 | Example 153 | >>> @log 154 | ... class MyClass(object): 155 | ... pass 156 | >>> 157 | >>> myclass = MyClass() 158 | >>> myclass.log.info('Hello World') 159 | 160 | """ 161 | 162 | module = cls.__module__ 163 | name = cls.__name__ 164 | 165 | # Package name appended, for filtering of LogRecord instances 166 | logname = "pyblish.%s.%s" % (module, name) 167 | cls.log = logging.getLogger(logname) 168 | 169 | # All messages are handled by root-logger 170 | cls.log.propagate = True 171 | 172 | return cls 173 | 174 | 175 | def parse_environment_paths(paths): 176 | """Given a (semi-)colon separated string of paths, return a list 177 | 178 | Example: 179 | >>> import os 180 | >>> parse_environment_paths("path1" + os.pathsep + "path2") 181 | ['path1', 'path2'] 182 | >>> parse_environment_paths("path1" + os.pathsep) 183 | ['path1', ''] 184 | 185 | Arguments: 186 | paths (str): Colon or semi-colon (depending on platform) 187 | separated string of paths. 188 | 189 | Returns: 190 | list of paths as string. 191 | 192 | """ 193 | 194 | paths_list = list() 195 | 196 | for path in paths.split(os.pathsep): 197 | paths_list.append(path) 198 | 199 | return paths_list 200 | 201 | 202 | def get_formatter(): 203 | """Return a default Pyblish formatter for logging 204 | 205 | Example: 206 | >>> import logging 207 | >>> log = logging.getLogger("myLogger") 208 | >>> handler = logging.StreamHandler() 209 | >>> handler.setFormatter(get_formatter()) 210 | 211 | """ 212 | 213 | formatter = logging.Formatter( 214 | '%(asctime)s - ' 215 | '%(levelname)s - ' 216 | '%(name)s - ' 217 | '%(message)s', 218 | '%H:%M:%S') 219 | return formatter 220 | 221 | 222 | def setup_log(root='pyblish', level=logging.DEBUG): 223 | """Setup a default logger for Pyblish 224 | 225 | Example: 226 | >>> log = setup_log() 227 | >>> log.info("Hello, World") 228 | 229 | """ 230 | 231 | formatter = logging.Formatter("%(levelname)s - %(message)s") 232 | handler = logging.StreamHandler() 233 | handler.setFormatter(formatter) 234 | 235 | log = logging.getLogger(root) 236 | log.propagate = True 237 | log.handlers[:] = [] 238 | log.addHandler(handler) 239 | 240 | log.setLevel(level) 241 | 242 | return log 243 | 244 | 245 | def main_package_path(): 246 | """Return path of main pyblish package""" 247 | lib_py_path = sys.modules[__name__].__file__ 248 | package_path = os.path.dirname(lib_py_path) 249 | return package_path 250 | 251 | 252 | def emit(signal, **kwargs): 253 | """Trigger registered callbacks 254 | 255 | Keyword arguments are passed from caller to callee. 256 | 257 | Arguments: 258 | signal (string): Name of signal emitted 259 | 260 | Example: 261 | >>> import sys 262 | >>> from . import plugin 263 | >>> plugin.register_callback( 264 | ... "mysignal", lambda data: sys.stdout.write(str(data))) 265 | ... 266 | >>> emit("mysignal", data={"something": "cool"}) 267 | {'something': 'cool'} 268 | 269 | """ 270 | 271 | for callback in _registered_callbacks.get(signal, []): 272 | try: 273 | callback(**kwargs) 274 | except Exception: 275 | file = six.StringIO() 276 | traceback.print_exc(file=file) 277 | sys.stderr.write(file.getvalue()) 278 | # Why the roundabout through StringIO? 279 | # 280 | # tests.lib.captured_stderr attempts to capture stderr 281 | # but doing so with plain print_exc() results in a type 282 | # error in Python 3. I'm not confident in Python 3 unicode 283 | # handling so there is likely a better way to solve this. 284 | # 285 | # TODO(marcus): Make it prettier 286 | 287 | 288 | def deprecated(func): 289 | """Deprecation decorator 290 | 291 | Attach this to deprecated functions or methods. 292 | 293 | """ 294 | 295 | @functools.wraps(func) 296 | def wrapper(*args, **kwargs): 297 | return func(*args, **kwargs) 298 | return wrapper 299 | -------------------------------------------------------------------------------- /pyblish/main.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from .util import * 3 | 4 | warnings.warn("main.py deprecated; use util.py") 5 | -------------------------------------------------------------------------------- /pyblish/plugins/collect_current_date.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | import pyblish.lib 4 | 5 | 6 | class CollectCurrentDate(pyblish.api.ContextPlugin): 7 | """Inject the current time into the Context""" 8 | 9 | order = pyblish.api.CollectorOrder 10 | label = "Current date" 11 | 12 | def process(self, context): 13 | context.data['date'] = pyblish.lib.time() 14 | -------------------------------------------------------------------------------- /pyblish/plugins/collect_current_user.py: -------------------------------------------------------------------------------- 1 | 2 | import getpass 3 | import pyblish.api 4 | 5 | 6 | class CollectCurrentUser(pyblish.api.ContextPlugin): 7 | """Inject the currently logged on user into the Context""" 8 | 9 | order = pyblish.api.CollectorOrder 10 | label = "Current user" 11 | 12 | def process(self, context): 13 | context.data['user'] = getpass.getuser() 14 | -------------------------------------------------------------------------------- /pyblish/plugins/collect_current_working_directory.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import pyblish.api 4 | 5 | 6 | class CollectCurrentWorkingDirectory(pyblish.api.ContextPlugin): 7 | """Inject the current working directory into Context""" 8 | 9 | order = pyblish.api.CollectorOrder 10 | label = "Current working directory" 11 | 12 | def process(self, context): 13 | context.data['cwd'] = os.getcwd() 14 | -------------------------------------------------------------------------------- /pyblish/util.py: -------------------------------------------------------------------------------- 1 | """Convenience functions for general publishing""" 2 | 3 | from __future__ import absolute_import 4 | 5 | # Standard library 6 | import logging 7 | import warnings 8 | 9 | # Local library 10 | from . import api, logic, plugin, lib 11 | 12 | log = logging.getLogger("pyblish.util") 13 | 14 | __all__ = [ 15 | "publish", 16 | "collect", 17 | "validate", 18 | "extract", 19 | "integrate", 20 | 21 | # Iterator counterparts 22 | "publish_iter", 23 | "collect_iter", 24 | "validate_iter", 25 | "extract_iter", 26 | "integrate_iter", 27 | ] 28 | 29 | 30 | def publish(context=None, plugins=None, targets=None): 31 | """Publish everything 32 | 33 | This function will process all available plugins of the 34 | currently running host, publishing anything picked up 35 | during collection. 36 | 37 | Arguments: 38 | context (Context, optional): Context, defaults to 39 | creating a new context 40 | plugins (list, optional): Plug-ins to include, 41 | defaults to results of discover() 42 | targets (list, optional): Targets to include for publish session. 43 | 44 | Returns: 45 | Context: The context processed by the plugins. 46 | 47 | Usage: 48 | >> context = plugin.Context() 49 | >> publish(context) # Pass.. 50 | >> context = publish() # ..or receive a new 51 | 52 | """ 53 | 54 | context = context if context is not None else api.Context() 55 | 56 | for _ in publish_iter(context, plugins, targets): 57 | pass 58 | 59 | return context 60 | 61 | 62 | def publish_iter(context=None, plugins=None, targets=None): 63 | """Publish iterator 64 | 65 | This function will process all available plugins of the 66 | currently running host, publishing anything picked up 67 | during collection. 68 | 69 | Arguments: 70 | context (Context, optional): Context, defaults to 71 | creating a new context 72 | plugins (list, optional): Plug-ins to include, 73 | defaults to results of discover() 74 | targets (list, optional): Targets to include for publish session. 75 | 76 | Yields: 77 | tuple of dict and Context: A tuple is returned with a dictionary and 78 | the Context object. The dictionary contains all the result 79 | information of a plugin process, and the Context is the Context 80 | after the plugin has been processed. 81 | 82 | Usage: 83 | >> context = plugin.Context() 84 | >> for result in util.publish_iter(context): 85 | print result 86 | >> for result in util.publish_iter(): 87 | print result 88 | 89 | """ 90 | for result in _convenience_iter(context, plugins, targets): 91 | yield result 92 | 93 | api.emit("published", context=context) 94 | 95 | 96 | def _convenience_iter(context=None, plugins=None, targets=None, order=None): 97 | # Must check against None, as objects be emptys 98 | context = api.Context() if context is None else context 99 | plugins = api.discover() if plugins is None else plugins 100 | 101 | if order is not None: 102 | plugins = list( 103 | Plugin for Plugin in plugins 104 | if lib.inrange(Plugin.order, order) 105 | ) 106 | 107 | # Do not consider inactive plug-ins 108 | plugins = list(p for p in plugins if p.active) 109 | collectors = list(p for p in plugins if lib.inrange( 110 | number=p.order, 111 | base=api.CollectorOrder) 112 | ) 113 | 114 | # Compute an approximation of all future tasks 115 | # NOTE: It's an approximation, because tasks are 116 | # dynamically determined at run-time by contents of 117 | # the context and families of contained instances; 118 | # each of which may differ between task. 119 | task_count = len(list(logic.Iterator(plugins, context, targets=targets))) 120 | 121 | # First pass, collection 122 | tasks_processed_count = 1 123 | for Plugin, instance in logic.Iterator(collectors, 124 | context, 125 | targets=targets): 126 | result = plugin.process(Plugin, context, instance) 127 | 128 | # Inject additional member for results here. 129 | result["progress"] = float(tasks_processed_count) / task_count 130 | 131 | tasks_processed_count += 1 132 | yield result 133 | 134 | # Exclude collectors from further processing 135 | plugins = list(p for p in plugins if p not in collectors) 136 | 137 | # Exclude plug-ins that do not have at 138 | # least one compatible instance. 139 | for Plugin in list(plugins): 140 | if Plugin.__instanceEnabled__: 141 | if not logic.instances_by_plugin(context, Plugin): 142 | plugins.remove(Plugin) 143 | 144 | # Mutable state, used in Iterator 145 | state = { 146 | "nextOrder": None, 147 | "ordersWithError": set() 148 | } 149 | 150 | # Second pass, the remainder 151 | for Plugin, instance in logic.Iterator(plugins, 152 | context, 153 | state, 154 | targets=targets): 155 | try: 156 | result = plugin.process(Plugin, context, instance) 157 | result["progress"] = ( 158 | float(tasks_processed_count) / task_count 159 | ) 160 | 161 | tasks_processed_count += 1 162 | except StopIteration: # End of items 163 | raise 164 | 165 | except Exception: # This is unexpected, most likely a bug 166 | log.error("An expected exception occurred.\n") 167 | raise 168 | 169 | else: 170 | # Make note of the order at which the 171 | # potential error error occured. 172 | if result["error"]: 173 | state["ordersWithError"].add(Plugin.order) 174 | 175 | if isinstance(result, Exception): 176 | log.error("An unexpected error happened: %s" % result) 177 | break 178 | 179 | error = result["error"] 180 | if error is not None: 181 | print(error) 182 | 183 | yield result 184 | 185 | 186 | def collect(context=None, plugins=None, targets=None): 187 | """Convenience function for collection-only 188 | 189 | _________ . . . . . . . . . . . . . . . . . . . 190 | | | . . . . . . 191 | | Collect |-->. Validate .-->. Extract .-->. Integrate . 192 | |_________| . . . . . . . . . . . . . . . . . . . 193 | 194 | """ 195 | 196 | context = context if context is not None else api.Context() 197 | for result in collect_iter(context, plugins, targets): 198 | pass 199 | 200 | return context 201 | 202 | 203 | def validate(context=None, plugins=None, targets=None): 204 | """Convenience function for validation-only 205 | 206 | . . . . . . __________ . . . . . . . . . . . . . 207 | . . | | . . . . 208 | . Collect .-->| Validate |-->. Extract .-->. Integrate . 209 | . . . . . . |__________| . . . . . . . . . . . . . 210 | 211 | """ 212 | 213 | context = context if context is not None else api.Context() 214 | for result in validate_iter(context, plugins, targets): 215 | pass 216 | 217 | return context 218 | 219 | 220 | def extract(context=None, plugins=None, targets=None): 221 | """Convenience function for extraction-only 222 | 223 | . . . . . . . . . . . . _________ . . . . . . . 224 | . . . . | | . . 225 | . Collect .-->. Validate .-->| Extract |-->. Integrate . 226 | . . . . . . . . . . . . |_________| . . . . . . . 227 | 228 | """ 229 | 230 | context = context if context is not None else api.Context() 231 | for result in extract_iter(context, plugins, targets): 232 | pass 233 | 234 | return context 235 | 236 | 237 | def integrate(context=None, plugins=None, targets=None): 238 | """Convenience function for integration-only 239 | 240 | . . . . . . . . . . . . . . . . . . ___________ 241 | . . . . . . | | 242 | . Collect .-->. Validate .-->. Extract .-->| Integrate | 243 | . . . . . . . . . . . . . . . . . . |___________| 244 | 245 | """ 246 | 247 | context = context if context is not None else api.Context() 248 | for result in integrate_iter(context, plugins, targets): 249 | pass 250 | 251 | return context 252 | 253 | 254 | def collect_iter(context=None, plugins=None, targets=None): 255 | for result in _convenience_iter(context, plugins, targets, 256 | order=api.CollectorOrder): 257 | yield result 258 | 259 | api.emit("collected", context=context) 260 | 261 | 262 | def validate_iter(context=None, plugins=None, targets=None): 263 | for result in _convenience_iter(context, plugins, targets, 264 | order=api.ValidatorOrder): 265 | yield result 266 | 267 | api.emit("validated", context=context) 268 | 269 | 270 | def extract_iter(context=None, plugins=None, targets=None): 271 | for result in _convenience_iter(context, plugins, targets, 272 | order=api.ExtractorOrder): 273 | yield result 274 | 275 | api.emit("extracted", context=context) 276 | 277 | 278 | def integrate_iter(context=None, plugins=None, targets=None): 279 | for result in _convenience_iter(context, plugins, targets, 280 | order=api.IntegratorOrder): 281 | yield result 282 | 283 | api.emit("integrated", context=context) 284 | 285 | 286 | def _convenience(context=None, plugins=None, targets=None, order=None): 287 | context = context if context is not None else api.Context() 288 | 289 | for result in _convenience_iter(context, plugins, targets, order): 290 | pass 291 | 292 | return context 293 | 294 | 295 | # Backwards compatibility 296 | select = collect 297 | conform = integrate 298 | run = publish # Alias 299 | 300 | 301 | def publish_all(context=None, plugins=None): 302 | warnings.warn("pyblish.util.publish_all has been " 303 | "deprecated; use publish()") 304 | return publish(context, plugins) 305 | 306 | 307 | def validate_all(context=None, plugins=None): 308 | warnings.warn("pyblish.util.validate_all has been " 309 | "deprecated; use collect() followed by validate()") 310 | context = collect(context, plugins) 311 | return validate(context, plugins) 312 | -------------------------------------------------------------------------------- /pyblish/vendor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyblish/pyblish-base/03cda36b26010642bcbdc8dbf2f256f298742f9f/pyblish/vendor/__init__.py -------------------------------------------------------------------------------- /pyblish/vendor/click/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | click 4 | ~~~~~ 5 | 6 | Click is a simple Python module that wraps the stdlib's optparse to make 7 | writing command line scripts fun. Unlike other modules, it's based around 8 | a simple API that does not come with too much magic and is composable. 9 | 10 | In case optparse ever gets removed from the stdlib, it will be shipped by 11 | this module. 12 | 13 | :copyright: (c) 2014 by Armin Ronacher. 14 | :license: BSD, see LICENSE for more details. 15 | """ 16 | 17 | # Core classes 18 | from .core import Context, BaseCommand, Command, MultiCommand, Group, \ 19 | CommandCollection, Parameter, Option, Argument 20 | 21 | # Decorators 22 | from .decorators import pass_context, pass_obj, make_pass_decorator, \ 23 | command, group, argument, option, confirmation_option, \ 24 | password_option, version_option, help_option 25 | 26 | # Types 27 | from .types import ParamType, File, Path, Choice, IntRange, STRING, INT, \ 28 | FLOAT, BOOL, UUID 29 | 30 | # Utilities 31 | from .utils import echo, get_binary_stream, get_text_stream, \ 32 | format_filename, get_app_dir 33 | 34 | # Terminal functions 35 | from .termui import prompt, confirm, get_terminal_size, echo_via_pager, \ 36 | progressbar, clear, style, unstyle, secho, edit, launch, getchar, \ 37 | pause 38 | 39 | # Exceptions 40 | from .exceptions import ClickException, UsageError, BadParameter, \ 41 | FileError, Abort 42 | 43 | # Formatting 44 | from .formatting import HelpFormatter, wrap_text 45 | 46 | # Parsing 47 | from .parser import OptionParser 48 | 49 | 50 | __all__ = [ 51 | # Core classes 52 | 'Context', 'BaseCommand', 'Command', 'MultiCommand', 'Group', 53 | 'CommandCollection', 'Parameter', 'Option', 'Argument', 54 | 55 | # Decorators 56 | 'pass_context', 'pass_obj', 'make_pass_decorator', 'command', 'group', 57 | 'argument', 'option', 'confirmation_option', 'password_option', 58 | 'version_option', 'help_option', 59 | 60 | # Types 61 | 'ParamType', 'File', 'Path', 'Choice', 'IntRange', 'STRING', 'INT', 62 | 'FLOAT', 'BOOL', 'UUID', 63 | 64 | # Utilities 65 | 'echo', 'get_binary_stream', 'get_text_stream', 'format_filename', 66 | 'get_app_dir', 67 | 68 | # Terminal functions 69 | 'prompt', 'confirm', 'get_terminal_size', 'echo_via_pager', 70 | 'progressbar', 'clear', 'style', 'unstyle', 'secho', 'edit', 'launch', 71 | 'getchar', 'pause', 72 | 73 | # Exceptions 74 | 'ClickException', 'UsageError', 'BadParameter', 'FileError', 75 | 'Abort', 76 | 77 | # Formatting 78 | 'HelpFormatter', 'wrap_text', 79 | 80 | # Parsing 81 | 'OptionParser', 82 | ] 83 | 84 | 85 | __version__ = '3.0-dev' 86 | -------------------------------------------------------------------------------- /pyblish/vendor/click/_bashcomplete.py: -------------------------------------------------------------------------------- 1 | import os 2 | from click.utils import echo 3 | from click.parser import split_arg_string 4 | from click.core import MultiCommand, Option 5 | 6 | 7 | COMPLETION_SCRIPT = ''' 8 | %(complete_func)s() { 9 | COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \\ 10 | COMP_CWORD=$COMP_CWORD \\ 11 | %(autocomplete_var)s=complete $1 ) ) 12 | return 0 13 | } 14 | 15 | complete -F %(complete_func)s -o default %(script_names)s 16 | ''' 17 | 18 | 19 | def get_completion_script(prog_name, complete_var): 20 | return (COMPLETION_SCRIPT % { 21 | 'complete_func': '_%s_completion' % prog_name, 22 | 'script_names': prog_name, 23 | 'autocomplete_var': complete_var, 24 | }).strip() + ';' 25 | 26 | 27 | def resolve_ctx(cli, prog_name, args): 28 | ctx = cli.make_context(prog_name, args, resilient_parsing=True) 29 | while ctx.args and isinstance(ctx.command, MultiCommand): 30 | cmd = ctx.command.get_command(ctx, ctx.args[0]) 31 | if cmd is None: 32 | return None 33 | ctx = cmd.make_context(ctx.args[0], ctx.args[1:], parent=ctx, 34 | resilient_parsing=True) 35 | return ctx 36 | 37 | 38 | def do_complete(cli, prog_name): 39 | cwords = split_arg_string(os.environ['COMP_WORDS']) 40 | cword = int(os.environ['COMP_CWORD']) 41 | args = cwords[1:cword] 42 | try: 43 | incomplete = cwords[cword] 44 | except IndexError: 45 | incomplete = '' 46 | 47 | ctx = resolve_ctx(cli, prog_name, args) 48 | if ctx is None: 49 | return True 50 | 51 | choices = [] 52 | if incomplete and not incomplete[:1].isalnum(): 53 | for param in ctx.command.params: 54 | if not isinstance(param, Option): 55 | continue 56 | choices.extend(param.opts) 57 | choices.extend(param.secondary_opts) 58 | elif isinstance(ctx.command, MultiCommand): 59 | choices.extend(ctx.command.list_commands(ctx)) 60 | 61 | for item in choices: 62 | if item.startswith(incomplete): 63 | echo(item) 64 | 65 | return True 66 | 67 | 68 | def bashcomplete(cli, prog_name, complete_var, complete_instr): 69 | if complete_instr == 'source': 70 | echo(get_completion_script(prog_name, complete_var)) 71 | return True 72 | elif complete_instr == 'complete': 73 | return do_complete(cli, prog_name) 74 | return False 75 | -------------------------------------------------------------------------------- /pyblish/vendor/click/_textwrap.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from contextlib import contextmanager 3 | 4 | 5 | class TextWrapper(textwrap.TextWrapper): 6 | 7 | def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): 8 | space_left = max(width - cur_len, 1) 9 | 10 | if self.break_long_words: 11 | last = reversed_chunks[-1] 12 | cut = last[:space_left] 13 | res = last[space_left:] 14 | cur_line.append(cut) 15 | reversed_chunks[-1] = res 16 | elif not cur_line: 17 | cur_line.append(reversed_chunks.pop()) 18 | 19 | @contextmanager 20 | def extra_indent(self, indent): 21 | old_initial_indent = self.initial_indent 22 | old_subsequent_indent = self.subsequent_indent 23 | self.initial_indent += indent 24 | self.subsequent_indent += indent 25 | try: 26 | yield 27 | finally: 28 | self.initial_indent = old_initial_indent 29 | self.subsequent_indent = old_subsequent_indent 30 | 31 | def indent_only(self, text): 32 | rv = [] 33 | for idx, line in enumerate(text.splitlines()): 34 | indent = self.initial_indent 35 | if idx > 0: 36 | indent = self.subsequent_indent 37 | rv.append(indent + line) 38 | return '\n'.join(rv) 39 | -------------------------------------------------------------------------------- /pyblish/vendor/click/decorators.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import inspect 3 | 4 | from functools import update_wrapper 5 | 6 | from ._compat import iteritems 7 | from .utils import echo 8 | 9 | 10 | def pass_context(f): 11 | """Marks a callback as wanting to receive the current context 12 | object as first argument. 13 | """ 14 | f.__click_pass_context__ = True 15 | return f 16 | 17 | 18 | def pass_obj(f): 19 | """Similar to :func:`pass_context`, but only pass the object on the 20 | context onwards (:attr:`Context.obj`). This is useful if that object 21 | represents the state of a nested system. 22 | """ 23 | @pass_context 24 | def new_func(*args, **kwargs): 25 | ctx = args[0] 26 | return ctx.invoke(f, ctx.obj, *args[1:], **kwargs) 27 | return update_wrapper(new_func, f) 28 | 29 | 30 | def make_pass_decorator(object_type, ensure=False): 31 | """Given an object type this creates a decorator that will work 32 | similar to :func:`pass_obj` but instead of passing the object of the 33 | current context, it will find the innermost context of type 34 | :func:`object_type`. 35 | 36 | This generates a decorator that works roughly like this:: 37 | 38 | from functools import update_wrapper 39 | 40 | def decorator(f): 41 | @pass_context 42 | def new_func(ctx, *args, **kwargs): 43 | obj = ctx.find_object(object_type) 44 | return ctx.invoke(f, obj, *args, **kwargs) 45 | return update_wrapper(new_func, f) 46 | return decorator 47 | 48 | :param object_type: the type of the object to pass. 49 | :param ensure: if set to `True`, a new object will be created and 50 | remembered on the context if it's not there yet. 51 | """ 52 | def decorator(f): 53 | @pass_context 54 | def new_func(*args, **kwargs): 55 | ctx = args[0] 56 | if ensure: 57 | obj = ctx.ensure_object(object_type) 58 | else: 59 | obj = ctx.find_object(object_type) 60 | if obj is None: 61 | raise RuntimeError('Managed to invoke callback without a ' 62 | 'context object of type %r existing' 63 | % object_type.__name__) 64 | return ctx.invoke(f, obj, *args[1:], **kwargs) 65 | return update_wrapper(new_func, f) 66 | return decorator 67 | 68 | 69 | def _make_command(f, name, attrs, cls): 70 | if isinstance(f, Command): 71 | raise TypeError('Attempted to convert a callback into a ' 72 | 'command twice.') 73 | try: 74 | params = f.__click_params__ 75 | params.reverse() 76 | del f.__click_params__ 77 | except AttributeError: 78 | params = [] 79 | help = attrs.get('help') 80 | if help is None: 81 | help = inspect.getdoc(f) 82 | if isinstance(help, bytes): 83 | help = help.decode('utf-8') 84 | else: 85 | help = inspect.cleandoc(help) 86 | attrs['help'] = help 87 | return cls(name=name or f.__name__.lower(), 88 | callback=f, params=params, **attrs) 89 | 90 | 91 | def command(name=None, cls=None, **attrs): 92 | """Creates a new :class:`Command` and uses the decorated function as 93 | callback. This will also automatically attach all decorated 94 | :func:`option`\s and :func:`argument`\s as parameters to the command. 95 | 96 | The name of the command defaults to the name of the function. If you 97 | want to change that, you can pass the intended name as the first 98 | argument. 99 | 100 | All keyword arguments are forwarded to the underlying command class. 101 | 102 | Once decorated the function turns into a :class:`Command` instance 103 | that can be invoked as a command line utility or be attached to a 104 | command :class:`Group`. 105 | 106 | :param name: the name of the command. This defaults to the function 107 | name. 108 | :param cls: the command class to instantiate. This defaults to 109 | :class:`Command`. 110 | """ 111 | if cls is None: 112 | cls = Command 113 | def decorator(f): 114 | return _make_command(f, name, attrs, cls) 115 | return decorator 116 | 117 | 118 | def group(name=None, **attrs): 119 | """Creates a new :class:`Group` with a function as callback. This 120 | works otherwise the same as :func:`command` just that the `cls` 121 | parameter is set to :class:`Group`. 122 | """ 123 | attrs.setdefault('cls', Group) 124 | return command(name, **attrs) 125 | 126 | 127 | def _param_memo(f, param): 128 | if isinstance(f, Command): 129 | f.params.append(param) 130 | else: 131 | if not hasattr(f, '__click_params__'): 132 | f.__click_params__ = [] 133 | f.__click_params__.append(param) 134 | 135 | 136 | def argument(*param_decls, **attrs): 137 | """Attaches an option to the command. All positional arguments are 138 | passed as parameter declarations to :class:`Argument`; all keyword 139 | arguments are forwarded unchanged. This is equivalent to creating an 140 | :class:`Option` instance manually and attaching it to the 141 | :attr:`Command.params` list. 142 | """ 143 | def decorator(f): 144 | _param_memo(f, Argument(param_decls, **attrs)) 145 | return f 146 | return decorator 147 | 148 | 149 | def option(*param_decls, **attrs): 150 | """Attaches an option to the command. All positional arguments are 151 | passed as parameter declarations to :class:`Option`; all keyword 152 | arguments are forwarded unchanged. This is equivalent to creating an 153 | :class:`Option` instance manually and attaching it to the 154 | :attr:`Command.params` list. 155 | """ 156 | def decorator(f): 157 | if 'help' in attrs: 158 | attrs['help'] = inspect.cleandoc(attrs['help']) 159 | _param_memo(f, Option(param_decls, **attrs)) 160 | return f 161 | return decorator 162 | 163 | 164 | def confirmation_option(*param_decls, **attrs): 165 | """Shortcut for confirmation prompts that can be ignored by passing 166 | ``--yes`` as parameter. 167 | 168 | This is equivalent to decorating a function with :func:`option` with 169 | the following parameters:: 170 | 171 | def callback(ctx, param, value): 172 | if not value: 173 | ctx.abort() 174 | 175 | @click.command() 176 | @click.option('--yes', is_flag=True, callback=callback, 177 | expose_value=False, prompt='Do you want to continue?') 178 | def dropdb(): 179 | pass 180 | """ 181 | def decorator(f): 182 | def callback(ctx, param, value): 183 | if not value: 184 | ctx.abort() 185 | attrs.setdefault('is_flag', True) 186 | attrs.setdefault('callback', callback) 187 | attrs.setdefault('expose_value', False) 188 | attrs.setdefault('prompt', 'Do you want to continue?') 189 | attrs.setdefault('help', 'Confirm the action without prompting.') 190 | return option(*(param_decls or ('--yes',)), **attrs)(f) 191 | return decorator 192 | 193 | 194 | def password_option(*param_decls, **attrs): 195 | """Shortcut for password prompts. 196 | 197 | This is equivalent to decorating a function with :func:`option` with 198 | the following parameters:: 199 | 200 | @click.command() 201 | @click.option('--password', prompt=True, confirmation_prompt=True, 202 | hide_input=True) 203 | def changeadmin(password): 204 | pass 205 | """ 206 | def decorator(f): 207 | attrs.setdefault('prompt', True) 208 | attrs.setdefault('confirmation_prompt', True) 209 | attrs.setdefault('hide_input', True) 210 | return option(*(param_decls or ('--password',)), **attrs)(f) 211 | return decorator 212 | 213 | 214 | def version_option(version=None, *param_decls, **attrs): 215 | """Adds a ``--version`` option which immediately ends the program 216 | printing out the version number. This is implemented as an eager 217 | option that prints the version and exits the program in the callback. 218 | 219 | :param version: the version number to show. If not provided Click 220 | attempts an auto discovery via setuptools. 221 | :param prog_name: the name of the program (defaults to autodetection) 222 | :param message: custom message to show instead of the default 223 | (``'%(prog)s, version %(version)s'``) 224 | :param others: everything else is forwarded to :func:`option`. 225 | """ 226 | if version is None: 227 | module = sys._getframe(1).f_globals.get('__name__') 228 | def decorator(f): 229 | prog_name = attrs.pop('prog_name', None) 230 | message = attrs.pop('message', '%(prog)s, version %(version)s') 231 | 232 | def callback(ctx, param, value): 233 | if not value or ctx.resilient_parsing: 234 | return 235 | prog = prog_name 236 | if prog is None: 237 | prog = ctx.find_root().info_name 238 | ver = version 239 | if ver is None: 240 | try: 241 | import pkg_resources 242 | except ImportError: 243 | pass 244 | else: 245 | for dist in pkg_resources.working_set: 246 | scripts = dist.get_entry_map().get('console_scripts') or {} 247 | for script_name, entry_point in iteritems(scripts): 248 | if entry_point.module_name == module: 249 | ver = dist.version 250 | break 251 | if ver is None: 252 | raise RuntimeError('Could not determine version') 253 | echo(message % { 254 | 'prog': prog, 255 | 'version': ver, 256 | }) 257 | ctx.exit() 258 | 259 | attrs.setdefault('is_flag', True) 260 | attrs.setdefault('expose_value', False) 261 | attrs.setdefault('is_eager', True) 262 | attrs.setdefault('help', 'Show the version and exit.') 263 | attrs['callback'] = callback 264 | return option(*(param_decls or ('--version',)), **attrs)(f) 265 | return decorator 266 | 267 | 268 | def help_option(*param_decls, **attrs): 269 | """Adds a ``--help`` option which immediately ends the program 270 | printing out the help page. This is usually unnecessary to add as 271 | this is added by default to all commands unless suppressed. 272 | 273 | Like :func:`version_option`, this is implemented as eager option that 274 | prints in the callback and exits. 275 | 276 | All arguments are forwarded to :func:`option`. 277 | """ 278 | def decorator(f): 279 | def callback(ctx, param, value): 280 | if value and not ctx.resilient_parsing: 281 | echo(ctx.get_help()) 282 | ctx.exit() 283 | attrs.setdefault('is_flag', True) 284 | attrs.setdefault('expose_value', False) 285 | attrs.setdefault('help', 'Show this message and exit.') 286 | attrs.setdefault('is_eager', True) 287 | attrs['callback'] = callback 288 | return option(*(param_decls or ('--help',)), **attrs)(f) 289 | return decorator 290 | 291 | 292 | # Circular dependencies between core and decorators 293 | from .core import Command, Group, Argument, Option 294 | -------------------------------------------------------------------------------- /pyblish/vendor/click/exceptions.py: -------------------------------------------------------------------------------- 1 | from ._compat import PY2, filename_to_ui, get_text_stderr 2 | from .utils import echo 3 | 4 | 5 | class ClickException(Exception): 6 | """An exception that Click can handle and show to the user.""" 7 | 8 | #: The exit code for this exception 9 | exit_code = 1 10 | 11 | def __init__(self, message): 12 | if PY2: 13 | Exception.__init__(self, message.encode('utf-8')) 14 | else: 15 | Exception.__init__(self, message) 16 | self.message = message 17 | 18 | def format_message(self): 19 | return self.message 20 | 21 | def show(self, file=None): 22 | if file is None: 23 | file = get_text_stderr() 24 | echo('Error: %s' % self.format_message(), file=file) 25 | 26 | 27 | class UsageError(ClickException): 28 | """An internal exception that signals a usage error. This typically 29 | aborts any further handling. 30 | 31 | :param message: the error message to display. 32 | :param ctx: optionally the context that caused this error. Click will 33 | fill in the context automatically in some situations. 34 | """ 35 | exit_code = 2 36 | 37 | def __init__(self, message, ctx=None): 38 | ClickException.__init__(self, message) 39 | self.ctx = ctx 40 | 41 | def show(self, file=None): 42 | if file is None: 43 | file = get_text_stderr() 44 | if self.ctx is not None: 45 | echo(self.ctx.get_usage() + '\n', file=file) 46 | echo('Error: %s' % self.format_message(), file=file) 47 | 48 | 49 | class BadParameter(UsageError): 50 | """An exception that formats out a standardized error message for a 51 | bad parameter. This is useful when thrown from a callback or type as 52 | Click will attach contextual information to it (for instance, which 53 | parameter it is). 54 | 55 | .. versionadded:: 2.0 56 | 57 | :param param: the parameter object that caused this error. This can 58 | be left out, and Click will attach this info itself 59 | if possible. 60 | :param param_hint: a string that shows up as parameter name. This 61 | can be used as alternative to `param` in cases 62 | where custom validation should happen. If it is 63 | a string it's used as such, if it's a list then 64 | each item is quoted and separated. 65 | """ 66 | 67 | def __init__(self, message, ctx=None, param=None, 68 | param_hint=None): 69 | UsageError.__init__(self, message, ctx) 70 | self.param = param 71 | self.param_hint = param_hint 72 | 73 | def format_message(self): 74 | if self.param_hint is not None: 75 | param_hint = self.param_hint 76 | elif self.param is not None: 77 | param_hint = self.param.opts or [self.param.name] 78 | else: 79 | return 'Invalid value: %s' % self.message 80 | if isinstance(param_hint, (tuple, list)): 81 | param_hint = ' / '.join('"%s"' % x for x in param_hint) 82 | return 'Invalid value for %s: %s' % (param_hint, self.message) 83 | 84 | 85 | class FileError(ClickException): 86 | """Raised if a file cannot be opened.""" 87 | 88 | def __init__(self, filename, hint=None): 89 | ui_filename = filename_to_ui(filename) 90 | if hint is None: 91 | hint = 'unknown error' 92 | ClickException.__init__(self, hint) 93 | self.ui_filename = ui_filename 94 | self.filename = filename 95 | 96 | def format_message(self): 97 | return 'Could not open file %s: %s' % (self.ui_filename, self.message) 98 | 99 | 100 | class Abort(RuntimeError): 101 | """An internal signalling exception that signals Click to abort.""" 102 | -------------------------------------------------------------------------------- /pyblish/vendor/click/formatting.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from .termui import get_terminal_size 3 | from .parser import split_opt 4 | from ._compat import term_len 5 | 6 | 7 | def measure_table(rows): 8 | widths = {} 9 | for row in rows: 10 | for idx, col in enumerate(row): 11 | widths[idx] = max(widths.get(idx, 0), term_len(col)) 12 | return tuple(y for x, y in sorted(widths.items())) 13 | 14 | 15 | def iter_rows(rows, col_count): 16 | for row in rows: 17 | row = tuple(row) 18 | yield row + ('',) * (col_count - len(row)) 19 | 20 | 21 | def wrap_text(text, width=78, initial_indent='', subsequent_indent='', 22 | preserve_paragraphs=False): 23 | """A helper function that intelligently wraps text. By default, it 24 | assumes that it operates on a single paragraph of text but if the 25 | `preserve_paragraphs` parameter is provided it will intelligently 26 | handle paragraphs (defined by two empty lines). 27 | 28 | If paragraphs are handled, a paragraph can be prefixed with an empty 29 | line containing the ``\\b`` character (``\\x08``) to indicate that 30 | no rewrapping should happen in that block. 31 | 32 | :param text: the text that should be rewrapped. 33 | :param width: the maximum width for the text. 34 | :param initial_indent: the initial indent that should be placed on the 35 | first line as a string. 36 | :param subsequent_indent: the indent string that should be placed on 37 | each consecutive line. 38 | :param preserve_paragraphs: if this flag is set then the wrapping will 39 | intelligently handle paragraphs. 40 | """ 41 | from ._textwrap import TextWrapper 42 | text = text.expandtabs() 43 | wrapper = TextWrapper(width, initial_indent=initial_indent, 44 | subsequent_indent=subsequent_indent, 45 | replace_whitespace=False) 46 | if not preserve_paragraphs: 47 | return wrapper.fill(text) 48 | 49 | p = [] 50 | buf = [] 51 | indent = None 52 | 53 | def _flush_par(): 54 | if not buf: 55 | return 56 | if buf[0].strip() == '\b': 57 | p.append((indent or 0, True, '\n'.join(buf[1:]))) 58 | else: 59 | p.append((indent or 0, False, ' '.join(buf))) 60 | del buf[:] 61 | 62 | for line in text.splitlines(): 63 | if not line: 64 | _flush_par() 65 | indent = None 66 | else: 67 | if indent is None: 68 | orig_len = term_len(line) 69 | line = line.lstrip() 70 | indent = orig_len - term_len(line) 71 | buf.append(line) 72 | _flush_par() 73 | 74 | rv = [] 75 | for indent, raw, text in p: 76 | with wrapper.extra_indent(' ' * indent): 77 | if raw: 78 | rv.append(wrapper.indent_only(text)) 79 | else: 80 | rv.append(wrapper.fill(text)) 81 | 82 | return '\n\n'.join(rv) 83 | 84 | 85 | class HelpFormatter(object): 86 | """This class helps with formatting text-based help pages. It's 87 | usually just needed for very special internal cases, but it's also 88 | exposed so that developers can write their own fancy outputs. 89 | 90 | At present, it always writes into memory. 91 | 92 | :param indent_increment: the additional increment for each level. 93 | :param width: the width for the text. This defaults to the terminal 94 | width clamped to a maximum of 78. 95 | """ 96 | 97 | def __init__(self, indent_increment=2, width=None): 98 | self.indent_increment = indent_increment 99 | if width is None: 100 | width = max(min(get_terminal_size()[0], 80) - 2, 50) 101 | self.width = width 102 | self.current_indent = 0 103 | self.buffer = [] 104 | 105 | def write(self, string): 106 | """Writes a unicode string into the internal buffer.""" 107 | self.buffer.append(string) 108 | 109 | def indent(self): 110 | """Increases the indentation.""" 111 | self.current_indent += self.indent_increment 112 | 113 | def dedent(self): 114 | """Decreases the indentation.""" 115 | self.current_indent -= self.indent_increment 116 | 117 | def write_usage(self, prog, args='', prefix='Usage: '): 118 | """Writes a usage line into the buffer. 119 | 120 | :param prog: the program name. 121 | :param args: whitespace separated list of arguments. 122 | :param prefix: the prefix for the first line. 123 | """ 124 | prefix = '%*s%s' % (self.current_indent, prefix, prog) 125 | self.write(prefix) 126 | 127 | text_width = max(self.width - self.current_indent - term_len(prefix), 10) 128 | indent = ' ' * (term_len(prefix) + 1) 129 | self.write(wrap_text(args, text_width, 130 | initial_indent=' ', 131 | subsequent_indent=indent)) 132 | 133 | self.write('\n') 134 | 135 | def write_heading(self, heading): 136 | """Writes a heading into the buffer.""" 137 | self.write('%*s%s:\n' % (self.current_indent, '', heading)) 138 | 139 | def write_paragraph(self): 140 | """Writes a paragraph into the buffer.""" 141 | if self.buffer: 142 | self.write('\n') 143 | 144 | def write_text(self, text): 145 | """Writes re-indented text into the buffer. This rewraps and 146 | preserves paragraphs. 147 | """ 148 | text_width = max(self.width - self.current_indent, 11) 149 | indent = ' ' * self.current_indent 150 | self.write(wrap_text(text, text_width, 151 | initial_indent=indent, 152 | subsequent_indent=indent, 153 | preserve_paragraphs=True)) 154 | self.write('\n') 155 | 156 | def write_dl(self, rows, col_max=30, col_spacing=2): 157 | """Writes a definition list into the buffer. This is how options 158 | and commands are usually formatted. 159 | 160 | :param rows: a list of two item tuples for the terms and values. 161 | :param col_max: the maximum width of the first column. 162 | :param col_spacing: the number of spaces between the first and 163 | second column. 164 | """ 165 | rows = list(rows) 166 | widths = measure_table(rows) 167 | if len(widths) != 2: 168 | raise TypeError('Expected two columns for definition list') 169 | 170 | first_col = min(widths[0], col_max) + col_spacing 171 | 172 | for first, second in iter_rows(rows, len(widths)): 173 | self.write('%*s%s' % (self.current_indent, '', first)) 174 | if not second: 175 | self.write('\n') 176 | continue 177 | if term_len(first) <= first_col - col_spacing: 178 | self.write(' ' * (first_col - term_len(first))) 179 | else: 180 | self.write('\n') 181 | self.write(' ' * (first_col + self.current_indent)) 182 | 183 | text_width = max(self.width - first_col - 2, 10) 184 | lines = iter(wrap_text(second, text_width).splitlines()) 185 | if lines: 186 | self.write(next(lines) + '\n') 187 | for line in lines: 188 | self.write('%*s%s\n' % ( 189 | first_col + self.current_indent, '', line)) 190 | else: 191 | self.write('\n') 192 | 193 | @contextmanager 194 | def section(self, name): 195 | """Helpful context manager that writes a paragraph, a heading, 196 | and the indents. 197 | 198 | :param name: the section name that is written as heading. 199 | """ 200 | self.write_paragraph() 201 | self.write_heading(name) 202 | self.indent() 203 | try: 204 | yield 205 | finally: 206 | self.dedent() 207 | 208 | @contextmanager 209 | def indentation(self): 210 | """A context manager that increases the indentation.""" 211 | self.indent() 212 | try: 213 | yield 214 | finally: 215 | self.dedent() 216 | 217 | def getvalue(self): 218 | """Returns the buffer contents.""" 219 | return ''.join(self.buffer) 220 | 221 | 222 | def join_options(options): 223 | """Given a list of option strings this joins them in the most appropriate 224 | way and returns them in the form ``(formatted_string, 225 | any_prefix_is_slash)`` where the second item in the tuple is a flag that 226 | indicates if any of the option prefixes was a slash. 227 | """ 228 | rv = [] 229 | any_prefix_is_slash = False 230 | for opt in options: 231 | prefix = split_opt(opt)[0] 232 | if prefix == '/': 233 | any_prefix_is_slash = True 234 | rv.append((len(prefix), opt)) 235 | 236 | rv.sort(key=lambda x: x[0]) 237 | 238 | rv = ', '.join(x[1] for x in rv) 239 | return rv, any_prefix_is_slash 240 | -------------------------------------------------------------------------------- /pyblish/vendor/click/testing.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | import tempfile 5 | import contextlib 6 | 7 | from pyblish.vendor import click 8 | 9 | from ._compat import iteritems, PY2 10 | 11 | 12 | if PY2: 13 | from cStringIO import StringIO 14 | else: 15 | import io 16 | from ._compat import _find_binary_reader 17 | 18 | 19 | class EchoingStdin(object): 20 | 21 | def __init__(self, input, output): 22 | self._input = input 23 | self._output = output 24 | 25 | def __getattr__(self, x): 26 | return getattr(self._input, x) 27 | 28 | def _echo(self, rv): 29 | self._output.write(rv) 30 | return rv 31 | 32 | def read(self, n=-1): 33 | return self._echo(self._input.read(n)) 34 | 35 | def readline(self, n=-1): 36 | return self._echo(self._input.readline(n)) 37 | 38 | def readlines(self): 39 | return [self._echo(x) for x in self._input.readlines()] 40 | 41 | def __iter__(self): 42 | return iter(self._echo(x) for x in self._input) 43 | 44 | def __repr__(self): 45 | return repr(self._input) 46 | 47 | 48 | def make_input_stream(input, charset): 49 | # Is already an input stream. 50 | if hasattr(input, 'read'): 51 | if PY2: 52 | return input 53 | rv = _find_binary_reader(input) 54 | if rv is not None: 55 | return rv 56 | raise TypeError('Could not find binary reader for input stream.') 57 | 58 | if input is None: 59 | input = b'' 60 | elif not isinstance(input, bytes): 61 | input = input.encode(charset) 62 | if PY2: 63 | return StringIO(input) 64 | return io.BytesIO(input) 65 | 66 | 67 | class Result(object): 68 | """Holds the captured result of an invoked CLI script.""" 69 | 70 | def __init__(self, runner, output_bytes, exit_code, exception, 71 | exc_info=None): 72 | #: The runner that created the result 73 | self.runner = runner 74 | #: The output as bytes. 75 | self.output_bytes = output_bytes 76 | #: The exit code as integer. 77 | self.exit_code = exit_code 78 | #: The exception that happend if one did. 79 | self.exception = exception 80 | #: The traceback 81 | self.exc_info = exc_info 82 | 83 | @property 84 | def output(self): 85 | """The output as unicode string.""" 86 | return self.output_bytes.decode(self.runner.charset, 'replace') \ 87 | .replace('\r\n', '\n') 88 | 89 | def __repr__(self): 90 | return '' % ( 91 | self.exception and repr(self.exception) or 'okay', 92 | ) 93 | 94 | 95 | class CliRunner(object): 96 | """The CLI runner provides functionality to invoke a Click command line 97 | script for unittesting purposes in a isolated environment. This only 98 | works in single-threaded systems without any concurrency as it changes the 99 | global interpreter state. 100 | 101 | :param charset: the character set for the input and output data. This is 102 | UTF-8 by default and should not be changed currently as 103 | the reporting to Click only works in Python 2 properly. 104 | :param env: a dictionary with environment variables for overriding. 105 | :param echo_stdin: if this is set to `True`, then reading from stdin writes 106 | to stdout. This is useful for showing examples in 107 | some circumstances. Note that regular prompts 108 | will automatically echo the input. 109 | """ 110 | 111 | def __init__(self, charset=None, env=None, echo_stdin=False): 112 | if charset is None: 113 | charset = 'utf-8' 114 | self.charset = charset 115 | self.env = env or {} 116 | self.echo_stdin = echo_stdin 117 | 118 | def get_default_prog_name(self, cli): 119 | """Given a command object it will return the default program name 120 | for it. The default is the `name` attribute or ``"root"`` if not 121 | set. 122 | """ 123 | return cli.name or 'root' 124 | 125 | def make_env(self, overrides=None): 126 | """Returns the environment overrides for invoking a script.""" 127 | rv = dict(self.env) 128 | if overrides: 129 | rv.update(overrides) 130 | return rv 131 | 132 | @contextlib.contextmanager 133 | def isolation(self, input=None, env=None): 134 | """A context manager that sets up the isolation for invoking of a 135 | command line tool. This sets up stdin with the given input data 136 | and `os.environ` with the overrides from the given dictionary. 137 | This also rebinds some internals in Click to be mocked (like the 138 | prompt functionality). 139 | 140 | This is automatically done in the :meth:`invoke` method. 141 | 142 | :param input: the input stream to put into sys.stdin. 143 | :param env: the environment overrides as dictionary. 144 | """ 145 | input = make_input_stream(input, self.charset) 146 | 147 | old_stdin = sys.stdin 148 | old_stdout = sys.stdout 149 | old_stderr = sys.stderr 150 | 151 | env = self.make_env(env) 152 | 153 | if PY2: 154 | sys.stdout = sys.stderr = bytes_output = StringIO() 155 | if self.echo_stdin: 156 | input = EchoingStdin(input, bytes_output) 157 | else: 158 | bytes_output = io.BytesIO() 159 | if self.echo_stdin: 160 | input = EchoingStdin(input, bytes_output) 161 | input = io.TextIOWrapper(input, encoding=self.charset) 162 | sys.stdout = sys.stderr = io.TextIOWrapper( 163 | bytes_output, encoding=self.charset) 164 | 165 | sys.stdin = input 166 | 167 | def visible_input(prompt=None): 168 | sys.stdout.write(prompt or '') 169 | val = input.readline().rstrip('\r\n') 170 | sys.stdout.write(val + '\n') 171 | sys.stdout.flush() 172 | return val 173 | 174 | def hidden_input(prompt=None): 175 | sys.stdout.write((prompt or '') + '\n') 176 | sys.stdout.flush() 177 | return input.readline().rstrip('\r\n') 178 | 179 | def _getchar(echo): 180 | char = sys.stdin.read(1) 181 | if echo: 182 | sys.stdout.write(char) 183 | sys.stdout.flush() 184 | return char 185 | 186 | old_visible_prompt_func = click.termui.visible_prompt_func 187 | old_hidden_prompt_func = click.termui.hidden_prompt_func 188 | old__getchar_func = click.termui._getchar 189 | click.termui.visible_prompt_func = visible_input 190 | click.termui.hidden_prompt_func = hidden_input 191 | click.termui._getchar = _getchar 192 | 193 | old_env = {} 194 | try: 195 | for key, value in iteritems(env): 196 | old_env[key] = os.environ.get(value) 197 | if value is None: 198 | try: 199 | del os.environ[key] 200 | except Exception: 201 | pass 202 | else: 203 | os.environ[key] = value 204 | yield bytes_output 205 | finally: 206 | for key, value in iteritems(old_env): 207 | if value is None: 208 | try: 209 | del os.environ[key] 210 | except Exception: 211 | pass 212 | else: 213 | os.environ[key] = value 214 | sys.stdout = old_stdout 215 | sys.stderr = old_stderr 216 | sys.stdin = old_stdin 217 | click.termui.visible_prompt_func = old_visible_prompt_func 218 | click.termui.hidden_prompt_func = old_hidden_prompt_func 219 | click.termui._getchar = old__getchar_func 220 | 221 | def invoke(self, cli, args=None, input=None, env=None, 222 | catch_exceptions=True, **extra): 223 | """Invokes a command in an isolated environment. The arguments are 224 | forwarded directly to the command line script, the `extra` keyword 225 | arguments are passed to the :meth:`~click.Command.main` function of 226 | the command. 227 | 228 | This returns a :class:`Result` object. 229 | 230 | .. versionadded:: 3.0 231 | The ``catch_exceptions`` parameter was added. 232 | 233 | .. versionchanged:: 3.0 234 | The result object now has an `exc_info` attribute with the 235 | traceback if available. 236 | 237 | :param cli: the command to invoke 238 | :param args: the arguments to invoke 239 | :param input: the input data for `sys.stdin`. 240 | :param env: the environment overrides. 241 | :param catch_exceptions: Whether to catch any other exceptions than 242 | ``SystemExit``. 243 | :param extra: the keyword arguments to pass to :meth:`main`. 244 | """ 245 | exc_info = None 246 | with self.isolation(input=input, env=env) as out: 247 | exception = None 248 | exit_code = 0 249 | 250 | try: 251 | cli.main(args=args or (), 252 | prog_name=self.get_default_prog_name(cli), **extra) 253 | except SystemExit as e: 254 | if e.code != 0: 255 | exception = e 256 | exit_code = e.code 257 | exc_info = sys.exc_info() 258 | except Exception as e: 259 | if not catch_exceptions: 260 | raise 261 | exception = e 262 | exit_code = -1 263 | exc_info = sys.exc_info() 264 | finally: 265 | sys.stdout.flush() 266 | output = out.getvalue() 267 | 268 | return Result(runner=self, 269 | output_bytes=output, 270 | exit_code=exit_code, 271 | exception=exception, 272 | exc_info=exc_info) 273 | 274 | @contextlib.contextmanager 275 | def isolated_filesystem(self): 276 | """A context manager that creates a temporary folder and changes 277 | the current working directory to it for isolated filesystem tests. 278 | """ 279 | cwd = os.getcwd() 280 | t = tempfile.mkdtemp() 281 | os.chdir(t) 282 | try: 283 | yield t 284 | finally: 285 | os.chdir(cwd) 286 | try: 287 | shutil.rmtree(t) 288 | except (OSError, IOError): 289 | pass 290 | -------------------------------------------------------------------------------- /pyblish/vendor/iscompatible.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python versioning with requirements.txt syntax 3 | ============================================== 4 | 5 | iscompatible v\ |version|. gives you the power of the pip requirements.txt 6 | syntax for everyday python packages, modules, classes or arbitrary 7 | functions. 8 | 9 | The requirements.txt syntax allows you to specify inexact matches 10 | between a set of requirements and a version. For example, let's 11 | assume that the single package foo-5.6.1 exists on disk. The 12 | following requirements are all compatible with foo-5.6.1. 13 | 14 | =========== ================================================= 15 | Requirement Description 16 | =========== ================================================= 17 | foo any version of foo 18 | foo>=5 any version of foo, above or equal to 5 19 | foo>=5.6 any version of foo, above or equal to 5.6 20 | foo==5.6.1 exact match 21 | foo>5 foo-5 or greater, including minor and patch 22 | foo>5, <5.7 foo-5 or greater, but less than foo-5.7 23 | foo>0, <5.7 any foo version less than foo-5.7 24 | =========== ================================================= 25 | 26 | Example: 27 | >>> iscompatible("foo>=5", (5, 6, 1)) 28 | True 29 | >>> iscompatible("foo>=5.6.1, <5.7", (5, 0, 0)) 30 | False 31 | >>> MyPlugin = type("MyPlugin", (), {'version': (5, 6, 1)}) 32 | >>> iscompatible("foo==5.6.1", MyPlugin.version) 33 | True 34 | 35 | References 36 | ^^^^^^^^^^ 37 | 38 | - `The requirements file-format`_ 39 | 40 | .. _The requirements file-format: https://pip.readthedocs.org/en/1.1/requirements.html#the-requirements-file-format 41 | .. _VCS: https://pip.readthedocs.org/en/1.1/requirements.html#version-control 42 | .. _extras: http://peak.telecommunity.com/DevCenter/setuptools#declaring-extras-optional-features-with-their-own-dependencies 43 | 44 | """ 45 | 46 | version_info = (0, 1, 1) 47 | __version__ = "%s.%s.%s" % version_info 48 | 49 | 50 | import re 51 | import operator 52 | 53 | 54 | def iscompatible(requirements, version): 55 | """Return whether or not `requirements` is compatible with `version` 56 | 57 | Arguments: 58 | requirements (str): Requirement to compare, e.g. foo==1.0.1 59 | version (tuple): Version to compare against, e.g. (1, 0, 1) 60 | 61 | Example: 62 | >>> iscompatible("foo", (1, 0, 0)) 63 | True 64 | >>> iscompatible("foo<=1", (0, 9, 0)) 65 | True 66 | >>> iscompatible("foo>=1, <1.3", (1, 2, 0)) 67 | True 68 | >>> iscompatible("foo>=0.9.9", (1, 0, 0)) 69 | True 70 | >>> iscompatible("foo>=1.1, <2.1", (2, 0, 0)) 71 | True 72 | >>> iscompatible("foo==1.0.0", (1, 0, 0)) 73 | True 74 | >>> iscompatible("foo==1.0.0", (1, 0, 1)) 75 | False 76 | 77 | """ 78 | 79 | results = list() 80 | 81 | for operator_string, requirement_string in parse_requirements(requirements): 82 | operator = operators[operator_string] 83 | required = string_to_tuple(requirement_string) 84 | result = operator(version, required) 85 | 86 | results.append(result) 87 | 88 | return all(results) 89 | 90 | 91 | def parse_requirements(line): 92 | """Return list of tuples with (operator, version) from `line` 93 | 94 | .. note:: This is a minimal re-implementation of 95 | pkg_utils.parse_requirements and doesn't include support 96 | for `VCS`_ or `extras`_. 97 | 98 | 99 | Example: 100 | >>> parse_requirements("foo==1.0.0") 101 | [('==', '1.0.0')] 102 | >>> parse_requirements("foo>=1.1.0") 103 | [('>=', '1.1.0')] 104 | >>> parse_requirements("foo>=1.1.0, <1.2") 105 | [('>=', '1.1.0'), ('<', '1.2')] 106 | 107 | 108 | """ 109 | 110 | LINE_END = re.compile(r"\s*(#.*)?$") 111 | DISTRO = re.compile(r"\s*((\w|[-.])+)") 112 | VERSION = re.compile(r"\s*(<=?|>=?|==|!=)\s*((\w|[-.])+)") 113 | COMMA = re.compile(r"\s*,") 114 | 115 | match = DISTRO.match(line) 116 | p = match.end() 117 | specs = list() 118 | 119 | while not LINE_END.match(line, p): 120 | match = VERSION.match(line, p) 121 | if not match: 122 | raise ValueError( 123 | "Expected version spec in", 124 | line, "at", line[p:]) 125 | 126 | specs.append(match.group(*(1, 2))) 127 | p = match.end() 128 | 129 | match = COMMA.match(line, p) 130 | if match: 131 | p = match.end() # Skip comma 132 | elif not LINE_END.match(line, p): 133 | raise ValueError( 134 | "Expected ',' or end-of-list in", 135 | line, "at", line[p:]) 136 | 137 | return specs 138 | 139 | 140 | def string_to_tuple(version): 141 | """Convert version as string to tuple 142 | 143 | Example: 144 | >>> string_to_tuple("1.0.0") 145 | (1, 0, 0) 146 | >>> string_to_tuple("2.5") 147 | (2, 5) 148 | 149 | """ 150 | 151 | return tuple(map(int, version.split("."))) 152 | 153 | 154 | operators = {"<": operator.lt, 155 | "<=": operator.le, 156 | "==": operator.eq, 157 | "!=": operator.ne, 158 | ">=": operator.ge, 159 | ">": operator.gt} 160 | -------------------------------------------------------------------------------- /pyblish/version.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION_MAJOR = 1 3 | VERSION_MINOR = 8 4 | VERSION_PATCH = 12 5 | 6 | version_info = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) 7 | version = '%i.%i.%i' % version_info 8 | __version__ = version 9 | 10 | __all__ = ['version', 'version_info', '__version__'] 11 | -------------------------------------------------------------------------------- /run_coverage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # Expose Pyblish to PYTHONPATH 5 | path = os.path.dirname(__file__) 6 | sys.path.insert(0, path) 7 | 8 | import nose 9 | 10 | if __name__ == '__main__': 11 | argv = sys.argv[:] 12 | argv.extend(['-c', '.noserc']) 13 | nose.main(argv=argv) 14 | -------------------------------------------------------------------------------- /run_testsuite.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import warnings 4 | 5 | # Expose Pyblish to PYTHONPATH 6 | path = os.path.dirname(__file__) 7 | sys.path.insert(0, path) 8 | 9 | import nose 10 | from pyblish.vendor import mock 11 | 12 | warnings.warn = mock.MagicMock() 13 | 14 | 15 | if __name__ == '__main__': 16 | argv = sys.argv[:] 17 | argv.extend(['--exclude=vendor', '--with-doctest', '--verbose']) 18 | nose.main(argv=argv) 19 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # This includes the license file in the wheel. 3 | license_file = LICENSE.txt 4 | 5 | [bdist_wheel] 6 | # This produces a "universal" (Py2+3) wheel. 7 | universal = 1 8 | 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import imp 3 | 4 | from setuptools import setup, find_packages 5 | 6 | with open("README.txt") as f: 7 | readme = f.read() 8 | 9 | 10 | version_file = os.path.abspath("pyblish/version.py") 11 | version_mod = imp.load_source("version", version_file) 12 | version = version_mod.version 13 | 14 | 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 2", 21 | "Programming Language :: Python :: 2.6", 22 | "Programming Language :: Python :: 2.7", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.1", 25 | "Programming Language :: Python :: 3.2", 26 | "Programming Language :: Python :: 3.3", 27 | "Programming Language :: Python :: 3.4", 28 | "Programming Language :: Python :: 3.5", 29 | "Topic :: Software Development :: Libraries :: Python Modules", 30 | "Topic :: Utilities" 31 | ] 32 | 33 | 34 | setup( 35 | name="pyblish-base", 36 | version=version, 37 | description="Plug-in driven automation framework for content", 38 | long_description=readme, 39 | author="Abstract Factory and Contributors", 40 | author_email="marcus@abstractfactory.io", 41 | url="https://github.com/pyblish/pyblish", 42 | license="LGPL", 43 | packages=find_packages(), 44 | zip_safe=False, 45 | classifiers=classifiers, 46 | package_data={ 47 | "pyblish": ["plugins/*.py", 48 | "*.yaml", 49 | "icons/*.svg"], 50 | }, 51 | entry_points={ 52 | "console_scripts": ["pyblish = pyblish.cli:main"] 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyblish/pyblish-base/03cda36b26010642bcbdc8dbf2f256f298742f9f/tests/__init__.py -------------------------------------------------------------------------------- /tests/lib.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | import tempfile 5 | import contextlib 6 | 7 | import pyblish 8 | import pyblish.cli 9 | import pyblish.plugin 10 | from pyblish.vendor import six 11 | 12 | # Setup 13 | HOST = 'python' 14 | FAMILY = 'test.family' 15 | 16 | REGISTERED = pyblish.plugin.registered_paths() 17 | PACKAGEPATH = pyblish.lib.main_package_path() 18 | ENVIRONMENT = os.environ.get("PYBLISHPLUGINPATH", "") 19 | PLUGINPATH = os.path.join(PACKAGEPATH, '..', 'tests', 'plugins') 20 | 21 | 22 | def setup(): 23 | """Disable default plugins and only use test plugins""" 24 | pyblish.plugin.deregister_all_paths() 25 | 26 | 27 | def setup_empty(): 28 | """Disable all plug-ins""" 29 | setup() 30 | pyblish.plugin.deregister_all_plugins() 31 | pyblish.plugin.deregister_all_paths() 32 | pyblish.plugin.deregister_all_hosts() 33 | pyblish.plugin.deregister_all_callbacks() 34 | pyblish.plugin.deregister_all_targets() 35 | pyblish.api.deregister_all_discovery_filters() 36 | 37 | 38 | def teardown(): 39 | """Restore previously REGISTERED paths""" 40 | 41 | pyblish.plugin.deregister_all_paths() 42 | for path in REGISTERED: 43 | pyblish.plugin.register_plugin_path(path) 44 | 45 | os.environ["PYBLISHPLUGINPATH"] = ENVIRONMENT 46 | pyblish.api.deregister_all_plugins() 47 | pyblish.api.deregister_all_hosts() 48 | pyblish.api.deregister_all_discovery_filters() 49 | pyblish.api.deregister_test() 50 | pyblish.api.__init__() 51 | 52 | 53 | @contextlib.contextmanager 54 | def captured_stdout(): 55 | """Temporarily reassign stdout to a local variable""" 56 | try: 57 | sys.stdout = six.StringIO() 58 | yield sys.stdout 59 | finally: 60 | sys.stdout = sys.__stdout__ 61 | 62 | 63 | @contextlib.contextmanager 64 | def captured_stderr(): 65 | """Temporarily reassign stderr to a local variable""" 66 | try: 67 | sys.stderr = six.StringIO() 68 | yield sys.stderr 69 | finally: 70 | sys.stderr = sys.__stderr__ 71 | 72 | 73 | @contextlib.contextmanager 74 | def tempdir(): 75 | """Provide path to temporary directory""" 76 | try: 77 | tempdir = tempfile.mkdtemp() 78 | yield tempdir 79 | finally: 80 | shutil.rmtree(tempdir) 81 | -------------------------------------------------------------------------------- /tests/plugins.py: -------------------------------------------------------------------------------- 1 | """Plugins for testing purposes. 2 | 3 | Source them like this from within a test function: 4 | 5 | api.deregister_all_paths() 6 | api.register_plugin_path(os.path.dirname(__file__)) 7 | 8 | This ensures that the plugins are actually loaded through `plugin.discover`. 9 | """ 10 | from pyblish import api 11 | 12 | 13 | class FailingExplicitPlugin(api.InstancePlugin): 14 | """Raise an exception.""" 15 | 16 | def process(self, instance): 17 | raise Exception("A test exception") 18 | 19 | 20 | class FailingImplicitPlugin(api.Validator): 21 | """Raise an exception.""" 22 | 23 | def process(self, instance): 24 | raise Exception("A test exception") 25 | -------------------------------------------------------------------------------- /tests/plugins/missing_extension/myCollector: -------------------------------------------------------------------------------- 1 | import pyblish.plugin 2 | 3 | class MyCollectorWithoutExtension(pyblish.plugin.Collector): 4 | hosts = ["*"] 5 | version = (0, 1, 0) 6 | 7 | def process_context(self, context): 8 | pass -------------------------------------------------------------------------------- /tests/plugins/missing_host/missing_host.py: -------------------------------------------------------------------------------- 1 | """This plugin is used to test how discover handles a mismatching host. 2 | note that there is a host in this plugin, but it is not registered by pyblish""" 3 | 4 | import pyblish.plugin 5 | 6 | @pyblish.api.log 7 | class CollectMissingHosts(pyblish.plugin.Collector): 8 | hosts = ['not_a_registered_host'] 9 | -------------------------------------------------------------------------------- /tests/plugins/private/_start_with_underscore.py: -------------------------------------------------------------------------------- 1 | import pyblish.plugin 2 | 3 | class MyPrivatePlugin(pyblish.plugin.Collector): 4 | 5 | hosts = ["*"] 6 | version = (0, 1, 0) 7 | 8 | def process_context(self, context): 9 | pass 10 | 11 | raise Exception("This should not be executed, plugin loading should be skipped") -------------------------------------------------------------------------------- /tests/pre11/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyblish/pyblish-base/03cda36b26010642bcbdc8dbf2f256f298742f9f/tests/pre11/__init__.py -------------------------------------------------------------------------------- /tests/pre11/lib.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyblish 4 | import pyblish.plugin 5 | 6 | # Setup 7 | HOST = 'python' 8 | FAMILY = 'test.family' 9 | 10 | REGISTERED = pyblish.plugin.registered_paths() 11 | PACKAGEPATH = pyblish.lib.main_package_path() 12 | PLUGINPATH = os.path.join(PACKAGEPATH, '..', 'tests', 'pre11', 'plugins') 13 | ENVIRONMENT = os.environ.get("PYBLISHPLUGINPATH", "") 14 | 15 | 16 | def setup(): 17 | """Disable default plugins and only use test plugins""" 18 | pyblish.plugin.deregister_all_paths() 19 | pyblish.plugin.register_plugin_path(PLUGINPATH) 20 | 21 | 22 | def setup_empty(): 23 | """Disable all plug-ins""" 24 | setup() 25 | pyblish.plugin.deregister_all_plugins() 26 | pyblish.plugin.deregister_all_paths() 27 | 28 | 29 | def setup_failing(): 30 | """Expose failing plugins to discovery mechanism""" 31 | setup() 32 | 33 | # Append failing plugins 34 | failing_path = os.path.join(PLUGINPATH, 'failing') 35 | pyblish.plugin.register_plugin_path(failing_path) 36 | 37 | 38 | def setup_duplicate(): 39 | """Expose duplicate plugins to discovery mechanism""" 40 | pyblish.plugin.deregister_all_paths() 41 | 42 | for copy in ('copy1', 'copy2'): 43 | path = os.path.join(PLUGINPATH, 'duplicate', copy) 44 | pyblish.plugin.register_plugin_path(path) 45 | 46 | 47 | def setup_wildcard(): 48 | pyblish.plugin.deregister_all_paths() 49 | 50 | wildcard_path = os.path.join(PLUGINPATH, 'wildcards') 51 | pyblish.plugin.register_plugin_path(wildcard_path) 52 | 53 | 54 | def setup_invalid(): 55 | """Expose invalid plugins to discovery mechanism""" 56 | pyblish.plugin.deregister_all_paths() 57 | failing_path = os.path.join(PLUGINPATH, 'invalid') 58 | pyblish.plugin.register_plugin_path(failing_path) 59 | 60 | 61 | def setup_full(): 62 | """Expose a full processing chain for testing""" 63 | setup() 64 | pyblish.plugin.deregister_all_paths() 65 | path = os.path.join(PLUGINPATH, 'full') 66 | pyblish.plugin.register_plugin_path(path) 67 | 68 | 69 | def setup_echo(): 70 | """Plugins that output information""" 71 | pyblish.plugin.deregister_all_paths() 72 | 73 | path = os.path.join(PLUGINPATH, 'echo') 74 | pyblish.plugin.register_plugin_path(path) 75 | 76 | 77 | def teardown(): 78 | """Restore previously REGISTERED paths""" 79 | 80 | pyblish.plugin.deregister_all_paths() 81 | for path in REGISTERED: 82 | pyblish.plugin.register_plugin_path(path) 83 | 84 | os.environ["PYBLISHPLUGINPATH"] = ENVIRONMENT 85 | pyblish.api.deregister_all_plugins() 86 | 87 | 88 | # CLI Fixtures 89 | 90 | 91 | def setup_cli(): 92 | os.environ["PYBLISHPLUGINPATH"] = PLUGINPATH 93 | 94 | 95 | def setup_failing_cli(): 96 | """Expose failing plugins to CLI discovery mechanism""" 97 | # Append failing plugins 98 | failing_path = os.path.join(PLUGINPATH, 'failing_cli') 99 | os.environ["PYBLISHPLUGINPATH"] = failing_path 100 | -------------------------------------------------------------------------------- /tests/pre11/plugins/conform_instances.py: -------------------------------------------------------------------------------- 1 | """Mockup of potential integration with 3rd-party task managment suite""" 2 | 3 | import pyblish.api 4 | from pyblish.vendor import mock 5 | 6 | api = mock.MagicMock() 7 | 8 | 9 | class ConformInstances(pyblish.api.Conformer): 10 | hosts = ['python'] 11 | families = ['test.family'] 12 | version = (0, 1, 0) 13 | 14 | def process_instance(self, instance): 15 | uri = instance.data('assetId') 16 | 17 | if uri: 18 | # This instance has an associated entity 19 | # in the database, emit event 20 | message = "{0} was recently published".format( 21 | instance.data('name')) 22 | api.login(user='Test', password='testpass613') 23 | api.notify(message, uri) 24 | 25 | instance.set_data('notified', value=True) 26 | -------------------------------------------------------------------------------- /tests/pre11/plugins/custom/validate_custom_instance.py: -------------------------------------------------------------------------------- 1 | """Mockup of potential integration with 3rd-party task managment suite""" 2 | 3 | import pyblish.api 4 | 5 | 6 | class ValidateCustomInstance(pyblish.api.Validator): 7 | hosts = ['python'] 8 | families = ['test.family'] 9 | version = (0, 1, 0) 10 | -------------------------------------------------------------------------------- /tests/pre11/plugins/duplicate/copy1/select_duplicate_instances.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | @pyblish.api.log 5 | class SelectDuplicateInstance(pyblish.api.Selector): 6 | hosts = ['python'] 7 | version = (0, 1, 0) 8 | -------------------------------------------------------------------------------- /tests/pre11/plugins/duplicate/copy2/select_duplicate_instances.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | @pyblish.api.log 5 | class SelectDuplicateInstance(pyblish.api.Selector): 6 | hosts = ['python'] 7 | version = (0, 1, 0) 8 | -------------------------------------------------------------------------------- /tests/pre11/plugins/echo/select_echo.py: -------------------------------------------------------------------------------- 1 | import pyblish 2 | 3 | 4 | class SelectEcho(pyblish.Selector): 5 | hosts = ['*'] 6 | version = (0, 0, 1) 7 | 8 | def process_context(self, context): 9 | print(context.data()) 10 | -------------------------------------------------------------------------------- /tests/pre11/plugins/extract_documents.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import pyblish.api 4 | 5 | @pyblish.api.log 6 | class ExtractDocuments(pyblish.api.Extractor): 7 | """Extract instances""" 8 | 9 | hosts = ['python'] 10 | families = ['test.family'] 11 | version = (0, 1, 0) 12 | 13 | def process_instance(self, instance): 14 | temp_dir = tempfile.mkdtemp() 15 | 16 | for document in instance: 17 | for name, content in document.items(): 18 | temp_file = os.path.join(temp_dir, 19 | '{0}.txt'.format(name)) 20 | with open(temp_file, 'w') as f: 21 | f.write(content) 22 | 23 | self.commit(temp_dir, instance) 24 | -------------------------------------------------------------------------------- /tests/pre11/plugins/extract_instances.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class ExtractInstances(pyblish.api.Extractor): 7 | """Extract instances""" 8 | 9 | hosts = ['python'] 10 | families = ['test.family'] 11 | version = (0, 1, 0) 12 | 13 | def process_instance(self, instance): 14 | self.log.debug("Extracting %s" % instance) 15 | -------------------------------------------------------------------------------- /tests/pre11/plugins/failing/conform_instances_fail.py: -------------------------------------------------------------------------------- 1 | """Mockup of potential integration with 3rd-party task managment suite""" 2 | 3 | 4 | import pyblish.api 5 | from pyblish.vendor import mock 6 | 7 | api = mock.MagicMock() 8 | 9 | 10 | class ConformInstancesFail(pyblish.api.Conformer): 11 | hosts = ['python'] 12 | families = ['test.family'] 13 | version = (0, 1, 0) 14 | 15 | def process_instance(self, instance): 16 | raise ValueError("Test fail") 17 | -------------------------------------------------------------------------------- /tests/pre11/plugins/failing/extract_instances_fail.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class ExtractInstancesFail(pyblish.api.Extractor): 7 | hosts = ['python'] 8 | families = ['test.family'] 9 | version = (0, 1, 0) 10 | 11 | def process_instance(self, instance): 12 | raise ValueError("Could not extract {0}".format(instance)) 13 | -------------------------------------------------------------------------------- /tests/pre11/plugins/failing/select_instances_fail.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class SelectInstancesError(pyblish.api.Selector): 7 | hosts = ['python'] 8 | version = (0, 1, 0) 9 | 10 | def process_context(self, context): 11 | raise ValueError("Test exception") 12 | -------------------------------------------------------------------------------- /tests/pre11/plugins/failing/validate_instances_fail.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class ValidateInstanceFail(pyblish.api.Validator): 7 | hosts = ['python'] 8 | families = ['test.family'] 9 | version = (0, 1, 0) 10 | 11 | def process_instance(self, instance): 12 | raise ValueError("Test fail") 13 | -------------------------------------------------------------------------------- /tests/pre11/plugins/failing_cli/extract_cli_instances.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | @pyblish.api.log 5 | class ExtractInstancesFail(pyblish.api.Extractor): 6 | hosts = ['python'] 7 | families = ['test.family'] 8 | version = (0, 1, 0) 9 | 10 | def process_instance(self, instance): 11 | pass 12 | -------------------------------------------------------------------------------- /tests/pre11/plugins/failing_cli/select_cli_instances.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | 3 | 4 | @pyblish.api.log 5 | class SelectCliInstances(pyblish.api.Selector): 6 | hosts = ['python'] 7 | version = (0, 1, 0) 8 | 9 | def process_context(self, context): 10 | raise ValueError(context.data("fail")) 11 | -------------------------------------------------------------------------------- /tests/pre11/plugins/full/conform_instances.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class ConformInstances(pyblish.api.Conformer): 7 | hosts = ['python'] 8 | families = ['full'] 9 | version = (0, 1, 0) 10 | 11 | def process_instance(self, instance): 12 | instance.set_data('conformed', True) 13 | -------------------------------------------------------------------------------- /tests/pre11/plugins/full/extract_instances.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class ExtractInstances(pyblish.api.Extractor): 7 | hosts = ['python'] 8 | families = ['full'] 9 | version = (0, 1, 0) 10 | 11 | def process_instance(self, instance): 12 | instance.set_data('extracted', True) 13 | -------------------------------------------------------------------------------- /tests/pre11/plugins/full/select_instances.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class SelectInstances(pyblish.api.Selector): 7 | hosts = ['python'] 8 | version = (0, 1, 0) 9 | 10 | def process_context(self, context): 11 | inst = context.create_instance(name='Test') 12 | inst.set_data('family', 'full') 13 | inst.set_data('selected', True) 14 | 15 | # The following will be set during 16 | # processing of other plugins 17 | inst.set_data('validated', False) 18 | inst.set_data('extracted', False) 19 | inst.set_data('conformed', False) 20 | -------------------------------------------------------------------------------- /tests/pre11/plugins/full/validate_instances.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class ValidateInstances(pyblish.api.Validator): 7 | hosts = ['python'] 8 | families = ['full'] 9 | version = (0, 1, 0) 10 | 11 | def process_instance(self, instance): 12 | instance.set_data('validated', True) 13 | -------------------------------------------------------------------------------- /tests/pre11/plugins/invalid/select_missing_hosts.py: -------------------------------------------------------------------------------- 1 | """This plugin is incomplete and can't be used""" 2 | 3 | import pyblish.api 4 | 5 | 6 | @pyblish.api.log 7 | class SelectMissingHosts(pyblish.api.Selector): 8 | """Select instances""" 9 | 10 | requires = False 11 | version = "Invalid" 12 | -------------------------------------------------------------------------------- /tests/pre11/plugins/invalid/validate_missing_families.py: -------------------------------------------------------------------------------- 1 | """This plugin is incomplete and can't be used""" 2 | 3 | import pyblish.api 4 | 5 | 6 | @pyblish.api.log 7 | class ValidateMissingFamilies(pyblish.api.Validator): 8 | """Select instances""" 9 | 10 | hosts = ['python'] 11 | version = list() 12 | -------------------------------------------------------------------------------- /tests/pre11/plugins/select_instances.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class SelectInstances(pyblish.api.Selector): 7 | """Select instances""" 8 | 9 | hosts = ['python'] 10 | version = (0, 1, 0) 11 | 12 | def process_context(self, context): 13 | instance = context.create_instance(name='inst1') 14 | 15 | for node in ('node1_PLY', 'node2_PLY', 'node3_GRP'): 16 | instance.add(node) 17 | 18 | for key, value in { 19 | 'publishable': True, 20 | 'family': 'test', 21 | 'startFrame': 1001, 22 | 'endFrame': 1025 23 | }.items(): 24 | 25 | instance.set_data(key, value) 26 | -------------------------------------------------------------------------------- /tests/pre11/plugins/validate_instances.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | import pyblish.api 4 | 5 | 6 | @pyblish.api.log 7 | class ValidateInstance(pyblish.api.Validator): 8 | """All nodes ends with a three-letter extension""" 9 | 10 | hosts = ['python'] 11 | families = ['test.family'] 12 | version = (0, 1, 0) 13 | 14 | def process_instance(self, instance): 15 | misnamed = list() 16 | 17 | for node in instance: 18 | self.log.debug("Validating {0}".format(node)) 19 | if not re.match(r"^\w+_\w{3}?$", node): 20 | misnamed.append(node) 21 | 22 | if misnamed: 23 | raise ValueError("{0} was named incorrectly".format(node)) 24 | -------------------------------------------------------------------------------- /tests/pre11/plugins/validate_other_instance.py: -------------------------------------------------------------------------------- 1 | 2 | import pyblish.api 3 | 4 | 5 | @pyblish.api.log 6 | class ValidateOtherInstance(pyblish.api.Validator): 7 | """All nodes ends with a three-letter extension""" 8 | 9 | hosts = ['python'] 10 | families = ['test.other.family'] 11 | version = (0, 1, 0) 12 | 13 | def process_instance(self, instance): 14 | return 15 | -------------------------------------------------------------------------------- /tests/pre11/plugins/wildcards/select_instances.py: -------------------------------------------------------------------------------- 1 | from pyblish import api 2 | 3 | 4 | @api.log 5 | class SelectInstances(api.Selector): 6 | hosts = ['*'] 7 | version = (0, 0, 1) 8 | 9 | def process_context(self, context): 10 | files = context.create_instance(name='Files') 11 | files.add('Test1') 12 | files.add('Test2') 13 | -------------------------------------------------------------------------------- /tests/pre11/plugins/wildcards/validate_instances.py: -------------------------------------------------------------------------------- 1 | from pyblish import api 2 | 3 | 4 | @api.log 5 | class ValidateInstances123(api.Validator): 6 | hosts = ['*'] 7 | families = ['*'] 8 | version = (0, 0, 1) 9 | 10 | def process_instance(self, instance): 11 | raise ValueError("I was called") 12 | -------------------------------------------------------------------------------- /tests/pre11/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyblish 4 | import pyblish.cli 5 | import pyblish.api 6 | 7 | from pyblish.vendor import six 8 | 9 | from . import lib 10 | 11 | from pyblish.vendor.click.testing import CliRunner 12 | from nose.tools import ( 13 | with_setup, 14 | ) 15 | from pyblish.vendor import mock 16 | 17 | 18 | def ctx(): 19 | """Return current Click context""" 20 | return pyblish.cli._ctx 21 | 22 | 23 | def context(): 24 | """Return current context""" 25 | return ctx().obj["context"] 26 | 27 | 28 | @with_setup(lib.setup_empty) 29 | def test_all_commands_run(): 30 | """All commands run without error""" 31 | 32 | for args in [[], # No argument 33 | ["--verbose"], 34 | ["publish"], 35 | ["--verbose", "publish"], 36 | ]: 37 | 38 | runner = CliRunner() 39 | result = runner.invoke(pyblish.cli.main, args) 40 | 41 | print("Args: %s" % args) 42 | print("Exit code: %s" % result.exit_code) 43 | print("Output: %s" % result.output) 44 | assert result.exit_code == 0 45 | 46 | 47 | def test_paths(): 48 | """Paths are correctly returned from cli""" 49 | plugin = pyblish.api 50 | for flag, func in six.iteritems({ 51 | "--paths": plugin.plugin_paths, 52 | "--registered-paths": plugin.registered_paths, 53 | "--environment-paths": plugin.environment_paths}): 54 | 55 | print("Flag: %s" % flag) 56 | runner = CliRunner() 57 | result = runner.invoke(pyblish.cli.main, [flag]) 58 | for path in func(): 59 | assert path in result.output 60 | 61 | 62 | def test_plugins(): 63 | """CLI returns correct plugins""" 64 | runner = CliRunner() 65 | result = runner.invoke(pyblish.cli.main, ["--plugins"]) 66 | 67 | for plugin in pyblish.api.discover(): 68 | print("Plugin: %s" % plugin.__name__) 69 | assert plugin.__name__ in result.output 70 | 71 | 72 | def test_plugins_path(): 73 | """Custom path via cli works""" 74 | custom_path = os.path.join(lib.PLUGINPATH, "custom") 75 | runner = CliRunner() 76 | result = runner.invoke(pyblish.cli.main, 77 | ["--plugin-path", 78 | custom_path, 79 | "--plugins"]) 80 | 81 | plugins = pyblish.api.discover(paths=[custom_path]) 82 | for plugin in plugins: 83 | print("Output: %s" % result.output) 84 | assert plugin.__name__ in result.output 85 | 86 | 87 | @with_setup(lib.setup_failing_cli, lib.teardown) 88 | def test_data(): 89 | """Injecting data works""" 90 | 91 | runner = CliRunner() 92 | runner.invoke(pyblish.cli.main, [ 93 | "--data", "key", "10", "publish"]) 94 | 95 | assert context().data["key"] == 10 96 | assert not context().has_data("notExist") 97 | 98 | 99 | @mock.patch("pyblish.cli.log") 100 | def test_invalid_data(mock_log): 101 | """Data not JSON-serialisable is treated as string""" 102 | 103 | runner = CliRunner() 104 | runner.invoke(pyblish.cli.main, 105 | ["--data", "key", "['test': 'fdf}"]) 106 | 107 | assert context().data["key"] == "['test': 'fdf}" 108 | 109 | 110 | def test_add_plugin_path(): 111 | """Adding a plugin-path works""" 112 | custom_path = os.path.join(lib.PLUGINPATH, "custom") 113 | 114 | runner = CliRunner() 115 | runner.invoke( 116 | pyblish.cli.main, 117 | ["--add-plugin-path", custom_path, "--paths"]) 118 | 119 | assert custom_path in ctx().obj["plugin_paths"] 120 | 121 | 122 | def test_version(): 123 | """Version returned matches version of Pyblish""" 124 | runner = CliRunner() 125 | result = runner.invoke(pyblish.cli.main, ["--version"]) 126 | print("Output: %s" % result.output) 127 | print("Version: %s" % pyblish.__version__) 128 | assert pyblish.__version__ in result.output 129 | -------------------------------------------------------------------------------- /tests/pre11/test_plugins.py: -------------------------------------------------------------------------------- 1 | 2 | # Standard library 3 | import os 4 | import random 5 | 6 | # Local library 7 | import pyblish.plugin 8 | 9 | from .lib import ( 10 | setup, 11 | teardown, 12 | setup_duplicate, 13 | setup_empty 14 | ) 15 | from nose.tools import ( 16 | with_setup, 17 | assert_equals, 18 | assert_true, 19 | assert_raises 20 | ) 21 | 22 | 23 | @with_setup(setup, teardown) 24 | def test_print_plugin(): 25 | """Printing plugin returns name of class""" 26 | plugins = pyblish.plugin.discover('validators') 27 | plugin = plugins[0] 28 | assert plugin.__name__ in repr(plugin()) 29 | assert plugin.__name__ == str(plugin()) 30 | 31 | 32 | @with_setup(setup, teardown) 33 | def test_name_override(): 34 | """Instances return either a data-member of name or its native name""" 35 | inst = pyblish.plugin.Instance(name='my_name') 36 | assert inst.data('name') == 'my_name' 37 | 38 | inst.set_data('name', value='overridden_name') 39 | assert inst.data('name') == 'overridden_name' 40 | 41 | 42 | @with_setup(setup_duplicate, teardown) 43 | def test_no_duplicate_plugins(): 44 | """Discovering plugins results in a single occurence of each plugin""" 45 | plugin_paths = pyblish.plugin.plugin_paths() 46 | assert_equals(len(plugin_paths), 2) 47 | 48 | plugins = pyblish.plugin.discover(type='selectors') 49 | 50 | # There are two plugins available, but one of them is 51 | # hidden under the duplicate module name. As a result, 52 | # only one of them is returned. A log message is printed 53 | # to alert the user. 54 | assert_equals(len(plugins), 1) 55 | 56 | 57 | def test_entities_prints_nicely(): 58 | """Entities Context and Instance prints nicely""" 59 | ctx = pyblish.plugin.Context() 60 | inst = ctx.create_instance(name='Test') 61 | assert 'Instance' in repr(inst) 62 | assert 'pyblish.plugin' in repr(inst) 63 | 64 | 65 | def test_deregister_path(): 66 | path = "/server/plugins" 67 | pyblish.plugin.register_plugin_path(path) 68 | assert os.path.normpath(path) in pyblish.plugin.registered_paths() 69 | pyblish.plugin.deregister_plugin_path(path) 70 | assert os.path.normpath(path) not in pyblish.plugin.registered_paths() 71 | 72 | 73 | def test_environment_paths(): 74 | """Registering via the environment works""" 75 | key = "PYBLISHPLUGINPATH" 76 | path = '/test/path' 77 | existing = os.environ.get(key) 78 | 79 | try: 80 | os.environ[key] = path 81 | assert path in pyblish.plugin.plugin_paths() 82 | finally: 83 | os.environ[key] = existing or '' 84 | 85 | 86 | def test_instances_by_plugin_invariant(): 87 | ctx = pyblish.plugin.Context() 88 | for i in range(10): 89 | inst = ctx.create_instance(name="Instance%i" % i) 90 | inst.set_data("family", "A") 91 | 92 | if i % 2: 93 | # Every other instance is of another family 94 | inst.set_data("family", "B") 95 | 96 | class MyPlugin(pyblish.plugin.Validator): 97 | hosts = ["python"] 98 | families = ["A"] 99 | 100 | def process(self, instance): 101 | pass 102 | 103 | compatible = pyblish.logic.instances_by_plugin(ctx, MyPlugin) 104 | 105 | # Test invariant 106 | # 107 | # in: [1, 2, 3, 4] 108 | # out: [1, 4] --> good 109 | # 110 | # in: [1, 2, 3, 4] 111 | # out: [2, 1, 4] --> bad 112 | # 113 | 114 | def test(): 115 | for instance in compatible: 116 | assert ctx.index(instance) >= compatible.index(instance) 117 | 118 | test() 119 | 120 | compatible.reverse() 121 | assert_raises(AssertionError, test) 122 | 123 | 124 | def test_plugins_by_family_wildcard(): 125 | """Plug-ins with wildcard family is included in every query""" 126 | Plugin1 = type("Plugin1", 127 | (pyblish.api.Validator,), 128 | {"families": ["myFamily"]}) 129 | Plugin2 = type("Plugin2", 130 | (pyblish.api.Validator,), 131 | {"families": ["*"]}) 132 | 133 | assert Plugin2 in pyblish.api.plugins_by_family( 134 | [Plugin1, Plugin2], "myFamily") 135 | 136 | 137 | @with_setup(setup, teardown) 138 | def test_plugins_sorted(): 139 | """Plug-ins are returned sorted by their `order` attribute""" 140 | plugins = pyblish.api.discover() 141 | random.shuffle(plugins) # Randomise their order 142 | pyblish.api.sort_plugins(plugins) 143 | 144 | order = 0 145 | for plugin in plugins: 146 | assert_true(plugin.order >= order) 147 | order = plugin.order 148 | 149 | assert order > 0, plugins 150 | 151 | 152 | @with_setup(setup_empty, teardown) 153 | def test_inmemory_plugins(): 154 | """In-memory plug-ins works fine""" 155 | 156 | class InMemoryPlugin(pyblish.api.Selector): 157 | hosts = ["*"] 158 | families = ["*"] 159 | 160 | def process_context(self, context): 161 | context.set_data("workingFine", True) 162 | 163 | pyblish.api.register_plugin(InMemoryPlugin) 164 | 165 | context = pyblish.api.Context() 166 | for result in pyblish.logic.process( 167 | func=pyblish.plugin.process, 168 | plugins=pyblish.api.discover, 169 | context=context): 170 | assert_true(result["plugin"].id == InMemoryPlugin.id) 171 | 172 | assert context.data("workingFine") is True 173 | 174 | 175 | @with_setup(setup_empty, teardown) 176 | def test_inmemory_query(): 177 | """Asking for registered plug-ins works well""" 178 | 179 | InMemoryPlugin = type("InMemoryPlugin", (pyblish.api.Selector,), {}) 180 | pyblish.api.register_plugin(InMemoryPlugin) 181 | assert pyblish.api.registered_plugins()[0].id == InMemoryPlugin.id 182 | 183 | 184 | @with_setup(setup_empty, teardown) 185 | def test_plugin_families_defaults(): 186 | """Plug-ins without specific families default to wildcard""" 187 | 188 | class SelectInstances(pyblish.api.Selector): 189 | def process(self, instance): 190 | pass 191 | 192 | instance = pyblish.api.Instance("MyInstance") 193 | instance.set_data("family", "SomeFamily") 194 | 195 | assert_equals(pyblish.api.instances_by_plugin( 196 | [instance], SelectInstances)[0], instance) 197 | 198 | class ValidateInstances(pyblish.api.Validator): 199 | def process(self, instance): 200 | pass 201 | 202 | assert_equals(pyblish.api.instances_by_plugin( 203 | [instance], ValidateInstances)[0], instance) 204 | -------------------------------------------------------------------------------- /tests/pre13/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyblish/pyblish-base/03cda36b26010642bcbdc8dbf2f256f298742f9f/tests/pre13/__init__.py -------------------------------------------------------------------------------- /tests/pre13/test_plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | 5 | import pyblish.api 6 | import pyblish.plugin 7 | from nose.tools import ( 8 | with_setup, 9 | assert_true, 10 | ) 11 | 12 | from pyblish.vendor import six 13 | 14 | from .. import lib 15 | 16 | 17 | def test_plugins_from_module(): 18 | """Getting plug-ins from a module works well""" 19 | import types 20 | 21 | module = types.ModuleType("myplugin") 22 | code = """ 23 | import pyblish.api 24 | 25 | class MyPlugin(pyblish.api.Plugin): 26 | def process(self, context): 27 | pass 28 | 29 | class NotSubclassed(object): 30 | def process(self, context): 31 | pass 32 | 33 | def not_a_plugin(): 34 | pass 35 | 36 | 37 | class InvalidPlugin(pyblish.api.Plugin): 38 | families = False 39 | 40 | 41 | class NotCompatible(pyblish.api.Plugin): 42 | hosts = ["not_compatible"] 43 | 44 | 45 | class BadRequires(pyblish.api.Plugin): 46 | requires = None 47 | 48 | 49 | class BadHosts(pyblish.api.Plugin): 50 | hosts = None 51 | 52 | 53 | class BadFamilies(pyblish.api.Plugin): 54 | families = None 55 | 56 | 57 | class BadHosts2(pyblish.api.Plugin): 58 | hosts = [None] 59 | 60 | 61 | class BadFamilies2(pyblish.api.Plugin): 62 | families = [None] 63 | 64 | 65 | """ 66 | 67 | six.exec_(code, module.__dict__) 68 | 69 | plugins = pyblish.plugin.plugins_from_module(module) 70 | 71 | assert [p.__name__ for p in plugins] == ["MyPlugin"], plugins 72 | 73 | 74 | @with_setup(lib.setup_empty, lib.teardown) 75 | def test_discover_globals(): 76 | """Modules imported in a plug-in are preserved in it's methods""" 77 | 78 | import types 79 | 80 | module = types.ModuleType("myplugin") 81 | code = """ 82 | import pyblish.api 83 | import threading 84 | 85 | local_variable_is_present = 5 86 | 87 | 88 | class MyPlugin(pyblish.api.Plugin): 89 | def module_is_present(self): 90 | return True if threading else False 91 | 92 | def local_variable_is_present(self): 93 | return True if local_variable_is_present else False 94 | 95 | def process(self, context): 96 | return True if context else False 97 | 98 | """ 99 | 100 | six.exec_(code, module.__dict__) 101 | MyPlugin = pyblish.plugin.plugins_from_module(module)[0] 102 | assert MyPlugin.__name__ == "MyPlugin" 103 | 104 | assert_true(MyPlugin().process(True)) 105 | assert_true(MyPlugin().module_is_present()) 106 | assert_true(MyPlugin().local_variable_is_present()) 107 | 108 | try: 109 | tempdir = tempfile.mkdtemp() 110 | tempplugin = os.path.join(tempdir, "my_plugin.py") 111 | with open(tempplugin, "w") as f: 112 | f.write(code) 113 | 114 | pyblish.api.register_plugin_path(tempdir) 115 | plugins = pyblish.api.discover() 116 | 117 | finally: 118 | shutil.rmtree(tempdir) 119 | 120 | assert len(plugins) == 1 121 | 122 | MyPlugin = plugins[0] 123 | 124 | assert_true(MyPlugin().process(True)) 125 | assert_true(MyPlugin().module_is_present()) 126 | assert_true(MyPlugin().local_variable_is_present()) 127 | 128 | 129 | @with_setup(lib.setup_empty, lib.teardown) 130 | def test_multi_families(): 131 | """Instances with multiple families works well""" 132 | 133 | count = {"#": 0} 134 | 135 | class CollectInstance(pyblish.api.Collector): 136 | def process(self, context): 137 | instance = context.create_instance("MyInstance") 138 | instance.data["families"] = ["geometry", "human"] 139 | 140 | class ValidateHumans(pyblish.api.Validator): 141 | families = ["human"] 142 | 143 | def process(self, instance): 144 | assert "human" in instance.data["families"] 145 | count["#"] += 10 146 | 147 | class ValidateGeometry(pyblish.api.Validator): 148 | families = ["geometry"] 149 | 150 | def process(self, instance): 151 | assert "geometry" in instance.data["families"] 152 | count["#"] += 100 153 | 154 | for plugin in (CollectInstance, ValidateHumans, ValidateGeometry): 155 | pyblish.api.register_plugin(plugin) 156 | 157 | pyblish.util.publish() 158 | 159 | assert count["#"] == 110, count["#"] 160 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | import tempfile 5 | 6 | import pyblish 7 | import pyblish.cli 8 | import pyblish.api 9 | from nose.tools import ( 10 | with_setup, 11 | assert_equals, 12 | ) 13 | from pyblish.vendor.click.testing import CliRunner 14 | from . import lib 15 | 16 | self = sys.modules[__name__] 17 | 18 | 19 | def setup(): 20 | self.tempdir = tempfile.mkdtemp() 21 | 22 | 23 | def teardown(): 24 | shutil.rmtree(self.tempdir) 25 | 26 | 27 | def ctx(): 28 | """Return current Click context""" 29 | return pyblish.cli._ctx 30 | 31 | 32 | def context(): 33 | """Return current context""" 34 | return ctx().obj["context"] 35 | 36 | 37 | def test_visualise_environment_paths(): 38 | """Visualising environment paths works well""" 39 | current_path = os.environ.get("PYBLISHPLUGINPATH") 40 | 41 | try: 42 | os.environ["PYBLISHPLUGINPATH"] = "/custom/path" 43 | 44 | runner = CliRunner() 45 | result = runner.invoke(pyblish.cli.main, ["--environment-paths"]) 46 | 47 | assert result.output.startswith("/custom/path"), result.output 48 | 49 | finally: 50 | if current_path is not None: 51 | os.environ["PYBLISHPLUGINPATH"] = current_path 52 | 53 | 54 | @with_setup(lib.setup_empty, lib.teardown) 55 | def test_publishing(): 56 | """Basic publishing works""" 57 | 58 | count = {"#": 0} 59 | 60 | class Collector(pyblish.api.ContextPlugin): 61 | order = pyblish.api.CollectorOrder 62 | 63 | def process(self, context): 64 | self.log.warning("Running") 65 | count["#"] += 1 66 | context.create_instance("MyInstance") 67 | 68 | class MyValidator(pyblish.api.InstancePlugin): 69 | order = pyblish.api.ValidatorOrder 70 | 71 | def process(self, instance): 72 | count["#"] += 10 73 | assert instance.data["name"] == "MyInstance" 74 | count["#"] += 100 75 | 76 | pyblish.api.register_plugin(Collector) 77 | pyblish.api.register_plugin(MyValidator) 78 | 79 | runner = CliRunner() 80 | result = runner.invoke(pyblish.cli.main, ["publish"]) 81 | print(result.output) 82 | 83 | assert count["#"] == 111, count 84 | 85 | 86 | @with_setup(lib.setup, lib.teardown) 87 | def test_environment_host_registration(): 88 | """Host registration from PYBLISH_HOSTS works""" 89 | 90 | count = {"#": 0} 91 | hosts = ["test1", "test2"] 92 | 93 | # Test single hosts 94 | class SingleHostCollector(pyblish.api.ContextPlugin): 95 | order = pyblish.api.CollectorOrder 96 | host = hosts[0] 97 | 98 | def process(self, context): 99 | count["#"] += 1 100 | 101 | pyblish.api.register_plugin(SingleHostCollector) 102 | 103 | os.environ["PYBLISH_HOSTS"] = hosts[0] 104 | 105 | runner = CliRunner() 106 | result = runner.invoke(pyblish.cli.main, ["publish"]) 107 | print(result.output) 108 | 109 | assert count["#"] == 1, count 110 | 111 | # Test multiple hosts 112 | pyblish.api.deregister_all_plugins() 113 | 114 | class MultipleHostsCollector(pyblish.api.ContextPlugin): 115 | order = pyblish.api.CollectorOrder 116 | host = hosts 117 | 118 | def process(self, context): 119 | count["#"] += 10 120 | 121 | pyblish.api.register_plugin(MultipleHostsCollector) 122 | 123 | os.environ["PYBLISH_HOSTS"] = os.pathsep.join(hosts) 124 | 125 | runner = CliRunner() 126 | result = runner.invoke(pyblish.cli.main, ["publish"]) 127 | print(result.output) 128 | 129 | assert count["#"] == 11, count 130 | 131 | 132 | @with_setup(lib.setup, lib.teardown) 133 | def test_show_gui(): 134 | """Showing GUI through cli works""" 135 | 136 | with tempfile.NamedTemporaryFile(dir=self.tempdir, 137 | delete=False, 138 | suffix=".py") as f: 139 | module_name = os.path.basename(f.name)[:-3] 140 | f.write(b"""\ 141 | def show(): 142 | print("Mock GUI shown successfully") 143 | 144 | if __name__ == '__main__': 145 | show() 146 | """) 147 | 148 | pythonpath = os.pathsep.join([ 149 | self.tempdir, 150 | os.environ.get("PYTHONPATH", "") 151 | ]) 152 | 153 | print(module_name) 154 | 155 | runner = CliRunner() 156 | result = runner.invoke( 157 | pyblish.cli.main, ["gui", module_name], 158 | env={"PYTHONPATH": pythonpath} 159 | ) 160 | 161 | assert_equals(result.output.splitlines()[-1].rstrip(), 162 | "Mock GUI shown successfully") 163 | assert_equals(result.exit_code, 0) 164 | 165 | 166 | @with_setup(lib.setup, lib.teardown) 167 | def test_uses_gui_from_env(): 168 | """Uses gui from environment var works""" 169 | 170 | with tempfile.NamedTemporaryFile(dir=self.tempdir, 171 | delete=False, 172 | suffix=".py") as f: 173 | module_name = os.path.basename(f.name)[:-3] 174 | f.write(b"""\ 175 | def show(): 176 | print("Mock GUI shown successfully") 177 | 178 | if __name__ == '__main__': 179 | show() 180 | """) 181 | 182 | pythonpath = os.pathsep.join([ 183 | self.tempdir, 184 | os.environ.get("PYTHONPATH", "") 185 | ]) 186 | 187 | runner = CliRunner() 188 | result = runner.invoke( 189 | pyblish.cli.main, ["gui"], 190 | env={ 191 | "PYTHONPATH": pythonpath, 192 | "PYBLISH_GUI": module_name 193 | } 194 | ) 195 | 196 | assert_equals(result.output.splitlines()[-1].rstrip(), 197 | "Mock GUI shown successfully") 198 | assert_equals(result.exit_code, 0) 199 | 200 | 201 | @with_setup(lib.setup, lib.teardown) 202 | def test_passing_data_to_gui(): 203 | """Passing data to GUI works""" 204 | 205 | with tempfile.NamedTemporaryFile(dir=self.tempdir, 206 | delete=False, 207 | suffix=".py") as f: 208 | module_name = os.path.basename(f.name)[:-3] 209 | f.write(b"""\ 210 | from pyblish import util 211 | 212 | def show(): 213 | context = util.publish() 214 | print(context.data["passedFromTest"]) 215 | 216 | if __name__ == '__main__': 217 | show() 218 | """) 219 | 220 | pythonpath = os.pathsep.join([ 221 | self.tempdir, 222 | os.environ.get("PYTHONPATH", "") 223 | ]) 224 | 225 | runner = CliRunner() 226 | result = runner.invoke( 227 | pyblish.cli.main, [ 228 | "--data", "passedFromTest", "Data passed successfully", 229 | "gui", module_name 230 | ], 231 | env={"PYTHONPATH": pythonpath} 232 | ) 233 | 234 | assert_equals(result.output.splitlines()[-1].rstrip(), 235 | "Data passed successfully") 236 | assert_equals(result.exit_code, 0) 237 | 238 | 239 | @with_setup(lib.setup, lib.teardown) 240 | def test_set_targets(): 241 | """Setting targets works""" 242 | 243 | pythonpath = os.pathsep.join([ 244 | self.tempdir, 245 | os.environ.get("PYTHONPATH", "") 246 | ]) 247 | 248 | count = {"#": 0} 249 | 250 | class CollectorOne(pyblish.api.ContextPlugin): 251 | order = pyblish.api.CollectorOrder 252 | targets = ["imagesequence"] 253 | 254 | def process(self, context): 255 | self.log.warning("Running {0}".format(self.targets)) 256 | count["#"] += 1 257 | context.create_instance("MyInstance") 258 | 259 | class CollectorTwo(pyblish.api.ContextPlugin): 260 | order = pyblish.api.CollectorOrder 261 | targets = ["model"] 262 | 263 | def process(self, context): 264 | self.log.warning("Running {0}".format(self.targets)) 265 | count["#"] += 2 266 | context.create_instance("MyInstance") 267 | 268 | pyblish.api.register_plugin(CollectorOne) 269 | pyblish.api.register_plugin(CollectorTwo) 270 | 271 | runner = CliRunner() 272 | result = runner.invoke(pyblish.cli.main, 273 | ["publish", "--targets", "imagesequence"], 274 | env={"PYTHONPATH": pythonpath}) 275 | 276 | print(result.output) 277 | assert count["#"] == 1, count 278 | 279 | 280 | @with_setup(lib.setup, lib.teardown) 281 | def test_set_targets_gui(): 282 | """Setting targets with gui""" 283 | 284 | with tempfile.NamedTemporaryFile(dir=self.tempdir, 285 | delete=False, 286 | suffix=".py") as f: 287 | module_name = os.path.basename(f.name)[:-3] 288 | f.write(b"""\ 289 | from pyblish import api 290 | 291 | def show(): 292 | targets = api.registered_targets() 293 | print(targets[0]) 294 | 295 | if __name__ == '__main__': 296 | show() 297 | """) 298 | 299 | pythonpath = os.pathsep.join([ 300 | self.tempdir, 301 | os.environ.get("PYTHONPATH", "") 302 | ]) 303 | 304 | # api.__init__ checks the PYBLISH_TARGETS variable 305 | runner = CliRunner() 306 | results = runner.invoke(pyblish.cli.main, 307 | ["gui", module_name], 308 | env={"PYTHONPATH": pythonpath, 309 | "PYBLISH_TARGETS": "imagesequence"}) 310 | 311 | result = results.output.splitlines()[-1].rstrip() 312 | assert_equals(result, "imagesequence") 313 | assert_equals(results.exit_code, 0) 314 | -------------------------------------------------------------------------------- /tests/test_context.py: -------------------------------------------------------------------------------- 1 | 2 | # Standard library 3 | import os 4 | 5 | # Local library 6 | import pyblish.lib 7 | import pyblish.plugin 8 | 9 | from . import lib 10 | from nose.tools import ( 11 | with_setup, 12 | raises 13 | ) 14 | 15 | 16 | package_path = pyblish.lib.main_package_path() 17 | plugin_path = os.path.join(package_path, 'tests', 'plugins') 18 | 19 | pyblish.plugin.deregister_all_paths() 20 | pyblish.plugin.register_plugin_path(plugin_path) 21 | 22 | 23 | @with_setup(lib.setup, lib.teardown) 24 | def test_data(): 25 | """The data() interface works""" 26 | 27 | ctx = pyblish.plugin.Context() 28 | 29 | # Not passing a key returns all data as a dict, 30 | # but there is none yet. 31 | assert ctx.data(key=None) == dict(), ctx.data(key=None) 32 | 33 | key = 'test_key' 34 | 35 | ctx.set_data(key=key, value=True) 36 | assert ctx.data(key=key) is True 37 | assert ctx.has_data(key=key) is True 38 | ctx.remove_data(key=key) 39 | assert ctx.data(key=key) is None 40 | assert ctx.has_data(key=key) is False 41 | 42 | 43 | @with_setup(lib.setup, lib.teardown) 44 | def test_add_remove_instances(): 45 | """Adding instances to context works""" 46 | ctx = pyblish.plugin.Context() 47 | inst = pyblish.plugin.Instance(name='Test', parent=ctx) 48 | ctx.remove(inst) 49 | 50 | 51 | @with_setup(lib.setup, lib.teardown) 52 | def test_instance_equality(): 53 | """Instance equality works""" 54 | inst1 = pyblish.plugin.Instance('Test1') 55 | inst2 = pyblish.plugin.Instance('Test2') 56 | inst3 = pyblish.plugin.Instance('Test2') 57 | 58 | assert inst1 != inst2 59 | assert inst2 != inst3 60 | 61 | 62 | def test_context_itemgetter(): 63 | """Context.get() works""" 64 | context = pyblish.api.Context() 65 | instanceA = context.create_instance("MyInstanceA") 66 | instanceB = context.create_instance("MyInstanceB") 67 | 68 | assert context[instanceA.id].name == "MyInstanceA" 69 | assert context[instanceB.id].name == "MyInstanceB" 70 | assert context.get(instanceA.id).name == "MyInstanceA" 71 | assert context.get(instanceB.id).name == "MyInstanceB" 72 | assert context[0].name == "MyInstanceA" 73 | assert context[1].name == "MyInstanceB" 74 | 75 | 76 | def test_in(): 77 | """Querying whether an Instance is in a Context works""" 78 | 79 | context = pyblish.api.Context() 80 | instance = context.create_instance("MyInstance") 81 | assert instance.id in context 82 | assert "NotExist" not in context 83 | 84 | 85 | def test_add_to_context(): 86 | """Adding to Context is deprecated, but still works""" 87 | context = pyblish.api.Context() 88 | instance = pyblish.api.Instance("MyInstance") 89 | context.add(instance) 90 | context.remove(instance) 91 | 92 | 93 | @raises(KeyError) 94 | def test_context_getitem_nonexisting(): 95 | """Getting a nonexisting item from Context throws a KeyError""" 96 | 97 | context = pyblish.api.Context() 98 | context.create_instance("MyInstance") 99 | assert context.get("NotExist") is None 100 | context["NotExist"] 101 | 102 | 103 | @raises(IndexError) 104 | def test_context_getitem_outofrange(): 105 | """Getting a item out of range throws an IndexError""" 106 | 107 | context = pyblish.api.Context() 108 | context.create_instance("MyInstance") 109 | context[10000] 110 | 111 | 112 | def test_context_getitem_validrange(): 113 | """Getting an existing item works well""" 114 | 115 | context = pyblish.api.Context() 116 | context.create_instance("MyInstance") 117 | assert context[0].data["name"] == "MyInstance" 118 | 119 | 120 | def test_context_instance_unique_id(): 121 | """Same named instances have unique ids""" 122 | 123 | context = pyblish.api.Context() 124 | instance1 = context.create_instance("MyInstance") 125 | instance2 = context.create_instance("MyInstance") 126 | assert instance1.id != instance2.id 127 | 128 | 129 | if __name__ == '__main__': 130 | test_add_remove_instances() 131 | -------------------------------------------------------------------------------- /tests/test_events.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import pyblish.util 3 | from nose.tools import ( 4 | with_setup, 5 | ) 6 | from . import lib 7 | 8 | 9 | @with_setup(lib.setup_empty) 10 | def test_published_event(): 11 | """published is emitted upon finished publish""" 12 | 13 | count = {"#": 0} 14 | 15 | def on_published(context): 16 | assert isinstance(context, pyblish.api.Context) 17 | count["#"] += 1 18 | 19 | pyblish.api.register_callback("published", on_published) 20 | pyblish.util.publish() 21 | 22 | assert count["#"] == 1, count 23 | 24 | 25 | @with_setup(lib.setup_empty) 26 | def test_validated_event(): 27 | """validated is emitted upon finished validation""" 28 | 29 | count = {"#": 0} 30 | 31 | def on_validated(context): 32 | assert isinstance(context, pyblish.api.Context) 33 | count["#"] += 1 34 | 35 | pyblish.api.register_callback("validated", on_validated) 36 | pyblish.util.validate() 37 | 38 | assert count["#"] == 1, count 39 | 40 | @with_setup(lib.setup_empty) 41 | def test_plugin_processed_event(): 42 | """pluginProcessed is emitted upon a plugin being processed, regardless of its success""" 43 | 44 | class MyContextCollector(pyblish.api.ContextPlugin): 45 | order = pyblish.api.CollectorOrder 46 | 47 | def process(self, context): 48 | context.create_instance("A") 49 | 50 | class CheckInstancePass(pyblish.api.InstancePlugin): 51 | order = pyblish.api.ValidatorOrder 52 | 53 | def process(self, instance): 54 | pass 55 | 56 | class CheckInstanceFail(pyblish.api.InstancePlugin): 57 | order = pyblish.api.ValidatorOrder 58 | 59 | def process(self, instance): 60 | raise Exception("Test Fail") 61 | 62 | pyblish.api.register_plugin(MyContextCollector) 63 | pyblish.api.register_plugin(CheckInstancePass) 64 | pyblish.api.register_plugin(CheckInstanceFail) 65 | 66 | 67 | count = {"#": 0} 68 | 69 | def on_processed(result): 70 | assert isinstance(result, dict) 71 | count["#"] += 1 72 | 73 | pyblish.api.register_callback("pluginProcessed", on_processed) 74 | pyblish.util.publish() 75 | 76 | assert count["#"] == 3, count 77 | 78 | @with_setup(lib.setup_empty) 79 | def test_plugin_failed_event(): 80 | """pluginFailed is emitted upon a plugin failing for any reason""" 81 | 82 | class MyContextCollector(pyblish.api.ContextPlugin): 83 | order = pyblish.api.CollectorOrder 84 | def process(self, context): 85 | context.create_instance("A") 86 | 87 | class CheckInstancePass(pyblish.api.InstancePlugin): 88 | order = pyblish.api.ValidatorOrder 89 | def process(self, instance): 90 | pass 91 | 92 | class CheckInstanceFail(pyblish.api.InstancePlugin): 93 | order = pyblish.api.ValidatorOrder 94 | def process(self, instance): 95 | raise Exception("Test Fail") 96 | 97 | pyblish.api.register_plugin(MyContextCollector) 98 | pyblish.api.register_plugin(CheckInstancePass) 99 | pyblish.api.register_plugin(CheckInstanceFail) 100 | 101 | count = {"#": 0} 102 | 103 | def on_failed(plugin, context, instance, error): 104 | assert issubclass(plugin, pyblish.api.InstancePlugin) #plugin == CheckInstanceFail 105 | assert isinstance(context, pyblish.api.Context) 106 | assert isinstance(instance, pyblish.api.Instance) 107 | assert isinstance(error, Exception) 108 | 109 | count["#"] += 1 110 | 111 | pyblish.api.register_callback("pluginFailed", on_failed) 112 | pyblish.util.publish() 113 | 114 | assert count["#"] == 1, count -------------------------------------------------------------------------------- /tests/test_logic.py: -------------------------------------------------------------------------------- 1 | import os 2 | import contextlib 3 | 4 | # Local library 5 | from . import lib 6 | 7 | from pyblish import api, logic, plugin, util 8 | 9 | from nose.tools import ( 10 | with_setup, 11 | assert_equals, 12 | ) 13 | 14 | 15 | @contextlib.contextmanager 16 | def no_guis(): 17 | os.environ.pop("PYBLISHGUI", None) 18 | for gui in logic.registered_guis(): 19 | logic.deregister_gui(gui) 20 | 21 | yield 22 | 23 | 24 | @with_setup(lib.setup, lib.teardown) 25 | def test_iterator(): 26 | """Iterator skips inactive plug-ins and instances""" 27 | 28 | count = {"#": 0} 29 | 30 | class MyCollector(api.ContextPlugin): 31 | order = api.CollectorOrder 32 | 33 | def process(self, context): 34 | inactive = context.create_instance("Inactive") 35 | active = context.create_instance("Active") 36 | 37 | inactive.data["publish"] = False 38 | active.data["publish"] = True 39 | 40 | count["#"] += 1 41 | 42 | class MyValidatorA(api.InstancePlugin): 43 | order = api.ValidatorOrder 44 | active = False 45 | 46 | def process(self, instance): 47 | count["#"] += 10 48 | 49 | class MyValidatorB(api.InstancePlugin): 50 | order = api.ValidatorOrder 51 | 52 | def process(self, instance): 53 | count["#"] += 100 54 | 55 | context = api.Context() 56 | plugins = [MyCollector, MyValidatorA, MyValidatorB] 57 | 58 | assert count["#"] == 0, count 59 | 60 | for Plugin, instance in logic.Iterator(plugins, context): 61 | assert instance.name != "Inactive" if instance else True 62 | assert Plugin.__name__ != "MyValidatorA" 63 | 64 | plugin.process(Plugin, context, instance) 65 | 66 | # Collector runs once, one Validator runs once 67 | assert count["#"] == 101, count 68 | 69 | 70 | def test_iterator_with_explicit_targets(): 71 | """Iterator skips non-targeted plug-ins""" 72 | 73 | count = {"#": 0} 74 | 75 | class MyCollectorA(api.ContextPlugin): 76 | order = api.CollectorOrder 77 | targets = ["studio"] 78 | 79 | def process(self, context): 80 | count["#"] += 1 81 | 82 | class MyCollectorB(api.ContextPlugin): 83 | order = api.CollectorOrder 84 | 85 | def process(self, context): 86 | count["#"] += 10 87 | 88 | class MyCollectorC(api.ContextPlugin): 89 | order = api.CollectorOrder 90 | targets = ["studio"] 91 | 92 | def process(self, context): 93 | count["#"] += 100 94 | 95 | context = api.Context() 96 | plugins = [MyCollectorA, MyCollectorB, MyCollectorC] 97 | 98 | assert count["#"] == 0, count 99 | 100 | for Plugin, instance in logic.Iterator( 101 | plugins, context, targets=["studio"] 102 | ): 103 | assert Plugin.__name__ != "MyCollectorB" 104 | 105 | plugin.process(Plugin, context, instance) 106 | 107 | # Collector runs once, one Validator runs once 108 | assert count["#"] == 101, count 109 | 110 | 111 | def test_register_gui(): 112 | """Registering at run-time takes precedence over those from environment""" 113 | 114 | with no_guis(): 115 | os.environ["PYBLISHGUI"] = "second,third" 116 | logic.register_gui("first") 117 | 118 | print(logic.registered_guis()) 119 | assert logic.registered_guis() == ["first", "second", "third"] 120 | 121 | with no_guis(): 122 | os.environ["PYBLISH_GUI"] = "second,third" 123 | logic.register_gui("first") 124 | 125 | print(logic.registered_guis()) 126 | assert logic.registered_guis() == ["first", "second", "third"] 127 | 128 | 129 | @with_setup(lib.setup_empty, lib.teardown) 130 | def test_subset_match(): 131 | """Plugin.match = api.Subset works as expected""" 132 | 133 | count = {"#": 0} 134 | 135 | class MyPlugin(api.InstancePlugin): 136 | families = ["a", "b"] 137 | match = api.Subset 138 | 139 | def process(self, instance): 140 | count["#"] += 1 141 | 142 | context = api.Context() 143 | 144 | context.create_instance("not_included_1", families=["a"]) 145 | context.create_instance("not_included_1", families=["x"]) 146 | context.create_instance("included_1", families=["a", "b"]) 147 | context.create_instance("included_2", families=["a", "b", "c"]) 148 | 149 | util.publish(context, plugins=[MyPlugin]) 150 | 151 | assert_equals(count["#"], 2) 152 | 153 | instances = logic.instances_by_plugin(context, MyPlugin) 154 | assert_equals(list(i.name for i in instances), 155 | ["included_1", "included_2"]) 156 | 157 | 158 | def test_subset_exact(): 159 | """Plugin.match = api.Exact works as expected""" 160 | 161 | count = {"#": 0} 162 | 163 | class MyPlugin(api.InstancePlugin): 164 | families = ["a", "b"] 165 | match = api.Exact 166 | 167 | def process(self, instance): 168 | count["#"] += 1 169 | 170 | context = api.Context() 171 | 172 | context.create_instance("not_included_1", families=["a"]) 173 | context.create_instance("not_included_1", families=["x"]) 174 | context.create_instance("not_included_3", families=["a", "b", "c"]) 175 | instance = context.create_instance("included_1", families=["a", "b"]) 176 | 177 | # Discard the solo-family member, which defaults to `default`. 178 | # 179 | # When using multiple families, it is common not to bother modifying 180 | # `family`, and in the future this member needn't be there at all and 181 | # may/should be removed. But till then, for complete clarity, it might 182 | # be worth removing this explicitly during the creation of instances 183 | # if instead choosing to use the `families` key. 184 | instance.data.pop("family") 185 | 186 | util.publish(context, plugins=[MyPlugin]) 187 | 188 | assert_equals(count["#"], 1) 189 | 190 | instances = logic.instances_by_plugin(context, MyPlugin) 191 | assert_equals(list(i.name for i in instances), ["included_1"]) 192 | 193 | 194 | def test_plugins_by_families(): 195 | """The right plug-ins are returned from plugins_by_families""" 196 | 197 | class ClassA(api.Collector): 198 | families = ["a"] 199 | 200 | class ClassB(api.Collector): 201 | families = ["b"] 202 | 203 | class ClassC(api.Collector): 204 | families = ["c"] 205 | 206 | class ClassD(api.Collector): 207 | families = ["a", "b"] 208 | match = api.Intersection 209 | 210 | class ClassE(api.Collector): 211 | families = ["a", "b"] 212 | match = api.Subset 213 | 214 | class ClassF(api.Collector): 215 | families = ["a", "b"] 216 | match = api.Exact 217 | 218 | assert logic.plugins_by_families( 219 | [ClassA, ClassB, ClassC], ["a", "z"]) == [ClassA] 220 | 221 | assert logic.plugins_by_families( 222 | [ClassD, ClassE, ClassF], ["a"]) == [ClassD] 223 | 224 | assert logic.plugins_by_families( 225 | [ClassD, ClassE, ClassF], ["a", "b"]) == [ClassD, ClassE, ClassF] 226 | 227 | assert logic.plugins_by_families( 228 | [ClassD, ClassE, ClassF], ["a", "b", "c"]) == [ClassD, ClassE] 229 | 230 | 231 | @with_setup(lib.setup_empty, lib.teardown) 232 | def test_extracted_traceback_contains_correct_backtrace(): 233 | api.register_plugin_path(os.path.dirname(__file__)) 234 | 235 | context = api.Context() 236 | context.create_instance('test instance') 237 | 238 | plugins = api.discover() 239 | plugins = [p for p in plugins if p.__name__ in 240 | ('FailingExplicitPlugin', 'FailingImplicitPlugin')] 241 | util.publish(context, plugins) 242 | 243 | for result in context.data['results']: 244 | assert result["error"].traceback[0] == plugins[0].__module__ 245 | formatted_tb = result['error'].formatted_traceback 246 | assert formatted_tb.startswith('Traceback (most recent call last):\n') 247 | assert formatted_tb.endswith('\nException: A test exception\n') 248 | assert 'File "{0}",'.format(plugins[0].__module__) in formatted_tb 249 | -------------------------------------------------------------------------------- /tests/test_misc.py: -------------------------------------------------------------------------------- 1 | from . import lib 2 | 3 | import pyblish.lib 4 | import pyblish.compat 5 | from nose.tools import ( 6 | with_setup 7 | ) 8 | 9 | 10 | @with_setup(lib.setup, lib.teardown) 11 | def test_compat(): 12 | """Using compatibility functions works""" 13 | pyblish.compat.sort([]) 14 | pyblish.compat.deregister_all() 15 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | import pyblish.api 2 | import pyblish.logic 3 | import pyblish.plugin 4 | 5 | from nose.tools import ( 6 | assert_equals, 7 | with_setup 8 | ) 9 | 10 | from . import lib 11 | 12 | 13 | @with_setup(lib.setup_empty, lib.teardown) 14 | def test_simple_discover(): 15 | """Simple plug-ins works well with discover()""" 16 | 17 | count = {"#": 0} 18 | 19 | class SimplePlugin(pyblish.api.Plugin): 20 | def process(self, context): 21 | self.log.info("Processing context..") 22 | self.log.info("Done!") 23 | count["#"] += 1 24 | 25 | class SimplePlugin2(pyblish.api.Validator): 26 | def process(self, context): 27 | self.log.info("Processing context..") 28 | self.log.info("Done!") 29 | count["#"] += 1 30 | 31 | pyblish.api.register_plugin(SimplePlugin) 32 | pyblish.api.register_plugin(SimplePlugin2) 33 | 34 | assert_equals( 35 | list(p.id for p in pyblish.api.discover()), 36 | list(p.id for p in [SimplePlugin, SimplePlugin2]) 37 | ) 38 | 39 | pyblish.util.publish() 40 | 41 | assert_equals(count["#"], 2) 42 | 43 | 44 | def test_simple_manual(): 45 | """Simple plug-ins work well""" 46 | 47 | count = {"#": 0} 48 | 49 | class SimplePlugin(pyblish.api.Plugin): 50 | def process(self): 51 | self.log.info("Processing..") 52 | self.log.info("Done!") 53 | count["#"] += 1 54 | 55 | pyblish.util.publish(plugins=[SimplePlugin]) 56 | 57 | assert_equals(count["#"], 1) 58 | 59 | 60 | def test_simple_instance(): 61 | """Simple plug-ins process instances as usual 62 | 63 | But considering they don't have an order, we will have to 64 | manually enforce an ordering if we are to expect 65 | them to run one after the other. 66 | 67 | """ 68 | 69 | count = {"#": 0} 70 | 71 | class SimpleSelector(pyblish.api.Plugin): 72 | """Runs once""" 73 | order = 0 74 | 75 | def process(self, context): 76 | instance = context.create_instance(name="A") 77 | instance.set_data("family", "familyA") 78 | 79 | instance = context.create_instance(name="B") 80 | instance.set_data("family", "familyB") 81 | 82 | count["#"] += 1 83 | 84 | class SimpleValidator(pyblish.api.Plugin): 85 | """Runs twice""" 86 | order = 1 87 | 88 | def process(self, instance): 89 | count["#"] += 10 90 | 91 | class SimpleValidatorForB(pyblish.api.Plugin): 92 | """Runs once, for familyB""" 93 | families = ["familyB"] 94 | order = 2 95 | 96 | def process(self, instance): 97 | count["#"] += 100 98 | 99 | pyblish.util.publish(plugins=[SimpleSelector, 100 | SimpleValidator, 101 | SimpleValidatorForB]) 102 | 103 | assert_equals(count["#"], 121) 104 | 105 | 106 | def test_simple_order(): 107 | """Simple plug-ins defaults to running *before* SVEC""" 108 | 109 | order = list() 110 | 111 | class SimplePlugin(pyblish.api.Plugin): 112 | def process(self): 113 | order.append(1) 114 | 115 | class SelectSomething1234(pyblish.api.Selector): 116 | def process(self): 117 | order.append(2) 118 | 119 | class ValidateSomething1234(pyblish.api.Validator): 120 | def process(self): 121 | order.append(3) 122 | 123 | class ExtractSomething1234(pyblish.api.Extractor): 124 | def process(self): 125 | order.append(4) 126 | 127 | for plugin in (ExtractSomething1234, 128 | ValidateSomething1234, 129 | SimplePlugin, 130 | SelectSomething1234): 131 | pyblish.api.register_plugin(plugin) 132 | 133 | plugins = pyblish.api.discover() 134 | context = pyblish.api.Context() 135 | for result in pyblish.logic.process( 136 | func=pyblish.plugin.process, 137 | plugins=plugins, 138 | context=context): 139 | print(result) 140 | 141 | assert_equals(order, [1, 2, 3, 4]) 142 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from . import lib 4 | 5 | from pyblish import api, util 6 | from nose.tools import ( 7 | with_setup 8 | ) 9 | 10 | 11 | def test_convenience_plugins_argument(): 12 | """util._convenience() `plugins` argument works 13 | 14 | Issue: #286 15 | 16 | """ 17 | 18 | count = {"#": 0} 19 | 20 | class PluginA(api.ContextPlugin): 21 | order = api.CollectorOrder 22 | 23 | def process(self, context): 24 | count["#"] += 1 25 | 26 | class PluginB(api.ContextPlugin): 27 | order = api.CollectorOrder 28 | 29 | def process(self, context): 30 | count["#"] += 10 31 | 32 | assert count["#"] == 0 33 | 34 | api.register_plugin(PluginA) 35 | util._convenience(plugins=[PluginB], order=0.5) 36 | 37 | assert count["#"] == 10, count 38 | 39 | 40 | @with_setup(lib.setup, lib.teardown) 41 | def test_convenience_functions(): 42 | """convenience functions works as expected""" 43 | 44 | count = {"#": 0} 45 | 46 | class Collector(api.ContextPlugin): 47 | order = api.CollectorOrder 48 | 49 | def process(self, context): 50 | context.create_instance("MyInstance") 51 | count["#"] += 1 52 | 53 | class Validator(api.InstancePlugin): 54 | order = api.ValidatorOrder 55 | 56 | def process(self, instance): 57 | count["#"] += 10 58 | 59 | class Extractor(api.InstancePlugin): 60 | order = api.ExtractorOrder 61 | 62 | def process(self, instance): 63 | count["#"] += 100 64 | 65 | class Integrator(api.ContextPlugin): 66 | order = api.IntegratorOrder 67 | 68 | def process(self, instance): 69 | count["#"] += 1000 70 | 71 | class PostIntegrator(api.ContextPlugin): 72 | order = api.IntegratorOrder + 0.1 73 | 74 | def process(self, instance): 75 | count["#"] += 10000 76 | 77 | class NotCVEI(api.ContextPlugin): 78 | """This plug-in is too far away from Integration to qualify as CVEI""" 79 | order = api.IntegratorOrder + 2.0 80 | 81 | def process(self, instance): 82 | count["#"] += 100000 83 | 84 | assert count["#"] == 0 85 | 86 | for Plugin in (Collector, 87 | Validator, 88 | Extractor, 89 | Integrator, 90 | PostIntegrator, 91 | NotCVEI): 92 | api.register_plugin(Plugin) 93 | 94 | context = util.collect() 95 | 96 | assert count["#"] == 1 97 | 98 | util.validate(context) 99 | 100 | assert count["#"] == 11 101 | 102 | util.extract(context) 103 | 104 | assert count["#"] == 111 105 | 106 | util.integrate(context) 107 | 108 | assert count["#"] == 11111 109 | 110 | 111 | @with_setup(lib.setup, lib.teardown) 112 | def test_multiple_instance_util_publish(): 113 | """Multiple instances work with util.publish() 114 | 115 | This also ensures it operates correctly with an 116 | InstancePlugin collector. 117 | 118 | """ 119 | 120 | count = {"#": 0} 121 | 122 | class MyContextCollector(api.ContextPlugin): 123 | order = api.CollectorOrder 124 | 125 | def process(self, context): 126 | context.create_instance("A") 127 | context.create_instance("B") 128 | count["#"] += 1 129 | 130 | class MyInstancePluginCollector(api.InstancePlugin): 131 | order = api.CollectorOrder + 0.1 132 | 133 | def process(self, instance): 134 | count["#"] += 1 135 | 136 | api.register_plugin(MyContextCollector) 137 | api.register_plugin(MyInstancePluginCollector) 138 | 139 | # Ensure it runs without errors 140 | util.publish() 141 | 142 | assert count["#"] == 3 143 | 144 | 145 | @with_setup(lib.setup, lib.teardown) 146 | def test_modify_context_during_CVEI(): 147 | """Custom logic made possible via convenience members""" 148 | 149 | count = {"#": 0} 150 | 151 | class MyCollector(api.ContextPlugin): 152 | order = api.CollectorOrder 153 | 154 | def process(self, context): 155 | camera = context.create_instance("MyCamera") 156 | model = context.create_instance("MyModel") 157 | 158 | camera.data["family"] = "camera" 159 | model.data["family"] = "model" 160 | 161 | count["#"] += 1 162 | 163 | class MyValidator(api.InstancePlugin): 164 | order = api.ValidatorOrder 165 | 166 | def process(self, instance): 167 | count["#"] += 10 168 | 169 | api.register_plugin(MyCollector) 170 | api.register_plugin(MyValidator) 171 | 172 | context = api.Context() 173 | 174 | assert count["#"] == 0, count 175 | 176 | util.collect(context) 177 | 178 | assert count["#"] == 1, count 179 | 180 | context[:] = filter(lambda i: i.data["family"] == "camera", context) 181 | 182 | util.validate(context) 183 | 184 | # Only model remains 185 | assert count["#"] == 11, count 186 | 187 | # No further processing occurs. 188 | util.extract(context) 189 | util.integrate(context) 190 | 191 | assert count["#"] == 11, count 192 | 193 | 194 | @with_setup(lib.setup, lib.teardown) 195 | def test_environment_host_registration(): 196 | """Host registration from PYBLISH_HOSTS works""" 197 | 198 | count = {"#": 0} 199 | hosts = ["test1", "test2"] 200 | 201 | # Test single hosts 202 | class SingleHostCollector(api.ContextPlugin): 203 | order = api.CollectorOrder 204 | host = hosts[0] 205 | 206 | def process(self, context): 207 | count["#"] += 1 208 | 209 | api.register_plugin(SingleHostCollector) 210 | 211 | context = api.Context() 212 | 213 | os.environ["PYBLISH_HOSTS"] = "test1" 214 | util.collect(context) 215 | 216 | assert count["#"] == 1, count 217 | 218 | # Test multiple hosts 219 | api.deregister_all_plugins() 220 | 221 | class MultipleHostsCollector(api.ContextPlugin): 222 | order = api.CollectorOrder 223 | host = hosts 224 | 225 | def process(self, context): 226 | count["#"] += 10 227 | 228 | api.register_plugin(MultipleHostsCollector) 229 | 230 | context = api.Context() 231 | 232 | os.environ["PYBLISH_HOSTS"] = os.pathsep.join(hosts) 233 | util.collect(context) 234 | 235 | assert count["#"] == 11, count 236 | 237 | 238 | @with_setup(lib.setup, lib.teardown) 239 | def test_publishing_explicit_targets(): 240 | """Publishing with explicit targets works""" 241 | 242 | count = {"#": 0} 243 | 244 | class plugin(api.ContextPlugin): 245 | targets = ["custom"] 246 | 247 | def process(self, context): 248 | count["#"] += 1 249 | 250 | api.register_plugin(plugin) 251 | 252 | util.publish(targets=["custom"]) 253 | 254 | assert count["#"] == 1, count 255 | 256 | 257 | def test_publishing_explicit_targets_with_global(): 258 | """Publishing with explicit and globally registered targets works""" 259 | 260 | count = {"#": 0} 261 | 262 | class Plugin1(api.ContextPlugin): 263 | targets = ["custom"] 264 | 265 | def process(self, context): 266 | count["#"] += 1 267 | 268 | class Plugin2(api.ContextPlugin): 269 | targets = ["foo"] 270 | 271 | def process(self, context): 272 | count["#"] += 10 273 | 274 | api.register_target("foo") 275 | api.register_target("custom") 276 | api.register_plugin(Plugin1) 277 | api.register_plugin(Plugin2) 278 | 279 | util.publish(targets=["custom"]) 280 | 281 | assert count["#"] == 1, count 282 | assert api.registered_targets() == ["foo", "custom"] 283 | 284 | api.deregister_all_targets() 285 | 286 | 287 | @with_setup(lib.setup, lib.teardown) 288 | def test_per_session_targets(): 289 | """Register targets per session works""" 290 | 291 | util.publish(targets=["custom"]) 292 | 293 | registered_targets = api.registered_targets() 294 | assert registered_targets == [], registered_targets 295 | 296 | 297 | @with_setup(lib.setup, lib.teardown) 298 | def test_publishing_collectors(): 299 | """Running collectors with targets works""" 300 | 301 | count = {"#": 0} 302 | 303 | class plugin(api.ContextPlugin): 304 | order = api.CollectorOrder 305 | targets = ["custom"] 306 | 307 | def process(self, context): 308 | count["#"] += 1 309 | 310 | api.register_plugin(plugin) 311 | 312 | util.collect(targets=["custom"]) 313 | 314 | assert count["#"] == 1, count 315 | 316 | 317 | @with_setup(lib.setup, lib.teardown) 318 | def test_publishing_validators(): 319 | """Running validators with targets works""" 320 | 321 | count = {"#": 0} 322 | 323 | class plugin(api.ContextPlugin): 324 | order = api.ValidatorOrder 325 | targets = ["custom"] 326 | 327 | def process(self, context): 328 | count["#"] += 1 329 | 330 | api.register_plugin(plugin) 331 | 332 | util.validate(targets=["custom"]) 333 | 334 | assert count["#"] == 1, count 335 | 336 | 337 | @with_setup(lib.setup, lib.teardown) 338 | def test_publishing_extractors(): 339 | """Running extractors with targets works""" 340 | 341 | count = {"#": 0} 342 | 343 | class plugin(api.ContextPlugin): 344 | order = api.ExtractorOrder 345 | targets = ["custom"] 346 | 347 | def process(self, context): 348 | count["#"] += 1 349 | 350 | api.register_plugin(plugin) 351 | 352 | util.extract(targets=["custom"]) 353 | 354 | assert count["#"] == 1, count 355 | 356 | 357 | @with_setup(lib.setup, lib.teardown) 358 | def test_publishing_integrators(): 359 | """Running integrators with targets works""" 360 | 361 | count = {"#": 0} 362 | 363 | class plugin(api.ContextPlugin): 364 | order = api.IntegratorOrder 365 | targets = ["custom"] 366 | 367 | def process(self, context): 368 | count["#"] += 1 369 | 370 | api.register_plugin(plugin) 371 | 372 | util.integrate(targets=["custom"]) 373 | 374 | assert count["#"] == 1, count 375 | 376 | 377 | @with_setup(lib.setup, lib.teardown) 378 | def test_progress_existence(): 379 | """Progress data member exists""" 380 | 381 | class plugin(api.ContextPlugin): 382 | pass 383 | 384 | api.register_plugin(plugin) 385 | 386 | result = next(util.publish_iter()) 387 | 388 | assert "progress" in result, result 389 | 390 | 391 | @with_setup(lib.setup, lib.teardown) 392 | def test_publish_iter_increment_progress(): 393 | """Publish iteration increments progress""" 394 | 395 | class pluginA(api.ContextPlugin): 396 | pass 397 | 398 | class pluginB(api.ContextPlugin): 399 | pass 400 | 401 | api.register_plugin(pluginA) 402 | api.register_plugin(pluginB) 403 | 404 | iterator = util.publish_iter() 405 | 406 | pluginA_progress = next(iterator)["progress"] 407 | pluginB_progress = next(iterator)["progress"] 408 | 409 | assert pluginA_progress < pluginB_progress 410 | --------------------------------------------------------------------------------