├── clean ├── pyorcy ├── tests │ ├── __init__.py │ ├── test_simple.py │ └── compute.py ├── version.py ├── cli.py └── __init__.py ├── requirements.txt ├── requirements_test.txt ├── MANIFEST.in ├── .gitignore ├── RELEASE_NOTES.rst ├── .travis.yml ├── TODO ├── examples ├── compute_main.py └── compute_function.py ├── LICENSE.txt ├── setup.py ├── ANNOUNCEMENT.rst └── README.rst /clean: -------------------------------------------------------------------------------- 1 | git clean -fdx 2 | -------------------------------------------------------------------------------- /pyorcy/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cython>=0.23 2 | -------------------------------------------------------------------------------- /pyorcy/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.2' 2 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | numpy 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include *.txt 3 | recursive-include pyorcy *.py 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | MANIFEST 4 | dist 5 | build 6 | *.egg-info 7 | .coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /RELEASE_NOTES.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Release notes for pyorcy 3 | ========================= 4 | 5 | Changes from 0.0 to 0.1 6 | ======================= 7 | 8 | - Initial release 9 | 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.7 5 | - 3.4 6 | - 3.5 7 | 8 | sudo: false 9 | 10 | install: 11 | - pip install --upgrade pip 12 | - pip install cython numpy pytest 13 | 14 | script: 15 | - py.test pyorcy 16 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Compile only when the function is requested (so that the pure python 2 | development cycle does not get an annoying compilation step). 3 | 4 | hesitations 5 | - find a cleaner import mechanism (in particular without the "if 'pyximport'") 6 | - Try to put the .pyx file together with the built files in ~/.pyxbld? 7 | -------------------------------------------------------------------------------- /pyorcy/tests/test_simple.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from __future__ import absolute_import 3 | 4 | 5 | from time import time 6 | import pyorcy 7 | from .compute import f 8 | 9 | 10 | def timef(n, use_cython): 11 | pyorcy.USE_CYTHON = use_cython 12 | t1 = time() 13 | v = f(n, n) 14 | delta = time() - t1 15 | return delta, v 16 | 17 | 18 | def test_compute(): 19 | n = 1000 20 | t1, v1 = timef(n, False) 21 | t2, v2 = timef(n, True) 22 | assert t1 > 10 * t2 # speed-up should be at least 10x (typically 300x) 23 | assert v1 == v2 24 | -------------------------------------------------------------------------------- /examples/compute_main.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | from time import time 4 | import pyorcy 5 | 6 | from compute_function import f 7 | 8 | 9 | def main(): 10 | n = int(sys.argv[1]) 11 | t1 = timef(n, use_cython=False) 12 | t2 = timef(n, use_cython=True) 13 | if t2 != 0: 14 | print("speedup: %.1f" % (t1 / t2)) 15 | 16 | def timef(n, use_cython): 17 | pyorcy.USE_CYTHON = use_cython 18 | t1 = time() 19 | v = f(n, n) 20 | delta = time() - t1 21 | print("n = %d f = %.1f use_cython = %s time: %.3fs" 22 | % (n, v, use_cython, delta)) 23 | return delta 24 | 25 | main() 26 | -------------------------------------------------------------------------------- /pyorcy/tests/compute.py: -------------------------------------------------------------------------------- 1 | import pyorcy 2 | import numpy as np 3 | #c cimport cython 4 | 5 | def value(i, j, price, amount): #p 6 | #c @cython.boundscheck(False) 7 | #c cdef double value(int i, int j, double[:] price, double[:] amount): 8 | #c cdef int a, p 9 | #c cdef double v 10 | v = price[i] * amount[j] 11 | return v 12 | 13 | @pyorcy.cythonize #p 14 | def f(n, m): #p 15 | #c def f(int n, int m): 16 | #c cdef int i, j 17 | #c cdef double v 18 | #c cdef double[:] price 19 | #c cdef double[:] amount 20 | price = np.linspace(1, 2, n) 21 | amount = np.linspace(3, 4, m) 22 | v = 0 23 | for i in range(n): 24 | for j in range(m): 25 | v += value(i, j, price, amount) 26 | return v 27 | -------------------------------------------------------------------------------- /examples/compute_function.py: -------------------------------------------------------------------------------- 1 | import pyorcy 2 | import numpy as np 3 | #c cimport cython 4 | 5 | def value(i, j, price, amount): #p 6 | #c @cython.boundscheck(False) 7 | #c cdef double value(int i, int j, double[:] price, double[:] amount): 8 | #c cdef int a, p 9 | #c cdef double v 10 | v = price[i] * amount[j] 11 | return v 12 | 13 | @pyorcy.cythonize #p 14 | def f(n, m): #p 15 | #c def f(int n, int m): 16 | #c cdef int i, j 17 | #c cdef double v 18 | #c cdef double[:] price 19 | #c cdef double[:] amount 20 | price = np.linspace(1, 2, n) 21 | amount = np.linspace(3, 4, m) 22 | v = 0 23 | for i in range(n): 24 | for j in range(m): 25 | v += value(i, j, price, amount) 26 | return v 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | pyorcy - Mix Python and Cython code in the same module. 2 | 3 | Copyright (C) 2016 Marko Loparic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import sys 3 | 4 | with open('README.rst') as f: 5 | long_description = f.read() 6 | 7 | with open('pyorcy/version.py') as f: 8 | exec(f.read()) 9 | 10 | with open('requirements.txt') as f: 11 | install_requires = f.read().splitlines() 12 | 13 | with open('requirements_test.txt') as f: 14 | tests_require = f.read().splitlines() 15 | 16 | setup( 17 | name = "pyorcy", 18 | version = __version__, 19 | packages = ['pyorcy', 'pyorcy.tests'], 20 | entry_points = { 21 | 'console_scripts' : [ 22 | 'pyorcy = pyorcy.cli:main', 23 | ] 24 | }, 25 | author = "Marko Loparic, Francesc Alted", 26 | author_email = "marko.loparic@gmail.com, faltet@gmail.com", 27 | description = "Mix Python and Cython code in the same module.", 28 | long_description = long_description, 29 | license = "MIT", 30 | keywords = ('compression', 'applied information theory'), 31 | url = "https://github.com/blosc/bloscpack", 32 | install_requires = install_requires, 33 | extras_require = dict(tests=tests_require), 34 | tests_require = tests_require, 35 | classifiers = ['Development Status :: 3 - Alpha', 36 | 'Environment :: Console', 37 | 'License :: OSI Approved :: MIT License', 38 | 'Operating System :: Microsoft :: Windows', 39 | 'Operating System :: POSIX', 40 | 'Programming Language :: Python', 41 | 'Topic :: Scientific/Engineering', 42 | 'Topic :: Utilities', 43 | 'Programming Language :: Python', 44 | 'Programming Language :: Python :: 2.6', 45 | 'Programming Language :: Python :: 2.7', 46 | 'Programming Language :: Python :: 3.4', 47 | 'Programming Language :: Python :: 3.5', 48 | ], 49 | ) 50 | -------------------------------------------------------------------------------- /pyorcy/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | from __future__ import absolute_import 5 | 6 | import argparse 7 | import sys 8 | import os.path 9 | import runpy 10 | 11 | import cython 12 | import pyorcy 13 | 14 | 15 | def create_parser(): 16 | """ Create and return the parser. """ 17 | parser = argparse.ArgumentParser( 18 | description='command line utility for the pyorcy package') 19 | 20 | # print version of pyorcy and cython itself 21 | version_str = ("pyorcy: {} cython: {}".format( 22 | pyorcy.__version__, cython.__version__)) 23 | parser.add_argument('-V', '--version', action='version', 24 | version=version_str) 25 | parser.add_argument('-v', '--verbose', 26 | action='store_true', 27 | default=False, 28 | help='be verbose about actions') 29 | mode_group = parser.add_mutually_exclusive_group(required=False) 30 | mode_group.add_argument('-p', '--python', 31 | action='store_true', 32 | default=False, 33 | help='use Python for evaluating function') 34 | mode_group.add_argument('-c', '--cython', 35 | action='store_true', 36 | default=True, 37 | help='use Cython for evaluating function') 38 | parser.add_argument('MODULE', nargs=1) 39 | parser.add_argument('mod_args', nargs=argparse.REMAINDER) 40 | return parser 41 | 42 | 43 | def main(): 44 | parser = create_parser() 45 | args = parser.parse_args() 46 | 47 | # Arguments that drive the behaviour of pyorcy 48 | pyorcy.USE_CYTHON = True 49 | if args.python: 50 | pyorcy.USE_CYTHON = False 51 | if args.verbose: 52 | pyorcy.VERBOSE = True 53 | 54 | # Add the location of the module to the sys.path 55 | module = args.MODULE[0] 56 | sys.path.append(os.path.dirname(module)) 57 | 58 | # Add remaining parameters in globals 59 | init_globals = {'__args__': args.mod_args} 60 | 61 | # Execute the module 62 | runpy.run_path(module, init_globals=init_globals, run_name="__main__") 63 | 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /ANNOUNCEMENT.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Announcing pyorcy 0.1 3 | ====================== 4 | 5 | What's new 6 | ========== 7 | 8 | This is the first release of pyorcy. Your input is more than welcome! 9 | 10 | For a more detailed change log, see: 11 | 12 | https://github.com/markolopa/pyorcy/blob/master/RELEASE_NOTES.rst 13 | 14 | 15 | What it is 16 | ========== 17 | 18 | Pyorcy has 2 purposes: 19 | 20 | #. Allow the mix of python and cython code in a single file. This can 21 | also be done with cython's "pure python" mode, but with import 22 | limitations. pyorcy gives you the full cython super-powers. 23 | 24 | #. Launch an automatic compilation, triggered by a function 25 | decorator. This mechanism is similar to what numba offers. 26 | 27 | So basically, you can develop and debug using the python mode. When 28 | you are happy with your function, then you just annotate the variables 29 | and add the decorator for automatic Cython code generation and 30 | compilation. Simple. 31 | 32 | Here it is a simple example of how pyorcy code looks like: 33 | 34 | .. code-block:: python 35 | 36 | import pyorcy 37 | import numpy as np 38 | #c cimport cython 39 | 40 | def value(i, j, price, amount): #p 41 | #c @cython.boundscheck(False) 42 | #c cdef double value(int i, int j, double[:] price, double[:] amount): 43 | #c cdef int a, p 44 | #c cdef double v 45 | v = price[i] * amount[j] 46 | return v 47 | 48 | @pyorcy.cythonize #p 49 | def f(n, m): #p 50 | #c def f(int n, int m): 51 | #c cdef int i, j 52 | #c cdef double v 53 | #c cdef double[:] price 54 | #c cdef double[:] amount 55 | price = np.linspace(1, 2, n) 56 | amount = np.linspace(3, 4, m) 57 | v = 0 58 | for i in range(n): 59 | for j in range(m): 60 | v += value(i, j, price, amount) 61 | return v 62 | 63 | You can check a complete example here: 64 | `examples/compute_main.py`_ 65 | and 66 | `examples/compute_function.py`_. 67 | 68 | 69 | More info 70 | ========= 71 | 72 | Visit the main pyorcy site repository at: 73 | http://github.com/markolopa/pyorcy 74 | 75 | License is MIT: 76 | https://github.com/markolopa/pyorcy/blob/master/LICENSE.txt 77 | 78 | 79 | ---- 80 | 81 | Enjoy! 82 | -------------------------------------------------------------------------------- /pyorcy/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from __future__ import absolute_import 3 | 4 | import sys 5 | import re 6 | import os 7 | import importlib 8 | import inspect 9 | import pyximport 10 | from .version import __version__ 11 | 12 | pyximport.install() 13 | 14 | 15 | # Operation defaults 16 | USE_CYTHON = True 17 | VERBOSE = False 18 | 19 | 20 | def extract_cython(path_in, force=False, verbose=True): 21 | """Extract cython code from the .py file and create a _cy.pyx file. 22 | 23 | The script is called by the cythonize decorator. 24 | """ 25 | 26 | if not path_in.endswith('.py'): 27 | raise ValueError("%s is not a python file" % path_in) 28 | 29 | path_out = path_in.replace('.py', '_cy.pyx') 30 | if (not force and os.path.exists(path_out) and 31 | os.path.getmtime(path_out) >= os.path.getmtime(path_in)): 32 | if verbose: 33 | print("File %s already exists" % path_out) 34 | return 35 | 36 | if verbose: 37 | print("Creating %s" % path_out) 38 | with open(path_out, 'w') as fobj: 39 | for line in open(path_in): 40 | line = line.rstrip() 41 | m = re.match(r'( *)(.*)#p *$', line) 42 | if m: 43 | line = m.group(1) + '#p ' + m.group(2) 44 | else: 45 | line = re.sub(r'#c ', '', line) 46 | fobj.write(line + '\n') 47 | 48 | 49 | def import_module(name): 50 | """Import a Cython module via pyximport machinery.""" 51 | path = name.split('.') 52 | package = '.'.join(path[:-1]) 53 | name_last = path[-1] 54 | if package: 55 | # when there is a package, let's add a preceding dot (absolute_import) 56 | name_last = '.' + name_last 57 | return importlib.import_module(name_last, package) 58 | 59 | 60 | def cythonize(func): 61 | "Function decorator for triggering the pyorcy mechanism." 62 | if USE_CYTHON: 63 | # inspect usage found in http://stackoverflow.com/a/7151403 64 | func_filepath = inspect.getframeinfo(inspect.getouterframes( 65 | inspect.currentframe())[1][0])[0] 66 | extract_cython(func_filepath, verbose=VERBOSE) 67 | module_name = func.__module__ + '_cy' 68 | module = import_module(module_name) 69 | func_cy = getattr(module, func.__name__) 70 | 71 | def wrapper(*arg, **kw): 72 | if USE_CYTHON: 73 | if VERBOSE: 74 | print("Running via Cython mode") 75 | return func_cy(*arg, **kw) 76 | else: 77 | if VERBOSE: 78 | print("Running via Python mode") 79 | return func(*arg, **kw) 80 | 81 | return wrapper 82 | 83 | 84 | def test(): 85 | "Programatically run tests." 86 | import pytest 87 | sys.exit(pytest.main()) 88 | 89 | 90 | if __name__ == '__main__': 91 | extract_cython(sys.argv[1]) 92 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | pyorcy 3 | ====== 4 | 5 | .. image:: https://travis-ci.org/markolopa/pyorcy.svg?branch=master 6 | :target: https://travis-ci.org/markolopa/pyorcy 7 | 8 | 9 | Pyorcy has 2 purposes: 10 | 11 | #. Allow the mix of python and cython code in a single file. This can 12 | also be done with cython's "pure python" mode, but with import 13 | limitations. pyorcy gives you the full cython super-powers. 14 | 15 | #. Launch an automatic compilation, triggered by a function 16 | decorator. This mechanism is similar to what numba offers. 17 | 18 | So basically, you can develop and debug using the python mode. When 19 | you are happy with your function, then you just annotate the variables 20 | and add the decorator for automatic Cython code generation and 21 | compilation. Simple. 22 | 23 | Mechanism 24 | --------- 25 | 26 | The user writes a python file which is the module. The function which 27 | is to have a speedup is decorated with the @cythonize decorator. 28 | Something like this: 29 | 30 | .. code-block:: python 31 | 32 | import pyorcy 33 | import numpy as np 34 | #c cimport cython 35 | 36 | def value(i, j, price, amount): #p 37 | #c @cython.boundscheck(False) 38 | #c cdef double value(int i, int j, double[:] price, double[:] amount): 39 | #c cdef int a, p 40 | #c cdef double v 41 | v = price[i] * amount[j] 42 | return v 43 | 44 | @pyorcy.cythonize #p 45 | def f(n, m): #p 46 | #c def f(int n, int m): 47 | #c cdef int i, j 48 | #c cdef double v 49 | #c cdef double[:] price 50 | #c cdef double[:] amount 51 | price = np.linspace(1, 2, n) 52 | amount = np.linspace(3, 4, m) 53 | v = 0 54 | for i in range(n): 55 | for j in range(m): 56 | v += value(i, j, price, amount) 57 | return v 58 | 59 | A cython (``.pyx``) file is extracted from the python file. This 60 | extracted ``.pyx`` file will differ from the corresponding ``.py`` 61 | file is two ways: 62 | 63 | - The comments starting with '#c ' are uncommented. 64 | - The lines ending with '#p' are commented out. 65 | 66 | You can check a complete example here: `examples/compute_main.py `_ and `examples/compute_function.py `_. 67 | 68 | Installation 69 | ------------ 70 | 71 | Use pip:: 72 | 73 | $ pip install pyorcy 74 | 75 | or download the sources and:: 76 | 77 | $ python setup.py install 78 | 79 | Getting started 80 | --------------- 81 | 82 | Programmatic approach 83 | ..................... 84 | 85 | With the `f()` function above, use: 86 | 87 | .. code-block:: python 88 | 89 | import pyorcy 90 | from compute_function import f 91 | 92 | def execf(n, use_cython): 93 | pyorcy.USE_CYTHON = use_cython 94 | pyorcy.VERBOSE = True 95 | v = f(n, n) 96 | return v 97 | 98 | So, basically import the `pyorcy` package and then set the 99 | `USE_CYTHON` and `VERBOSE` module variables to your taste. Easy uh? 100 | 101 | There is a script in the examples/ folder that uses this technique. 102 | Go to the main pyorcy directory and type:: 103 | 104 | $ PYTHONPATH=. python examples/compute_main.py 1000 105 | Creating .../pyorcy/examples/compute_function_cy.pyx 106 | n = 1000 f = 5250000.0 use_cython = False time: 0.373s 107 | n = 1000 f = 5250000.0 use_cython = True time: 0.001s 108 | speedup: 311.9 109 | 110 | Type the command once again to see what happens when the cython code is 111 | already compiled and execution is immediate:: 112 | 113 | $ PYTHONPATH=. python examples/compute_main.py 1000 114 | File .../pyorcy/examples/compute_function_cy.pyx already exists 115 | n = 1000 f = 5250000.0 use_cython = False time: 0.375s 116 | n = 1000 f = 5250000.0 use_cython = True time: 0.001s 117 | speedup: 314.2 118 | 119 | Have a look at the examples/ directory for more hints on using pyorcy. 120 | 121 | Via the pyorcy utility 122 | ...................... 123 | 124 | There is another way to use the pyorcy package via its `pyorcy` 125 | utility:: 126 | 127 | $ time pyorcy -v --python examples/module_main.py 1000 128 | Running via Python mode 129 | n = 1000 f = 5250000.0 time: 0.528s 130 | 131 | real 0m0.748s 132 | user 0m0.720s 133 | sys 0m0.024s 134 | 135 | Now, using Cython:: 136 | 137 | $ time pyorcy -v --cython examples/module_main.py 1000 138 | Running via Cython mode 139 | Creating examples/compute_function_cy.pyx 140 | n = 1000 f = 5250000.0 time: 0.001s 141 | 142 | real 0m3.864s 143 | user 0m3.752s 144 | sys 0m0.088s 145 | 146 | Although we see that the time for the computation is very small, the 147 | global execution time for the script is quite large. This is due to 148 | the compilation time (.pyx -> .c creation + C compiling time). 149 | However, the Cython version and the compiled extension are cached so 150 | that next time that the module is executed the cached versions are 151 | used instead:: 152 | 153 | $ time pyorcy -v --cython examples/module_main.py 1000 154 | Running via Cython mode 155 | File examples/compute_function_cy.pyx already exists 156 | n = 1000 f = 5250000.0 time: 0.001s 157 | 158 | real 0m0.264s 159 | user 0m0.240s 160 | sys 0m0.020s 161 | 162 | This utility allows to execute complete modules with the @cythonize 163 | decorators in either '--python' (useful for debugging) or '--cython' 164 | mode (the default). 165 | 166 | Testing 167 | ------- 168 | 169 | Before installing, you can test the package like this:: 170 | 171 | $ py.test pyorcy 172 | 173 | And after installing with (although this might fail if you install as 174 | root and run tests as a regular user):: 175 | 176 | $ python -c"import pyorcy; pyorcy.test() 177 | 178 | Troubleshooting 179 | --------------- 180 | 181 | If you get:: 182 | 183 | ImportError: Building module compute_cy failed: ['DistutilsPlatformError: Unable to find vcvarsall.bat\n'] 184 | 185 | like I did, contact me. I have found a workaround. 186 | 187 | My use case 188 | ----------- 189 | 190 | Here is why is pyorcy is important for my work. 191 | 192 | I work in a team of engineers and mathematicians. They have learnt 193 | python but not cython. Recently I have proposed a library with some 194 | cython code. This added dependency has created resistance to the 195 | acceptance of my code. Firstly, we met problems with compatibility 196 | with Cython, Anaconda and virtual environments. Secondly, when my 197 | collegues find bugs, they are not happy to depend on my help. They 198 | want to do the debugging themselves. As they don't know Cython and are 199 | uncomfortable with the compilation issues, I decided to provide two 200 | versions of my code, one in pure python and another in Cython. Of 201 | course maintaining two versions of my functions is not an advisable 202 | approach. Using cython pure python mode is not an option since the 203 | code needs advanced cython capabilities. 204 | 205 | With pyorcy the user can then add a ``pyorcy.USE_CYTHON = False`` 206 | before the function call that they want to debug and proceed the 207 | debugging in the pure python version, being able to add prints and 208 | pbd without having to recompile, nor having to learn cython. 209 | 210 | Before presenting pyorcy, a colleague suggested me to switch from 211 | cython to numba. This would solve some of the issues, but I would 212 | loose the freedom that cython gives (e.g. mix pure C code when needed) 213 | and the wonderful html output (which gives us a perfect control of 214 | what runs behind the scenes). Pyorcy comes partly as an answer to his 215 | suggestion. 216 | --------------------------------------------------------------------------------