';
20 | }
21 |
22 | $('.rain.front-row').append(drops);
23 | $('.rain.back-row').append(backDrops);
24 | }
25 |
26 | $('.splat-toggle.toggle').on('click', function() {
27 | $('body').toggleClass('splat-toggle');
28 | $('.splat-toggle.toggle').toggleClass('active');
29 | makeItRain();
30 | });
31 |
32 | $('.back-row-toggle.toggle').on('click', function() {
33 | $('body').toggleClass('back-row-toggle');
34 | $('.back-row-toggle.toggle').toggleClass('active');
35 | makeItRain();
36 | });
37 |
38 | $('.single-toggle.toggle').on('click', function() {
39 | $('body').toggleClass('single-toggle');
40 | $('.single-toggle.toggle').toggleClass('active');
41 | makeItRain();
42 | });
43 |
44 | makeItRain();
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """A cross-platform launcher for film and games projects, built on Rez"""
2 |
3 | import os
4 | from setuptools import setup, find_packages
5 | from allzpark.version import version
6 |
7 | # Git is required for deployment
8 | assert len(version.split(".")) == 3, (
9 | "Could not compute patch version, make sure `git` is\n"
10 | "available and see version.py for details")
11 |
12 | classifiers = [
13 | "Development Status :: 4 - Beta",
14 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
15 | "Intended Audience :: Developers",
16 | "Operating System :: OS Independent",
17 | "Programming Language :: Python",
18 | "Programming Language :: Python :: 2",
19 | "Programming Language :: Python :: 2.7",
20 | "Programming Language :: Python :: 3",
21 | "Programming Language :: Python :: 3.6",
22 | "Programming Language :: Python :: 3.7",
23 | "Topic :: Utilities",
24 | "Topic :: Software Development",
25 | "Topic :: Software Development :: Libraries :: Python Modules",
26 | ]
27 |
28 | # Store version alongside package
29 | dirname = os.path.dirname(__file__)
30 | fname = os.path.join(dirname, "allzpark", "__version__.py")
31 | with open(fname, "w") as f:
32 | f.write("version = \"%s\"\n" % version)
33 |
34 | setup(
35 | name="allzpark",
36 | version=version,
37 | description=__doc__,
38 | keywords="launcher package resolve version software management",
39 | long_description=__doc__,
40 | url="https://github.com/mottosso/allzpark",
41 | author="Marcus Ottosson",
42 | author_email="konstruktion@gmail.com",
43 | license="LGPL",
44 | zip_safe=False,
45 | packages=find_packages(),
46 | package_data={
47 | "allzpark": [
48 | "resources/*.png",
49 | "resources/*.css",
50 | "resources/*.svg",
51 | "resources/fonts/*/*.ttf",
52 | ]
53 | },
54 | entry_points={
55 | "console_scripts": [
56 | "allzpark = allzpark.cli:main",
57 |
58 | # Alias
59 | "azp = allzpark.cli:main",
60 | ]
61 | },
62 | classifiers=classifiers,
63 | install_requires=[
64 | "bleeding-rez>=2.38.2",
65 | "allzparkdemo>=1",
66 |
67 | # Specifically for Python 2..
68 | "PySide; python_version<'3'",
69 |
70 | # ..and likewise for Python 3
71 | "PySide2; python_version>'3'",
72 | ],
73 | python_requires=">2.7, <4",
74 | )
75 |
--------------------------------------------------------------------------------
/docs/pages/quickstart.md:
--------------------------------------------------------------------------------
1 | This page will get you up and running with Allzpark in less than 2 minutes.
2 |
3 |
4 |
5 | ### Quickstart
6 |
7 | The below commands will install Allzpark and its dependencies, including Rez.
8 |
9 | ```bash
10 | python -m pip install allzpark --upgrade
11 | rez bind --quickstart
12 | allzpark --demo --clean
13 | ```
14 |
15 | > Skip the `--clean` flag to preserve preferences, such as window layout, between runs.
16 |
17 |
18 |
19 | #### Troubleshooting
20 |
21 | Everything ok?
22 |
23 | ??? quote "No module named pip"
24 | For this to work, we'll need pip.
25 |
26 | - [Reference](https://pip.pypa.io/en/stable/installing/)
27 |
28 | ```bash
29 | curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
30 | python get-pip.py
31 | ```
32 |
33 | If that didn't work, have a look at to install pip for your platform.
34 |
35 | **Examples**
36 |
37 | - CentOS 7 - `yum install python-pip`
38 | - Ubuntu 18 - `apt install python3-pip`
39 |
40 | ??? quote "Permission denied"
41 | The above command assumes admin/sudo access to your machine which isn't always the case. If so, you can install Allzpark into a virtual environment.
42 |
43 | **Python 3**
44 |
45 | ```bash
46 | $ python -m venv allzpark-venv
47 | $ allzpark-venv\Scripts\activate
48 | (allzpark-venv) $ pip install allzpark
49 | ```
50 |
51 | **Python 2**
52 |
53 | ```bash
54 | $ python -m pip install virtualenv
55 | $ python -m virtualenv allzpark-venv
56 | $ allzpark-venv\Scripts\activate
57 | (allzpark-venv) $ pip install allzpark
58 | ```
59 |
60 | ??? quote "rez not found"
61 | If installation went successfully, but you aren't able to call `rez` then odds are the Python executable path isn't on your `PATH`. On Windows, this directory is typically at `c:\python37\scripts` but may vary depending on how Python was installed, and varies across platforms.
62 |
63 | Following the installation of `rez`, you should have gotten a message about which path was missing from your `PATH`, you can either add this yourself, or use the `virtualenv` method from the above `Permission denied` box.
64 |
65 | **Example message**
66 |
67 | ```powershell
68 | The script allzpark.exe and azp.exe are installed in 'C:\Python37\Scripts' which is not on PATH
69 | Consider adding this directory to PATH
70 | ```
71 |
72 | ??? quote "Something else happened"
73 | Oh no! I'd like to know about what happened, please let me know [here](https://github.com/mottosso/allzpark/issues).
74 |
75 |
76 |
77 | #### Result
78 |
79 | If everything went well, you should now be presented with this!
80 |
81 | 
82 |
83 |
84 |
85 | ### Next Steps
86 |
87 | From here, try launching your favourite application, navigate the interface and make yourself at home. Then have a look at these to learn more.
88 |
89 | > Note that the applications provided are examples and may not work as-is on your system.
90 |
91 | - [Create a new profile](/getting-started)
92 | - [Create a new application](/getting-started/#your-first-application)
93 |
--------------------------------------------------------------------------------
/allzpark/vendor/transitions/extensions/factory.py:
--------------------------------------------------------------------------------
1 | """
2 | transitions.extensions.factory
3 | ------------------------------
4 |
5 | This module contains the definitions of classes which combine the functionality of transitions'
6 | extension modules. These classes can be accessed by names as well as through a static convenience
7 | factory object.
8 | """
9 |
10 | from ..core import Machine
11 |
12 | from .nesting import HierarchicalMachine, NestedTransition, NestedEvent
13 | from .locking import LockedMachine, LockedEvent
14 | from .diagrams import GraphMachine, TransitionGraphSupport, NestedGraph
15 |
16 |
17 | class MachineFactory(object):
18 | """
19 | Convenience factory for machine class retrieval.
20 | """
21 |
22 | # get one of the predefined classes which fulfill the criteria
23 | @staticmethod
24 | def get_predefined(graph=False, nested=False, locked=False):
25 | """ A function to retrieve machine classes by required functionality.
26 | Args:
27 | graph (bool): Whether the returned class should contain graph support.
28 | nested: Whether the returned machine class should support nested states.
29 | locked: Whether the returned class should facilitate locks for threadsafety.
30 |
31 | Returns (class): A machine class with the specified features.
32 | """
33 | return _CLASS_MAP[(graph, nested, locked)]
34 |
35 |
36 | class NestedGraphTransition(TransitionGraphSupport, NestedTransition):
37 | """
38 | A transition type to be used with (subclasses of) `HierarchicalGraphMachine` and
39 | `LockedHierarchicalGraphMachine`.
40 | """
41 | pass
42 |
43 |
44 | class LockedNestedEvent(LockedEvent, NestedEvent):
45 | """
46 | An event type to be used with (subclasses of) `LockedHierarchicalMachine`
47 | and `LockedHierarchicalGraphMachine`.
48 | """
49 | pass
50 |
51 |
52 | class HierarchicalGraphMachine(GraphMachine, HierarchicalMachine):
53 | """
54 | A hierarchical state machine with graph support.
55 | """
56 |
57 | transition_cls = NestedGraphTransition
58 | graph_cls = NestedGraph
59 |
60 |
61 | class LockedHierarchicalMachine(LockedMachine, HierarchicalMachine):
62 | """
63 | A threadsafe hierarchical machine.
64 | """
65 |
66 | event_cls = LockedNestedEvent
67 |
68 |
69 | class LockedGraphMachine(GraphMachine, LockedMachine):
70 | """
71 | A threadsafe machine with graph support.
72 | """
73 | pass
74 |
75 |
76 | class LockedHierarchicalGraphMachine(GraphMachine, LockedMachine, HierarchicalMachine):
77 | """
78 | A threadsafe hiearchical machine with graph support.
79 | """
80 |
81 | transition_cls = NestedGraphTransition
82 | event_cls = LockedNestedEvent
83 | graph_cls = NestedGraph
84 |
85 |
86 | # 3d tuple (graph, nested, locked)
87 | _CLASS_MAP = {
88 | (False, False, False): Machine,
89 | (False, False, True): LockedMachine,
90 | (False, True, False): HierarchicalMachine,
91 | (False, True, True): LockedHierarchicalMachine,
92 | (True, False, False): GraphMachine,
93 | (True, False, True): LockedGraphMachine,
94 | (True, True, False): HierarchicalGraphMachine,
95 | (True, True, True): LockedHierarchicalGraphMachine
96 | }
97 |
--------------------------------------------------------------------------------
/docs/pages/gui.md:
--------------------------------------------------------------------------------
1 | This page is specifically about the Allzpark graphical user interface.
2 |
3 |
4 |
5 | ### Advanced Controls
6 |
7 | In the [Preferences](#preferences) you'll find an option to enable "Advanced Controls". These are designed to separate what is useful to an artist versus a developer.
8 |
9 | 
10 |
11 |
12 |
13 | ### Multiple Application Versions
14 |
15 | Sometimes you need multiple versions of a single application accessible via the same profile. Per default, Allzpark only displays the latest version of the versions available.
16 |
17 | **allzparkconfig.py**
18 |
19 | Here's the default.
20 |
21 | ```py
22 | def applications_from_package(variant):
23 | requirements = variant.requires or []
24 |
25 | apps = list(
26 | str(req)
27 | for req in requirements
28 | if req.weak
29 | )
30 |
31 | return apps
32 | ```
33 |
34 | **allzparkconfig.py**
35 |
36 | As you can see, the default only returns the one request for an application. But Allzpark will display every version you return, here's an example of that.
37 |
38 | ```py
39 | def applications_from_package(variant):
40 | from allzpark import _rezapi as rez
41 |
42 | # May not be defined
43 | requirements = variant.requires or []
44 |
45 | apps = list(
46 | str(req)
47 | for req in requirements
48 | if req.weak
49 | )
50 |
51 | # Strip the "weak" property of the request, else iter_packages
52 | # isn't able to find the requested versions.
53 | apps = [rez.PackageRequest(req.strip("~")) for req in apps]
54 |
55 | # Expand versions into their full range
56 | # E.g. maya-2018|2019 == ["maya-2018", "maya-2019"]
57 | flattened = list()
58 | for request in apps:
59 | flattened += rez.find(
60 | request.name,
61 | range_=request.range,
62 | )
63 |
64 | # Return strings
65 | apps = list(
66 | "%s==%s" % (package.name, package.version)
67 | for package in flattened
68 | )
69 |
70 | return apps
71 | ```
72 |
73 |
74 |
75 | ### Applications from Data
76 |
77 | In addition to the above, you could also specify applications explicitly in your profile data.
78 |
79 | **Alita/package.py**
80 |
81 | ```py
82 | name = "alita"
83 | version = "1.0"
84 | _data = {
85 | "apps": ["maya-2018", "vs-2019", "zbrush", "mudbox"]
86 | }
87 | ```
88 |
89 | **allzparkconfig.py**
90 |
91 | ```py
92 | def applications_from_package(package):
93 | try:
94 | return package._data["apps"]
95 |
96 | except (AttributeError, KeyError):
97 | # If there isn't any data, just do what you normally do
98 | from allzpark.allzparkconfig import _applications_from_package
99 |
100 | # Every variable and function from allzparkconfig has this hidden
101 | # alternative reference, with a "_" prefix.
102 | return _applications_from_package(package)
103 | ```
104 |
105 | And as a side-note, here you can also return multiple versions of a given application.
106 |
107 | ```py
108 | name = "alita"
109 | version = "1.0"
110 | _data = {
111 | "apps": [
112 | "maya-2018",
113 | "maya-2019",
114 | "maya-2020",
115 | ]
116 | }
117 | ```
118 |
--------------------------------------------------------------------------------
/docs/pages/getting-advanced.md:
--------------------------------------------------------------------------------
1 | This page carries on from a successful 👈 [Getting Started](/getting-started) into making it relevant for use in VFX and games production.
2 |
3 |
4 |
5 | ### Getting Advanced
6 |
7 | So you've taken Allzpark for a spin and found that it is wonderful, good for you! Here's how you can take it to the next level, by learning about how Allzpark and Rez works in a shared environment.
8 |
9 |
10 |
11 | ### Performance pt. 1
12 |
13 | - [ ] Memcached and why you need it
14 | - [ ] Memcached configuration recommendations
15 |
16 | !!! note "Work in progress"
17 | [Let me know](https://github.com/mottosso/allzpark/issues) if you would like me to flesh out this section, or if you are already familiar with Rez and `memcached`, help out by [contributing to the project](/contributing).
18 |
19 |
20 |
21 | ### Performance pt. 2
22 |
23 | - [ ] Packages are folders on your filesystem, your filesystem is slow
24 | - [ ] Localisation and remote labour
25 |
26 | !!! note "Work in progress"
27 | [Let me know](https://github.com/mottosso/allzpark/issues) if you would like me to flesh out this section.
28 |
29 |
30 |
31 | ### Releasing Packages
32 |
33 | - [ ] Shared package repository
34 | - [ ] Read/write permissions
35 | - [ ] Automatic relase via GitLab CI
36 |
37 | !!! note "Work in progress"
38 | [Let me know](https://github.com/mottosso/allzpark/issues) if you would like me to flesh out this section, or if you are already familiar with Rez and `rez release` or `rez build --prefix`, help out by [contributing to the project](/contributing).
39 |
40 |
41 |
42 | ### Rezifying Allzpark
43 |
44 | Learn how to close the loop and make Allzpark into just another Rez package.
45 |
46 | !!! note "Work in progress"
47 | [Let me know](https://github.com/mottosso/allzpark/issues) if you would like me to flesh out this section, or if you are already familiar with Rez, help out by [contributing to the project](/contributing).
48 |
49 |
50 |
51 | ### Rezifying Rez
52 |
53 | Turn the loop into a spiral by making Rez another shared package. Key point here being, how can you keep everyone on the same version of Rez, if Rez is the thing keeping everyone at the same version of software?
54 |
55 | - [ ] Rez is just another Python package, can reside anywhere
56 | - [ ] So can Python
57 | - [ ] Only thing to keep in mind is performance; on Windows more of an issue, on Linux not so much
58 |
59 | !!! note "Work in progress"
60 | [Let me know](https://github.com/mottosso/allzpark/issues) if you would like me to flesh out this section, or if you are already familiar with Rez, help out by [contributing to the project](/contributing).
61 |
62 |
63 |
64 | ### Beta Package
65 |
66 | Learn about how to release packages to a smaller test audience without disrupting normal operation, with the `.beta` suffix.
67 |
68 | !!! note "Work in progress"
69 | [Let me know](https://github.com/mottosso/allzpark/issues) if you would like me to flesh out this section, or if you are already familiar with Rez, help out by [contributing to the project](/contributing).
70 |
71 |
72 |
73 | ### Package Encapsulation
74 |
75 | Learn about the importance and utility of keeping packages self-contained and not reference anything outside of its own root directory.
76 |
77 | !!! note "Work in progress"
78 | [Let me know](https://github.com/mottosso/allzpark/issues) if you would like me to flesh out this section, or if you are already familiar with Rez, help out by [contributing to the project](/contributing).
79 |
80 |
--------------------------------------------------------------------------------
/allzpark/_rezapi.py:
--------------------------------------------------------------------------------
1 | # API wrapper for Rez
2 |
3 | from rez.resolved_context import ResolvedContext as env
4 | from rez.packages_ import iter_packages as find
5 | from rez.package_copy import copy_package
6 | from rez.package_filter import Rule, PackageFilterList
7 | from rez.package_repository import package_repository_manager
8 | from rez.packages_ import Package
9 | from rez.utils.formatting import PackageRequest
10 | from rez.system import system
11 | from rez.config import config
12 | from rez.util import which
13 | from rez import __version__ as version
14 | from rez.exceptions import (
15 | PackageFamilyNotFoundError,
16 | RexUndefinedVariableError,
17 | ResolvedContextError,
18 | RexError,
19 | PackageCommandError,
20 | PackageRequestError,
21 | PackageNotFoundError,
22 | RezError,
23 | )
24 | from rez.utils.graph_utils import save_graph
25 |
26 |
27 | def clear_caches():
28 | for path in config.packages_path:
29 | repo = package_repository_manager.get_repository(path)
30 | repo.clear_caches()
31 |
32 |
33 | def find_one(name, range_=None, paths=None, package_filter=None):
34 | """
35 | Find next package version
36 |
37 | Args:
38 | name (str): Name of the rez package
39 | range_ (VersionRange or str, optional): Limits versions to range
40 | paths (list of str, optional): Paths to search for packages
41 | package_filter (PackageFilter, optional): Limits versions to those
42 | that match package filter
43 |
44 | Returns:
45 | rez.packages_.Package
46 | """
47 | if package_filter:
48 | return next(package_filter.iter_packages(name, range_, paths))
49 | else:
50 | return next(find(name, range_, paths))
51 |
52 |
53 | def find_latest(name, range_=None, paths=None, package_filter=None):
54 | """
55 | Find latest package version
56 |
57 | Args:
58 | name (str): Name of the rez package
59 | range_ (VersionRange or str, optional): Limits versions to range
60 | paths (list of str, optional): Paths to search for packages
61 | package_filter (PackageFilter, optional): Limits versions to those
62 | that match package filter
63 |
64 | Returns:
65 | rez.packages_.Package
66 | """
67 | if package_filter:
68 | it = package_filter.iter_packages(name, range_, paths)
69 | else:
70 | it = find(name, range_, paths)
71 |
72 | it = sorted(it, key=lambda pkg: pkg.version)
73 |
74 | try:
75 | return list(it)[-1]
76 | except IndexError:
77 | raise PackageNotFoundError(
78 | "package family not found: %s" % name
79 | )
80 |
81 |
82 | try:
83 | from rez import __project__ as project
84 | except ImportError:
85 | # nerdvegas/rez
86 | project = "rez"
87 |
88 |
89 | __all__ = [
90 | "env",
91 | "find",
92 | "find_one",
93 | "find_latest",
94 | "config",
95 | "version",
96 | "project",
97 | "copy_package",
98 | "package_repository_manager",
99 | "system",
100 |
101 | # Classes
102 | "Package",
103 | "PackageRequest",
104 |
105 | # Exceptions
106 | "PackageFamilyNotFoundError",
107 | "ResolvedContextError",
108 | "RexUndefinedVariableError",
109 | "RexError",
110 | "PackageCommandError",
111 | "PackageNotFoundError",
112 | "PackageRequestError",
113 | "RezError",
114 |
115 | # Filters
116 | "Rule",
117 | "PackageFilterList",
118 |
119 | # Extras
120 | "which",
121 | "save_graph",
122 | "clear_caches",
123 | ]
124 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Application launcher and environment management for 21st century games and digital post-production, built with bleeding-rez and Qt.py
21 |
22 |
23 |
24 | #### News
25 |
26 | | Date | Release | Notes
27 | |:-------------|:--------|:----------
28 | | October 2020 | [1.3](https://github.com/mottosso/allzpark/releases/tag/1.3.0) | Dedicated profiles panel
29 | | August 2019 | 1.2 | First official release
30 |
31 | - See [all releases](https://github.com/mottosso/allzpark/releases)
32 |
33 |
34 |
35 | ### What is it?
36 |
37 | It's an application launcher, for when you need control over what software and which versions of software belong to a given project. It builds on the self-hosted package manager and environment management framework [bleeding-rez](https://github.com/mottosso/bleeding-rez), providing both a visual and textual interface for launching software in a reproducible way.
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | ### Usage
49 |
50 | Allzpark runs on Windows, Linux and MacOS, using Python 2 or 3 and any binding of Qt, and is available via `pip`.
51 |
52 | ```bash
53 | pip install allzpark
54 | ```
55 |
56 | See [Quickstart](https://allzpark.com/quickstart) for more details and tutorials.
57 |
58 | **Some Table of Contents**
59 |
60 | - [Landing Page](https://allzpark.com)
61 | - [Getting Started](https://allzpark.com/getting-started)
62 | - [Getting Advanced](https://allzpark.com/getting-advanced)
63 | - [Getting Rez'd](https://allzpark.com/rez)
64 | - [Contributing](https://allzpark.com/contributing)
65 | - [...](https://allzpark.com)
66 |
67 |
68 |
69 | ### Updating the Docs
70 |
71 | I'd like for this to happen during CI, but till then there's a `deploy.ps1` in the `docs/` directory.
72 |
73 | ```bash
74 | cd allzpark\docs
75 | . deploy.ps1
76 | ```
77 |
78 | This will build the docs and deploy it onto the `gh-pages` branch, which is reflected live after about 1 min.
79 |
--------------------------------------------------------------------------------
/tests/test_docks.py:
--------------------------------------------------------------------------------
1 |
2 | from tests import util
3 |
4 |
5 | class TestDocks(util.TestBase):
6 |
7 | def test_feature_blocked_on_failed_app(self):
8 | """Test feature blocked if application is broken"""
9 | util.memory_repository({
10 | "foo": {
11 | "1.0.0": {
12 | "name": "foo",
13 | "version": "1.0.0",
14 | "requires": [
15 | "~app_A", # missing package (broken app)
16 | "~app_B",
17 | ],
18 | },
19 | },
20 | "app_B": {"1": {"name": "app_B", "version": "1"}},
21 | })
22 | self.ctrl_reset(["foo"])
23 |
24 | self.set_preference("showAdvancedControls", True)
25 |
26 | context_a = self.ctrl.state["rezContexts"]["app_A==None"]
27 | context_b = self.ctrl.state["rezContexts"]["app_B==1"]
28 |
29 | self.assertFalse(context_a.success)
30 | self.assertTrue(context_b.success)
31 |
32 | for app, state in {"app_A==None": False, "app_B==1": True}.items():
33 | self.select_application(app)
34 |
35 | dock = self.show_dock("environment", on_page="diagnose")
36 | self.assertEqual(dock._widgets["compute"].isEnabled(), state)
37 |
38 | dock = self.show_dock("context", on_page="code")
39 | self.assertEqual(dock._widgets["printCode"].isEnabled(), state)
40 |
41 | dock = self.show_dock("app")
42 | self.assertEqual(dock._widgets["launchBtn"].isEnabled(), state)
43 |
44 | def test_version_editable_on_show_all_versions(self):
45 | """Test version is editable when show all version enabled"""
46 | self._test_version_editable(show_all_version=True)
47 |
48 | def test_version_editable_on_not_show_all_versions(self):
49 | """Test version is not editable when show all version disabled"""
50 | self._test_version_editable(show_all_version=False)
51 |
52 | def _test_version_editable(self, show_all_version):
53 | util.memory_repository({
54 | "foo": {
55 | "1": {"name": "foo", "version": "1",
56 | "requires": ["~app_A", "~app_B"]},
57 | "2": {"name": "foo", "version": "2",
58 | "requires": ["~app_A", "~app_B"]},
59 | },
60 | "app_A": {"1": {"name": "app_A", "version": "1"}},
61 | "app_B": {"1": {"name": "app_B", "version": "1",
62 | "requires": ["bar"]}},
63 | "bar": {"1": {"name": "bar", "version": "1"},
64 | "2": {"name": "bar", "version": "2"}}
65 | })
66 | self.ctrl_reset(["foo"])
67 |
68 | self.set_preference("showAdvancedControls", True)
69 | self.set_preference("showAllVersions", show_all_version)
70 | self.wait(200) # wait for reset
71 |
72 | self.select_application("app_B==1")
73 |
74 | dock = self.show_dock("packages")
75 | view = dock._widgets["view"]
76 | proxy = view.model()
77 | model = proxy.sourceModel()
78 |
79 | for pkg, state in {"foo": False, # profile can't change version here
80 | "bar": show_all_version,
81 | "app_B": False}.items():
82 | index = model.findIndex(pkg)
83 | index = proxy.mapFromSource(index)
84 |
85 | rect = view.visualRect(index)
86 | position = rect.center()
87 | with util.patch_cursor_pos(view.mapToGlobal(position)):
88 | dock.on_right_click(position)
89 | menu = self.get_menu(dock)
90 | edit_action = next((a for a in menu.actions()
91 | if a.text() == "Edit"), None)
92 | if edit_action is None:
93 | self.fail("No version edit action.")
94 |
95 | self.assertEqual(
96 | edit_action.isEnabled(), state,
97 | "Package '%s' version edit state is incorrect." % pkg
98 | )
99 |
100 | self.wait(200)
101 | menu.close()
102 |
--------------------------------------------------------------------------------
/docs/pages/windows.md:
--------------------------------------------------------------------------------
1 | Both Allzpark and Rez are cross-platform, but each platform has a few gotchas to keep in mind. Here's a quick primer on how to make the most out of Allzpark and Rez on the Windows operating system.
2 |
3 |
4 |
5 | ## Long File Paths
6 |
7 | Windows has a max path length of 260 characters, which can become an issue for packages on a long repository path and multiple variants.
8 |
9 |
10 |
11 | ### Problem
12 |
13 | ```bash
14 | # Repository root
15 | \\mylongstudioaddress.local\main\common\utilities\packages\internal
16 |
17 | # Package
18 | \maya_essentials\1.42.5beta\platform-windows\arch-AMD64\os-windows-10.0.1803
19 |
20 | # Payload
21 | \python\maya_essentials\utilities\__init__.py
22 | ```
23 |
24 | **188 characters**
25 |
26 | That's a relatively common path to a Python package, packaged with Rez, and we're already close to the 260 character limit. Now take backslashes into account, and that Python and friends escape those prior to using them. There are 16 backslashes in there, which adds another 16 characters.
27 |
28 | ```bash
29 | # Before
30 | \long\path
31 |
32 | # After
33 | \\long\\path
34 | ```
35 |
36 | **204 characters**
37 |
38 | We still haven't changed the path, and yet the length has increased. Now take into account some libraries taking extra precautions and escapes even estaped backslashes.
39 |
40 | ```bash
41 | # Before
42 | \\long\\path
43 |
44 | # After
45 | \\\\long\\\\path
46 | ```
47 |
48 | That adds yet another 32 characters.
49 |
50 | **236 characters**
51 |
52 | And again, we haven't changed our path, and yet this is what some tools will be working with, leaving you with very little room.
53 |
54 |
55 |
56 | ### Solution
57 |
58 | You've got at least three options here.
59 |
60 | 1. Patch your paths
61 | 1. Patch Rez
62 | 2. Patch Windows
63 |
64 | **Patch Paths**
65 |
66 | The most straightforward, but likely difficult, thing to do is to avoid long paths altogether.
67 |
68 | - Use a short hostname
69 | - Use a short repository path
70 | - Abbreviate Python libraries
71 | - Don't use Python packages from PyPI with long names
72 |
73 | But a lot of this is not practical, and merely postpones the issue.
74 |
75 | **Patch Rez**
76 |
77 | I've investigated what it would take to make changes to Rez that facilitate longer paths, and found that there is a prefix you can use for paths that will "force" Windows to interpret paths longer than 260 characters.
78 |
79 | ```bash
80 | # Before
81 | c:\long\path.exe
82 |
83 | # After
84 | \\?\c:\long\path.exe
85 | ```
86 |
87 | Since paths are entirely managed by Rez, it wouldn't be unreasonable to wrap any path creation call to prefix the results with `\\?\` if the user was running Windows. But I couldn't find a single-point-of-entry for these, as paths were generated all over the place. Rightly so; it would be borderline overengineering to wrap all calls to e.g. `os.path.join` or `os.getcwd` into a "prefixer" just for this occasion. It would however have helped in this particular case.
88 |
89 | Furthermore, this would only really apply to Windows 10 and above, since from what I gather this (poorly documented) feature is only available there; possibly related to this next feature.
90 |
91 | **Patch Windows**
92 |
93 | You wouldn't think this is an option, but it just might be.
94 |
95 | This technically doesn't count as patching Windows, but because we're changing a fundamental component of the OS - something each applications has till now taken for granted - it may cause all sorts of havok for applications that depend on the 260 character limit.
96 |
97 | > Relevant comic https://xkcd.com/1172/
98 |
99 | Since June 20th 2017, users of Windows 10 1607 have had the ability to enable support for "long paths".
100 |
101 | ```powershell
102 | # From an administrator PowerShell session
103 | Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem -Name LongPathsEnabled -Value 1 -Type DWord
104 | ```
105 |
106 | This would effectively prepend `\\?\` to every path "under the hood", solving the issue. But at what cost?
107 |
108 | Let the community know if you encounter any issues by making [an issue](https://github.com/mottosso/allzpark/issues/new).
109 |
110 |
111 |
112 | ## Process Tree
113 |
114 | Virtualenv is one way of using Rez on Windows, and if you do then the `rez.exe` executable is generated during `pip install` and works by spawning a `python.exe` process, also generated by `pip`, which in turn calls on your system `python.exe`. Here's what spawning your own Python session from within a Rez context looks like.
115 |
116 | 
117 |
118 |
119 |
120 | ## Maya and Quicktime
121 |
122 | Typically, playblasting to `.mp4` or `.mov` with Maya requires a recent install of Quicktime on the local machine. Let's have a look at how to approach this with Rez.
123 |
124 | > How *does* one approach this with Rez? Submit [a PR](https://github.com/mottosso/allzpark) today!
125 |
126 |
127 |
--------------------------------------------------------------------------------
/docs/theme/landing.css:
--------------------------------------------------------------------------------
1 | /* Styling specifically for the landing page */
2 |
3 | .vboxlayout {
4 | display: flex;
5 | flex-direction: column;
6 | }
7 |
8 | .hboxlayout {
9 | display: flex;
10 | flex-direction: row;
11 | justify-content: space-between;
12 | align-items: flex-start;
13 | }
14 |
15 | #pypiBadge {
16 | padding-top: 5px;
17 | }
18 |
19 | .vboxlayout p {
20 | margin-top: 0;
21 | font-size: 1.3em;
22 | font-weight: 300;
23 | }
24 |
25 | .row {
26 | flex-direction: row;
27 | }
28 |
29 | .column {
30 | flex-direction: column;
31 | }
32 |
33 | .column-reverse {
34 | flex-direction: column-reverse;
35 | }
36 |
37 | .row-reverse {
38 | flex-direction: row-reverse;
39 | }
40 |
41 | .justify-left {
42 | justify-content: flex-start;
43 | }
44 |
45 | .justify-right {
46 | justify-content: flex-end;
47 | }
48 |
49 | .justify-center {
50 | justify-content: center;
51 | }
52 |
53 | .flex-item {
54 | flex: 1;
55 | }
56 |
57 | .align-center {
58 | align-items: center;
59 | }
60 |
61 | .space {
62 | min-height: 100px;
63 | min-width: 100px;
64 | }
65 |
66 | .smallspace {
67 | min-height: 30px;
68 | min-width: 30px;
69 | }
70 |
71 | .auto-margin {
72 | margin: auto auto;
73 | }
74 |
75 | .stretch {
76 | flex-grow: 1;
77 | }
78 |
79 | .poster {
80 | box-shadow: 0 0 1.2rem rgba(0,0,0,.2), 0 0.2rem 0.4rem rgba(0,0,0,.2);
81 | border: 1px solid #777;
82 | }
83 |
84 | .md-typeset h2 {
85 | font-family: "Peinture Fraiche";
86 | font-size: 4.0em;
87 | color: #222;
88 | margin-top: 0;
89 | line-height: 1em;
90 | }
91 |
92 | .landing {
93 | display: none;
94 | }
95 |
96 | #title {
97 | font-family: "Peinture Fraiche";
98 | font-size: 6.0em;
99 | color: #222;
100 | margin-bottom: 0;
101 | }
102 |
103 | #landing {
104 | align-items: center;
105 | }
106 |
107 | #description {
108 | font-size: 1em;
109 | max-width: 420px;
110 | padding-right: 50px;
111 | }
112 |
113 | #conclusion {
114 | text-align: center;
115 | }
116 |
117 | [data-md-color-primary] .md-typeset a.button {
118 | font-family: "Roboto", Corbel, Avenir, "Lucida Grande", "Lucida Sans", sans-serif;
119 | max-width: 400px;
120 | min-width: 120px;
121 | -webkit-appearance: none;
122 | -moz-appearance: none;
123 | display: block;
124 | border-radius: 3px;
125 | margin: 5px;
126 | margin-left: 0;
127 | margin-right: 10px;
128 | padding: 15px;
129 | text-align: center;
130 | text-decoration: none;
131 | font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
132 | color: #eee;
133 | transition: background 1s cubic-bezier(0.1, 0.7, 0.1, 1);
134 | }
135 |
136 | /* Little pen icon, to edit the page*/
137 | .md-content__icon {
138 | display: none;
139 | }
140 |
141 | .button:active {
142 | position: relative;
143 | top: 6px;
144 | }
145 |
146 | .green {
147 | background: #5aa926;
148 | box-shadow: 0px 6px #418624;
149 | }
150 |
151 | .green:active {
152 | background: #79c15c;
153 | box-shadow: 0px 0px #79c15c;
154 | }
155 |
156 | .green:hover {
157 | background: #79c15c;
158 | }
159 |
160 | .blue {
161 | background: #3498db;
162 | box-shadow: 0px 6px #2980b9;
163 | }
164 |
165 | .blue:active {
166 | background: #2980b9;
167 | box-shadow: 0px 0px #2980b9;
168 | }
169 |
170 | .blue:hover {
171 | background: #4BA4DE;
172 | }
173 |
174 | .red {
175 | box-shadow: 0px 6px #c0392b;
176 | background: #e74c3c;
177 | }
178 |
179 | .red:active {
180 | box-shadow: 0px 0px #c0392b;
181 | background: #c0392b;
182 | }
183 |
184 | .red:hover {
185 | background: #E55E50;
186 | position: relative;
187 | }
188 |
189 | .md-typeset a {
190 | color: #0070e6;
191 | font-weight: 400;
192 | }
193 |
194 | .md-typeset .codehilite, .md-typeset .highlight {
195 | min-width: 50%;
196 | }
197 |
198 | .codehilite code, .md-typeset .codehilite pre, .md-typeset .highlight code, .md-typeset .highlight pre {
199 | font-family: "Source Code Pro";
200 | font-size: 1.11em;
201 | }
202 |
203 | /* Smaller width */
204 | @media only screen and (max-width: 1219px) {
205 | .landing {
206 | display: inherit;
207 | }
208 | }
209 |
210 | /* Mobile */
211 | @media only screen and (max-width: 60em) {
212 | .hboxlayout {
213 | flex-direction: column;
214 | justify-content: center;
215 | align-content: start;
216 | align-items: center;
217 | }
218 |
219 | .vboxlayout {
220 | align-items: center;
221 | }
222 |
223 | .vboxlayout p {
224 | width: 75%;
225 | }
226 |
227 | .md-typeset img {
228 | margin: 0 auto;
229 | }
230 |
231 | [data-md-color-primary] .md-typeset a.button {
232 | margin: 5px auto;
233 | }
234 |
235 | #description {
236 | padding: 0;
237 | width: inherit;
238 | }
239 |
240 | .container {
241 | margin: 0px auto;
242 | padding-top: 20px;
243 | text-align: center;
244 | }
245 |
246 | h2 {
247 | padding-top: 100px;
248 | text-align: center;
249 | }
250 |
251 | p {
252 | text-align: center;
253 | }
254 |
255 | .space {
256 | min-height: 0px;
257 | }
258 | }
--------------------------------------------------------------------------------
/tests/test_profiles.py:
--------------------------------------------------------------------------------
1 |
2 | from unittest import mock
3 | from tests import util
4 |
5 |
6 | class TestProfiles(util.TestBase):
7 |
8 | def test_reset(self):
9 | """Test session reset
10 | """
11 | util.memory_repository({
12 | "foo": {
13 | "1.0.0": {
14 | "name": "foo",
15 | "version": "1.0.0",
16 | }
17 | },
18 | "bar": {
19 | "1.0.0": {
20 | "name": "bar",
21 | "version": "1.0.0",
22 | }
23 | }
24 | })
25 | with self.wait_signal(self.ctrl.resetted):
26 | self.ctrl.reset(["foo", "bar"])
27 | self.wait(timeout=200)
28 | self.assertEqual(self.ctrl.state.state, "noapps")
29 |
30 | # last profile will be selected by default
31 | self.assertEqual("bar", self.ctrl.state["profileName"])
32 | self.assertEqual(["foo", "bar"], list(self.ctrl.state["rezProfiles"]))
33 |
34 | def test_select_profile_with_out_apps(self):
35 | """Test selecting profile that has no apps
36 | """
37 | util.memory_repository({
38 | "foo": {
39 | "1.0.0": {
40 | "name": "foo",
41 | "version": "1.0.0",
42 | }
43 | },
44 | "bar": {
45 | "1.0.0": {
46 | "name": "bar",
47 | "version": "1.0.0",
48 | }
49 | }
50 | })
51 | with self.wait_signal(self.ctrl.resetted):
52 | self.ctrl.reset(["foo", "bar"])
53 | self.wait(timeout=200)
54 | self.assertEqual(self.ctrl.state.state, "noapps")
55 |
56 | with self.wait_signal(self.ctrl.state_changed, "noapps"):
57 | self.ctrl.select_profile("foo")
58 | # wait enter 'noapps' state
59 |
60 | self.assertEqual("foo", self.ctrl.state["profileName"])
61 |
62 | def test_profile_list_apps(self):
63 | """Test listing apps from profile
64 | """
65 | util.memory_repository({
66 | "foo": {
67 | "1.0.0": {
68 | "name": "foo",
69 | "version": "1.0.0",
70 | "requires": [
71 | "lib_foo",
72 | "~app_A",
73 | "~app_B",
74 | ],
75 | }
76 | },
77 | "app_A": {
78 | "1.0.0": {
79 | "name": "app_A",
80 | "version": "1.0.0",
81 | }
82 | },
83 | "app_B": {
84 | "1.0.0": {
85 | "name": "app_B",
86 | "version": "1.0.0",
87 | }
88 | },
89 | "lib_foo": {
90 | "1.0.0": {
91 | "name": "lib_foo",
92 | "version": "1.0.0",
93 | }
94 | },
95 | })
96 | self.ctrl_reset(["foo"])
97 |
98 | with self.wait_signal(self.ctrl.state_changed, "ready"):
99 | self.ctrl.select_profile("foo")
100 |
101 | self.assertEqual(
102 | [
103 | "app_A==1.0.0",
104 | "app_B==1.0.0",
105 | ],
106 | list(self.ctrl.state["rezApps"].keys())
107 | )
108 |
109 | def test_profile_listing_without_root_err(self):
110 | """Listing profile without root will raise AssertionError"""
111 | self.assertRaises(AssertionError, self.ctrl.reset)
112 | self.assertRaises(AssertionError, self.ctrl.list_profiles)
113 |
114 | def test_profile_listing_callable_root_err(self):
115 | """Listing profile with bad callable will prompt error message"""
116 | import traceback
117 | import logging
118 | from allzpark import control
119 |
120 | traceback.print_exc = mock.MagicMock(name="traceback.print_exc")
121 | self.ctrl.error = mock.MagicMock(name="Controller.error")
122 |
123 | def bad_root():
124 | raise Exception("This should be caught.")
125 | self.ctrl.list_profiles(bad_root)
126 |
127 | # ctrl.error must be called in all cases
128 | self.ctrl.error.assert_called_once()
129 | # traceback.print_exc should be called if logging level is set
130 | # lower than INFO, e.g. DEBUG or NOTSET
131 | if control.log.level < logging.INFO:
132 | traceback.print_exc.assert_called_once()
133 |
134 | def test_profile_listing_invalid_type_root_err(self):
135 | """Listing profile with invalid input type will raise TypeError"""
136 | self.assertRaises(TypeError, self.ctrl.list_profiles, {"foo"})
137 |
138 | def test_profile_listing_filter_out_empty_names(self):
139 | """Listing profile with empty names will be filtered"""
140 | expected = ["foo", "bar"]
141 | profiles = self.ctrl.list_profiles(expected + [None, ""])
142 | self.assertEqual(profiles, expected)
143 |
--------------------------------------------------------------------------------
/tests/util.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | import time
4 | import unittest
5 | import contextlib
6 |
7 |
8 | MEMORY_LOCATION = "memory@any"
9 |
10 |
11 | def memory_repository(packages):
12 | from rezplugins.package_repository import memory
13 | from allzpark import _rezapi as rez
14 |
15 | class MemoryVariantRes(memory.MemoryVariantResource):
16 | def _root(self): # implement `root` to work with localz
17 | return MEMORY_LOCATION
18 |
19 | manager = rez.package_repository_manager
20 | repository = manager.get_repository(MEMORY_LOCATION)
21 | repository.pool.resource_classes[MemoryVariantRes.key] = MemoryVariantRes
22 | repository.data = packages
23 |
24 |
25 | class TestBase(unittest.TestCase):
26 |
27 | def setUp(self):
28 | from allzpark import cli
29 |
30 | os.environ["ALLZPARK_PREFERENCES_NAME"] = "preferences_test"
31 | os.environ["REZ_PACKAGES_PATH"] = MEMORY_LOCATION
32 |
33 | app, ctrl = cli.initialize(clean=True, verbose=3)
34 | window = cli.launch(ctrl)
35 |
36 | size = window.size()
37 | window.resize(size.width() + 80, size.height() + 80)
38 |
39 | self.app = app
40 | self.ctrl = ctrl
41 | self.window = window
42 | self.patched_allzparkconfig = dict()
43 |
44 | self.wait(timeout=50)
45 |
46 | def tearDown(self):
47 | self.wait(timeout=500)
48 | self.window.close()
49 | self.ctrl.deleteLater()
50 | self.window.deleteLater()
51 | self._restore_allzparkconfig()
52 | time.sleep(0.1)
53 |
54 | def _restore_allzparkconfig(self):
55 | from allzpark import allzparkconfig
56 |
57 | for name, value in self.patched_allzparkconfig.items():
58 | setattr(allzparkconfig, name, value)
59 |
60 | self.patched_allzparkconfig.clear()
61 |
62 | def patch_allzparkconfig(self, name, value):
63 | from allzpark import allzparkconfig
64 |
65 | if name not in self.patched_allzparkconfig:
66 | original = getattr(allzparkconfig, name)
67 | self.patched_allzparkconfig[name] = original
68 |
69 | setattr(allzparkconfig, name, value)
70 |
71 | def set_preference(self, name, value):
72 | preferences = self.window._docks["preferences"]
73 | arg = next((opt for opt in preferences.options
74 | if opt["name"] == name), None)
75 | if not arg:
76 | self.fail("Preference doesn't have this setting: %s" % name)
77 |
78 | try:
79 | arg.write(value)
80 | except Exception as e:
81 | self.fail("Preference '%s' set failed: %s" % (name, str(e)))
82 |
83 | def show_dock(self, name, on_page=None):
84 | dock = self.window._docks[name]
85 | dock.toggle.setChecked(True)
86 | dock.toggle.clicked.emit()
87 | self.wait(timeout=50)
88 |
89 | if on_page is not None:
90 | tabs = dock._panels["central"]
91 | page = dock._pages[on_page]
92 | index = tabs.indexOf(page)
93 | tabs.tabBar().setCurrentIndex(index)
94 |
95 | return dock
96 |
97 | def ctrl_reset(self, profiles):
98 | with self.wait_signal(self.ctrl.resetted):
99 | self.ctrl.reset(profiles)
100 | self.wait(timeout=200)
101 | self.assertEqual(self.ctrl.state.state, "ready")
102 |
103 | def select_application(self, app_request):
104 | apps = self.window._widgets["apps"]
105 | proxy = apps.model()
106 | model = proxy.sourceModel()
107 | index = model.findIndex(app_request)
108 | index = proxy.mapFromSource(index)
109 |
110 | sel_model = apps.selectionModel()
111 | sel_model.select(index, sel_model.ClearAndSelect | sel_model.Rows)
112 | self.wait(50)
113 |
114 | def wait(self, timeout=1000):
115 | from allzpark.vendor.Qt import QtCore
116 |
117 | loop = QtCore.QEventLoop(self.window)
118 | timer = QtCore.QTimer(self.window)
119 |
120 | def on_timeout():
121 | timer.stop()
122 | loop.quit()
123 |
124 | timer.timeout.connect(on_timeout)
125 | timer.start(timeout)
126 | loop.exec_()
127 |
128 | @contextlib.contextmanager
129 | def wait_signal(self, signal, on_value=None, timeout=1000):
130 | from allzpark.vendor.Qt import QtCore
131 |
132 | loop = QtCore.QEventLoop(self.window)
133 | timer = QtCore.QTimer(self.window)
134 | state = {"received": False}
135 |
136 | if on_value is None:
137 | def trigger(*args):
138 | state["received"] = True
139 | timer.stop()
140 | loop.quit()
141 | else:
142 | def trigger(value):
143 | if value == on_value:
144 | state["received"] = True
145 | timer.stop()
146 | loop.quit()
147 |
148 | def on_timeout():
149 | timer.stop()
150 | loop.quit()
151 | self.fail("Signal waiting timeout.")
152 |
153 | signal.connect(trigger)
154 | timer.timeout.connect(on_timeout)
155 |
156 | try:
157 | yield
158 | finally:
159 | if not state["received"]:
160 | timer.start(timeout)
161 | loop.exec_()
162 |
163 | def get_menu(self, widget):
164 | from allzpark.vendor.Qt import QtWidgets
165 | menus = widget.findChildren(QtWidgets.QMenu, "")
166 | menu = next((m for m in menus if m.isVisible()), None)
167 | if menu:
168 | return menu
169 | else:
170 | self.fail("This widget doesn't have menu.")
171 |
172 |
173 | @contextlib.contextmanager
174 | def patch_cursor_pos(point):
175 | from allzpark.vendor.Qt import QtGui
176 |
177 | origin_pos = getattr(QtGui.QCursor, "pos")
178 | setattr(QtGui.QCursor, "pos", lambda: point)
179 | try:
180 | yield
181 | finally:
182 | setattr(QtGui.QCursor, "pos", origin_pos)
183 |
--------------------------------------------------------------------------------
/allzpark/allzparkconfig.py:
--------------------------------------------------------------------------------
1 | """The Allzpark configuration file
2 |
3 | Copy this onto your local drive and make modifications.
4 | Anything not specified in your copy is inherited from here.
5 |
6 | ALLZPARK_CONFIG_FILE=/path/to/allzparkconfig.py
7 |
8 | """
9 |
10 | import os as __os
11 |
12 |
13 | # Load this profile on startup.
14 | # Defaults to the first available from `profiles`
15 | startup_profile = "" # (optional)
16 |
17 | # Pre-select this application in the list of applications,
18 | # if it exists in the startup profile.
19 | startup_application = "" # (optional)
20 |
21 | # Default filter, editable via the Preferences page
22 | exclude_filter = "*.beta"
23 |
24 | # Where to go when clicking the logo
25 | help_url = "https://allzpark.com"
26 |
27 |
28 | def profiles():
29 | """Return list of profiles
30 |
31 | This function is called asynchronously, and is suitable
32 | for making complex filesystem or database queries.
33 | Can also be a variable of type tuple or list
34 |
35 | """
36 |
37 | try:
38 | return __os.listdir(__os.path.expanduser("~/profiles"))
39 | except IOError:
40 | return []
41 |
42 |
43 | def applications():
44 | """Return list of applications
45 |
46 | Applications are typically provided by the profile,
47 | this function is called when "Show all apps" is enabled.
48 |
49 | """
50 |
51 | return []
52 |
53 |
54 | def applications_from_package(variant):
55 | """Return applications relative `variant`
56 |
57 | Returns:
58 | list of strings: E.g. ['appA', 'appB==2019']
59 |
60 | """
61 |
62 | from . import _rezapi as rez
63 |
64 | # May not be defined
65 | requirements = variant.requires or []
66 |
67 | apps = list(
68 | str(req)
69 | for req in requirements
70 | if req.weak
71 | )
72 |
73 | return apps
74 |
75 |
76 | def metadata_from_package(variant):
77 | """Return metadata relative `variant`
78 |
79 | Blocking call, during change of profile.
80 |
81 | IMPORTANT: this function must return at least the
82 | members part of the original function, else the program
83 | will not function. Very few safeguards are put in place
84 | in favour of performance.
85 |
86 | Arguments:
87 | variant (rez.packages_.Variant): Package from which to retrieve data
88 |
89 | Returns:
90 | dict: See function for values and types
91 |
92 | """
93 |
94 | data = getattr(variant, "_data", {})
95 |
96 | return dict(data, **{
97 |
98 | # Guaranteed keys, with default values
99 | "label": data.get("label", variant.name),
100 | "background": data.get("background"),
101 | "icon": data.get("icon", ""),
102 | "hidden": data.get("hidden", False),
103 | })
104 |
105 |
106 | def protected_preferences():
107 | """Protect preference settings
108 |
109 | Prevent clueless one from touching danger settings.
110 |
111 | Following is a list of preference names that you may lock:
112 | * showAllApps (bool)
113 | * showHiddenApps (bool)
114 | * showAllVersions (bool)
115 | * patchWithFilter (bool)
116 | * clearCacheTimeout (int)
117 | * exclusionFilter (str)
118 |
119 | This should return a preference name and default value paired
120 | dict. For example: {"showAllVersions": False}
121 |
122 | Returns:
123 | dict
124 |
125 | """
126 | return dict()
127 |
128 |
129 | def themes():
130 | """Allzpark GUI theme list provider
131 |
132 | This will only be called once on startup.
133 |
134 | Each theme in list is a dict object, for example:
135 |
136 | {
137 | "name": "theme_name",
138 | "source": "my_style.css",
139 | "keywords": {"base-tone": "red", "res": "path-to-icons"},
140 | }
141 |
142 | * `name` is the theme name, this is required.
143 | * `source` can be a file path or plain css code, this is required.
144 | * `keywords` is optional, must be dict type if provided, will be
145 | used to string format the css code.
146 |
147 | Returns:
148 | list
149 |
150 | """
151 | return []
152 |
153 |
154 | def application_parent_environment():
155 | """Application's launching environment
156 |
157 | You may want to set this so the application won't be inheriting current
158 | environment which is used to launch Allzpark. E.g. when Allzaprk is
159 | launched from a Rez resolved context.
160 |
161 | But if using bleeding-rez, and `config.inherit_parent_environment` is
162 | set to False, config will be respected and this will be ignored.
163 |
164 | Returns:
165 | dict
166 |
167 | """
168 | return None
169 |
170 |
171 | def subprocess_encoding():
172 | """Codec that should be used to decode subprocess stdout/stderr
173 |
174 | See https://docs.python.org/3/library/codecs.html#standard-encodings
175 |
176 | Returns:
177 | str: name of codec
178 |
179 | """
180 | # nerdvegas/rez sets `encoding='utf-8'` when `universal_newlines=True` and
181 | # `encoding` is not in Popen kwarg.
182 | return "utf-8"
183 |
184 |
185 | def unicode_decode_error_handler():
186 | """Error handler for handling UnicodeDecodeError in subprocess
187 |
188 | See https://docs.python.org/3/library/codecs.html#error-handlers
189 |
190 | Returns:
191 | str: name of registered error handler
192 |
193 | """
194 | import codecs
195 | import locale
196 |
197 | def decode_with_preferred_encoding(exception):
198 | encoding = locale.getpreferredencoding(do_setlocale=False)
199 | invalid_bytes = exception.object[exception.start:]
200 |
201 | text = invalid_bytes.decode(encoding,
202 | # second fallback
203 | errors="backslashreplace")
204 |
205 | return text, len(exception.object)
206 |
207 | handler_name = "decode_with_preferred_encoding"
208 | try:
209 | codecs.lookup_error(handler_name)
210 | except LookupError:
211 | codecs.register_error(handler_name, decode_with_preferred_encoding)
212 |
213 | return handler_name
214 |
--------------------------------------------------------------------------------
/allzpark/util.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import time
4 | import traceback
5 | import functools
6 | import contextlib
7 | import logging
8 | import collections
9 | import webbrowser
10 | import subprocess
11 |
12 | from .vendor import six
13 | from .vendor.Qt import QtCore
14 |
15 | _lru_cache = {}
16 | _threads = []
17 | _basestring = six.string_types[0] # For Python 2/3
18 | _log = logging.getLogger(__name__)
19 | _timer = (time.process_time
20 | if six.PY3 else (time.time if os.name == "nt" else time.clock))
21 |
22 | USE_THREADING = not bool(os.getenv("ALLZPARK_NOTHREADING"))
23 |
24 |
25 | @contextlib.contextmanager
26 | def timing():
27 | t0 = _timer()
28 | result = type("timing", (object,), {"duration": None})
29 | try:
30 | yield result
31 | finally:
32 | t1 = _timer()
33 | result.duration = t1 - t0
34 |
35 |
36 | def delay(func, delay=50):
37 | """Postpone `func` by `delay` milliseconds
38 |
39 | This is used to allow Qt to finish rendering prior
40 | to occupying the main thread. Such as calling some
41 | CPU-heavy function on a `QPushButton.pressed` event,
42 | which would normally freeze the GUI without letting
43 | the button unclick itself, resulting in unexpected
44 | visual artifacts.
45 |
46 | """
47 |
48 | QtCore.QTimer.singleShot(delay, func)
49 |
50 |
51 | def async_(func):
52 | """No-op decorator, used to visually distinguish async_ functions"""
53 | return func
54 |
55 |
56 | def cached(func):
57 | """Cache returnvalue of `func`"""
58 |
59 | @functools.wraps(func)
60 | def wrapper(*args, **kwargs):
61 | key = "{func}:{args}:{kwargs}".format(
62 | func=func.__name__,
63 | args=", ".join(str(arg) for arg in args),
64 | kwargs=", ".join(
65 | "%s=%s" % (key, value)
66 | for key, value in kwargs.items()
67 | )
68 | )
69 |
70 | try:
71 | value = _lru_cache[key]
72 | except KeyError:
73 | value = func(*args, **kwargs)
74 | _lru_cache[key] = value
75 |
76 | return value
77 | return wrapper
78 |
79 |
80 | def windows_taskbar_compat():
81 | """Enable icon and taskbar grouping for Windows 7+"""
82 |
83 | import ctypes
84 | ctypes.windll.shell32.\
85 | SetCurrentProcessExplicitAppUserModelID(
86 | u"allzpark")
87 |
88 |
89 | if USE_THREADING:
90 | def defer(target,
91 | args=None,
92 | kwargs=None,
93 | on_success=lambda object: None,
94 | on_failure=lambda exception: None):
95 | """Perform operation in thread with callback
96 |
97 | Arguments:
98 | target (callable): Method or function to call
99 | callback (callable, optional): Method or function to call
100 | once `target` has finished.
101 |
102 | Returns:
103 | None
104 |
105 | """
106 |
107 | thread = Thread(target, args, kwargs, on_success, on_failure)
108 | thread.finished.connect(lambda: _threads.remove(thread))
109 | thread.start()
110 |
111 | # Cache until finished
112 | # If we didn't do this, Python steps in to garbage
113 | # collect the thread before having had time to finish,
114 | # resulting in an exception.
115 | _threads.append(thread)
116 |
117 | return thread
118 |
119 | else:
120 | # Debug mode, execute "threads" immediately on the main thread
121 | _log.warning("Threading disabled")
122 |
123 | def defer(target,
124 | args=None,
125 | kwargs=None,
126 | on_success=lambda object: None,
127 | on_failure=lambda exception: None):
128 | try:
129 | result = target(*(args or []), **(kwargs or {}))
130 | except Exception as e:
131 | error = traceback.format_exc()
132 | on_failure(e, error)
133 | else:
134 | on_success(result)
135 |
136 |
137 | class Thread(QtCore.QThread):
138 | succeeded = QtCore.Signal(object)
139 | failed = QtCore.Signal(Exception, _basestring)
140 |
141 | def __init__(self,
142 | target,
143 | args=None,
144 | kwargs=None,
145 | on_success=None,
146 | on_failure=None):
147 | super(Thread, self).__init__()
148 |
149 | self.args = args or list()
150 | self.kwargs = kwargs or dict()
151 | self.target = target
152 | self.on_success = on_success
153 | self.on_failure = on_failure
154 |
155 | connection = QtCore.Qt.BlockingQueuedConnection
156 |
157 | if on_success is not None:
158 | self.succeeded.connect(self.on_success, type=connection)
159 |
160 | if on_failure is not None:
161 | self.failed.connect(self.on_failure, type=connection)
162 |
163 | def run(self, *args, **kwargs):
164 | try:
165 | result = self.target(*self.args, **self.kwargs)
166 |
167 | except Exception as e:
168 | error = traceback.format_exc()
169 | return self.failed.emit(e, error)
170 |
171 | else:
172 | self.succeeded.emit(result)
173 |
174 |
175 | def iterable(arg):
176 | return (
177 | isinstance(arg, collections.Iterable)
178 | and not isinstance(arg, six.string_types)
179 | )
180 |
181 |
182 | def open_file_location(fname):
183 | if os.path.exists(fname):
184 | if os.name == "nt":
185 | subprocess.Popen("explorer /select,%s" % fname)
186 | else:
187 | webbrowser.open(os.path.dirname(fname))
188 | else:
189 | raise OSError("%s did not exist" % fname)
190 |
191 |
192 | def normpath(path):
193 | return os.path.normpath(
194 | os.path.normcase(os.path.abspath(path)).replace("\\", "/")
195 | )
196 |
197 |
198 | def normpaths(*paths):
199 | return list(map(normpath, paths))
200 |
201 |
202 | def atoi(text):
203 | return int(text) if text.isdigit() else text
204 |
205 |
206 | def natural_keys(text):
207 | """Key for use with sorted(key=) and str.sort(key=)
208 |
209 | alist.sort(key=natural_keys) sorts in human order
210 | http://nedbatchelder.com/blog/200712/human_sorting.html
211 | (See Toothy's implementation in the comments)
212 |
213 | """
214 |
215 | return [atoi(c) for c in re.split(r'(\d+)', text)]
216 |
--------------------------------------------------------------------------------
/docs/pages/contributing.md:
--------------------------------------------------------------------------------
1 | Thanks for considering making a contribution to the Allzpark project!
2 |
3 | The goal of Allzpark is making film and games productions more fun to work on, for artists and developers alike. Almost every company with any experience working in this field has felt the pain of managing software and versions when all you really want to do is make great pictures. Allzpark can really help with that, and you can really help Allzpark!
4 |
5 |
6 |
7 | ### Quickstart
8 |
9 | Allzpark works out of the Git repository.
10 |
11 | ```bash
12 | git clone https://github.com/mottosso/allzpark.git
13 | cd allzpark
14 | python -m allzpark --demo
15 | ```
16 |
17 | Get the up-to-date requirements by having a copy of Allzpark already installed.
18 |
19 | - See [Quickstart](/quickstart) for details
20 |
21 |
22 |
23 | ### Architecture
24 |
25 | The front-end is written in Python and [Qt.py](https://github.com/mottosso/Qt.py), and the back-end is [bleeding-rez](https://github.com/mottosso/bleeding-rez). You are welcome to contribute to either of these projects.
26 |
27 | Graphically, the interface is written in standard Qt idioms, like MVC to separate between logic and visuals. The window itself is an instance of `QMainWindow`, whereby each "tab" is a regular `QDockWidget`, which is how you can move them around and dock them freely.
28 |
29 | - [model.py](https://github.com/mottosso/allzpark/blob/master/allzpark/model.py)
30 | - [view.py](https://github.com/mottosso/allzpark/blob/master/allzpark/view.py)
31 | - [control.py](https://github.com/mottosso/allzpark/blob/master/allzpark/control.py)
32 |
33 | User preferences is stored in a `QSettings` object, including window layout. See `view.py:Window.closeEvent()` for how that works.
34 |
35 |
36 |
37 | ### Development
38 |
39 | To make changes and/or contribute to Allzpark, here's how to run it from its Git repository.
40 |
41 | ```bash
42 | git clone https://github.com/mottosso/allzpark.git
43 | cd allzpark
44 | python -m allzpark
45 | ```
46 |
47 | From here, Python picks up the `allzpark` package from the current working directory, and everything is set to go. For use with Rez, try this.
48 |
49 | ```bash
50 | # powershell
51 | git clone https://github.com/mottosso/allzpark.git
52 | cd allzpark
53 | . env.ps1
54 | > python -m allzpark
55 | ```
56 |
57 | This will ensure a reproducible environment via Rez packages.
58 |
59 |
60 |
61 | ### Versioning
62 |
63 | You typically won't have to manually increment the version of this project.
64 |
65 | Instead, you can find the current version based on the current commit.
66 |
67 | ```bash
68 | python -m allzpark --version
69 | 1.3.5
70 | ```
71 |
72 | This is the version to be used when making a new GitHub release, and the version used by setup.py during release on PyPI (in case you should accidentally tag your GitHub release errouneously).
73 |
74 | Major and minor versions are incremented for breaking and new changes respectively, the patch version however is special. It is incremented automatically in correspondance with the current commit number. E.g. commit number 200 yields a patch number of 200. See `allzpark/version.py` for details.
75 |
76 | To see the patch version as you develop, ensure `git` is available on PATH, as it is used to detect the commit number at launch. Once built and distributed to PyPI, this number is then embedded into the resulting package. See `setup.py` for details.
77 |
78 |
79 |
80 | ### Resources
81 |
82 | The current icon set is from [Haiku](https://github.com/darealshinji/haiku-icons).
83 |
84 |
85 |
86 | ### Guidelines
87 |
88 | There are a few ways you can contribute to this project.
89 |
90 | 1. Use it and report any issues [here](https://github.com/mottosso/allzpark/issues)
91 | 1. Submit ideas for improvements or new features [here](https://github.com/mottosso/allzpark/issues)
92 | 1. Add or improve [this documentation](https://github.com/mottosso/allzpark/tree/master/docs)
93 | 1. Help write tests to avoid regressions and help future contributors spot mistakes
94 |
95 | Any other thoughts on how you would like to contribute? [Let me know](https://github.com/mottosso/allzpark/issues).
96 |
97 |
98 |
99 | ### Documentation
100 |
101 | The documentation you are reading right now is hosted in the Allzpark git repository on GitHub, and built with a static site-generator called [mkdocs](https://www.mkdocs.org/) along with a theme called [mkdocs-material](https://squidfunk.github.io/mkdocs-material/).
102 |
103 | Mkdocs can host the entirety of the website on your local machine, and automatically update whenever you make changes to the Markdown documents. Here's how you can get started.
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | You can either use Rez and Pipz.
113 |
114 | ```powershell
115 | cd allzpark\docs
116 | rez env pipz -- install -r requirements.txt
117 | . serve.ps1
118 | ```
119 |
120 | Or install dependencies into your system-wide Python.
121 |
122 | ```powershell
123 | cd allzpark\docs
124 | pip install -r requirements.txt
125 | mkdocs serve
126 | ```
127 |
128 |
147 |
148 | You should see a message about how to browse to the locally hosted documentation in your console.
149 |
150 |
151 |
152 | #### Guidelines
153 |
154 | Things to keep in mind as you contribute to the documentation
155 |
156 | - **Windows-first** Allzpark is for all platforms, but Windows-users are typically less tech-savvy than Linux and MacOS users and the documentation should reflect that.
157 | - **Try-catch** When documenting a series of steps to accomplish a task, start with the minimal ideal case, and then "catch" potential errors afterwards. This helps keep the documentation from branching out too far, and facilitates a cursory skimming of the documentation. See [quickstart](/quickstart) for an example.
158 |
--------------------------------------------------------------------------------
/allzpark/resources.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | from collections import OrderedDict as odict
4 | from . import allzparkconfig
5 | from .vendor.Qt import QtGui
6 |
7 | dirname = os.path.dirname(__file__)
8 | _cache = {}
9 | _themes = odict()
10 |
11 |
12 | def px(value, scale=1.0):
13 | return int(value * scale)
14 |
15 |
16 | def find(*paths):
17 | fname = os.path.join(dirname, "resources", *paths)
18 | fname = os.path.normpath(fname) # Conform slashes and backslashes
19 | return fname.replace("\\", "/") # Cross-platform compatibility
20 |
21 |
22 | def pixmap(*paths):
23 | path = find(*paths)
24 | basename = paths[-1]
25 | name, ext = os.path.splitext(basename)
26 |
27 | if not ext:
28 | path += ".png"
29 |
30 | try:
31 | pixmap = _cache[paths]
32 | except KeyError:
33 | pixmap = QtGui.QPixmap(find(*paths))
34 | _cache[paths] = pixmap
35 |
36 | return pixmap
37 |
38 |
39 | def icon(*paths):
40 | return QtGui.QIcon(pixmap(*paths))
41 |
42 |
43 | def load_themes():
44 | _themes.clear()
45 | for theme in default_themes() + allzparkconfig.themes():
46 | _themes[theme["name"]] = theme
47 |
48 |
49 | def theme_names():
50 | for name in _themes.keys():
51 | yield name
52 |
53 |
54 | def load_theme(name=None):
55 | if name:
56 | theme = _themes.get(name)
57 | if theme is None:
58 | print("No theme named: %s" % name)
59 | return
60 | else:
61 | theme = next(iter(_themes.values()))
62 |
63 | source = theme["source"]
64 | keywords = theme.get("keywords", dict())
65 |
66 | if any(source.endswith(ext) for ext in [".css", ".qss"]):
67 | if not os.path.isfile(source):
68 | print("Theme stylesheet file not found: %s" % source)
69 | return
70 | else:
71 | with open(source) as f:
72 | css = f.read()
73 | else:
74 | # plain css code
75 | css = source
76 |
77 | _cache["_keywordsCache_"] = keywords
78 | _cache["_logColorCache_"] = {
79 | logging.DEBUG: keywords.get("log.debug", "lightgrey"),
80 | logging.INFO: keywords.get("log.info", "grey"),
81 | logging.WARNING: keywords.get("log.warning", "darkorange"),
82 | logging.ERROR: keywords.get("log.error", "lightcoral"),
83 | logging.CRITICAL: keywords.get("log.critical", "red"),
84 | }
85 |
86 | return format_stylesheet(css)
87 |
88 |
89 | def format_stylesheet(css):
90 | try:
91 | return css % _cache["_keywordsCache_"]
92 | except KeyError as e:
93 | print("Stylesheet format failed: %s" % str(e))
94 | return ""
95 |
96 |
97 | def log_level_color(level):
98 | log_colors = _cache.get("_logColorCache_", dict())
99 | return log_colors.get(level, "grey")
100 |
101 |
102 | def default_themes():
103 | _load_fonts()
104 | res_root = os.path.join(dirname, "resources").replace("\\", "/")
105 | return [
106 | {
107 | "name": "default-dark",
108 | "source": find("style-dark.css"),
109 | "keywords": {
110 | "prim": "#2E2C2C",
111 | "brightest": "#403E3D",
112 | "bright": "#383635",
113 | "base": "#2E2C2C",
114 | "dim": "#21201F",
115 | "dimmest": "#141413",
116 | "hover": "rgba(104, 182, 237, 60)",
117 | "highlight": "rgb(110, 191, 245)",
118 | "highlighted": "#111111",
119 | "active": "silver",
120 | "inactive": "dimGray",
121 | "console": "#161616",
122 | "log.debug": "lightgrey",
123 | "log.info": "grey",
124 | "log.warning": "darkorange",
125 | "log.error": "lightcoral",
126 | "log.critical": "red",
127 | "res": res_root,
128 | }
129 | },
130 | {
131 | "name": "default-light",
132 | "source": find("style-light.css"),
133 | "keywords": {
134 | "prim": "#FFFFFF",
135 | "brightest": "#FDFDFD",
136 | "bright": "#F9F9F9",
137 | "base": "#EFEFEF",
138 | "dim": "#DFDFDF",
139 | "dimmest": "#CFCFCF",
140 | "hover": "rgba(122, 194, 245, 60)",
141 | "highlight": "rgb(136, 194, 235)",
142 | "highlighted": "#111111",
143 | "active": "black",
144 | "inactive": "gray",
145 | "console": "#363636",
146 | "log.debug": "lightgrey",
147 | "log.info": "grey",
148 | "log.warning": "darkorange",
149 | "log.error": "lightcoral",
150 | "log.critical": "red",
151 | "res": res_root,
152 | }
153 | },
154 | ]
155 |
156 |
157 | def _load_fonts():
158 | """Load default fonts from resources"""
159 | _res_root = os.path.join(dirname, "resources").replace("\\", "/")
160 |
161 | font_root = os.path.join(_res_root, "fonts")
162 | fonts = [
163 | "opensans/OpenSans-Bold.ttf",
164 | "opensans/OpenSans-BoldItalic.ttf",
165 | "opensans/OpenSans-ExtraBold.ttf",
166 | "opensans/OpenSans-ExtraBoldItalic.ttf",
167 | "opensans/OpenSans-Italic.ttf",
168 | "opensans/OpenSans-Light.ttf",
169 | "opensans/OpenSans-LightItalic.ttf",
170 | "opensans/OpenSans-Regular.ttf",
171 | "opensans/OpenSans-Semibold.ttf",
172 | "opensans/OpenSans-SemiboldItalic.ttf",
173 |
174 | "jetbrainsmono/JetBrainsMono-Bold.ttf"
175 | "jetbrainsmono/JetBrainsMono-Bold-Italic.ttf"
176 | "jetbrainsmono/JetBrainsMono-ExtraBold.ttf"
177 | "jetbrainsmono/JetBrainsMono-ExtraBold-Italic.ttf"
178 | "jetbrainsmono/JetBrainsMono-ExtraLight.ttf"
179 | "jetbrainsmono/JetBrainsMono-ExtraLight-Italic.ttf"
180 | "jetbrainsmono/JetBrainsMono-Italic.ttf"
181 | "jetbrainsmono/JetBrainsMono-Light.ttf"
182 | "jetbrainsmono/JetBrainsMono-Light-Italic.ttf"
183 | "jetbrainsmono/JetBrainsMono-Medium.ttf"
184 | "jetbrainsmono/JetBrainsMono-Medium-Italic.ttf"
185 | "jetbrainsmono/JetBrainsMono-Regular.ttf"
186 | "jetbrainsmono/JetBrainsMono-SemiLight.ttf"
187 | "jetbrainsmono/JetBrainsMono-SemiLight-Italic.ttf"
188 | ]
189 |
190 | for font in fonts:
191 | path = os.path.join(font_root, font)
192 | QtGui.QFontDatabase.addApplicationFont(path)
193 |
--------------------------------------------------------------------------------
/docs/theme/extra.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Peinture Fraiche";
3 | src: url("Peinture Fraiche.ttf") format("truetype");
4 | }
5 |
6 | body,
7 | input {
8 | font-family: "Roboto", helvetica;
9 | }
10 |
11 | .md-grid {
12 | max-width: 57rem;
13 | }
14 |
15 | /* There's typically some space at the top of each article
16 | that we don't need, since we aren't showing h1 */
17 | .md-content__inner:before {
18 | height: 0;
19 | }
20 |
21 | .md-content__inner {
22 | padding-top: 0;
23 | }
24 |
25 | /* Items in a list are typically too far apart */
26 | .md-typeset ol li, .md-typeset ul li {
27 | margin-bottom: 5px;
28 | }
29 |
30 | /* Faded items in the table of contents, as the user scrolls */
31 | .md-nav__link[data-md-state=blur] {
32 | color: rgba(0,0,0,.34);
33 | }
34 |
35 | .md-typeset, .md-sidebar {
36 | font-family: Segoe UI,
37 | Roboto,
38 | Oxygen,
39 | Ubuntu,
40 | Cantarell,
41 | Fira Sans,
42 | Droid Sans,
43 | Helvetica Neue,
44 | sans-serif;
45 | text-size-adjust: 100%;
46 | font-size: 16px;
47 | line-height: 1.4;
48 | color: #424242;
49 | font-weight: 400;
50 | -webkit-font-smoothing: antialiased;
51 | }
52 |
53 | .md-typeset code {
54 | box-shadow: none;
55 | border: 1px solid #eee;
56 | color: #222;
57 | padding: 0 3px;
58 | background-color: transparent;
59 | }
60 |
61 | .md-typeset ol, .md-typeset ul {
62 | line-height: 1.3em;
63 | }
64 |
65 | /* Reduce distance between help-boxes */
66 | .md-typeset .admonition, .md-typeset details {
67 | margin: 6px 0;
68 |
69 | /* This is normally much too small */
70 | font-size: inherit;
71 | }
72 |
73 | .md-typeset details.quote>summary {
74 | font-size: 0.7rem;
75 | font-weight: 400;
76 | }
77 |
78 | /* Remove padding from lists, it doesn't look good */
79 | .md-typeset ol, .md-typeset ul {
80 | margin-left: 0;
81 | }
82 |
83 | .md-typeset .codehilite, .md-typeset .highlight {
84 | min-width: 50%;
85 | background: transparent;
86 | border: 1px solid #eee;
87 | margin-top: 0;
88 | }
89 |
90 | pre {
91 | color: #ccc !important;
92 | }
93 |
94 | .md-clipboard:before {
95 | color: rgb(200, 200, 200);
96 | }
97 |
98 | /* Hovering a link */
99 | .md-nav__link:focus, .md-nav__link:hover {
100 | color: #86c8ef;
101 | }
102 |
103 | /* Default has some extra space here,
104 | which interfers with our use of background-color */
105 | .md-nav__item:last-child {
106 | padding-bottom: 0px;
107 | }
108 |
109 | .codehilite:hover .md-clipboard:before,.md-typeset .highlight:hover .md-clipboard:before,pre:hover .md-clipboard:before {
110 | color: rgba(200, 200, 200, 0.54) !important
111 | }
112 |
113 | .codehilite code, .md-typeset .codehilite pre, .md-typeset .highlight code, .md-typeset .highlight pre {
114 | font-family: "Source Code Pro";
115 | font-size: 0.9em;
116 | }
117 |
118 | input, select, textarea{
119 | color: green;
120 | }
121 |
122 | .md-header[data-md-state] {
123 | box-shadow: none;
124 | border-bottom: 1px solid #f3f3f3;
125 | }
126 |
127 | .md-typeset a, .md-typeset a:before {
128 | color: #03a9f4;
129 | }
130 |
131 | .md-typeset h2 {
132 | font-family: "Peinture Fraiche";
133 | font-size: 5em;
134 | color: #222;
135 | line-height: 0.8;
136 | }
137 |
138 | [data-md-color-primary=white] .md-search__input {
139 | background-color: transparent;
140 | border: 1px solid #f3f3f3;
141 | }
142 |
143 | h2, h3, h4, b, strong {
144 | color: #333;
145 | }
146 |
147 | .md-nav__item.md-nav__item--active {
148 | background-color: #e4f3ff;
149 | }
150 |
151 | .md-nav__link {
152 | margin-top: 0;
153 | padding-top: 4px;
154 | padding-bottom: 4px;
155 | }
156 |
157 | .md-typeset h1 {
158 | display: none;
159 | }
160 |
161 | .md-typeset h3, .md-typeset h4 {
162 | font-weight: bold;
163 | }
164 |
165 | .md-footer-copyright__highlight {
166 | display: none;
167 | }
168 |
169 | /* Hide left-sidebar header */
170 | .md-nav__title--site {
171 | display: none;
172 | }
173 |
174 | .md-nav {
175 | font-size: .6rem;
176 | line-height: 1.3;
177 | color: #696969;
178 | }
179 |
180 | nav.md-nav.md-nav--primary {
181 | font-size: .65rem;
182 | }
183 |
184 | .md-footer {
185 | padding-top: 100px;
186 | }
187 |
188 | div.tabs {
189 | padding-left: 1px;
190 | }
191 |
192 | div.tabs button.active {
193 | color: #1f1f1f;
194 | position: relative;
195 | border-bottom: 6px solid steelblue;
196 | }
197 |
198 | div.tabs button {
199 | outline: none;
200 | cursor: pointer;
201 | position: relative;
202 | color: #6363635e;
203 | border: none;
204 | border-top-left-radius: 4px;
205 | border-top-right-radius: 4px;
206 | margin: 6px 14px 7px 0px;
207 | transition: 0.2s;
208 | font-size: 0.7rem;
209 | }
210 |
211 | .tab-content {
212 | background: white;
213 | display: none;
214 | padding: 0;
215 | border: none;
216 | }
217 |
218 | .md-typeset .tab-content .codehilitetable .linenos {
219 | display: none;
220 | }
221 |
222 | .md-typeset .tab-content .codehilitetable {
223 | margin: 0;
224 | }
225 |
226 | button.tab p {
227 | padding: 0;
228 | margin: 0;
229 | letter-spacing: 0.1rem;
230 | }
231 |
232 | button.tab div.tab-gap {
233 | position: absolute;
234 | width: 100%;
235 | height: 3px;
236 | bottom: -2px;
237 | margin: 0;
238 | padding: 0;
239 | background: #ffffff;
240 | z-index: 5;
241 | left: 0;
242 | display: none;
243 | }
244 |
245 | button.tab.active div.tab-gap {
246 | display: block;
247 | }
248 |
249 | div.tabs button.active p {
250 | text-shadow: none;
251 | }
252 |
253 | /* Only visible on a larger surface area (not mobile) */
254 | .floating-image {
255 | display: none;
256 | }
257 |
258 | @media only screen and (min-width: 60em) {
259 | [data-md-toggle]:checked~.md-header .md-search__input+.md-search__icon, [data-md-toggle]:checked~.md-header .md-search__input::placeholder {
260 | color: rgba(0, 0, 0, .30);
261 | }
262 |
263 | .md-search__input+.md-search__icon, .md-search__input::placeholder {
264 | color: rgba(0, 0, 0, .30);
265 | }
266 |
267 | .floating-image {
268 | float: right;
269 | display: inherit;
270 | }
271 |
272 | }
273 |
274 | @media only screen and (min-width: 76.25em) {
275 | .md-sidebar--primary .md-sidebar__inner {
276 | border-right: 1px solid rgba(0, 0, 0, .07);
277 | }
278 |
279 | .md-sidebar--secondary .md-sidebar__inner {
280 | border-left: 1px solid rgba(0, 0, 0, .07);
281 | }
282 |
283 | }
284 |
285 | [data-md-color-primary=white] .md-nav__link--active, [data-md-color-primary=white] .md-nav__link:active {
286 | color: #03a9f4;
287 | }
--------------------------------------------------------------------------------
/allzpark/vendor/transitions/extensions/locking.py:
--------------------------------------------------------------------------------
1 | """
2 | transitions.extensions.factory
3 | ------------------------------
4 |
5 | Adds locking to machine methods as well as model functions that trigger events.
6 | Additionally, the user can inject her/his own context manager into the machine if required.
7 | """
8 |
9 | from collections import defaultdict
10 | from functools import partial
11 | from threading import Lock
12 | import inspect
13 | import warnings
14 | import logging
15 |
16 | from ..core import Machine, Event, listify
17 |
18 | _LOGGER = logging.getLogger(__name__)
19 | _LOGGER.addHandler(logging.NullHandler())
20 |
21 | # this is a workaround for dill issues when partials and super is used in conjunction
22 | # without it, Python 3.0 - 3.3 will not support pickling
23 | # https://github.com/pytransitions/transitions/issues/236
24 | _super = super
25 |
26 | try:
27 | from contextlib import nested # Python 2
28 | from thread import get_ident
29 | # with nested statements now raise a DeprecationWarning. Should be replaced with ExitStack-like approaches.
30 | warnings.simplefilter('ignore', DeprecationWarning)
31 |
32 | except ImportError:
33 | from contextlib import ExitStack, contextmanager
34 | from threading import get_ident
35 |
36 | @contextmanager
37 | def nested(*contexts):
38 | """ Reimplementation of nested in Python 3. """
39 | with ExitStack() as stack:
40 | for ctx in contexts:
41 | stack.enter_context(ctx)
42 | yield contexts
43 |
44 |
45 | class PicklableLock(object):
46 | """ A wrapper for threading.Lock which discards its state during pickling and
47 | is reinitialized unlocked when unpickled.
48 | """
49 |
50 | def __init__(self):
51 | self.lock = Lock()
52 |
53 | def __getstate__(self):
54 | return ''
55 |
56 | def __setstate__(self, value):
57 | return self.__init__()
58 |
59 | def __enter__(self):
60 | self.lock.__enter__()
61 |
62 | def __exit__(self, exc_type, exc_val, exc_tb):
63 | self.lock.__exit__(exc_type, exc_val, exc_tb)
64 |
65 |
66 | class LockedEvent(Event):
67 | """ An event type which uses the parent's machine context map when triggered. """
68 |
69 | def trigger(self, model, *args, **kwargs):
70 | """ Extends transitions.core.Event.trigger by using locks/machine contexts. """
71 | # pylint: disable=protected-access
72 | # noinspection PyProtectedMember
73 | # LockedMachine._locked should not be called somewhere else. That's why it should not be exposed
74 | # to Machine users.
75 | if self.machine._locked != get_ident():
76 | with nested(*self.machine.model_context_map[model]):
77 | return _super(LockedEvent, self).trigger(model, *args, **kwargs)
78 | else:
79 | return _super(LockedEvent, self).trigger(model, *args, **kwargs)
80 |
81 |
82 | class LockedMachine(Machine):
83 | """ Machine class which manages contexts. In it's default version the machine uses a `threading.Lock`
84 | context to lock access to its methods and event triggers bound to model objects.
85 | Attributes:
86 | machine_context (dict): A dict of context managers to be entered whenever a machine method is
87 | called or an event is triggered. Contexts are managed for each model individually.
88 | """
89 |
90 | event_cls = LockedEvent
91 |
92 | def __init__(self, *args, **kwargs):
93 | self._locked = 0
94 |
95 | try:
96 | self.machine_context = listify(kwargs.pop('machine_context'))
97 | except KeyError:
98 | self.machine_context = [PicklableLock()]
99 |
100 | self.machine_context.append(self)
101 | self.model_context_map = defaultdict(list)
102 |
103 | _super(LockedMachine, self).__init__(*args, **kwargs)
104 |
105 | def add_model(self, model, initial=None, model_context=None):
106 | """ Extends `transitions.core.Machine.add_model` by `model_context` keyword.
107 | Args:
108 | model (list or object): A model (list) to be managed by the machine.
109 | initial (string or State): The initial state of the passed model[s].
110 | model_context (list or object): If passed, assign the context (list) to the machines
111 | model specific context map.
112 | """
113 | models = listify(model)
114 | model_context = listify(model_context) if model_context is not None else []
115 | output = _super(LockedMachine, self).add_model(models, initial)
116 |
117 | for mod in models:
118 | mod = self if mod == 'self' else mod
119 | self.model_context_map[mod].extend(self.machine_context)
120 | self.model_context_map[mod].extend(model_context)
121 |
122 | return output
123 |
124 | def remove_model(self, model):
125 | """ Extends `transitions.core.Machine.remove_model` by removing model specific context maps
126 | from the machine when the model itself is removed. """
127 | models = listify(model)
128 |
129 | for mod in models:
130 | del self.model_context_map[mod]
131 |
132 | return _super(LockedMachine, self).remove_model(models)
133 |
134 | def __getattribute__(self, item):
135 | get_attr = _super(LockedMachine, self).__getattribute__
136 | tmp = get_attr(item)
137 | if not item.startswith('_') and inspect.ismethod(tmp):
138 | return partial(get_attr('_locked_method'), tmp)
139 | return tmp
140 |
141 | def __getattr__(self, item):
142 | try:
143 | return _super(LockedMachine, self).__getattribute__(item)
144 | except AttributeError:
145 | return _super(LockedMachine, self).__getattr__(item)
146 |
147 | # Determine if the returned method is a partial and make sure the returned partial has
148 | # not been created by Machine.__getattr__.
149 | # https://github.com/tyarkoni/transitions/issues/214
150 | def _add_model_to_state(self, state, model):
151 | _super(LockedMachine, self)._add_model_to_state(state, model) # pylint: disable=protected-access
152 | for prefix in ['enter', 'exit']:
153 | callback = "on_{0}_".format(prefix) + state.name
154 | func = getattr(model, callback, None)
155 | if isinstance(func, partial) and func.func != state.add_callback:
156 | state.add_callback(prefix, callback)
157 |
158 | def _locked_method(self, func, *args, **kwargs):
159 | if self._locked != get_ident():
160 | with nested(*self.machine_context):
161 | return func(*args, **kwargs)
162 | else:
163 | return func(*args, **kwargs)
164 |
165 | def __enter__(self):
166 | self._locked = get_ident()
167 |
168 | def __exit__(self, *exc):
169 | self._locked = 0
170 |
--------------------------------------------------------------------------------
/docs/pages/about.md:
--------------------------------------------------------------------------------
1 | This section outlines the rationale behind Allzpark, to help you determine whether or not it is of use to you.
2 |
3 |
4 |
5 | ### Background
6 |
7 | Allzpark (a.k.a. LaunchApp2) started as a 4-month commission for the Japanese [Studio Anima](http://www.studioanima.co.jp/). Time was divided into roughly these parts.
8 |
9 | 1. **Week 0-0** Tour of physical building, infrastructure and crew
10 | 1. **Week 1-2** Requirements gathering, an evaluation if current system
11 | 1. **Week 3-4** Evaluation of off-the-shelf options, e.g. Rez
12 | 1. **Week 5-6** Evaluation of studio, system and personnel resources
13 | 4. **Week 7-8** Integration and testing of fundamental infrastucture software, Ansible
14 | 5. **Week 9-10** Research and development of Rez to fit the criteria and initial [prototype](https://github.com/mottosso/rez-for-projects)
15 | 1. **Week 11-12** Conversion of existing package repository
16 | 1. **Week 13-14** Implementation of graphical user interface, LaunchApp2
17 | 1. **Week 15-16** Refinement of features, including localisation
18 | 1. **Week 17-18** Final integration and training of staff
19 |
20 |
21 |
22 | ### Journal
23 |
24 | Allzpark was initially an internal project, never intended to be open sourced. As a result, the first 2 months of development are locked away behind an internal journal for the company (due to disclosure of sensitive information).
25 |
26 | Luckily, it was around this time that Allzpark got approved for open source and when I was able to start sharing its development publicly, so that you are able to take part in the design decisions made, the why and how. This way, you're able to accurately determine whether a solution to a new problem takes the original requirements into consideration; something all too often lost in software projects.
27 |
28 | - [Journal](https://github.com/mottosso/allzpark/issues/1)
29 |
30 |
31 |
32 | ## Story time
33 |
34 | When Hannah - working at a digital production company like Framestore or ILM - arrives at work in the morning, she typically types something like this into her console.
35 |
36 | ```powershell
37 | go gravity
38 | maya
39 | ```
40 |
41 | What this does is put Hannah in the "context" of the `gravity` project. The subsequent call to `maya` then launches a given application, in this case Autodesk Maya. But which version? And why does it matter?
42 |
43 |
44 |
45 | ### A closer look
46 |
47 | To better understand what's happening here, let's take a closer look at what these commands do. Following the command `go gravity`, a few things happen.
48 |
49 | 1. The argument `gravity` is correlated to a project (either on disk or database)
50 | 2. The project is associated with metadata, detailing what software and versions are in use
51 | - `maya-2015`
52 | - `arnold-4.12`
53 | - `mgear-2.4`
54 | - `fbake-4.1`
55 | - `fasset-1.14`
56 | - `...`
57 | 3. The associated software is loaded into command-line `environment`
58 |
59 | At this point, the subsequent command `maya` unambiguously refers to `maya-2015`, which is how Framestore - and virtually every visual effects, feature animation, commercial and games facility - is able to tie a specific version of each set of software to a given project.
60 |
61 | Why is this important? The answer lies in **interoperability**.
62 |
63 | You see, whatever comes out of Hannah's department must interoperate with subsequent departments. Like an assembly line, the pace of the line remains consistent till the end, and every tool depends on the output of whatever came before it.
64 |
65 | This holds true for individual applications, like Maya or Visual Studio, but also sub-components of applications - plug-ins.
66 |
67 | Take `arnold-4.12` as an example. This particular version needs to interoperate with `maya-2015`.
68 |
69 | ```powershell
70 | 2015 2016 2017 2018 2019
71 | maya |--------------------------|
72 | arnold-1 |-------|
73 | arnold-2 |-----------|
74 | arnold-3 |-----------|
75 | arnold-4 |----------|
76 | ```
77 |
78 | In order to leverage `maya-2015` for a given project, your choice of `arnold` is limited to those that support it, or vice versa.
79 |
80 | ```powershell
81 | interoperable
82 | slice
83 | maya |-----------------|------|---|
84 | arnold-1 |-------| | |
85 | arnold-2 |-----------| | |
86 | arnold-3 |------|------|
87 | arnold-4 |-|------|---|
88 | | |
89 | ```
90 |
91 | This issue is compounded by the number of libraries and plug-ins you use for a given project. Consider `openimageio`, `qt`, `ilmbase` and other off-the-shelf projects you may want to employ in a given project, and you can start to get some idea of how narrow
92 |
93 | It is then further compounded by in-house development projects, such as your [*pipeline*](http://getavalon.github.io).
94 |
95 | None of this would have been a problem, if you were able to say:
96 |
97 | 1. We will ever only work on a single project at a time
98 | 1. We know which versions to use
99 | 1. We don't develop any new software ourselves
100 |
101 | In which case you could simply install each of these applications and get to work. But more often than not, things change. And in order to facilitate this change, there needs to be a system in place to help manage the combinatorial complexity of applications, software, and projects.
102 |
103 |
104 |
105 | ### Rez Users
106 |
107 | Here are some of the studios using Rez today, along with some approximate numbers (sources linked).
108 |
109 | | Studio | Active | People | Disk | Packages | Versions | Frequency | Source
110 | |:---------------|:-------|:-------|:-------|:---------|:---------|:----------|:-----------
111 | | Anima | 2019- | 100 | 30 GB | 199 | 2133 | 5 / day | -
112 | | RodeoFX | 2019- | 200 | 223 GB | 400 | 6732 | - | [a][]
113 | | Animal Logic | 2018- | 999 | 2 TB | 1552 | 44939 | 20 / day | [a][]
114 | | Mackievision | 2019- | 500 | | | | | -
115 | | Imageworks | 2019- | 999 | | | | | -
116 | | Puppetworks | 2019- | 200 | | | | | -
117 | | ToonBox | 2017- | | | | | | [f][]
118 | | Pixomondo | 2019- | | | | | | [b][]
119 | | Freefolk | 2019- | | | | | | [b][]
120 | | MPC | 2019- | | | | | | [b][]
121 | | Squeeze Studio | 2019- | | | | | | [c][]
122 | | Mikros | 2019- | | | | | | [c][]
123 | | Brunch Studio | 2019- | | | | | | [d][]
124 | | WWFX | 2019- | | | | | | [e][]
125 |
126 | [a]: https://groups.google.com/forum/#!topic/rez-config/GMiof1NEQoo
127 | [b]: https://groups.google.com/forum/#!searchin/rez-config/advice$20or$20tips$20on$20getting$20latest$20%7Csort:date/rez-config/-fmvH5mv9wM/cCWqh9BlFQAJ
128 | [c]: https://groups.google.com/forum/#!searchin/rez-config/Proper$20way$20to$20resolve$20an$20environment$20for$20an$20embedded$20python$20environment%7Csort:date/rez-config/2IWclNTJEk0/4B_hGWuxBQAJ
129 | [d]: https://groups.google.com/forum/#!msg/rez-config/Z7NdidsJNUY/2zYgVKsoEAAJ
130 | [e]: https://groups.google.com/forum/#!topic/rez-config/j78X0Qv3arM
131 | [f]: https://github.com/nerdvegas/rez/commit/8ca303d
132 |
--------------------------------------------------------------------------------
/allzpark/vendor/transitions/extensions/states.py:
--------------------------------------------------------------------------------
1 | """
2 | transitions.extensions.states
3 | -----------------------------
4 |
5 | This module contains mix ins which can be used to extend state functionality.
6 | """
7 |
8 | from threading import Timer
9 | import logging
10 | import inspect
11 |
12 | from ..core import MachineError, listify, State
13 |
14 | _LOGGER = logging.getLogger(__name__)
15 | _LOGGER.addHandler(logging.NullHandler())
16 |
17 |
18 | class Tags(State):
19 | """ Allows states to be tagged.
20 | Attributes:
21 | tags (list): A list of tag strings. `State.is_` may be used
22 | to check if is in the list.
23 | """
24 | def __init__(self, *args, **kwargs):
25 | """
26 | Args:
27 | **kwargs: If kwargs contains `tags`, assign them to the attribute.
28 | """
29 | self.tags = kwargs.pop('tags', [])
30 | super(Tags, self).__init__(*args, **kwargs)
31 |
32 | def __getattr__(self, item):
33 | if item.startswith('is_'):
34 | return item[3:] in self.tags
35 | return super(Tags, self).__getattribute__(item)
36 |
37 |
38 | class Error(Tags):
39 | """ This mix in builds upon tag and should be used INSTEAD of Tags if final states that have
40 | not been tagged with 'accepted' should throw an `MachineError`.
41 | """
42 |
43 | def __init__(self, *args, **kwargs):
44 | """
45 | Args:
46 | **kwargs: If kwargs contains the keywork `accepted` add the 'accepted' tag to a tag list
47 | which will be forwarded to the Tags constructor.
48 | """
49 | tags = kwargs.get('tags', [])
50 | accepted = kwargs.pop('accepted', False)
51 | if accepted:
52 | tags.append('accepted')
53 | kwargs['tags'] = tags
54 | super(Error, self).__init__(*args, **kwargs)
55 |
56 | def enter(self, event_data):
57 | """ Extends transitions.core.State.enter. Throws a `MachineError` if there is
58 | no leaving transition from this state and 'accepted' is not in self.tags.
59 | """
60 | if not event_data.machine.get_triggers(self.name) and not self.is_accepted:
61 | raise MachineError("Error state '{0}' reached!".format(self.name))
62 | super(Error, self).enter(event_data)
63 |
64 |
65 | class Timeout(State):
66 | """ Adds timeout functionality to a state. Timeouts are handled model-specific.
67 | Attributes:
68 | timeout (float): Seconds after which a timeout function should be called.
69 | on_timeout (list): Functions to call when a timeout is triggered.
70 | """
71 |
72 | dynamic_methods = ['on_timeout']
73 |
74 | def __init__(self, *args, **kwargs):
75 | """
76 | Args:
77 | **kwargs: If kwargs contain 'timeout', assign the float value to self.timeout. If timeout
78 | is set, 'on_timeout' needs to be passed with kwargs as well or an AttributeError will
79 | be thrown. If timeout is not passed or equal 0.
80 | """
81 | self.timeout = kwargs.pop('timeout', 0)
82 | self._on_timeout = None
83 | if self.timeout > 0:
84 | try:
85 | self.on_timeout = kwargs.pop('on_timeout')
86 | except KeyError:
87 | raise AttributeError("Timeout state requires 'on_timeout' when timeout is set.")
88 | else:
89 | self._on_timeout = kwargs.pop('on_timeout', [])
90 | self.runner = {}
91 | super(Timeout, self).__init__(*args, **kwargs)
92 |
93 | def enter(self, event_data):
94 | """ Extends `transitions.core.State.enter` by starting a timeout timer for the current model
95 | when the state is entered and self.timeout is larger than 0.
96 | """
97 | if self.timeout > 0:
98 | timer = Timer(self.timeout, self._process_timeout, args=(event_data,))
99 | timer.setDaemon(True)
100 | timer.start()
101 | self.runner[id(event_data.model)] = timer
102 | super(Timeout, self).enter(event_data)
103 |
104 | def exit(self, event_data):
105 | """ Extends `transitions.core.State.exit` by canceling a timer for the current model. """
106 | timer = self.runner.get(id(event_data.model), None)
107 | if timer is not None and timer.is_alive():
108 | timer.cancel()
109 | super(Timeout, self).exit(event_data)
110 |
111 | def _process_timeout(self, event_data):
112 | _LOGGER.debug("%sTimeout state %s. Processing callbacks...", event_data.machine.name, self.name)
113 | for callback in self.on_timeout:
114 | event_data.machine.callback(callback, event_data)
115 | _LOGGER.info("%sTimeout state %s processed.", event_data.machine.name, self.name)
116 |
117 | @property
118 | def on_timeout(self):
119 | """ List of strings and callables to be called when the state timeouts. """
120 | return self._on_timeout
121 |
122 | @on_timeout.setter
123 | def on_timeout(self, value):
124 | """ Listifies passed values and assigns them to on_timeout."""
125 | self._on_timeout = listify(value)
126 |
127 |
128 | class Volatile(State):
129 | """ Adds scopes/temporal variables to the otherwise persistent state objects.
130 | Attributes:
131 | volatile_cls (cls): Class of the temporal object to be initiated.
132 | volatile_hook (string): Model attribute name which will contain the volatile instance.
133 | """
134 |
135 | def __init__(self, *args, **kwargs):
136 | """
137 | Args:
138 | **kwargs: If kwargs contains `volatile`, always create an instance of the passed class
139 | whenever the state is entered. The instance is assigned to a model attribute which
140 | can be passed with the kwargs keyword `hook`. If hook is not passed, the instance will
141 | be assigned to the 'attribute' scope. If `volatile` is not passed, an empty object will
142 | be assigned to the model's hook.
143 | """
144 | self.volatile_cls = kwargs.pop('volatile', VolatileObject)
145 | self.volatile_hook = kwargs.pop('hook', 'scope')
146 | super(Volatile, self).__init__(*args, **kwargs)
147 | self.initialized = True
148 |
149 | def enter(self, event_data):
150 | """ Extends `transitions.core.State.enter` by creating a volatile object and assign it to
151 | the current model's hook. """
152 | setattr(event_data.model, self.volatile_hook, self.volatile_cls())
153 | super(Volatile, self).enter(event_data)
154 |
155 | def exit(self, event_data):
156 | """ Extends `transitions.core.State.exit` by deleting the temporal object from the model. """
157 | super(Volatile, self).exit(event_data)
158 | try:
159 | delattr(event_data.model, self.volatile_hook)
160 | except AttributeError:
161 | pass
162 |
163 |
164 | def add_state_features(*args):
165 | """ State feature decorator. Should be used in conjunction with a custom Machine class. """
166 | def _class_decorator(cls):
167 | class CustomState(type('CustomState', args, {}), cls.state_cls):
168 | """ The decorated State. It is based on the State class used by the decorated Machine. """
169 | pass
170 |
171 | method_list = sum([c.dynamic_methods for c in inspect.getmro(CustomState) if hasattr(c, 'dynamic_methods')], [])
172 | CustomState.dynamic_methods = set(method_list)
173 | cls.state_cls = CustomState
174 | return cls
175 | return _class_decorator
176 |
177 |
178 | class VolatileObject(object):
179 | """ Empty Python object which can be used to assign attributes to."""
180 | pass
181 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | ---
2 | trigger:
3 |
4 | # Already default, but made explicit here
5 | branches:
6 | include: ["*"]
7 |
8 | # Ensure Azure triggers a build on a new tag
9 | # We use these for GitHub releases
10 | tags:
11 | include: ["*"]
12 |
13 | paths:
14 | # Do not trigger a build on changes at these paths
15 | exclude:
16 | - docs/*
17 | - .gitignore
18 | - LICENSE.txt
19 | - README.md
20 |
21 |
22 | jobs:
23 |
24 | # -----------------------------------------------------------------------
25 | #
26 | # Test
27 | #
28 | # -----------------------------------------------------------------------
29 |
30 | - job: Ubuntu
31 | pool:
32 | vmImage: "ubuntu-20.04" # Focal
33 | strategy:
34 | matrix:
35 | Py35-Rez:
36 | python.version: "3.5"
37 | rez.project: "rez"
38 |
39 | Py35-BleedingRez:
40 | python.version: "3.5"
41 | rez.project: "bleeding-rez"
42 |
43 | Py36-Rez:
44 | python.version: "3.6"
45 | rez.project: "rez"
46 |
47 | Py36-BleedingRez:
48 | python.version: "3.6"
49 | rez.project: "bleeding-rez"
50 |
51 | Py37-Rez:
52 | python.version: "3.7"
53 | rez.project: "rez"
54 |
55 | Py37-BleedingRez:
56 | python.version: "3.7"
57 | rez.project: "bleeding-rez"
58 |
59 | steps:
60 | - task: UsePythonVersion@0
61 | inputs:
62 | versionSpec: "$(python.version)"
63 | displayName: "Use Python $(python.version)"
64 |
65 | - script: |
66 | git clone https://github.com/nerdvegas/rez.git rez-src
67 | cd rez-src
68 | sudo pip install .
69 | condition: eq(variables['rez.project'], 'rez')
70 | displayName: "Install rez (pip for API)"
71 |
72 | - script: |
73 | sudo pip install bleeding-rez
74 | condition: eq(variables['rez.project'], 'bleeding-rez')
75 | displayName: "Install bleeding-rez"
76 |
77 | - script: |
78 | sudo apt-get install python-pyside
79 | sudo python -c "from PySide import QtCore;print(QtCore.__version__)"
80 | condition: startsWith(variables['python.version'], '2.')
81 | displayName: "Install PySide"
82 |
83 | - script: |
84 | sudo apt-get install python3-pyside2.qtcore \
85 | python3-pyside2.qtgui \
86 | python3-pyside2.qtwidgets \
87 | python3-pyside2.qtsvg
88 | sudo pip install pyside2
89 | sudo python -c "from PySide2 import QtCore;print(QtCore.__version__)"
90 | condition: startsWith(variables['python.version'], '3.')
91 | displayName: "Install PySide2"
92 |
93 | - script: |
94 | sudo pip install nose
95 | displayName: "Install test tools"
96 |
97 | - script: |
98 | sudo pip install . --no-deps
99 | displayName: "Install allzpark"
100 |
101 | - script: |
102 | sudo apt-get install xvfb
103 | displayName: "Setup Xvfb"
104 |
105 | - script: |
106 | export DISPLAY=:99
107 | xvfb-run sudo nosetests
108 | displayName: "Run tests"
109 |
110 |
111 | - job: MacOS
112 | pool:
113 | vmImage: "macOS-10.15"
114 | strategy:
115 | matrix:
116 | Py37-Rez:
117 | python.version: "3.7"
118 | rez.project: "rez"
119 |
120 | Py37-BleedingRez:
121 | python.version: "3.7"
122 | rez.project: "bleeding-rez"
123 |
124 | steps:
125 | - task: UsePythonVersion@0
126 | inputs:
127 | versionSpec: "$(python.version)"
128 | displayName: "Use Python $(python.version)"
129 |
130 | - script: |
131 | git clone https://github.com/nerdvegas/rez.git rez-src
132 | cd rez-src
133 | pip install .
134 | condition: eq(variables['rez.project'], 'rez')
135 | displayName: "Install rez (pip for API)"
136 |
137 | - script: |
138 | pip install bleeding-rez
139 | condition: eq(variables['rez.project'], 'bleeding-rez')
140 | displayName: "Install bleeding-rez"
141 |
142 | - script: |
143 | brew tap cartr/qt4
144 | brew install qt@4
145 | pip install PySide
146 | condition: startsWith(variables['python.version'], '2.')
147 | displayName: "Install PySide"
148 |
149 | - script: |
150 | pip install PySide2
151 | condition: startsWith(variables['python.version'], '3.')
152 | displayName: "Install PySide2"
153 |
154 | - script: |
155 | pip install nose
156 | displayName: "Install test tools"
157 |
158 | - script: |
159 | pip install . --no-deps
160 | displayName: "Install allzpark"
161 |
162 | - script: |
163 | nosetests
164 | displayName: "Run tests"
165 |
166 | - job: Windows
167 | pool:
168 | vmImage: windows-latest
169 | strategy:
170 | matrix:
171 | Py37-Rez:
172 | python.version: "3.7"
173 | rez.project: "rez"
174 |
175 | Py37-BleedingRez:
176 | python.version: "3.7"
177 | rez.project: "bleeding-rez"
178 |
179 | steps:
180 | - task: UsePythonVersion@0
181 | inputs:
182 | versionSpec: "$(python.version)"
183 | displayName: "Use Python $(python.version)"
184 |
185 | - script: |
186 | git clone https://github.com/nerdvegas/rez.git rez-src
187 | cd rez-src
188 | pip install .
189 | condition: eq(variables['rez.project'], 'rez')
190 | displayName: "Install rez (pip for API)"
191 |
192 | - script: |
193 | pip install bleeding-rez
194 | condition: eq(variables['rez.project'], 'bleeding-rez')
195 | displayName: "Install bleeding-rez"
196 |
197 | - script: |
198 | pip install PySide
199 | condition: startsWith(variables['python.version'], '2.')
200 | displayName: "Install PySide"
201 |
202 | - script: |
203 | pip install PySide2
204 | condition: startsWith(variables['python.version'], '3.')
205 | displayName: "Install PySide2"
206 |
207 | - script: |
208 | pip install nose
209 | displayName: "Install test tools"
210 |
211 | - script: |
212 | pip install . --no-deps
213 | displayName: "Install allzpark"
214 |
215 | - script: |
216 | nosetests
217 | displayName: "Run tests"
218 |
219 |
220 | # -----------------------------------------------------------------------
221 | #
222 | # Deploy to PyPI
223 | #
224 | # -----------------------------------------------------------------------
225 |
226 | - job: Deploy
227 | condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
228 | pool:
229 | vmImage: "ubuntu-latest"
230 | strategy:
231 | matrix:
232 | Python37:
233 | python.version: "3.7"
234 |
235 | steps:
236 | - task: UsePythonVersion@0
237 | inputs:
238 | versionSpec: "$(python.version)"
239 | displayName: "Use Python $(python.version)"
240 |
241 | - script: |
242 | pip install wheel twine
243 | python setup.py sdist bdist_wheel
244 | echo [distutils] > ~/.pypirc
245 | echo index-servers=pypi >> ~/.pypirc
246 | echo [pypi] >> ~/.pypirc
247 | echo username=$_LOGIN >> ~/.pypirc
248 | echo password=$_PASSWORD >> ~/.pypirc
249 | twine upload dist/*
250 | displayName: "Deploy to PyPI"
251 |
252 | # Decrypt secret variables provided by Azure web console
253 | env:
254 | _LOGIN: $(PYPI_LOGIN)
255 | _PASSWORD: $(PYPI_PASSWORD)
256 |
--------------------------------------------------------------------------------
/LICENCE.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 |
--------------------------------------------------------------------------------
/docs/pages/avalon.md:
--------------------------------------------------------------------------------
1 | This page provides a transition guide from using [Avalon](https://getavalon.github.io)'s native Launcher to Allzpark.
2 |
3 |
4 |
5 | ### Walkthrough
6 |
7 |
8 |
9 |
10 |
11 | ### Quickstart
12 |
13 | This part assumes a successful [Quickstart](/quickstart).
14 |
15 |
40 |
41 | ```bash
42 | # Convert PyPI packages to Rez
43 | git clone https://github.com/mottosso/rez-pipz.git
44 | cd rez-pipz
45 | rez build --install
46 | cd ..
47 |
48 | # Install PyPI dependencies
49 | rez env pipz -- install avalon-core avalon-colorbleed
50 | rez env pipz -- install pyqt5==5.8 # Any PyQt5 would do, this one requires Python 3
51 |
52 | # Install Avalon demo project
53 | git clone https://github.com/mottosso/bleed.git
54 | cd bleed
55 | rez build --install
56 | python -m avalon.inventory --save
57 |
58 | # Launch!
59 | allzpark --demo
60 | ```
61 |
62 | ??? question "`~/projects`"
63 | For the purposes of this demo, the Avalon projects are assumed to be in this directory.
64 |
65 | ??? question "pipz"
66 | These are the base requirements for running Avalon
67 |
68 | ??? question "REZ_PACKAGES_PATH"
69 | For the purposes of this demo, we'll expose the Allzpark demo packages, primarily `maya`, `blender` and `rezutil` for use when building `bleed`. We could also have made a `maya`, `blender` and `rezutil` package globally, and skipped this step.
70 |
71 | ??? question "avalon.inventory"
72 | We'll also need this project uploaded to Avalon's MongoDB database. I'd like to skip this step, to instead automatically create projects post-launch, based on variables set in the Allzpark profile.
73 |
74 |
75 |
76 | ### Bleed Profile
77 |
78 | Let's have a look at how `bleed` is laid out.
79 |
80 | - [bleed/package.py](https://github.com/mottosso/bleed/blob/master/package.py)
81 |
82 | ```py
83 | name = "bleed"
84 | version = "1.0.15"
85 | build_command = "python -m rezutil build {root}"
86 | private_build_requires = ["rezutil-1"]
87 | ```
88 |
89 | Nothing special here; we're building on the `rezutil` package from the Allzpark demo library to simplify the installation somewhat, it handles copying of the contained `userSetup.py`.
90 |
91 | ```py
92 | _requires = [
93 | "~blender==2.80.0",
94 | "~maya==2015.0.0|2016.0.2|2017.0.4|2018.0.6",
95 |
96 | "pymongo-3.4+",
97 |
98 | "avalon_core-5.2+",
99 | "avalon_colorbleed-1",
100 | ]
101 | ```
102 |
103 | Again we're referencing `blender` and `maya` from the demo library, in this case a number of versions of Maya to cover all bases. Allzpark displays every version that matches this pattern, in this case 4 versions of Maya.
104 |
105 | `pymongo` is the only real dependency to Avalon, the others being vendored, due to being the only one that isn't a pure-Python library. Finally, Avalon core and the colorbleed config is added are requirements to this profile.
106 |
107 | ```py
108 | @late()
109 | def requires():
110 | global this
111 | global request
112 | global in_context
113 |
114 | requires = this._requires
115 |
116 | # Add request-specific requirements
117 | if in_context():
118 | if "maya" in request:
119 | requires += [
120 | "mgear",
121 | ]
122 |
123 | return requires
124 | ```
125 |
126 | You'll notices the previous `_requires = []` had an underscore in it, which makes it invisible to Rez. Instead, we use this function `requires()` with a `@late()` decorator which makes Rez compute the requirements of this package when called, as opposed to when built.
127 |
128 | If called during built, the previously specified requirements are included. However, when called the `in_context()` function evaluates to `True` which in turn queries the request we made, e.g. `rez env bleed maya`, for whether "maya" was included. If so, then it goes ahead and appends `mgear` as another requirement for this profile.
129 |
130 | This is how you can specifiy *conditional* requirements for a given profile, requirements that come into effect only when used in combination with a particular set of requirements, like `maya`. In this case, `mgear` is only relevant to Maya, and not Blender.
131 |
132 | ```py
133 | def commands():
134 | import os
135 | import tempfile
136 |
137 | global env
138 | global this
139 | global request
140 |
141 | # Better suited for a global/studio package
142 | projects = r"p:\projects" if os.name == "nt" else "~/projects"
143 |
144 | env["AVALON_PROJECTS"] = projects
145 | env["AVALON_CONFIG"] = "colorbleed"
146 | env["AVALON_PROJECT"] = this.name
147 | env["AVALON_EARLY_ADOPTER"] = "yes"
148 | ```
149 |
150 | Next we give configure Allzpark with the necessary environment variables.
151 |
152 | - `AVALON_PROJECTS` is typically a global value for your studio, and better suited for a package required by every profile, like a `global` or `studio` package. I've included it here to keep the example self-contained.
153 | - `AVALON_CONFIG` here we reference the `avalon_colorbleed` requirement
154 | - `AVALON_PROJECT` storing the project name into the environment, referencing the `this` variable, which is the equivalent of `self` from within a class; it references the members from outside of the `commands()` function, in this case the `name` of the package itself; "bleed"
155 | - `AVALON_EARLY_ADOPTER` finally enabling some of the later features of Avalon
156 |
157 | ```py
158 | if "maya" in request:
159 | env["PYTHONPATH"].append("{root}/maya") # userSetup.py
160 | ```
161 |
162 | Another conditional event; the `bleed` package includes a folder of profile-specific Maya scripts that are added to `PYTHONPATH` only if `maya` is part of the request.
163 |
164 | ```py
165 | env["AVALON_TASK"] = "modeling"
166 | env["AVALON_ASSET"] = "hero"
167 | env["AVALON_SILO"] = "asset"
168 | env["AVALON_WORKDIR"] = tempfile.gettempdir()
169 | ```
170 |
171 | Finally, the members that we need to get rid of from the application launching process of Avalon; these need to happen post-launch.
172 |
173 |
174 |
175 | ### Differences
176 |
177 | Overall, Allzpark and Launcher are very similar.
178 |
179 | - **All Knowing** Launcher has all the information related to a project, asset and task
180 | - We'll need to split this responsibility and let Allzpark handle anything related to application startup, but leave assets and tasks to Avalon
181 | - **Working Directory** Launcher is responsible for creating a working directory, *prior* to application launch
182 | - Because is knows all of these things, it's a good fit for creating the initial working directory wherein an application saves data, like Maya's `workspace.mel` file and associated hierarchy. Because Allzpark isn't concerned with such things, we'll need to let the host deal with this.
183 | - An upside of this is that artists would then be able to switch task/asset/shot without a restart
184 |
185 |
186 |
187 | ### Todo
188 |
189 | - [ ] **DB** Create Avalon MongoDB project document post-launch
190 | - [ ] **Working Directory** Create working directory post-launch
191 | - E.g. via "Set Context"
192 | - [x] Create new assets interactively, rather than from .toml
193 | - I.e. launch "Project Manager" from Allzpark
194 |
195 | The above example works, but embeds too much information into the Allzpark profile, notably these:
196 |
197 | ```py
198 | env["AVALON_TASK"] = "modeling"
199 | env["AVALON_ASSET"] = "hero"
200 | env["AVALON_SILO"] = "asset"
201 | env["AVALON_WORKDIR"] = tempfile.gettempdir()
202 | ```
203 |
--------------------------------------------------------------------------------
/allzpark/vendor/QtImageViewer.py:
--------------------------------------------------------------------------------
1 | """ QtImageViewer.py: PyQt image viewer widget for a QPixmap in a QGraphicsView scene with mouse zooming and panning.
2 |
3 | """
4 |
5 | from .Qt.QtCore import Qt, QRectF, Signal
6 | from .Qt.QtGui import QImage, QPixmap, QPainterPath
7 | from .Qt.QtWidgets import QGraphicsView, QGraphicsScene
8 |
9 | from .Qt import QtCore, QtGui
10 |
11 |
12 | __author__ = "Marcel Goldschen-Ohm "
13 | __version__ = '0.9.0'
14 |
15 |
16 | class QtImageViewer(QGraphicsView):
17 | """ PyQt image viewer widget for a QPixmap in a QGraphicsView scene with mouse zooming and panning.
18 |
19 | Displays a QImage or QPixmap (QImage is internally converted to a QPixmap).
20 | To display any other image format, you must first convert it to a QImage or QPixmap.
21 |
22 | Some useful image format conversion utilities:
23 | qimage2ndarray: NumPy ndarray <==> QImage (https://github.com/hmeine/qimage2ndarray)
24 | ImageQt: PIL Image <==> QImage (https://github.com/python-pillow/Pillow/blob/master/PIL/ImageQt.py)
25 |
26 | Mouse interaction:
27 | Left mouse button drag: Pan image.
28 | Right mouse button drag: Zoom box.
29 | Right mouse button doubleclick: Zoom to show entire image.
30 | """
31 |
32 | # Mouse button signals emit image scene (x, y) coordinates.
33 | # !!! For image (row, column) matrix indexing, row = y and column = x.
34 | leftMouseButtonPressed = Signal(float, float)
35 | rightMouseButtonPressed = Signal(float, float)
36 | leftMouseButtonReleased = Signal(float, float)
37 | rightMouseButtonReleased = Signal(float, float)
38 | leftMouseButtonDoubleClicked = Signal(float, float)
39 | rightMouseButtonDoubleClicked = Signal(float, float)
40 |
41 | def __init__(self):
42 | QGraphicsView.__init__(self)
43 |
44 | # Image is displayed as a QPixmap in a QGraphicsScene attached to this QGraphicsView.
45 | self.scene = QGraphicsScene()
46 | self.setScene(self.scene)
47 | self.setCursor(QtCore.Qt.PointingHandCursor)
48 |
49 | # Store a local handle to the scene's current image pixmap.
50 | self._pixmapHandle = None
51 |
52 | # Image aspect ratio mode.
53 | # !!! ONLY applies to full image. Aspect ratio is always ignored when zooming.
54 | # Qt.IgnoreAspectRatio: Scale image to fit viewport.
55 | # Qt.KeepAspectRatio: Scale image to fit inside viewport, preserving aspect ratio.
56 | # Qt.KeepAspectRatioByExpanding: Scale image to fill the viewport, preserving aspect ratio.
57 | self.aspectRatioMode = Qt.KeepAspectRatio
58 |
59 | # Scroll bar behaviour.
60 | # Qt.ScrollBarAlwaysOff: Never shows a scroll bar.
61 | # Qt.ScrollBarAlwaysOn: Always shows a scroll bar.
62 | # Qt.ScrollBarAsNeeded: Shows a scroll bar only when zoomed.
63 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
64 | self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
65 |
66 | # Stack of QRectF zoom boxes in scene coordinates.
67 | self.zoomStack = []
68 |
69 | # Flags for enabling/disabling mouse interaction.
70 | self.canZoom = True
71 | self.canPan = True
72 |
73 | def hasImage(self):
74 | """ Returns whether or not the scene contains an image pixmap.
75 | """
76 | return self._pixmapHandle is not None
77 |
78 | def clearImage(self):
79 | """ Removes the current image pixmap from the scene if it exists.
80 | """
81 | if self.hasImage():
82 | self.scene.removeItem(self._pixmapHandle)
83 | self._pixmapHandle = None
84 |
85 | def pixmap(self):
86 | """ Returns the scene's current image pixmap as a QPixmap, or else None if no image exists.
87 | :rtype: QPixmap | None
88 | """
89 | if self.hasImage():
90 | return self._pixmapHandle.pixmap()
91 | return None
92 |
93 | def image(self):
94 | """ Returns the scene's current image pixmap as a QImage, or else None if no image exists.
95 | :rtype: QImage | None
96 | """
97 | if self.hasImage():
98 | return self._pixmapHandle.pixmap().toImage()
99 | return None
100 |
101 | def setImage(self, image):
102 | """ Set the scene's current image pixmap to the input QImage or QPixmap.
103 | Raises a RuntimeError if the input image has type other than QImage or QPixmap.
104 | :type image: QImage | QPixmap
105 | """
106 | if type(image) is QPixmap:
107 | pixmap = image
108 | elif type(image) is QImage:
109 | pixmap = QPixmap.fromImage(image)
110 | else:
111 | raise RuntimeError("ImageViewer.setImage: Argument must be a QImage or QPixmap.")
112 | if self.hasImage():
113 | self._pixmapHandle.setPixmap(pixmap)
114 | else:
115 | self._pixmapHandle = self.scene.addPixmap(pixmap)
116 |
117 | self._pixmapHandle.setTransformationMode(QtCore.Qt.SmoothTransformation)
118 | self.setSceneRect(QRectF(pixmap.rect())) # Set scene size to image size.
119 | self.setRenderHints(QtGui.QPainter.Antialiasing |
120 | QtGui.QPainter.SmoothPixmapTransform)
121 |
122 | self.updateViewer()
123 |
124 | def updateViewer(self):
125 | """ Show current zoom (if showing entire image, apply current aspect ratio mode).
126 | """
127 | if not self.hasImage():
128 | return
129 | if len(self.zoomStack) and self.sceneRect().contains(self.zoomStack[-1]):
130 | self.fitInView(self.zoomStack[-1], Qt.KeepAspectRatio) # Show zoomed rect (ignore aspect ratio).
131 | else:
132 | self.zoomStack = [] # Clear the zoom stack (in case we got here because of an invalid zoom).
133 | self.fitInView(self.sceneRect(), self.aspectRatioMode) # Show entire image (use current aspect ratio mode).
134 |
135 | def resizeEvent(self, event):
136 | """ Maintain current zoom on resize.
137 | """
138 | self.updateViewer()
139 |
140 | def mousePressEvent(self, event):
141 | """ Start mouse pan or zoom mode.
142 | """
143 | scenePos = self.mapToScene(event.pos())
144 | if event.button() == Qt.LeftButton:
145 | if self.canPan:
146 | self.setDragMode(QGraphicsView.ScrollHandDrag)
147 | self.leftMouseButtonPressed.emit(scenePos.x(), scenePos.y())
148 | elif event.button() == Qt.RightButton:
149 | if self.canZoom:
150 | self.setDragMode(QGraphicsView.RubberBandDrag)
151 | self.rightMouseButtonPressed.emit(scenePos.x(), scenePos.y())
152 | QGraphicsView.mousePressEvent(self, event)
153 |
154 | def mouseReleaseEvent(self, event):
155 | """ Stop mouse pan or zoom mode (apply zoom if valid).
156 | """
157 | QGraphicsView.mouseReleaseEvent(self, event)
158 | scenePos = self.mapToScene(event.pos())
159 | if event.button() == Qt.LeftButton:
160 | self.setDragMode(QGraphicsView.NoDrag)
161 | self.leftMouseButtonReleased.emit(scenePos.x(), scenePos.y())
162 | elif event.button() == Qt.RightButton:
163 | if self.canZoom:
164 | viewBBox = self.zoomStack[-1] if len(self.zoomStack) else self.sceneRect()
165 | selectionBBox = self.scene.selectionArea().boundingRect().intersected(viewBBox)
166 | self.scene.setSelectionArea(QPainterPath()) # Clear current selection area.
167 | if selectionBBox.isValid() and (selectionBBox != viewBBox):
168 | self.zoomStack.append(selectionBBox)
169 | self.updateViewer()
170 | self.setDragMode(QGraphicsView.NoDrag)
171 | self.rightMouseButtonReleased.emit(scenePos.x(), scenePos.y())
172 |
173 | def mouseDoubleClickEvent(self, event):
174 | """ Show entire image.
175 | """
176 | scenePos = self.mapToScene(event.pos())
177 | if event.button() == Qt.LeftButton:
178 | self.leftMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
179 | elif event.button() == Qt.RightButton:
180 | if self.canZoom:
181 | self.zoomStack = [] # Clear zoom stack.
182 | self.updateViewer()
183 | self.rightMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
184 | QGraphicsView.mouseDoubleClickEvent(self, event)
185 |
--------------------------------------------------------------------------------
/docs/pages/rez.md:
--------------------------------------------------------------------------------
1 | This page is dedicated to learning Rez by example, utilising more of Rez's functionality as we go.
2 |
3 |
4 |
5 | ## Basics
6 |
7 | Let's start with the basics.
8 |
9 |
10 |
11 | ### Shortest Possible Example
12 |
13 | Create and use a new package from scratch in under 40 seconds.
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ```powershell
23 | mkdir mypackage # Name of your Git project
24 | cd mypackage # Rez definition
25 | @"
26 | name = "mypackage" # Rez package name
27 | version = "1.0" # Rez package version
28 | build_command = False # Called when building package
29 | "@ | Add-Content package.py
30 | rez build --install # Build package
31 | rez env mypackage # Use package
32 | > # A new environment with your package
33 | ```
34 |
35 |
36 |
37 |
38 |
39 | ```bash
40 | mkdir mypackage # Name of your Git project
41 | cd mypackage # Rez definition
42 | echo name = "mypackage" >> package.py # Rez package name
43 | echo version = "1.0" >> package.py # Rez package version
44 | echo build_command = False >> package.py # Called when building package
45 | rez build --install # Build package
46 | rez env mypackage # Use package
47 | > # A new environment with your package
48 | ```
49 |
50 |
51 |
52 | - The `>` symbol means you are in a Rez "context".
53 | - Type `exit` to exit the context.
54 |
55 |
56 |
57 | ### Environment Variables
58 |
59 | Most packages will modify their environment in some way.
60 |
61 | **package.py**
62 |
63 | ```python
64 | name = "mypackage"
65 | version = "1.1"
66 | build_command = False
67 |
68 | def commands():
69 | global env # Global variable available to `commands()`
70 | env["MYVARIABLE"] = "Yes"
71 | ```
72 |
73 | This package will assign `"Yes"` to MYVARIABLE.
74 |
75 | - `env` A global Python variable representing the environment
76 | - `env["MYVARIABLE"]` - An environment variable
77 | - `env.MYVARIABLE` - This is also OK
78 |
79 |
105 |
106 |
107 |
108 | ### Environment Paths
109 |
110 | A package can also modify paths, like `PATH` and `PYTHONPATH`, without removing what was there before.
111 |
112 | **package.py**
113 |
114 | ```python
115 | name = "mypackage"
116 | version = "1.2"
117 | build_command = False
118 |
119 | def commands():
120 | global env
121 | env["PYTHONPATH"].prepend("{root}")
122 | env["PYTHONPATH"].prepend("{root}/python")
123 | ```
124 |
125 | This package will assign `"{root}"` to `PYTHONPATH`.
126 |
127 | - `{root}` expands to the absolute path to the installed package
128 | - `env["PYTHONPATH"].prepend()` - Prepend a value to this variable
129 | - `env["PYTHONPATH"].append()` - Append a value to this variable
130 |
131 |
60 | Works on your machine?
61 |
62 |
63 | Allzpark is a package-based launcher, which means that everything related to a project is encapsulated into individual, version controlled and dependency managed "packages". Each coming together to form an environment identical across your development machine and anywhere your software is used.
64 |
65 |
156 | Reap optimal performance across the slowest of networks and disks with localisation, by turning any package into a locally accessible resource.
157 |
158 |
202 |
203 | Preview the environment, prior to launching an application. Make changes interactively as you develop or debug complex dependency chains.
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
222 |
223 |
224 |
225 |
226 |
Customisation
227 |
228 | Full theming support with pre-made color palettes to choose from. Interactively edit the underlying CSS and store them as your own.
229 |
230 |
231 | Drag panels around, establish a super-layout with everything visible at once.
232 |