├── reg ├── tests │ ├── __init__.py │ ├── fixtures │ │ ├── __init__.py │ │ └── module.py │ ├── test_docgen.py │ ├── test_classdispatch.py │ ├── test_arginfo.py │ ├── test_predicate.py │ ├── test_registry.py │ └── test_dispatch_method.py ├── error.py ├── __init__.py ├── cache.py ├── arginfo.py ├── context.py ├── predicate.py └── dispatch.py ├── doc ├── changes.rst ├── internals.rst ├── index.rst ├── api.rst ├── developing.rst ├── make.bat ├── Makefile ├── patterns.rst ├── history.rst ├── conf.py ├── context.rst └── usage.rst ├── develop_requirements.txt ├── MANIFEST.in ├── setup.cfg ├── .gitignore ├── .pre-commit-config.yaml ├── CREDITS.txt ├── README.rst ├── tox.ini ├── setup.py ├── LICENSE.txt ├── profdispatch.py ├── .github └── workflows │ └── main.yml ├── perf.py ├── tox_perf.py └── CHANGES.txt /reg/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reg/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/changes.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.txt 2 | -------------------------------------------------------------------------------- /reg/error.py: -------------------------------------------------------------------------------- 1 | class RegistrationError(Exception): 2 | """Registration error.""" 3 | -------------------------------------------------------------------------------- /develop_requirements.txt: -------------------------------------------------------------------------------- 1 | # development 2 | -e '.[test,coverage,pep8,docs]' 3 | pre-commit 4 | tox >= 2.4.1 5 | radon 6 | 7 | # releaser 8 | zest.releaser[recommended] 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.rst *.cfg *.py *.ini *.toml *.yaml 2 | exclude .installed.cfg 3 | recursive-include reg *.py 4 | recursive-include tests *.py 5 | recursive-include doc *.rst Makefile *.py *.bat 6 | include .coveragerc -------------------------------------------------------------------------------- /doc/internals.rst: -------------------------------------------------------------------------------- 1 | Internals 2 | ========= 3 | 4 | This section of the documentation descibes the internal code objects 5 | of Reg. The information included here is of interest only if you are 6 | thinking of contributing to Reg. 7 | 8 | .. autoclass:: reg.predicate.PredicateRegistry 9 | :members: 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | show-source = True 3 | ignore = E203, E731, W503 4 | max-line-length = 88 5 | 6 | [tool:pytest] 7 | testpaths = reg 8 | 9 | [coverage:run] 10 | omit = reg/tests/* 11 | source = reg 12 | 13 | [coverage:report] 14 | show_missing = True 15 | 16 | [zest.releaser] 17 | create-wheel = yes 18 | 19 | [bdist_wheel] 20 | universal = 1 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized 2 | __pycache__/ 3 | *.py[co] 4 | 5 | # Distribution / packaging 6 | *.egg-info/ 7 | /build/ 8 | /dist/ 9 | pip-wheel-metadata 10 | 11 | # Virtualenv 12 | /env/ 13 | /src/ 14 | 15 | # Unit test / coverage reports 16 | .cache/ 17 | /.coverage 18 | /htmlcov 19 | /.tox 20 | 21 | # Sphinx documentation 22 | /doc/build/ 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 20.8b1 4 | hooks: 5 | - id: black 6 | - repo: https://github.com/PyCQA/flake8 7 | rev: "3.8.4" 8 | hooks: 9 | - id: flake8 10 | - repo: https://github.com/asottile/pyupgrade 11 | rev: v2.7.4 12 | hooks: 13 | - id: pyupgrade 14 | args: [--py36-plus] 15 | -------------------------------------------------------------------------------- /reg/tests/fixtures/module.py: -------------------------------------------------------------------------------- 1 | "Sample module for testing autodoc." 2 | 3 | from reg import dispatch_method, dispatch 4 | 5 | 6 | class Foo: 7 | "Class for foo objects." 8 | 9 | @dispatch_method("obj") 10 | def bar(self, obj): 11 | "Return the bar of an object." 12 | return "default" 13 | 14 | def baz(self, obj): 15 | "Return the baz of an object." 16 | 17 | 18 | @dispatch("obj") 19 | def foo(obj): 20 | "return the foo of an object." 21 | return "default" 22 | -------------------------------------------------------------------------------- /reg/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .dispatch import dispatch, Dispatch, LookupEntry 3 | from .context import ( 4 | dispatch_method, 5 | DispatchMethod, 6 | methodify, 7 | clean_dispatch_methods, 8 | ) 9 | from .arginfo import arginfo 10 | from .error import RegistrationError 11 | from .predicate import ( 12 | Predicate, 13 | KeyIndex, 14 | ClassIndex, 15 | match_key, 16 | match_instance, 17 | match_class, 18 | ) 19 | from .cache import DictCachingKeyLookup, LruCachingKeyLookup 20 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Reg's documentation 2 | ============================== 3 | 4 | Reg is a Python library that provides generic function support to 5 | Python. It help you build powerful registration and configuration APIs 6 | for your application, library or framework. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | usage 12 | context 13 | patterns 14 | api 15 | developing 16 | internals 17 | history 18 | changes 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | CREDITS 2 | ------- 3 | 4 | Development 5 | ----------- 6 | 7 | * Martijn Faassen 8 | 9 | * Stefano Taschini 10 | 11 | With contributions by 12 | --------------------- 13 | 14 | * Henri Hulski (build environment) 15 | 16 | * Thomas Lotze (co-authored original core mapping code) 17 | 18 | Special contributions 19 | --------------------- 20 | 21 | * Special thanks to CONTACT software (http://contact.de) 22 | 23 | Special inspiration 24 | ------------------- 25 | 26 | Reg is heavily inspired by ``zope.interface`` and ``zope.component``, 27 | by Jim Fulton and a lot of Zope developers. It is also inspired by 28 | Pyramid's view predicate system. 29 | 30 | Thanks also to Chris McDonough who showed the way, reimplementing code 31 | to make it better. 32 | 33 | See also doc/history.rst for a more detailed history of its development. 34 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/morepath/reg/workflows/CI/badge.svg?branch=master 2 | :target: https://github.com/morepath/reg/actions?workflow=CI 3 | :alt: CI Status 4 | 5 | .. image:: https://coveralls.io/repos/github/morepath/reg/badge.svg?branch=master 6 | :target: https://coveralls.io/github/morepath/reg?branch=master 7 | 8 | .. image:: https://img.shields.io/pypi/v/reg.svg 9 | :target: https://pypi.org/project/reg/ 10 | 11 | .. image:: https://img.shields.io/pypi/pyversions/reg.svg 12 | :target: https://pypi.org/project/reg/ 13 | 14 | 15 | Reg: Clever Dispatch 16 | ==================== 17 | 18 | Reg is a Python library that provides generic function support to 19 | Python. It help you build powerful registration and configuration APIs 20 | for your application, library or framework. 21 | 22 | Documentation_. 23 | 24 | .. _Documentation: http://reg.readthedocs.org 25 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37, py38, py39, pypy3, coverage, pre-commit, docs, perf 3 | skipsdist = True 4 | skip_missing_interpreters = True 5 | 6 | [testenv] 7 | usedevelop = True 8 | extras = test 9 | 10 | commands = pytest {posargs} 11 | 12 | [testenv:coverage] 13 | basepython = python 14 | extras = test 15 | coverage 16 | 17 | commands = pytest --cov --cov-fail-under=100 {posargs} 18 | 19 | [testenv:pre-commit] 20 | deps = pre-commit 21 | commands = pre-commit run --all-files 22 | 23 | [testenv:docs] 24 | basepython = python3 25 | extras = docs 26 | 27 | commands = sphinx-build -b doctest doc {envtmpdir} 28 | 29 | [testenv:perf] 30 | basepython = python3 31 | extras = 32 | 33 | commands = python {toxinidir}/tox_perf.py 34 | 35 | [gh-actions] 36 | python = 37 | 3.6: py36 38 | 3.7: py37, perf 39 | 3.8: py38 40 | 3.9: py39, pre-commit, mypy, coverage 41 | 42 | [flake8] 43 | max-line-length = 88 44 | ignore = 45 | E231 # clashes with black 46 | W503 47 | E731 # todo 48 | -------------------------------------------------------------------------------- /doc/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. py:module:: reg 5 | 6 | Dispatch functions 7 | ------------------ 8 | 9 | .. autofunction:: dispatch 10 | 11 | .. autoclass:: Dispatch 12 | :members: 13 | 14 | .. autofunction:: match_key 15 | 16 | .. autofunction:: match_instance 17 | 18 | .. autofunction:: match_class 19 | 20 | .. autoclass:: LookupEntry 21 | :members: 22 | 23 | .. autoclass:: DictCachingKeyLookup 24 | :members: 25 | 26 | .. autoclass:: LruCachingKeyLookup 27 | :members: 28 | 29 | Context-specific dispatch methods 30 | --------------------------------- 31 | 32 | .. autofunction:: dispatch_method 33 | 34 | .. autoclass:: DispatchMethod 35 | :members: 36 | :inherited-members: 37 | 38 | .. autofunction:: clean_dispatch_methods 39 | 40 | .. autofunction:: methodify 41 | 42 | Errors 43 | ------ 44 | 45 | .. autoexception:: RegistrationError 46 | 47 | Argument introspection 48 | ---------------------- 49 | 50 | .. autofunction:: arginfo 51 | 52 | Low-level predicate support 53 | --------------------------- 54 | 55 | Typically, you'd be using :func:`reg.match_key`, 56 | :func:`reg.match_instance`, and :func:`reg.match_class` to define 57 | predicates. Should you require finer control, you can use the 58 | following classes: 59 | 60 | .. autoclass:: Predicate 61 | :members: 62 | 63 | .. autoclass:: ClassIndex 64 | :members: 65 | 66 | .. autoclass:: KeyIndex 67 | :members: 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | long_description = "\n".join( 4 | ( 5 | open("README.rst", encoding="utf-8").read(), 6 | open("CHANGES.txt", encoding="utf-8").read(), 7 | ) 8 | ) 9 | 10 | setup( 11 | name="reg", 12 | version="0.13.dev0", 13 | description="Clever dispatch", 14 | long_description=long_description, 15 | author="Martijn Faassen", 16 | author_email="faassen@startifact.com", 17 | license="BSD", 18 | url="http://reg.readthedocs.io", 19 | packages=find_packages(), 20 | include_package_data=True, 21 | zip_safe=False, 22 | classifiers=[ 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: BSD License", 25 | "Topic :: Software Development :: Libraries :: Python Modules", 26 | "Programming Language :: Python :: 3.6", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: Implementation :: PyPy", 31 | "Development Status :: 5 - Production/Stable", 32 | ], 33 | install_requires=["setuptools", "repoze.lru"], 34 | extras_require=dict( 35 | test=["pytest >= 2.9.0", "sphinx", "pytest-remove-stale-bytecode"], 36 | pep8=["flake8", "black"], 37 | coverage=["pytest-cov"], 38 | docs=["sphinx"], 39 | ), 40 | ) 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2016, Reg Developers 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL FANSTATIC DEVELOPERS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /profdispatch.py: -------------------------------------------------------------------------------- 1 | from cProfile import run 2 | from reg import dispatch 3 | from reg import LruCachingKeyLookup 4 | 5 | 6 | def get_key_lookup(r): 7 | return LruCachingKeyLookup( 8 | r, 9 | component_cache_size=5000, 10 | all_cache_size=5000, 11 | fallback_cache_size=5000, 12 | ) 13 | 14 | 15 | @dispatch(get_key_lookup=get_key_lookup) 16 | def args0(): 17 | raise NotImplementedError() 18 | 19 | 20 | @dispatch("a", get_key_lookup=get_key_lookup) 21 | def args1(a): 22 | raise NotImplementedError() 23 | 24 | 25 | @dispatch("a", "b", get_key_lookup=get_key_lookup) 26 | def args2(a, b): 27 | raise NotImplementedError() 28 | 29 | 30 | @dispatch("a", "b", "c", get_key_lookup=get_key_lookup) 31 | def args3(a, b, c): 32 | raise NotImplementedError() 33 | 34 | 35 | @dispatch("a", "b", "c", "d", get_key_lookup=get_key_lookup) 36 | def args4(a, b, c, d): 37 | raise NotImplementedError() 38 | 39 | 40 | class Foo: 41 | pass 42 | 43 | 44 | def myargs0(): 45 | return "args0" 46 | 47 | 48 | def myargs1(a): 49 | return "args1" 50 | 51 | 52 | def myargs2(a, b): 53 | return "args2" 54 | 55 | 56 | def myargs3(a, b, c): 57 | return "args3" 58 | 59 | 60 | def myargs4(a, b, c, d): 61 | return "args4" 62 | 63 | 64 | args0.register(myargs0) 65 | args1.register(myargs1, a=Foo) 66 | args2.register(myargs2, a=Foo, b=Foo) 67 | args3.register(myargs3, a=Foo, b=Foo, c=Foo) 68 | args4.register(myargs4, a=Foo, b=Foo, c=Foo, d=Foo) 69 | 70 | 71 | def repeat_args4(): 72 | for i in range(10000): 73 | args4(Foo(), Foo(), Foo(), Foo()) 74 | 75 | 76 | run("repeat_args4()", sort="tottime") 77 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events 6 | push: 7 | pull_request: 8 | schedule: 9 | - cron: '0 12 * * 0' # run once a week on Sunday 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | tests: 17 | name: "Python ${{ matrix.python-version }}" 18 | runs-on: "ubuntu-latest" 19 | 20 | strategy: 21 | matrix: 22 | python-version: ["3.6", "3.7", "3.8", "3.9", "pypy3"] 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 27 | - uses: "actions/checkout@v2" 28 | - uses: "actions/setup-python@v2" 29 | with: 30 | python-version: "${{ matrix.python-version }}" 31 | - name: "Install dependencies" 32 | run: | 33 | set -xe 34 | python -VV 35 | python -m site 36 | python -m pip install --upgrade pip setuptools wheel 37 | python -m pip install --upgrade virtualenv tox tox-gh-actions 38 | - name: "Run tox targets for ${{ matrix.python-version }}" 39 | run: "python -m tox" 40 | 41 | - name: "Report to coveralls" 42 | # coverage is only created in the py39 environment 43 | # --service=github is a workaround for bug 44 | # https://github.com/coveralls-clients/coveralls-python/issues/251 45 | if: "matrix.python-version == '3.9'" 46 | run: | 47 | pip install coveralls 48 | coveralls --service=github 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /reg/cache.py: -------------------------------------------------------------------------------- 1 | from repoze.lru import lru_cache 2 | 3 | 4 | class Cache(dict): 5 | """A dict to cache a function.""" 6 | 7 | def __init__(self, func): 8 | self.func = func 9 | 10 | def __missing__(self, key): 11 | self[key] = result = self.func(key) 12 | return result 13 | 14 | 15 | class DictCachingKeyLookup: 16 | """A key lookup that caches. 17 | 18 | Implements the read-only API of :class:`reg.PredicateRegistry` using 19 | a cache to speed up access. 20 | 21 | This cache is backed by a simple dictionary so could potentially 22 | grow large if the dispatch in question can be called with a large 23 | combination of arguments that result in a large range of different 24 | predicate keys. If so, you can use 25 | :class:`reg.LruCachingKeyLookup` instead. 26 | 27 | :param: key_lookup - the :class:`PredicateRegistry` to cache. 28 | 29 | """ 30 | 31 | def __init__(self, key_lookup): 32 | self.key_lookup = key_lookup 33 | self.component = Cache(key_lookup.component).__getitem__ 34 | self.fallback = Cache(key_lookup.fallback).__getitem__ 35 | self.all = Cache(lambda key: list(key_lookup.all(key))).__getitem__ 36 | 37 | 38 | class LruCachingKeyLookup: 39 | """A key lookup that caches. 40 | 41 | Implements the read-only API of :class:`reg.PredicateRegistry`, using 42 | a cache to speed up access. 43 | 44 | The cache is LRU so won't grow beyond a certain limit, preserving 45 | memory. This is only useful if you except the access pattern to 46 | your function to involve a huge range of different predicate keys. 47 | 48 | :param: key_lookup - the :class:`PredicateRegistry` to cache. 49 | :param component_cache_size: how many cache entries to store for 50 | the :meth:`component` method. This is also used by dispatch 51 | calls. 52 | :param all_cache_size: how many cache entries to store for the 53 | the :meth:`all` method. 54 | :param fallback_cache_size: how many cache entries to store for 55 | the :meth:`fallback` method. 56 | """ 57 | 58 | def __init__( 59 | self, 60 | key_lookup, 61 | component_cache_size, 62 | all_cache_size, 63 | fallback_cache_size, 64 | ): 65 | self.key_lookup = key_lookup 66 | self.component = lru_cache(component_cache_size)(key_lookup.component) 67 | self.fallback = lru_cache(fallback_cache_size)(key_lookup.fallback) 68 | self.all = lru_cache(all_cache_size)(lambda key: list(key_lookup.all(key))) 69 | -------------------------------------------------------------------------------- /perf.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | 3 | from reg import dispatch 4 | from reg import DictCachingKeyLookup 5 | 6 | 7 | def get_key_lookup(r): 8 | return DictCachingKeyLookup(r) 9 | 10 | 11 | @dispatch(get_key_lookup=get_key_lookup) 12 | def args0(): 13 | raise NotImplementedError() 14 | 15 | 16 | @dispatch("a", get_key_lookup=get_key_lookup) 17 | def args1(a): 18 | raise NotImplementedError() 19 | 20 | 21 | @dispatch("a", "b", get_key_lookup=get_key_lookup) 22 | def args2(a, b): 23 | raise NotImplementedError() 24 | 25 | 26 | @dispatch("a", "b", "c", get_key_lookup=get_key_lookup) 27 | def args3(a, b, c): 28 | raise NotImplementedError() 29 | 30 | 31 | @dispatch("a", "b", "c", "d", get_key_lookup=get_key_lookup) 32 | def args4(a, b, c, d): 33 | raise NotImplementedError() 34 | 35 | 36 | class Foo: 37 | pass 38 | 39 | 40 | def myargs0(): 41 | return "args0" 42 | 43 | 44 | def myargs1(a): 45 | return "args1" 46 | 47 | 48 | def myargs2(a, b): 49 | return "args2" 50 | 51 | 52 | def myargs3(a, b, c): 53 | return "args3" 54 | 55 | 56 | def myargs4(a, b, c, d): 57 | return "args4" 58 | 59 | 60 | args0.register(myargs0) 61 | args1.register(myargs1, a=Foo) 62 | args2.register(myargs2, a=Foo, b=Foo) 63 | args3.register(myargs3, a=Foo, b=Foo, c=Foo) 64 | args4.register(myargs4, a=Foo, b=Foo, c=Foo, d=Foo) 65 | 66 | 67 | def docall0(): 68 | args0() 69 | 70 | 71 | def docall1(): 72 | args1(Foo()) 73 | 74 | 75 | def docall2(): 76 | args2(Foo(), Foo()) 77 | 78 | 79 | def docall3(): 80 | args3(Foo(), Foo(), Foo()) 81 | 82 | 83 | def docall4(): 84 | args4(Foo(), Foo(), Foo(), Foo()) 85 | 86 | 87 | def plain_docall0(): 88 | myargs0() 89 | 90 | 91 | def plain_docall4(): 92 | myargs4(Foo(), Foo(), Foo(), Foo()) 93 | 94 | 95 | print("dispatch 0 args") 96 | print(timeit.timeit("docall0()", setup="from __main__ import docall0")) 97 | 98 | print("dispatch 1 args") 99 | print(timeit.timeit("docall1()", setup="from __main__ import docall1")) 100 | 101 | print("dispatch 2 args") 102 | print(timeit.timeit("docall2()", setup="from __main__ import docall2")) 103 | 104 | print("dispatch 3 args") 105 | print(timeit.timeit("docall3()", setup="from __main__ import docall3")) 106 | 107 | print("dispatch 4 args") 108 | print(timeit.timeit("docall4()", setup="from __main__ import docall4")) 109 | 110 | print("Plain func 0 args") 111 | print(timeit.timeit("plain_docall0()", setup="from __main__ import plain_docall0")) 112 | 113 | print("Plain func 4 args") 114 | print(timeit.timeit("plain_docall4()", setup="from __main__ import plain_docall4")) 115 | -------------------------------------------------------------------------------- /reg/tests/test_docgen.py: -------------------------------------------------------------------------------- 1 | import pydoc 2 | from sphinx.application import Sphinx 3 | from .fixtures.module import Foo, foo 4 | 5 | 6 | def rstrip_lines(s): 7 | "Delete trailing spaces from each line in s." 8 | return "\n".join(line.rstrip() for line in s.splitlines()) 9 | 10 | 11 | def test_dispatch_method_class_help(capsys): 12 | pydoc.help(Foo) 13 | out, err = capsys.readouterr() 14 | assert ( 15 | rstrip_lines(out) 16 | == """\ 17 | Help on class Foo in module reg.tests.fixtures.module: 18 | 19 | class Foo({builtins}.object) 20 | | Class for foo objects. 21 | | 22 | | Methods defined here: 23 | | 24 | | bar(self, obj) 25 | | Return the bar of an object. 26 | | 27 | | baz(self, obj) 28 | | Return the baz of an object. 29 | | 30 | | ---------------------------------------------------------------------- 31 | | Data descriptors defined here: 32 | | 33 | | __dict__ 34 | | dictionary for instance variables (if defined) 35 | | 36 | | __weakref__ 37 | | list of weak references to the object (if defined) 38 | """.format( 39 | builtins=object.__module__ 40 | ) 41 | ) 42 | 43 | 44 | def test_dispatch_method_help(capsys): 45 | pydoc.help(Foo.bar) 46 | out, err = capsys.readouterr() 47 | assert ( 48 | rstrip_lines(out) 49 | == """\ 50 | Help on function bar in module reg.tests.fixtures.module: 51 | 52 | bar(self, obj) 53 | Return the bar of an object. 54 | """ 55 | ) 56 | 57 | 58 | def test_dispatch_help(capsys): 59 | pydoc.help(foo) 60 | out, err = capsys.readouterr() 61 | assert ( 62 | rstrip_lines(out) 63 | == """\ 64 | Help on function foo in module reg.tests.fixtures.module: 65 | 66 | foo(obj) 67 | return the foo of an object. 68 | """ 69 | ) 70 | 71 | 72 | def test_autodoc(tmpdir): 73 | root = str(tmpdir) 74 | tmpdir.join("conf.py").write("extensions = ['sphinx.ext.autodoc']\n") 75 | tmpdir.join("contents.rst").write( 76 | ".. automodule:: reg.tests.fixtures.module\n" " :members:\n" 77 | ) 78 | # status=None makes Sphinx completely quiet, in case you run 79 | # py.test with the -s switch. For debugging you might want to 80 | # remove it. 81 | app = Sphinx(root, root, root + "/build", root, "text", status=None) 82 | app.build() 83 | assert ( 84 | tmpdir.join("build/contents.txt").read() 85 | == """\ 86 | Sample module for testing autodoc. 87 | 88 | class reg.tests.fixtures.module.Foo 89 | 90 | Class for foo objects. 91 | 92 | bar(obj) 93 | 94 | Return the bar of an object. 95 | 96 | baz(obj) 97 | 98 | Return the baz of an object. 99 | 100 | reg.tests.fixtures.module.foo(obj) 101 | 102 | return the foo of an object. 103 | """ 104 | ) 105 | -------------------------------------------------------------------------------- /reg/tests/test_classdispatch.py: -------------------------------------------------------------------------------- 1 | from ..dispatch import dispatch 2 | from ..predicate import match_class 3 | 4 | 5 | class DemoClass: 6 | pass 7 | 8 | 9 | class SpecialClass: 10 | pass 11 | 12 | 13 | class Foo: 14 | def __repr__(self): 15 | return "" 16 | 17 | 18 | class Bar: 19 | def __repr__(self): 20 | return "" 21 | 22 | 23 | def test_dispatch_basic(): 24 | @dispatch(match_class("cls")) 25 | def something(cls): 26 | raise NotImplementedError() 27 | 28 | def something_for_object(cls): 29 | return "Something for %s" % cls 30 | 31 | something.register(something_for_object, cls=object) 32 | 33 | assert something(DemoClass) == (f"Something for ") 34 | 35 | assert something.by_args(DemoClass).component is something_for_object 36 | assert something.by_args(DemoClass).all_matches == [something_for_object] 37 | 38 | 39 | def test_classdispatch_multidispatch(): 40 | @dispatch(match_class("cls"), "other") 41 | def something(cls, other): 42 | raise NotImplementedError() 43 | 44 | def something_for_object_and_object(cls, other): 45 | return "Something, other is object: %s" % other 46 | 47 | def something_for_object_and_foo(cls, other): 48 | return "Something, other is Foo: %s" % other 49 | 50 | something.register(something_for_object_and_object, cls=object, other=object) 51 | 52 | something.register(something_for_object_and_foo, cls=object, other=Foo) 53 | 54 | assert something(DemoClass, Bar()) == ( 55 | "Something, other is object: " 56 | ) 57 | assert something(DemoClass, Foo()) == ("Something, other is Foo: ") 58 | 59 | 60 | def test_classdispatch_extra_arguments(): 61 | @dispatch(match_class("cls")) 62 | def something(cls, extra): 63 | raise NotImplementedError() 64 | 65 | def something_for_object(cls, extra): 66 | return "Extra: %s" % extra 67 | 68 | something.register(something_for_object, cls=object) 69 | 70 | assert something(DemoClass, "foo") == "Extra: foo" 71 | 72 | 73 | def test_classdispatch_no_arguments(): 74 | @dispatch() 75 | def something(): 76 | raise NotImplementedError() 77 | 78 | def something_impl(): 79 | return "Something!" 80 | 81 | something.register(something_impl) 82 | 83 | assert something() == "Something!" 84 | 85 | 86 | def test_classdispatch_override(): 87 | @dispatch(match_class("cls")) 88 | def something(cls): 89 | raise NotImplementedError() 90 | 91 | def something_for_object(cls): 92 | return "Something for %s" % cls 93 | 94 | def something_for_special(cls): 95 | return "Special for %s" % cls 96 | 97 | something.register(something_for_object, cls=object) 98 | something.register(something_for_special, cls=SpecialClass) 99 | 100 | assert something(SpecialClass) == (f"Special for ") 101 | 102 | 103 | def test_classdispatch_fallback(): 104 | @dispatch() 105 | def something(cls): 106 | return "Fallback" 107 | 108 | assert something(DemoClass) == "Fallback" 109 | -------------------------------------------------------------------------------- /tox_perf.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | 3 | from reg import dispatch 4 | from reg import DictCachingKeyLookup 5 | 6 | 7 | def get_key_lookup(r): 8 | return DictCachingKeyLookup(r) 9 | 10 | 11 | @dispatch(get_key_lookup=get_key_lookup) 12 | def args0(): 13 | raise NotImplementedError() 14 | 15 | 16 | @dispatch("a", get_key_lookup=get_key_lookup) 17 | def args1(a): 18 | raise NotImplementedError() 19 | 20 | 21 | @dispatch("a", "b", get_key_lookup=get_key_lookup) 22 | def args2(a, b): 23 | raise NotImplementedError() 24 | 25 | 26 | @dispatch("a", "b", "c", get_key_lookup=get_key_lookup) 27 | def args3(a, b, c): 28 | raise NotImplementedError() 29 | 30 | 31 | @dispatch("a", "b", "c", "d", get_key_lookup=get_key_lookup) 32 | def args4(a, b, c, d): 33 | raise NotImplementedError() 34 | 35 | 36 | class Foo: 37 | pass 38 | 39 | 40 | def myargs0(): 41 | return "args0" 42 | 43 | 44 | def myargs1(a): 45 | return "args1" 46 | 47 | 48 | def myargs2(a, b): 49 | return "args2" 50 | 51 | 52 | def myargs3(a, b, c): 53 | return "args3" 54 | 55 | 56 | def myargs4(a, b, c, d): 57 | return "args4" 58 | 59 | 60 | args0.register(myargs0) 61 | args1.register(myargs1, a=Foo) 62 | args2.register(myargs2, a=Foo, b=Foo) 63 | args3.register(myargs3, a=Foo, b=Foo, c=Foo) 64 | args4.register(myargs4, a=Foo, b=Foo, c=Foo, d=Foo) 65 | 66 | 67 | def docall0(): 68 | args0() 69 | 70 | 71 | def docall1(): 72 | args1(Foo()) 73 | 74 | 75 | def docall2(): 76 | args2(Foo(), Foo()) 77 | 78 | 79 | def docall3(): 80 | args3(Foo(), Foo(), Foo()) 81 | 82 | 83 | def docall4(): 84 | args4(Foo(), Foo(), Foo(), Foo()) 85 | 86 | 87 | def plain_docall0(): 88 | myargs0() 89 | 90 | 91 | def plain_docall4(): 92 | myargs4(Foo(), Foo(), Foo(), Foo()) 93 | 94 | 95 | plain_zero_time = timeit.timeit( 96 | "plain_docall0()", setup="from __main__ import plain_docall0" 97 | ) 98 | 99 | print("\nPerformance test") 100 | print("================") 101 | 102 | print("dispatch 0 args") 103 | print( 104 | "{:.2f}".format( 105 | timeit.timeit("docall0()", setup="from __main__ import docall0") 106 | / plain_zero_time 107 | ) 108 | + "x" 109 | ) 110 | 111 | print("dispatch 1 args") 112 | print( 113 | "{:.2f}".format( 114 | timeit.timeit("docall1()", setup="from __main__ import docall1") 115 | / plain_zero_time 116 | ) 117 | + "x" 118 | ) 119 | 120 | print("dispatch 2 args") 121 | print( 122 | "{:.2f}".format( 123 | timeit.timeit("docall2()", setup="from __main__ import docall2") 124 | / plain_zero_time 125 | ) 126 | + "x" 127 | ) 128 | 129 | print("dispatch 3 args") 130 | print( 131 | "{:.2f}".format( 132 | timeit.timeit("docall3()", setup="from __main__ import docall3") 133 | / plain_zero_time 134 | ) 135 | + "x" 136 | ) 137 | 138 | print("dispatch 4 args") 139 | print( 140 | "{:.2f}".format( 141 | timeit.timeit("docall4()", setup="from __main__ import docall4") 142 | / plain_zero_time 143 | ) 144 | + "x" 145 | ) 146 | 147 | print("Plain func 0 args") 148 | print("1.00x (base duration)") 149 | 150 | print("Plain func 4 args") 151 | print( 152 | "{:.2f}".format( 153 | timeit.timeit("plain_docall4()", setup="from __main__ import plain_docall4") 154 | / plain_zero_time 155 | ) 156 | + "x" 157 | ) 158 | -------------------------------------------------------------------------------- /reg/arginfo.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | 4 | def arginfo(callable): 5 | """Get information about the arguments of a callable. 6 | 7 | Returns a :class:`inspect.FullArgSpec` object as for 8 | :func:`inspect.getfullargspec`. 9 | 10 | :func:`inspect.getfullargspec` returns information about the arguments 11 | of a function. arginfo also works for classes and instances with a 12 | __call__ defined. Unlike getfullargspec, arginfo treats bound methods 13 | like functions, so that the self argument is not reported. 14 | 15 | arginfo returns ``None`` if given something that is not callable. 16 | 17 | arginfo caches previous calls (except for instances with a 18 | __call__), making calling it repeatedly cheap. 19 | 20 | This was originally inspired by the pytest.core varnames() function, 21 | but has been completely rewritten to handle class constructors, 22 | also show other getarginfo() information, and for readability. 23 | """ 24 | try: 25 | return arginfo._cache[callable] 26 | except KeyError: 27 | # Try to get __call__ function from the cache. 28 | try: 29 | return arginfo._cache[callable.__call__] 30 | except (AttributeError, KeyError): 31 | pass 32 | func, cache_key, remove_self = get_callable_info(callable) 33 | if func is None: 34 | return None 35 | result = inspect.getfullargspec(func) 36 | if remove_self: 37 | args = result.args[1:] 38 | result = inspect.FullArgSpec( 39 | args, 40 | result.varargs, 41 | result.varkw, 42 | result.defaults, 43 | result.kwonlyargs, 44 | result.kwonlydefaults, 45 | result.annotations, 46 | ) 47 | arginfo._cache[cache_key] = result 48 | return result 49 | 50 | 51 | def is_cached(callable): 52 | if callable in arginfo._cache: 53 | return True 54 | return callable.__call__ in arginfo._cache 55 | 56 | 57 | arginfo._cache = {} 58 | arginfo.is_cached = is_cached 59 | 60 | 61 | def get_callable_info(callable): 62 | """Get information about a callable. 63 | 64 | Returns a tuple of: 65 | 66 | * actual function/method that can be inspected with inspect.getfullargspec. 67 | 68 | * cache key to use to cache results. 69 | 70 | * whether to remove self or not. 71 | 72 | Note that in Python 3, __init__ is not a method, but we still 73 | want to remove self from it. 74 | 75 | If not inspectable (None, None, False) is returned. 76 | """ 77 | if inspect.isfunction(callable): 78 | return callable, callable, False 79 | if inspect.ismethod(callable): 80 | return callable, callable, True 81 | if inspect.isclass(callable): 82 | return get_class_init(callable), callable, True 83 | try: 84 | callable = getattr(callable, "__call__") 85 | return callable, callable, True 86 | except AttributeError: 87 | return None, None, False 88 | 89 | 90 | def fake_empty_init(): 91 | pass # pragma: nocoverage 92 | 93 | 94 | class Dummy: 95 | pass 96 | 97 | 98 | WRAPPER_DESCRIPTOR = Dummy.__init__ 99 | 100 | 101 | def get_class_init(class_): 102 | func = class_.__init__ 103 | 104 | # If this is a new-style class and there is no __init__ 105 | # defined this is a WRAPPER_DESCRIPTOR. 106 | if func is WRAPPER_DESCRIPTOR: 107 | return fake_empty_init 108 | return func 109 | -------------------------------------------------------------------------------- /doc/developing.rst: -------------------------------------------------------------------------------- 1 | Developing Reg 2 | ============== 3 | 4 | Install Reg for development 5 | --------------------------- 6 | 7 | .. highlight:: console 8 | 9 | Clone Reg from github:: 10 | 11 | $ git clone git@github.com:morepath/reg.git 12 | 13 | If this doesn't work and you get an error 'Permission denied (publickey)', 14 | you need to upload your ssh public key to github_. 15 | 16 | Then go to the reg directory:: 17 | 18 | $ cd reg 19 | 20 | Make sure you have virtualenv_ installed. 21 | 22 | Create a new virtualenv for Python 3 inside the reg directory:: 23 | 24 | $ virtualenv -p python3 env/py3 25 | 26 | Activate the virtualenv:: 27 | 28 | $ source env/py3/bin/activate 29 | 30 | Make sure you have recent setuptools and pip installed:: 31 | 32 | $ pip install -U setuptools pip 33 | 34 | Install the various dependencies and development tools from 35 | develop_requirements.txt:: 36 | 37 | $ pip install -Ur develop_requirements.txt 38 | 39 | For upgrading the requirements just run the command again. 40 | 41 | .. note:: 42 | 43 | The following commands work only if you have the virtualenv activated. 44 | 45 | .. _github: https://help.github.com/articles/generating-an-ssh-key 46 | 47 | .. _virtualenv: https://pypi.python.org/pypi/virtualenv 48 | 49 | Install pre-commit hook for Black integration 50 | --------------------------------------------- 51 | 52 | We're using Black_ for formatting the code and it's recommended to 53 | install the `pre-commit hook`_ for Black integration before committing:: 54 | 55 | $ pre-commit install 56 | 57 | .. _`pre-commit hook`: https://black.readthedocs.io/en/stable/version_control_integration.html 58 | 59 | Running the tests 60 | ----------------- 61 | 62 | You can run the tests using `py.test`_:: 63 | 64 | $ py.test 65 | 66 | To generate test coverage information as HTML do:: 67 | 68 | $ py.test --cov --cov-report html 69 | 70 | You can then point your web browser to the ``htmlcov/index.html`` file 71 | in the project directory and click on modules to see detailed coverage 72 | information. 73 | 74 | .. _`py.test`: http://pytest.org/latest/ 75 | 76 | Black 77 | ----- 78 | 79 | To format the code with the `Black Code Formatter`_ run in the root directory:: 80 | 81 | $ black morepath 82 | 83 | Black has also integration_ for the most popular editors. 84 | 85 | .. _`Black Code Formatter`: https://black.readthedocs.io 86 | .. _integration: https://black.readthedocs.io/en/stable/editor_integration.html 87 | 88 | Running the documentation tests 89 | ------------------------------- 90 | 91 | The documentation contains code. To check these code snippets, you 92 | can run this code using this command:: 93 | 94 | (py3) $ sphinx-build -b doctest doc doc/build/doctest 95 | 96 | Or alternatively if you have ``Make`` installed:: 97 | 98 | (py3) $ cd doc 99 | (py3) $ make doctest 100 | 101 | Or from the Reg project directory:: 102 | 103 | (py3) $ make -C doc doctest 104 | 105 | Building the HTML documentation 106 | ------------------------------- 107 | 108 | To build the HTML documentation (output in ``doc/build/html``), run:: 109 | 110 | $ sphinx-build doc doc/build/html 111 | 112 | Or alternatively if you have ``Make`` installed:: 113 | 114 | $ cd doc 115 | $ make html 116 | 117 | Or from the Reg project directory:: 118 | 119 | $ make -C doc html 120 | 121 | Various checking tools 122 | ---------------------- 123 | 124 | flake8_ is a tool that can do various checks for common Python 125 | mistakes using pyflakes_, check for PEP8_ style compliance and 126 | can do `cyclomatic complexity`_ checking. To do pyflakes and pep8 127 | checking do:: 128 | 129 | $ flake8 reg 130 | 131 | To also show cyclomatic complexity, use this command:: 132 | 133 | $ flake8 --max-complexity=10 reg 134 | 135 | .. _flake8: https://pypi.python.org/pypi/flake8 136 | 137 | .. _pyflakes: https://pypi.python.org/pypi/pyflakes 138 | 139 | .. _pep8: http://www.python.org/dev/peps/pep-0008/ 140 | 141 | .. _`cyclomatic complexity`: https://en.wikipedia.org/wiki/Cyclomatic_complexity 142 | 143 | Tox 144 | --- 145 | 146 | With tox you can test Morepath under different Python environments. 147 | 148 | We have Travis continuous integration installed on Morepath's github 149 | repository and it runs the same tox tests after each checkin. 150 | 151 | First you should install all Python versions which you want to 152 | test. The versions which are not installed will be skipped. You should 153 | at least install Python 3.7 which is required by flake8, coverage and 154 | doctests. 155 | 156 | One tool you can use to install multiple versions of Python is pyenv_. 157 | 158 | To find out which test environments are defined for Morepath in tox.ini run:: 159 | 160 | $ tox -l 161 | 162 | You can run all tox tests with:: 163 | 164 | $ tox 165 | 166 | You can also specify a test environment to run e.g.:: 167 | 168 | $ tox -e py37 169 | $ tox -e pep8 170 | $ tox -e docs 171 | 172 | To run a simple performance test you can use:: 173 | 174 | $ tox -e perf 175 | 176 | .. _pyenv: https://github.com/yyuu/pyenv 177 | -------------------------------------------------------------------------------- /reg/context.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from types import MethodType 3 | from .dispatch import dispatch, Dispatch, format_signature, execute 4 | from .arginfo import arginfo 5 | 6 | 7 | class dispatch_method(dispatch): 8 | """Decorator to make a method on a context class dispatch. 9 | 10 | This takes the predicates to dispatch on as zero or more parameters. 11 | 12 | :param predicates: sequence of :class:`Predicate` instances to do 13 | the dispatch on. You create predicates using 14 | :func:`reg.match_instance`, :func:`reg.match_key`, 15 | :func:`reg.match_class`, or with a custom predicate class. 16 | 17 | You can also pass in plain string argument, which is turned into 18 | a :func:`reg.match_instance` predicate. 19 | :param get_key_lookup: a function that gets a 20 | :class:`PredicateRegistry` instance and returns a key lookup. A 21 | :class:`PredicateRegistry` instance is itself a key lookup, but 22 | you can return a caching key lookup (such as 23 | :class:`reg.DictCachingKeyLookup` or 24 | :class:`reg.LruCachingKeyLookup`) to make it more efficient. 25 | :param first_invocation_hook: a callable that accepts an instance of the 26 | class in which this decorator is used. It is invoked the first 27 | time the method is invoked. 28 | 29 | """ 30 | 31 | def __init__(self, *predicates, **kw): 32 | self.first_invocation_hook = kw.pop("first_invocation_hook", lambda x: None) 33 | super().__init__(*predicates, **kw) 34 | self._cache = {} 35 | 36 | def __call__(self, callable): 37 | self.callable = callable 38 | return self 39 | 40 | def __get__(self, obj, type=None): 41 | # we get the method from the cache 42 | # this guarantees that we distinguish between dispatches 43 | # on a per class basis, and on the name of the method 44 | 45 | dispatch = self._cache.get(type) 46 | 47 | if dispatch is None: 48 | # if this is the first time we access the dispatch method, 49 | # we create it and store it in the cache 50 | dispatch = DispatchMethod( 51 | self.predicates, self.callable, self.get_key_lookup 52 | ).call 53 | self._cache[type] = dispatch 54 | 55 | # we cannot attach the dispatch method to the class 56 | # directly (skipping the descriptor during next access) here, 57 | # because we need to return a distinct dispatch for each 58 | # class, including subclasses. 59 | if obj is None: 60 | # we access it through the class directly, so unbound 61 | return dispatch 62 | 63 | self.first_invocation_hook(obj) 64 | 65 | # if we access the instance, we simulate binding it 66 | bound = MethodType(dispatch, obj) 67 | # we store it on the instance, so that next time we 68 | # access this, we do not hit the descriptor anymore 69 | # but return the bound dispatch function directly 70 | setattr(obj, self.callable.__name__, bound) 71 | return bound 72 | 73 | 74 | class DispatchMethod(Dispatch): 75 | def by_args(self, *args, **kw): 76 | """Lookup an implementation by invocation arguments. 77 | 78 | :param args: positional arguments used in invocation. 79 | :param kw: named arguments used in invocation. 80 | :returns: a :class:`reg.LookupEntry`. 81 | """ 82 | return super().by_args(None, *args, **kw) 83 | 84 | 85 | def methodify(func, selfname=None): 86 | """Turn a function into a method, if needed. 87 | 88 | If ``selfname`` is not specified, wrap the function so that it 89 | takes an additional first argument, like a method. 90 | 91 | If ``selfname`` is specified, check whether it is the same as the 92 | name of the first argument of ``func``. If itsn't, wrap the 93 | function so that it takes an additional first argument, with the 94 | name specified by ``selfname``. 95 | 96 | If it is, the signature of ``func`` needn't be amended, but 97 | wrapping might still be necessary. 98 | 99 | In all cases, :func:`inspect_methodified` lets you retrieve the wrapped 100 | function. 101 | 102 | :param func: the function to turn into method. 103 | 104 | :param selfname: if specified, the name of the argument 105 | referencing the class instance. Typically, ``"self"``. 106 | 107 | :returns: function that can be used as a method when assigned to a 108 | class. 109 | 110 | """ 111 | args = arginfo(func) 112 | if args is None: 113 | raise TypeError("methodify must take a callable") 114 | if args.args[:1] != [selfname]: 115 | # Add missing self to the signature: 116 | code_template = ( 117 | "def wrapper({selfname}, {signature}): return _func({signature})" 118 | ) 119 | elif inspect.ismethod(func): 120 | # Bound method: must be wrapped despite same signature: 121 | code_template = "def wrapper({signature}): return _func({signature})" 122 | else: 123 | # No wrapping needed: 124 | return func 125 | code_source = code_template.format( 126 | signature=format_signature(args), selfname=selfname or "_" 127 | ) 128 | return execute(code_source, _func=func)["wrapper"] 129 | 130 | 131 | def clean_dispatch_methods(cls): 132 | """For a given class clean all dispatch methods. 133 | 134 | This resets their registry to the original state using 135 | :meth:`reg.DispatchMethod.clean`. 136 | 137 | :param cls: a class that has :class:`reg.DispatchMethod` methods on it. 138 | """ 139 | for name in dir(cls): 140 | attr = getattr(cls, name) 141 | if inspect.isfunction(attr) and hasattr(attr, "clean"): 142 | attr.clean() 143 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Reg.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Reg.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Reg.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Reg.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Reg" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Reg" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /reg/tests/test_arginfo.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ..arginfo import arginfo 3 | 4 | 5 | def func_no_args(): 6 | pass 7 | 8 | 9 | class ObjNoArgs: 10 | def __call__(self): 11 | pass 12 | 13 | 14 | obj_no_args = ObjNoArgs() 15 | 16 | 17 | class MethodNoArgs: 18 | def method(self): 19 | pass 20 | 21 | 22 | method_no_args = MethodNoArgs() 23 | 24 | 25 | class StaticMethodNoArgs: 26 | @staticmethod 27 | def method(): 28 | pass 29 | 30 | 31 | class ClassMethodNoArgs: 32 | @classmethod 33 | def method(cls): 34 | pass 35 | 36 | 37 | class ClassNoInit: 38 | pass 39 | 40 | 41 | class ClassNoArgs: 42 | def __init__(self): 43 | pass 44 | 45 | 46 | class ClassicNoInit: 47 | pass 48 | 49 | 50 | class ClassicNoArgs: 51 | def __init__(self): 52 | pass 53 | 54 | 55 | class InheritedNoInit(ClassNoInit): 56 | pass 57 | 58 | 59 | class InheritedNoArgs(ClassNoArgs): 60 | pass 61 | 62 | 63 | class ClassicInheritedNoInit(ClassicNoInit): 64 | pass 65 | 66 | 67 | class ClassicInheritedNoArgs(ClassicNoArgs): 68 | pass 69 | 70 | 71 | @pytest.mark.parametrize( 72 | "callable", 73 | [ 74 | func_no_args, 75 | obj_no_args, 76 | method_no_args.method, 77 | StaticMethodNoArgs.method, 78 | ClassMethodNoArgs.method, 79 | ClassNoInit, 80 | ClassNoArgs, 81 | ClassicNoInit, 82 | ClassicNoArgs, 83 | InheritedNoInit, 84 | InheritedNoArgs, 85 | ClassicInheritedNoInit, 86 | ClassicInheritedNoArgs, 87 | ], 88 | ) 89 | def test_arginfo_no_args(callable): 90 | info = arginfo(callable) 91 | assert info.args == [] 92 | assert info.varargs is None 93 | assert info.varkw is None 94 | assert info.defaults is None 95 | 96 | 97 | def func_args(a): 98 | pass 99 | 100 | 101 | class ObjArgs: 102 | def __call__(self, a): 103 | pass 104 | 105 | 106 | obj_args = ObjArgs() 107 | 108 | 109 | class MethodArgs: 110 | def method(self, a): 111 | pass 112 | 113 | 114 | method_args = MethodArgs() 115 | 116 | 117 | class StaticMethodArgs: 118 | @staticmethod 119 | def method(a): 120 | pass 121 | 122 | 123 | class ClassMethodArgs: 124 | @classmethod 125 | def method(cls, a): 126 | pass 127 | 128 | 129 | class ClassArgs: 130 | def __init__(self, a): 131 | pass 132 | 133 | 134 | class ClassicArgs: 135 | def __init__(self, a): 136 | pass 137 | 138 | 139 | class InheritedArgs(ClassArgs): 140 | pass 141 | 142 | 143 | class ClassicInheritedArgs(ClassicArgs): 144 | pass 145 | 146 | 147 | @pytest.mark.parametrize( 148 | "callable", 149 | [ 150 | func_args, 151 | obj_args, 152 | method_args.method, 153 | StaticMethodArgs.method, 154 | ClassMethodArgs.method, 155 | ClassArgs, 156 | ClassicArgs, 157 | InheritedArgs, 158 | ClassicInheritedArgs, 159 | ], 160 | ) 161 | def test_arginfo_args(callable): 162 | info = arginfo(callable) 163 | assert info.args == ["a"] 164 | assert info.varargs is None 165 | assert info.varkw is None 166 | assert info.defaults is None 167 | 168 | 169 | def func_varargs(*args): 170 | pass 171 | 172 | 173 | class ObjVarargs: 174 | def __call__(self, *args): 175 | pass 176 | 177 | 178 | obj_varargs = ObjVarargs() 179 | 180 | 181 | class MethodVarargs: 182 | def method(self, *args): 183 | pass 184 | 185 | 186 | method_varargs = MethodVarargs() 187 | 188 | 189 | class ClassVarargs: 190 | def __init__(self, *args): 191 | pass 192 | 193 | 194 | class ClassicVarargs: 195 | def __init__(self, *args): 196 | pass 197 | 198 | 199 | class InheritedVarargs(ClassVarargs): 200 | pass 201 | 202 | 203 | class ClassicInheritedVarargs(ClassicVarargs): 204 | pass 205 | 206 | 207 | @pytest.mark.parametrize( 208 | "callable", 209 | [ 210 | func_varargs, 211 | obj_varargs, 212 | method_varargs.method, 213 | ClassVarargs, 214 | ClassicVarargs, 215 | InheritedVarargs, 216 | ClassicInheritedVarargs, 217 | ], 218 | ) 219 | def test_arginfo_varargs(callable): 220 | info = arginfo(callable) 221 | assert info.args == [] 222 | assert info.varargs == "args" 223 | assert info.varkw is None 224 | assert info.defaults is None 225 | 226 | 227 | def func_keywords(**kw): 228 | pass 229 | 230 | 231 | class ObjKeywords: 232 | def __call__(self, **kw): 233 | pass 234 | 235 | 236 | obj_keywords = ObjKeywords() 237 | 238 | 239 | class MethodKeywords: 240 | def method(self, **kw): 241 | pass 242 | 243 | 244 | method_keywords = MethodKeywords() 245 | 246 | 247 | class ClassKeywords: 248 | def __init__(self, **kw): 249 | pass 250 | 251 | 252 | class ClassicKeywords: 253 | def __init__(self, **kw): 254 | pass 255 | 256 | 257 | class InheritedKeywords(ClassKeywords): 258 | pass 259 | 260 | 261 | class ClassicInheritedKeywords(ClassicKeywords): 262 | pass 263 | 264 | 265 | @pytest.mark.parametrize( 266 | "callable", 267 | [ 268 | func_keywords, 269 | obj_keywords, 270 | method_keywords.method, 271 | ClassKeywords, 272 | ClassicKeywords, 273 | InheritedKeywords, 274 | ClassicInheritedKeywords, 275 | ], 276 | ) 277 | def test_arginfo_keywords(callable): 278 | info = arginfo(callable) 279 | assert info.args == [] 280 | assert info.varargs is None 281 | assert info.varkw == "kw" 282 | assert info.defaults is None 283 | 284 | 285 | def func_defaults(a=1): 286 | pass 287 | 288 | 289 | class ObjDefaults: 290 | def __call__(self, a=1): 291 | pass 292 | 293 | 294 | obj_defaults = ObjDefaults() 295 | 296 | 297 | class MethodDefaults: 298 | def method(self, a=1): 299 | pass 300 | 301 | 302 | method_defaults = MethodDefaults() 303 | 304 | 305 | class ClassDefaults: 306 | def __init__(self, a=1): 307 | pass 308 | 309 | 310 | class ClassicDefaults: 311 | def __init__(self, a=1): 312 | pass 313 | 314 | 315 | class InheritedDefaults(ClassDefaults): 316 | pass 317 | 318 | 319 | class ClassicInheritedDefaults(ClassicDefaults): 320 | pass 321 | 322 | 323 | @pytest.mark.parametrize( 324 | "callable", 325 | [ 326 | func_defaults, 327 | obj_defaults, 328 | method_defaults.method, 329 | ClassDefaults, 330 | ClassicDefaults, 331 | InheritedDefaults, 332 | ClassicInheritedDefaults, 333 | ], 334 | ) 335 | def test_arginfo_defaults(callable): 336 | info = arginfo(callable) 337 | assert info.args == ["a"] 338 | assert info.varargs is None 339 | assert info.varkw is None 340 | assert info.defaults == (1,) 341 | 342 | 343 | # Information on builtin functions is not reported. These can 344 | # still be called with mapply, but only using positional arguments. 345 | def test_arginfo_builtin(): 346 | info = arginfo(int) 347 | assert info.args == [] 348 | assert info.varargs is None 349 | assert info.varkw is None 350 | assert info.defaults is None 351 | 352 | 353 | def test_arginfo_cache(): 354 | def foo(a): 355 | pass 356 | 357 | assert not arginfo.is_cached(foo) 358 | arginfo(foo) 359 | assert arginfo.is_cached(foo) 360 | 361 | 362 | def test_arginfo_cache_callable(): 363 | class Foo: 364 | def __call__(self): 365 | pass 366 | 367 | foo = Foo() 368 | assert not arginfo.is_cached(foo) 369 | arginfo(foo) 370 | assert arginfo.is_cached(foo) 371 | -------------------------------------------------------------------------------- /doc/patterns.rst: -------------------------------------------------------------------------------- 1 | Patterns 2 | ======== 3 | 4 | Here we look at a number of patterns you can implement with Reg. 5 | 6 | Adapters 7 | -------- 8 | 9 | What if we wanted to add a feature that required multiple methods, not 10 | just one? You can use the adapter pattern for this. 11 | 12 | Let's imagine we have a feature to get the icon for a content object 13 | in our CMS, and that this consists of two methods, with a way to get a 14 | small icon and a large icon. We want this API: 15 | 16 | .. testcode:: 17 | 18 | from abc import ABCMeta, abstractmethod 19 | 20 | class Icon(object): 21 | __metaclass__ = ABCMeta 22 | @abstractmethod 23 | def small(self): 24 | """Get the small icon.""" 25 | 26 | @abstractmethod 27 | def large(self): 28 | """Get the large icon.""" 29 | 30 | .. sidebar:: abc module? 31 | 32 | We've used the standard Python `abc module`_ to set the API in 33 | stone. But that's just a convenient standard way to express it. The 34 | ``abc`` module is not in any way required by Reg. You don't need to 35 | implement the API in a base class either. We just do it in this 36 | example to be explicit. 37 | 38 | .. _`abc module`: http://docs.python.org/2/library/abc.html 39 | 40 | We define ``Document`` and ``Image`` content classes: 41 | 42 | .. testcode:: 43 | 44 | class Document(object): 45 | def __init__(self, text): 46 | self.text = text 47 | 48 | class Image(object): 49 | def __init__(self, bytes): 50 | self.bytes = bytes 51 | 52 | Let's implement the ``Icon`` API for ``Document``: 53 | 54 | .. testcode:: 55 | 56 | def load_icon(path): 57 | return path # pretend we load the path here and return an image obj 58 | 59 | class DocumentIcon(Icon): 60 | def __init__(self, document): 61 | self.document = document 62 | 63 | def small(self): 64 | if not self.document.text: 65 | return load_icon('document_small_empty.png') 66 | return load_icon('document_small.png') 67 | 68 | def large(self): 69 | if not self.document.text: 70 | return load_icon('document_large_empty.png') 71 | return load_icon('document_large.png') 72 | 73 | The constructor of ``DocumentIcon`` receives a ``Document`` instance 74 | as its first argument. The implementation of the ``small`` and 75 | ``large`` methods uses this instance to determine what icon to produce 76 | depending on whether the document is empty or not. 77 | 78 | We can call ``DocumentIcon`` an adapter, as it adapts the original 79 | ``Document`` class to provide an icon API for it. We can use it 80 | manually: 81 | 82 | .. doctest:: 83 | 84 | >>> doc = Document("Hello world") 85 | >>> icon_api = DocumentIcon(doc) 86 | >>> icon_api.small() 87 | 'document_small.png' 88 | >>> icon_api.large() 89 | 'document_large.png' 90 | 91 | But we want to be able to use the ``Icon`` API generically, so let's 92 | create a generic function that gives us an implementation of ``Icon`` 93 | back for any object: 94 | 95 | .. testcode:: 96 | 97 | import reg 98 | 99 | @reg.dispatch('obj') 100 | def icon(obj): 101 | raise NotImplementedError 102 | 103 | We can now register the ``DocumentIcon`` adapter class for this 104 | function and ``Document``: 105 | 106 | .. testcode:: 107 | 108 | icon.register(DocumentIcon, obj=Document) 109 | 110 | We can now use the generic ``icon`` to get ``Icon`` API for a 111 | document: 112 | 113 | .. doctest:: 114 | 115 | >>> api = icon(doc) 116 | >>> api.small() 117 | 'document_small.png' 118 | >>> api.large() 119 | 'document_large.png' 120 | 121 | We can also register a ``FolderIcon`` adapter for ``Folder``, a 122 | ``ImageIcon`` adapter for ``Image``, and so on. For the sake of 123 | brevity let's just define one for ``Image`` here: 124 | 125 | .. testcode:: 126 | 127 | class ImageIcon(Icon): 128 | def __init__(self, image): 129 | self.image = image 130 | 131 | def small(self): 132 | return load_icon('image_small.png') 133 | 134 | def large(self): 135 | return load_icon('image_large.png') 136 | 137 | icon.register(ImageIcon, obj=Image) 138 | 139 | Now we can use ``icon`` to retrieve the ``Icon`` API for any item in 140 | the system for which an adapter was registered: 141 | 142 | .. doctest:: 143 | 144 | >>> icon(doc).small() 145 | 'document_small.png' 146 | >>> icon(doc).large() 147 | 'document_large.png' 148 | >>> image = Image('abc') 149 | >>> icon(image).small() 150 | 'image_small.png' 151 | >>> icon(image).large() 152 | 'image_large.png' 153 | 154 | Service Discovery 155 | ----------------- 156 | 157 | Some applications need configurable services. The application may for 158 | instance need a way to send email, but you don't want to hardcode any 159 | particular way into your app, but instead leave this to a particular 160 | deployment-specific configuration. You can use the Reg infrastructure 161 | for this as well. 162 | 163 | The simplest way to do this with Reg is by using a generic service lookup 164 | function: 165 | 166 | .. testcode:: 167 | 168 | @reg.dispatch() 169 | def emailer(): 170 | raise NotImplementedError 171 | 172 | Here we've created a generic function that takes no arguments (and 173 | thus does no dynamic dispatch). But you can still plug its actual 174 | implementation into the registry from elsewhere: 175 | 176 | .. testcode:: 177 | 178 | sent = [] 179 | 180 | def send_email(sender, subject, body): 181 | # some specific way to send email 182 | sent.append((sender, subject, body)) 183 | 184 | def actual_emailer(): 185 | return send_email 186 | 187 | emailer.register(actual_emailer) 188 | 189 | Now when we call emailer, we'll get the specific service we want: 190 | 191 | .. doctest:: 192 | 193 | >>> the_emailer = emailer() 194 | >>> the_emailer('someone@example.com', 'Hello', 'hello world!') 195 | >>> sent 196 | [('someone@example.com', 'Hello', 'hello world!')] 197 | 198 | In this case we return the function ``send_email`` from the 199 | ``emailer()`` function, but we could return any object we want that 200 | implements the service, such as an instance with a more extensive API. 201 | 202 | Replacing class methods 203 | ----------------------- 204 | 205 | Reg generic functions can be used to replace methods, so that you can 206 | follow the open/closed principle and add functionality to a class 207 | without modifying it. This works for instance methods, but what about 208 | ``classmethod``? This takes the *class* as the first argument, not an 209 | instance. You can configure ``@reg.dispatch`` decorator with a special 210 | :class:`Predicate` instance that lets you dispatch on a class argument 211 | instead of an instance argument. 212 | 213 | Here's what it looks like: 214 | 215 | .. testcode:: 216 | 217 | @reg.dispatch(reg.match_class('cls')) 218 | def something(cls): 219 | raise NotImplementedError() 220 | 221 | Note the call to :func:`match_class` here. This lets us specify that 222 | we want to dispatch on the class, in this case we simply want the 223 | ``cls`` argument. 224 | 225 | Let's use it: 226 | 227 | .. testcode:: 228 | 229 | def something_for_object(cls): 230 | return "Something for %s" % cls 231 | 232 | something.register(something_for_object, cls=object) 233 | 234 | class DemoClass(object): 235 | pass 236 | 237 | When we now call ``something()`` with ``DemoClass`` as the first 238 | argument we get the expected output: 239 | 240 | .. doctest:: 241 | 242 | >>> something(DemoClass) 243 | "Something for " 244 | 245 | This also knows about inheritance. So, you can write more specific 246 | implementations for particular classes: 247 | 248 | .. testcode:: 249 | 250 | class ParticularClass(object): 251 | pass 252 | 253 | def something_particular(cls): 254 | return "Particular for %s" % cls 255 | 256 | something.register( 257 | something_particular, 258 | cls=ParticularClass) 259 | 260 | When we call ``something`` now with ``ParticularClass`` as the argument, 261 | then ``something_particular`` is called: 262 | 263 | .. doctest:: 264 | 265 | >>> something(ParticularClass) 266 | "Particular for " 267 | -------------------------------------------------------------------------------- /reg/predicate.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from operator import itemgetter 3 | from itertools import product 4 | 5 | from .error import RegistrationError 6 | 7 | 8 | class Predicate: 9 | """A dispatch predicate. 10 | 11 | :param name: name used to identify the predicate when specifying 12 | its expected value in :meth:`reg.Dispatch.register`. 13 | :param index: a function that constructs an index given 14 | a fallback argument; typically you supply either a :class:`KeyIndex` 15 | or :class:`ClassIndex`. 16 | :param get_key: a callable that accepts a dictionary with the invocation 17 | arguments of the generic function and returns a key to be used for 18 | dispatching. 19 | :param fallback: optional fallback value. The fallback of the 20 | the most generic index for which no values could be 21 | found is used. 22 | :param default: default expected value of the predicate, to be 23 | used by :meth:`reg.Dispatch.register` whenever the expected 24 | value for the predicate is not given explicitly. 25 | 26 | """ 27 | 28 | def __init__(self, name, index, get_key=None, fallback=None, default=None): 29 | self.name = name 30 | self.index = index 31 | self.fallback = fallback 32 | self.get_key = get_key 33 | self.default = default 34 | 35 | def create_index(self): 36 | return self.index(self.fallback) 37 | 38 | def key_by_predicate_name(self, d): 39 | return d.get(self.name, self.default) 40 | 41 | 42 | def match_key(name, func=None, fallback=None, default=None): 43 | """Predicate that returns a value used for dispatching. 44 | 45 | :name: predicate name. 46 | :func: a callable that accepts the same arguments as the generic 47 | function and returns the value used for dispatching. The 48 | returned value must be of an immutable type. 49 | 50 | If ``None``, use a callable returning the argument 51 | with the same name as the predicate. 52 | :fallback: the fallback value. By default it is ``None``. 53 | :default: optional default value. 54 | :returns: a :class:`Predicate`. 55 | 56 | """ 57 | if func is None: 58 | get_key = itemgetter(name) 59 | else: 60 | get_key = lambda d: func(**d) 61 | return Predicate(name, KeyIndex, get_key, fallback, default) 62 | 63 | 64 | def match_instance(name, func=None, fallback=None, default=None): 65 | """Predicate that returns an instance whose class is used for dispatching. 66 | 67 | :name: predicate name. 68 | 69 | :func: a callable that accepts the same arguments as the generic 70 | function and returns the instance whose class is used for 71 | dispatching. If ``None``, use a callable returning the argument 72 | with the same name as the predicate. 73 | :fallback: the fallback value. By default it is ``None``. 74 | :default: optional default value. 75 | :returns: a :class:`Predicate`. 76 | 77 | """ 78 | if func is None: 79 | get_key = lambda d: d[name].__class__ 80 | else: 81 | get_key = lambda d: func(**d).__class__ 82 | return Predicate(name, ClassIndex, get_key, fallback, default) 83 | 84 | 85 | def match_class(name, func=None, fallback=None, default=None): 86 | """Predicate that returns a class used for dispatching. 87 | 88 | :name: predicate name. 89 | 90 | :func: a callable that accepts the same arguments as the generic 91 | function and returns a class used for 92 | dispatching. If ``None``, use a callable returning the argument 93 | with the same name as the predicate. 94 | :fallback: the fallback value. By default it is ``None``. 95 | :default: optional default value. 96 | :returns: a :class:`Predicate`. 97 | 98 | """ 99 | if func is None: 100 | get_key = itemgetter(name) 101 | else: 102 | get_key = lambda d: func(**d) 103 | return Predicate(name, ClassIndex, get_key, fallback, default) 104 | 105 | 106 | _emptyset = frozenset() 107 | 108 | 109 | class KeyIndex(dict): 110 | def __init__(self, fallback=None): 111 | self.fallback = fallback 112 | 113 | def __missing__(self, key): 114 | return _emptyset 115 | 116 | def permutations(self, key): 117 | """Permutations for a simple immutable key. 118 | 119 | There is only a single permutation: the key itself. 120 | """ 121 | yield key 122 | 123 | 124 | class ClassIndex(KeyIndex): 125 | def permutations(self, key): 126 | """Permutations for class key. 127 | 128 | Returns class and its base classes in mro order. If a classic 129 | class in Python 2, smuggle in ``object`` as the base class 130 | anyway to make lookups consistent. 131 | """ 132 | for class_ in inspect.getmro(key): 133 | yield class_ 134 | if class_ is not object: 135 | yield object # pragma: no cover 136 | 137 | 138 | class PredicateRegistry: 139 | def __init__(self, *predicates): 140 | self.known_keys = set() 141 | self.known_values = set() 142 | self.predicates = predicates 143 | self.indexes = [predicate.create_index() for predicate in predicates] 144 | key_getters = [p.get_key for p in predicates] 145 | if len(predicates) == 0: 146 | self.key = lambda **kw: () 147 | elif len(predicates) == 1: 148 | (p,) = key_getters 149 | self.key = lambda **kw: (p(kw),) 150 | elif len(predicates) == 2: 151 | p, q = key_getters 152 | self.key = lambda **kw: (p(kw), q(kw)) 153 | elif len(predicates) == 3: 154 | p, q, r = key_getters 155 | self.key = lambda **kw: (p(kw), q(kw), r(kw)) 156 | else: 157 | self.key = lambda **kw: tuple([p(kw) for p in key_getters]) 158 | 159 | def register(self, key, value): 160 | if key in self.known_keys: 161 | raise RegistrationError(f"Already have registration for key: {key}") 162 | for index, key_item in zip(self.indexes, key): 163 | index.setdefault(key_item, set()).add(value) 164 | self.known_keys.add(key) 165 | self.known_values.add(value) 166 | 167 | def get(self, keys): 168 | # do an intersection of all sets that result from index lookup 169 | # this code is a bit convoluted for performance reasons. 170 | sets = (index[key] for index, key in zip(self.indexes, keys)) 171 | # besides doing the intersection, 172 | # this returns the known values if there are no indexes at all 173 | return next(sets, self.known_values).intersection(*sets) 174 | 175 | def permutations(self, keys): 176 | return product( 177 | *(index.permutations(key) for index, key in zip(self.indexes, keys)) 178 | ) 179 | 180 | def key(self, **kw): 181 | """Construct a dispatch key from the arguments of a generic function. 182 | 183 | :param kw: a dictionary with the arguments passed to a generic 184 | function. 185 | :returns: a tuple, to be used as a key for dispatching. 186 | 187 | """ 188 | # Overwritten by init 189 | 190 | def key_dict_to_predicate_key(self, d): 191 | """Construct a dispatch key from predicate values. 192 | 193 | Uses ``name`` and ``default`` attributes of predicates to 194 | construct the dispatch key. 195 | 196 | :param d: dictionary mapping predicate names to predicate 197 | values. If a predicate is missing from ``d``, its default 198 | expected value is used. 199 | :returns: a tuple, to be used as a key for dispatching. 200 | """ 201 | return tuple([p.key_by_predicate_name(d) for p in self.predicates]) 202 | 203 | def component(self, keys): 204 | return next(self.all(keys), None) 205 | 206 | def fallback(self, keys): 207 | result = None 208 | for index, key in zip(self.indexes, keys): 209 | for k in index.permutations(key): 210 | match = index[k] 211 | if match: 212 | break 213 | else: 214 | # no matching permutation for this key, so this is the fallback 215 | return index.fallback 216 | if result is None: 217 | result = match 218 | else: 219 | result = result.intersection(match) 220 | # as soon as the intersection becomes empty, we have a failed 221 | # match 222 | if not result: 223 | return index.fallback 224 | 225 | def all(self, key): 226 | for p in self.permutations(key): 227 | yield from self.get(p) 228 | -------------------------------------------------------------------------------- /doc/history.rst: -------------------------------------------------------------------------------- 1 | History of Reg 2 | ============== 3 | 4 | Reg was written by Martijn Faassen. The core mapping code was 5 | originally co-authored by Thomas Lotze, though this has since been 6 | subsumed into the generalized predicate architecture. After a few 7 | years of use, Stefano Taschini initiated a large refactoring and API 8 | redesign. 9 | 10 | Reg is a predicate dispatch implementation for Python, with support 11 | for multiple dispatch registries in the same runtime. It was 12 | originally heavily inspired by the Zope Component Architecture (ZCA) 13 | consisting of the ``zope.interface`` and ``zope.component`` 14 | packages. Reg has strongly evolved since its inception into a general 15 | function dispatch library. Reg's codebase is completely separate from 16 | the ZCA and it has an entirely different API. At the end I've included 17 | a brief history of the ZCA. 18 | 19 | The primary use case for Reg has been the Morepath_ web framework, 20 | which uses it heavily. 21 | 22 | .. _Morepath: http://morepath.readthedocs.io 23 | 24 | Reg History 25 | ----------- 26 | 27 | The Reg code went through a quite bit of history as our insights 28 | evolved. 29 | 30 | iface 31 | ~~~~~ 32 | 33 | The core registry (mapping) code was conceived by Thomas Lotze and 34 | Martijn Faassen as a speculative sandbox project in January 35 | of 2010. It was called ``iface`` then: 36 | 37 | http://svn.zope.org/Sandbox/faassen/iface/ 38 | 39 | This registry was instrumental in getting Reg started, but was 40 | subsequently removed in a later refactoring. 41 | 42 | crom 43 | ~~~~ 44 | 45 | In early 2012, Martijn was at a sprint in Nürnberg, Germany organized 46 | by Novareto. Inspired by discussions with the sprint participants, 47 | particularly the Cromlech developers Souheil Chelfouh and Alex Garel, 48 | Martijn created a project called Crom: 49 | 50 | https://github.com/faassen/crom 51 | 52 | Crom focused on rethinking component and adapter registration and 53 | lookup APIs, but was still based on ``zope.interface`` for its 54 | fundamental ``AdapterRegistry`` implementation and the ``Interface`` 55 | metaclass. Martijn worked a bit on Crom after the sprint, but soon 56 | moved on to other matters. 57 | 58 | iface + crom 59 | ~~~~~~~~~~~~ 60 | 61 | At the Plone conference held in Arnhem, the Netherlands in October 62 | 2012, Martijn gave a lightning talk about Crom, which was received 63 | positively, which reignited his interest. In the end of 2012 Martijn 64 | mailed Thomas Lotze to ask to merge iface into Crom, and he gave his 65 | kind permission. 66 | 67 | The core registry code of iface was never quite finished however, and 68 | while the iface code was now in Crom, Crom didn't use it yet. Thus it 69 | lingered some more. 70 | 71 | ZCA-style Reg 72 | ~~~~~~~~~~~~~ 73 | 74 | In July 2013 in development work for CONTACT (contact.de), Martijn 75 | found himself in need of clever registries. Crom also had some 76 | configuration code intermingled with the component architecture code, 77 | and Martijn wanted to separate this out. 78 | 79 | So Martijn reorganized the code yet again into another project, this 80 | one: Reg. Martijn then finished the core mapping code and hooked it up 81 | to the Crom-style API, which he refactored further. For interfaces, he 82 | used Python's ``abc`` module. 83 | 84 | For a while during internal development this codebase was called 85 | ``Comparch``, but this conflicted with another name so he decided to 86 | call it ``Reg``, short for registry, as it's really about clever 87 | registries more than anything else. 88 | 89 | This version of Reg was still very similar in concepts to the Zope 90 | Component Architecture, though it used a streamlined API. This 91 | streamlined API lead to further developments. 92 | 93 | Generic dispatch 94 | ~~~~~~~~~~~~~~~~ 95 | 96 | After Martijn's first announcement_ of Reg to the world in September 97 | 2013 he got a question why it shouldn't just use PEP 443, which has a 98 | generic function implementation (single dispatch). This lead to the 99 | idea of converting Reg into a generic function implementation (with 100 | multiple dispatch), as it was already very close. After talking to 101 | some people about this at PyCon DE in october, Martijn did the 102 | refactoring_ to use generic functions throughout and no interfaces for 103 | lookup. Martijn then used this version of Reg in Morepath for about a 104 | year. 105 | 106 | .. _announcement: http://blog.startifact.com/posts/reg-component-architecture-reimagined.html 107 | 108 | .. _refactoring: http://blog.startifact.com/posts/reg-now-with-more-generic.html 109 | 110 | Predicate dispatch 111 | ~~~~~~~~~~~~~~~~~~ 112 | 113 | In October 2014 Martijn had some experience with using Reg and found 114 | some of its limitations: 115 | 116 | * Reg would try to dispatch on *all* non-keyword arguments of a function. 117 | This is not what is desired in many cases. We need a way to dispatch only 118 | on specified arguments and leave others alone. 119 | 120 | * Reg had an undocumented predicate subsystem used to implement view 121 | lookup in Morepath. A new requirement lead to the requirement to 122 | dispatch on the class of an instance, and while Reg's generic 123 | dispatch system could do it, the predicate subsystem could 124 | not. Enabling this required a major reorganization of Reg. 125 | 126 | * Martijn realized that such a reorganized predicate system could 127 | actually be used to generalize the way Reg worked based on how 128 | predicates worked. 129 | 130 | * This would allow predicates to play along in Reg's caching 131 | infrastructure, which could then speed up Morepath's view lookups. 132 | 133 | * A specific use case to replace class methods caused me to introduce 134 | ``reg.classgeneric``. This could be subsumed in a generalized 135 | predicate infrastructure as well. 136 | 137 | So in October 2014, Martijn refactored Reg once again in the light of 138 | this, generalizing the generic dispatch further to `predicate 139 | dispatch`_, and replacing the iface-based registry. This refactoring 140 | resulted in a smaller, more unified codebase that has more features 141 | and was also faster. 142 | 143 | .. _`predicate dispatch`: https://en.wikipedia.org/wiki/Predicate_dispatch 144 | 145 | Removing implicitness and inverting layers 146 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 147 | 148 | Reg used an implicit ``lookup`` system to find the current registry to 149 | use for dispatch. This allows Morepath to compose larger applications 150 | out of smaller registries, each with their own dispatch context. As an 151 | alternative to the implicit system, you could also pass in a custom 152 | ``lookup`` argument to the function to indicate the current registry. 153 | 154 | In 2016 Stefano Taschini started pushing on Morepath's use of dispatch 155 | functions and their implicit nature. Subsequent discussions with 156 | Martijn led to the insight that if we approached dispatch functions as 157 | dispatch *methods* on a context class (the Morepath application), we 158 | could get rid of the implicit behavior altogether, while gaining 159 | performance as we'd use Python's method mechanism. 160 | 161 | In continuing discussions, Stefano also suggested that there was no 162 | need for Reg in cases where the dispatch behavior of Reg was not 163 | needed. This led to the insight that this non-dispatch behavior could 164 | be installed as methods directly on the context class. 165 | 166 | Stefano also proposed that Reg could be internally simplified if we 167 | made the multiple registry behavior less central to the 168 | implementation, and let each dispatch function maintain its own 169 | registry. Stefano and Martijn then worked on an implementation where 170 | the dispatch method behavior is layered on top of a simpler dispatch 171 | function layer. 172 | 173 | Brief history of Zope Component Architecture 174 | -------------------------------------------- 175 | 176 | Reg is heavily inspired by ``zope.interface`` and ``zope.component``, 177 | by Jim Fulton and a lot of Zope developers, though Reg has undergone a 178 | significant evolution since then. ``zope.interface`` has a long 179 | history, going all the way back to December 1998, when a scarecrow 180 | interface package was released for discussion: 181 | 182 | http://old.zope.org/Members/jim/PythonInterfaces/Summary/ 183 | 184 | http://old.zope.org/Members/jim/PythonInterfaces/Interface/ 185 | 186 | A later version of this codebase found itself in Zope, as ``Interface``: 187 | 188 | http://svn.zope.org/Zope/tags/2-8-6/lib/python/Interface/ 189 | 190 | A new version called zope.interface was developed for the Zope 3 191 | project, somewhere around the year 2001 or 2002 (code historians, 192 | please dig deeper and let me know). On top of this a zope.component 193 | library was constructed which added registration and lookup APIs on 194 | top of the core zope.interface code. 195 | 196 | zope.interface and zope.component are widely used as the core of the 197 | Zope 3 project. zope.interface was adopted by other projects, such as 198 | Zope 2, Twisted, Grok, BlueBream and Pyramid. 199 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Reg documentation build configuration file, created by 3 | # sphinx-quickstart on Wed Sep 18 18:38:12 2013. 4 | # 5 | # This file is execfile()d with the current directory set to its containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | import os 14 | import pkg_resources 15 | from datetime import datetime 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | # sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | # needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = [ 30 | "sphinx.ext.doctest", 31 | "sphinx.ext.autodoc", 32 | "sphinx.ext.intersphinx", 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = ".rst" 40 | 41 | # The encoding of source files. 42 | # source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = "index" 46 | 47 | # General information about the project. 48 | project = "Reg" 49 | copyright = "2010 - %s, Morepath Developers" % datetime.today().year 50 | 51 | # The version info for the project you're documenting, acts as replacement for 52 | # |version| and |release|, also used in various other places throughout the 53 | # built documents. 54 | # 55 | # The short X.Y version. 56 | version = pkg_resources.get_distribution("reg").version 57 | # The full version, including alpha/beta/rc tags. 58 | release = version 59 | if release.endswith("dev"): 60 | release = "%s (unreleased)" % release 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | # today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | # today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = ["build"] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all documents. 77 | # default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | # add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | # add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | # show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = "sphinx" 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | # modindex_common_prefix = [] 95 | 96 | 97 | # -- Options for HTML output --------------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = "default" 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | # html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | # html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | # html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | # html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | # html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | # html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = [] 131 | 132 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 133 | # using the given strftime format. 134 | # html_last_updated_fmt = '%b %d, %Y' 135 | 136 | # If true, SmartyPants will be used to convert quotes and dashes to 137 | # typographically correct entities. 138 | # html_use_smartypants = True 139 | 140 | # Custom sidebar templates, maps document names to template names. 141 | # html_sidebars = {} 142 | 143 | # Additional templates that should be rendered to pages, maps page names to 144 | # template names. 145 | # html_additional_pages = {} 146 | 147 | # If false, no module index is generated. 148 | # html_domain_indices = True 149 | 150 | # If false, no index is generated. 151 | # html_use_index = True 152 | 153 | # If true, the index is split into individual pages for each letter. 154 | # html_split_index = False 155 | 156 | # If true, links to the reST sources are added to the pages. 157 | # html_show_sourcelink = True 158 | 159 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 160 | # html_show_sphinx = True 161 | 162 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 163 | # html_show_copyright = True 164 | 165 | # If true, an OpenSearch description file will be output, and all pages will 166 | # contain a tag referring to it. The value of this option must be the 167 | # base URL from which the finished HTML is served. 168 | # html_use_opensearch = '' 169 | 170 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 171 | # html_file_suffix = None 172 | 173 | # Output file base name for HTML help builder. 174 | htmlhelp_basename = "Regdoc" 175 | 176 | 177 | # -- Options for LaTeX output -------------------------------------------------- 178 | 179 | latex_elements = { 180 | # The paper size ('letterpaper' or 'a4paper'). 181 | # 'papersize': 'letterpaper', 182 | # The font size ('10pt', '11pt' or '12pt'). 183 | # 'pointsize': '10pt', 184 | # Additional stuff for the LaTeX preamble. 185 | # 'preamble': '', 186 | } 187 | 188 | # Grouping the document tree into LaTeX files. List of tuples 189 | # (source start file, target name, title, author, documentclass [howto/manual]). 190 | latex_documents = [ 191 | ("index", "Reg.tex", "Reg Documentation", "Martijn Faassen", "manual"), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | # latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | # latex_use_parts = False 201 | 202 | # If true, show page references after internal links. 203 | # latex_show_pagerefs = False 204 | 205 | # If true, show URL addresses after external links. 206 | # latex_show_urls = False 207 | 208 | # Documents to append as an appendix to all manuals. 209 | # latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | # latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [("index", "reg", "Reg Documentation", ["Martijn Faassen"], 1)] 220 | 221 | # If true, show URL addresses after external links. 222 | # man_show_urls = False 223 | 224 | 225 | # -- Options for Texinfo output ------------------------------------------------ 226 | 227 | # Grouping the document tree into Texinfo files. List of tuples 228 | # (source start file, target name, title, author, 229 | # dir menu entry, description, category) 230 | texinfo_documents = [ 231 | ( 232 | "index", 233 | "Reg", 234 | "Reg Documentation", 235 | "Martijn Faassen", 236 | "Reg", 237 | "One line description of project.", 238 | "Miscellaneous", 239 | ), 240 | ] 241 | 242 | # Documents to append as an appendix to all manuals. 243 | # texinfo_appendices = [] 244 | 245 | # If false, no module index is generated. 246 | # texinfo_domain_indices = True 247 | 248 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 249 | # texinfo_show_urls = 'footnote' 250 | 251 | doctest_path = [os.path.abspath("..")] 252 | 253 | intersphinx_mapping = {"python": ("http://docs.python.org/3.5", None)} 254 | -------------------------------------------------------------------------------- /doc/context.rst: -------------------------------------------------------------------------------- 1 | Context-based dispatch 2 | ====================== 3 | 4 | Introduction 5 | ------------ 6 | 7 | Consider this advanced use case for Reg: we have a runtime with 8 | multiple contexts. For each context, you want the dispatch behavior to 9 | be different. Concretely, if you have an application where you can 10 | call a view dispatch function, you want it to execute a different 11 | function and return a different value in each separate context. 12 | 13 | The Morepath web framework uses this feature of Reg to allow the 14 | developer to compose a larger application from multiple smaller ones. 15 | 16 | You can define application context as a class. This context class 17 | defines dispatch *methods*. When you subclass the context class, you 18 | establish a new context: each subclass has entirely different dispatch 19 | registrations, and shares nothing with its base class. 20 | 21 | A Context Class 22 | --------------- 23 | 24 | Here is a concrete example. First we define a context class we call 25 | ``A``, and a ``view`` dispatch method on it: 26 | 27 | .. testcode:: 28 | 29 | import reg 30 | 31 | class A(object): 32 | @reg.dispatch_method( 33 | reg.match_instance('obj'), 34 | reg.match_key('request_method', 35 | lambda self, obj, request: request.request_method)) 36 | def view(self, obj, request): 37 | return "default" 38 | 39 | Note that since ``view`` is a method we define a ``self`` argument. 40 | 41 | To have something to view, We define ``Document`` and ``Image`` 42 | content classes: 43 | 44 | .. testcode:: 45 | 46 | class Document(object): 47 | def __init__(self, text): 48 | self.text = text 49 | 50 | class Image(object): 51 | def __init__(self, bytes): 52 | self.bytes = bytes 53 | 54 | We also need a request class: 55 | 56 | .. testcode:: 57 | 58 | class Request(object): 59 | def __init__(self, request_method, body=''): 60 | self.request_method = request_method 61 | self.body = body 62 | 63 | To try this out, we need to create an instance of the context class: 64 | 65 | .. testcode:: 66 | 67 | a = A() 68 | 69 | Before we register anything, we get the default result we defined 70 | in the method: 71 | 72 | .. doctest:: 73 | 74 | >>> doc = Document('Hello world!') 75 | >>> a.view(doc, Request('GET')) 76 | 'default' 77 | >>> a.view(doc, Request('POST', 'new content')) 78 | 'default' 79 | >>> image = Image('abc') 80 | >>> a.view(image, Request('GET')) 81 | 'default' 82 | 83 | Here are the functions we are going to register: 84 | 85 | .. testcode:: 86 | 87 | def document_get(obj, request): 88 | return "Document text is: " + obj.text 89 | 90 | def document_post(obj, request): 91 | obj.text = request.body 92 | return "We changed the document" 93 | 94 | def image_get(obj, request): 95 | return obj.bytes 96 | 97 | def image_post(obj, request): 98 | obj.bytes = request.body 99 | return "We changed the image" 100 | 101 | We now want to register them with our context. To do so, we need to 102 | access the dispatch function through its class (``A``), not its 103 | instance (``a``). All instances of ``A`` (but not instances of its 104 | subclasses as we will see later) share the same registrations. 105 | 106 | We use :func:`reg.methodify` to do the registration, to keep our view 107 | functions the same as when context is not in use. We will see an 108 | example without :func:`reg.methodify` later: 109 | 110 | .. testcode:: 111 | 112 | from reg import methodify 113 | A.view.register(methodify(document_get), 114 | request_method='GET', 115 | obj=Document) 116 | A.view.register(methodify(document_post), 117 | request_method='POST', 118 | obj=Document) 119 | A.view.register(methodify(image_get), 120 | request_method='GET', 121 | obj=Image) 122 | A.view.register(methodify(image_post), 123 | request_method='POST', 124 | obj=Image) 125 | 126 | Now that we've registered some functions, we get the expected behavior 127 | when we call ``a.view``: 128 | 129 | .. doctest:: 130 | 131 | >>> a.view(doc, Request('GET')) 132 | 'Document text is: Hello world!' 133 | >>> a.view(doc, Request('POST', 'New content')) 134 | 'We changed the document' 135 | >>> doc.text 136 | 'New content' 137 | >>> a.view(image, Request('GET')) 138 | 'abc' 139 | >>> a.view(image, Request('POST', "new data")) 140 | 'We changed the image' 141 | >>> image.bytes 142 | 'new data' 143 | 144 | A new context 145 | ------------- 146 | 147 | Okay, we associate a dispatch method with a context class, but what is the 148 | point? The point is that we can introduce a new context that has 149 | different behavior now. To do, we subclass ``A``: 150 | 151 | .. testcode:: 152 | 153 | class B(A): 154 | pass 155 | 156 | At this point the new ``B`` context is empty of specific behavior, 157 | even though it subclasses ``A``: 158 | 159 | .. doctest:: 160 | 161 | >>> b = B() 162 | >>> b.view(doc, Request('GET')) 163 | 'default' 164 | >>> b.view(doc, Request('POST', 'New content')) 165 | 'default' 166 | >>> b.view(image, Request('GET')) 167 | 'default' 168 | >>> b.view(image, Request('POST', "new data")) 169 | 'default' 170 | 171 | We can now do our registrations. Let's register the same 172 | behavior for documents as we did for ``Context``: 173 | 174 | .. testcode:: 175 | 176 | B.view.register(methodify(document_get), 177 | request_method='GET', 178 | obj=Document) 179 | B.view.register(methodify(document_post), 180 | request_method='POST', 181 | obj=Document) 182 | 183 | But we install *different* behavior for ``Image``: 184 | 185 | .. testcode:: 186 | 187 | def b_image_get(obj, request): 188 | return 'New image GET' 189 | 190 | def b_image_post(obj, request): 191 | return 'New image POST' 192 | 193 | B.view.register(methodify(b_image_get), 194 | request_method='GET', 195 | obj=Image) 196 | B.view.register(methodify(b_image_post), 197 | request_method='POST', 198 | obj=Image) 199 | 200 | Calling ``view`` for ``Document`` works as before: 201 | 202 | .. doctest:: 203 | 204 | >>> b.view(doc, Request('GET')) 205 | 'Document text is: New content' 206 | 207 | But the behavior for ``Image`` instances is different in the ``B`` 208 | context: 209 | 210 | .. doctest:: 211 | 212 | >>> b.view(image, Request('GET')) 213 | 'New image GET' 214 | >>> b.view(image, Request('POST', "new data")) 215 | 'New image POST' 216 | 217 | Note that the original context ``A`` is of course unaffected and still 218 | has the behavior we registered for it: 219 | 220 | .. doctest:: 221 | 222 | >>> a.view(image, Request('GET')) 223 | 'new data' 224 | 225 | The idea is that you can create a framework around your base context 226 | class. Where this base context class needs to have dispatch behavior, 227 | you define dispatch methods. You then create different subclasses of 228 | the base context class and register different behaviors for them. This 229 | is what Morepath does with its ``App`` class. 230 | 231 | Call method in the same context 232 | ------------------------------- 233 | 234 | What if in a dispatch implementation you find you need to call another 235 | dispatch method? How to access the context? You can do this by 236 | registering a function that get a context as its first argument. As an 237 | example, we modify our document functions so that ``document_post`` 238 | uses the other: 239 | 240 | .. testcode:: 241 | 242 | def c_document_get(context, obj, request): 243 | return "Document text is: " + obj.text 244 | 245 | def c_document_post(context, obj, request): 246 | obj.text = request.body 247 | return "Changed: " + context.view(obj, Request('GET')) 248 | 249 | Now ``c_document_post`` uses the ``view`` dispatch method on the 250 | context. We need to register these methods using 251 | :meth:`reg.Dispatch.register` without :func:`reg.methodify`. This way 252 | they get the context as the first argument. Let's create a new context 253 | and do so: 254 | 255 | .. testcode:: 256 | 257 | class C(A): 258 | pass 259 | 260 | C.view.register(c_document_get, 261 | request_method='GET', 262 | obj=Document) 263 | C.view.register(c_document_post, 264 | request_method='POST', 265 | obj=Document) 266 | 267 | We now get the expected behavior: 268 | 269 | .. doctest:: 270 | 271 | >>> c = C() 272 | >>> c.view(doc, Request('GET')) 273 | 'Document text is: New content' 274 | >>> c.view(doc, Request('POST', 'Very new content')) 275 | 'Changed: Document text is: Very new content' 276 | 277 | You could have used :func:`reg.methodify` for this too, as 278 | ``methodify`` inspects the first argument and if it's identical to the 279 | second argument to ``methodify``, it will pass in the context as that 280 | argument. 281 | 282 | .. testcode:: 283 | 284 | class D(A): 285 | pass 286 | 287 | D.view.register(methodify(c_document_get, 'context'), 288 | request_method='GET', 289 | obj=Document) 290 | D.view.register(methodify(c_document_post, 'context'), 291 | request_method='POST', 292 | obj=Document) 293 | .. doctest:: 294 | 295 | >>> d = D() 296 | >>> d.view(doc, Request('GET')) 297 | 'Document text is: Very new content' 298 | >>> d.view(doc, Request('POST', 'Even newer content')) 299 | 'Changed: Document text is: Even newer content' 300 | 301 | The default value for the second argument to ``methodify`` is ``app``. 302 | -------------------------------------------------------------------------------- /reg/dispatch.py: -------------------------------------------------------------------------------- 1 | from functools import partial, wraps 2 | from collections import namedtuple 3 | from .predicate import match_instance 4 | from .predicate import PredicateRegistry 5 | from .arginfo import arginfo 6 | from .error import RegistrationError 7 | 8 | 9 | class dispatch: 10 | """Decorator to make a function dispatch based on its arguments. 11 | 12 | This takes the predicates to dispatch on as zero or more 13 | parameters. 14 | 15 | :param predicates: sequence of :class:`reg.Predicate` instances to 16 | do the dispatch on. You create predicates using 17 | :func:`reg.match_instance`, :func:`reg.match_key`, 18 | :func:`reg.match_class`, or with a custom predicate class. You 19 | can also pass in plain string argument, which is turned into a 20 | :func:`reg.match_instance` predicate. 21 | :param get_key_lookup: a function that gets a 22 | :class:`PredicateRegistry` instance and returns a key lookup. A 23 | :class:`PredicateRegistry` instance is itself a key lookup, but 24 | you can return a caching key lookup (such as 25 | :class:`reg.DictCachingKeyLookup` or 26 | :class:`reg.LruCachingKeyLookup`) to make it more efficient. 27 | :returns: a function that you can use as if it were a 28 | :class:`reg.Dispatch` instance. 29 | 30 | """ 31 | 32 | def __init__(self, *predicates, **kw): 33 | self.predicates = [self._make_predicate(predicate) for predicate in predicates] 34 | self.get_key_lookup = kw.pop("get_key_lookup", identity) 35 | 36 | def _make_predicate(self, predicate): 37 | if isinstance(predicate, str): 38 | return match_instance(predicate) 39 | return predicate 40 | 41 | def __call__(self, callable): 42 | return Dispatch(self.predicates, callable, self.get_key_lookup).call 43 | 44 | 45 | def identity(registry): 46 | return registry 47 | 48 | 49 | class LookupEntry(namedtuple("LookupEntry", "lookup key")): 50 | """The dispatch data associated to a key.""" 51 | 52 | __slots__ = () 53 | 54 | @property 55 | def component(self): 56 | """The function to dispatch to, excluding fallbacks.""" 57 | return self.lookup.component(self.key) 58 | 59 | @property 60 | def fallback(self): 61 | """The approriate fallback implementation.""" 62 | return self.lookup.fallback(self.key) 63 | 64 | @property 65 | def matches(self): 66 | """An iterator over all the compatible implementations.""" 67 | return self.lookup.all(self.key) 68 | 69 | @property 70 | def all_matches(self): 71 | """The list of all compatible implementations.""" 72 | return list(self.matches) 73 | 74 | 75 | class Dispatch: 76 | """Dispatch function. 77 | 78 | You can register implementations based on particular predicates. The 79 | dispatch function dispatches to these implementations based on its 80 | arguments. 81 | 82 | :param predicates: a list of predicates. 83 | :param callable: the Python function object to register dispatch 84 | implementations for. The signature of an implementation needs to 85 | match that of this function. This function is used as a fallback 86 | implementation that is called if no specific implementations match. 87 | :param get_key_lookup: a function that gets a 88 | :class:`PredicateRegistry` instance and returns a key lookup. A 89 | :class:`PredicateRegistry` instance is itself a key lookup, but 90 | you can return a caching key lookup (such as 91 | :class:`reg.DictCachingKeyLookup` or 92 | :class:`reg.LruCachingKeyLookup`) to make it more efficient. 93 | """ 94 | 95 | def __init__(self, predicates, callable, get_key_lookup): 96 | self.wrapped_func = callable 97 | self.get_key_lookup = get_key_lookup 98 | self._original_predicates = predicates 99 | self._define_call() 100 | self._register_predicates(predicates) 101 | 102 | def _register_predicates(self, predicates): 103 | self.registry = PredicateRegistry(*predicates) 104 | self.predicates = predicates 105 | self.call.key_lookup = self.key_lookup = self.get_key_lookup(self.registry) 106 | self.call.__globals__.update( 107 | _registry_key=self.registry.key, 108 | _component_lookup=self.key_lookup.component, 109 | _fallback_lookup=self.key_lookup.fallback, 110 | ) 111 | self._predicate_key.__globals__.update( 112 | _registry_key=self.registry.key, 113 | _return_type=partial(LookupEntry, self.key_lookup), 114 | ) 115 | 116 | def _define_call(self): 117 | # We build the generic function on the fly. Its definition 118 | # requires the signature of the wrapped function and the 119 | # arguments needed by the registered predicates 120 | # (predicate_args): 121 | code_template = """\ 122 | def call({signature}): 123 | _key = _registry_key({predicate_args}) 124 | return (_component_lookup(_key) or 125 | _fallback_lookup(_key) or 126 | _fallback)({signature}) 127 | """ 128 | 129 | args = arginfo(self.wrapped_func) 130 | signature = format_signature(args) 131 | predicate_args = ", ".join("{0}={0}".format(x) for x in args.args) 132 | code_source = code_template.format( 133 | signature=signature, predicate_args=predicate_args 134 | ) 135 | 136 | # We now compile call to byte-code: 137 | self.call = call = wraps(self.wrapped_func)( 138 | execute( 139 | code_source, 140 | _registry_key=None, 141 | _component_lookup=None, 142 | _fallback_lookup=None, 143 | _fallback=self.wrapped_func, 144 | )["call"] 145 | ) 146 | 147 | # We copy over the defaults from the wrapped function. 148 | call.__defaults__ = args.defaults 149 | 150 | # Make the methods available as attributes of call 151 | for k in dir(type(self)): 152 | if not k.startswith("_"): 153 | setattr(call, k, getattr(self, k)) 154 | call.wrapped_func = self.wrapped_func 155 | 156 | # We now build the implementation for the predicate_key method 157 | self._predicate_key = execute( 158 | "def predicate_key({signature}):\n" 159 | " return _return_type(_registry_key({predicate_args}))".format( 160 | signature=format_signature(args), predicate_args=predicate_args 161 | ), 162 | _registry_key=None, 163 | _return_type=None, 164 | )["predicate_key"] 165 | 166 | def clean(self): 167 | """Clean up implementations and added predicates. 168 | 169 | This restores the dispatch function to its original state, 170 | removing registered implementations and predicates added 171 | using :meth:`reg.Dispatch.add_predicates`. 172 | """ 173 | self._register_predicates(self._original_predicates) 174 | 175 | def add_predicates(self, predicates): 176 | """Add new predicates. 177 | 178 | Extend the predicates used by this predicates. This can be 179 | used to add predicates that are configured during startup time. 180 | 181 | Note that this clears up any registered implementations. 182 | 183 | :param predicates: a list of predicates to add. 184 | """ 185 | self._register_predicates(self.predicates + predicates) 186 | 187 | def register(self, func=None, **key_dict): 188 | """Register an implementation. 189 | 190 | If ``func`` is not specified, this method can be used as a 191 | decorator and the decorated function will be used as the 192 | actual ``func`` argument. 193 | 194 | :param func: a function that implements behavior for this 195 | dispatch function. It needs to have the same signature as 196 | the original dispatch function. If this is a 197 | :class:`reg.DispatchMethod`, then this means it needs to 198 | take a first context argument. 199 | :param key_dict: keyword arguments describing the registration, 200 | with as keys predicate name and as values predicate values. 201 | :returns: ``func``. 202 | """ 203 | if func is None: 204 | return partial(self.register, **key_dict) 205 | validate_signature(func, self.wrapped_func) 206 | predicate_key = self.registry.key_dict_to_predicate_key(key_dict) 207 | self.registry.register(predicate_key, func) 208 | return func 209 | 210 | def by_args(self, *args, **kw): 211 | """Lookup an implementation by invocation arguments. 212 | 213 | :param args: positional arguments used in invocation. 214 | :param kw: named arguments used in invocation. 215 | :returns: a :class:`reg.LookupEntry`. 216 | """ 217 | return self._predicate_key(*args, **kw) 218 | 219 | def by_predicates(self, **predicate_values): 220 | """Lookup an implementation by predicate values. 221 | 222 | :param predicate_values: the values of the predicates to lookup. 223 | :returns: a :class:`reg.LookupEntry`. 224 | """ 225 | return LookupEntry( 226 | self.key_lookup, 227 | self.registry.key_dict_to_predicate_key(predicate_values), 228 | ) 229 | 230 | 231 | def validate_signature(f, dispatch): 232 | f_arginfo = arginfo(f) 233 | if f_arginfo is None: 234 | raise RegistrationError( 235 | "Cannot register non-callable for dispatch " "%r: %r" % (dispatch, f) 236 | ) 237 | if not same_signature(arginfo(dispatch), f_arginfo): 238 | raise RegistrationError( 239 | "Signature of callable dispatched to (%r) " 240 | "not that of dispatch (%r)" % (f, dispatch) 241 | ) 242 | 243 | 244 | def format_signature(args): 245 | return ", ".join( 246 | args.args 247 | + (["*" + args.varargs] if args.varargs else []) 248 | + (["**" + args.varkw] if args.varkw else []) 249 | ) 250 | 251 | 252 | def same_signature(a, b): 253 | """Check whether a arginfo and b arginfo are the same signature. 254 | 255 | Actual names of arguments may differ. Default arguments may be 256 | different. 257 | """ 258 | a_args = set(a.args) 259 | b_args = set(b.args) 260 | return len(a_args) == len(b_args) and a.varargs == b.varargs and a.varkw == b.varkw 261 | 262 | 263 | def execute(code_source, **namespace): 264 | """Execute code in a namespace, returning the namespace.""" 265 | code_object = compile(code_source, f"", "exec") 266 | exec(code_object, namespace) 267 | return namespace 268 | -------------------------------------------------------------------------------- /reg/tests/test_predicate.py: -------------------------------------------------------------------------------- 1 | from ..predicate import ( 2 | KeyIndex, 3 | ClassIndex, 4 | PredicateRegistry, 5 | match_instance, 6 | match_key, 7 | ) 8 | from ..error import RegistrationError 9 | import pytest 10 | 11 | 12 | def test_key_index_permutations(): 13 | i = KeyIndex() 14 | assert list(i.permutations("GET")) == ["GET"] 15 | 16 | 17 | def test_class_index_permutations(): 18 | class Foo: 19 | pass 20 | 21 | class Bar(Foo): 22 | pass 23 | 24 | class Qux: 25 | pass 26 | 27 | i = ClassIndex() 28 | 29 | assert list(i.permutations(Foo)) == [Foo, object] 30 | assert list(i.permutations(Bar)) == [Bar, Foo, object] 31 | assert list(i.permutations(Qux)) == [Qux, object] 32 | 33 | 34 | def test_multi_class_predicate_permutations(): 35 | class ABase: 36 | pass 37 | 38 | class ASub(ABase): 39 | pass 40 | 41 | class BBase: 42 | pass 43 | 44 | class BSub(BBase): 45 | pass 46 | 47 | i = PredicateRegistry(match_instance("a"), match_instance("a")) 48 | 49 | assert list(i.permutations([ASub, BSub])) == [ 50 | (ASub, BSub), 51 | (ASub, BBase), 52 | (ASub, object), 53 | (ABase, BSub), 54 | (ABase, BBase), 55 | (ABase, object), 56 | (object, BSub), 57 | (object, BBase), 58 | (object, object), 59 | ] 60 | 61 | 62 | def test_multi_key_predicate_permutations(): 63 | i = PredicateRegistry( 64 | match_key("a"), 65 | match_key("b"), 66 | match_key("c"), 67 | ) 68 | 69 | assert list(i.permutations(["A", "B", "C"])) == [("A", "B", "C")] 70 | 71 | 72 | def test_registry_single_key_predicate(): 73 | r = PredicateRegistry(match_key("a")) 74 | 75 | r.register(("A",), "A value") 76 | 77 | assert r.component(("A",)) == "A value" 78 | assert r.component(("B",)) is None 79 | assert list(r.all(("A",))) == ["A value"] 80 | assert list(r.all(("B",))) == [] 81 | 82 | 83 | def test_registry_single_class_predicate(): 84 | r = PredicateRegistry(match_instance("a")) 85 | 86 | class Foo: 87 | pass 88 | 89 | class FooSub(Foo): 90 | pass 91 | 92 | class Qux: 93 | pass 94 | 95 | r.register((Foo,), "foo") 96 | 97 | assert r.component((Foo,)) == "foo" 98 | assert r.component((FooSub,)) == "foo" 99 | assert r.component((Qux,)) is None 100 | assert list(r.all((Foo,))) == ["foo"] 101 | assert list(r.all((FooSub,))) == ["foo"] 102 | assert list(r.all((Qux,))) == [] 103 | 104 | 105 | def test_registry_single_classic_class_predicate(): 106 | r = PredicateRegistry(match_instance("a")) 107 | 108 | class Foo: 109 | pass 110 | 111 | class FooSub(Foo): 112 | pass 113 | 114 | class Qux: 115 | pass 116 | 117 | r.register((Foo,), "foo") 118 | 119 | assert r.component((Foo,)) == "foo" 120 | assert r.component((FooSub,)) == "foo" 121 | assert r.component((Qux,)) is None 122 | assert list(r.all((Foo,))) == ["foo"] 123 | assert list(r.all((FooSub,))) == ["foo"] 124 | assert list(r.all((Qux,))) == [] 125 | 126 | 127 | def test_registry_single_class_predicate_also_sub(): 128 | r = PredicateRegistry(match_instance("a")) 129 | 130 | class Foo: 131 | pass 132 | 133 | class FooSub(Foo): 134 | pass 135 | 136 | class Qux: 137 | pass 138 | 139 | r.register((Foo,), "foo") 140 | r.register((FooSub,), "sub") 141 | 142 | assert r.component((Foo,)) == "foo" 143 | assert r.component((FooSub,)) == "sub" 144 | assert r.component((Qux,)) is None 145 | assert list(r.all((Foo,))) == ["foo"] 146 | assert list(r.all((FooSub,))) == ["sub", "foo"] 147 | assert list(r.all((Qux,))) == [] 148 | 149 | 150 | def test_registry_multi_class_predicate(): 151 | r = PredicateRegistry( 152 | match_instance("a"), 153 | match_instance("b"), 154 | ) 155 | 156 | class A: 157 | pass 158 | 159 | class AA(A): 160 | pass 161 | 162 | class B: 163 | pass 164 | 165 | class BB(B): 166 | pass 167 | 168 | r.register((A, B), "foo") 169 | 170 | assert r.component((A, B)) == "foo" 171 | assert r.component((AA, BB)) == "foo" 172 | assert r.component((AA, B)) == "foo" 173 | assert r.component((A, BB)) == "foo" 174 | assert r.component((A, object)) is None 175 | assert r.component((object, B)) is None 176 | 177 | assert list(r.all((A, B))) == ["foo"] 178 | assert list(r.all((AA, BB))) == ["foo"] 179 | assert list(r.all((AA, B))) == ["foo"] 180 | assert list(r.all((A, BB))) == ["foo"] 181 | assert list(r.all((A, object))) == [] 182 | assert list(r.all((object, B))) == [] 183 | 184 | 185 | def test_registry_multi_mixed_predicate_class_key(): 186 | r = PredicateRegistry( 187 | match_instance("a"), 188 | match_key("b"), 189 | ) 190 | 191 | class A: 192 | pass 193 | 194 | class AA(A): 195 | pass 196 | 197 | class Unknown: 198 | pass 199 | 200 | r.register((A, "B"), "foo") 201 | 202 | assert r.component((A, "B")) == "foo" 203 | assert r.component((A, "unknown")) is None 204 | assert r.component((AA, "B")) == "foo" 205 | assert r.component((AA, "unknown")) is None 206 | assert r.component((Unknown, "B")) is None 207 | 208 | assert list(r.all((A, "B"))) == ["foo"] 209 | assert list(r.all((A, "unknown"))) == [] 210 | assert list(r.all((AA, "B"))) == ["foo"] 211 | assert list(r.all((AA, "unknown"))) == [] 212 | assert list(r.all((Unknown, "B"))) == [] 213 | 214 | 215 | def test_registry_multi_mixed_predicate_key_class(): 216 | r = PredicateRegistry( 217 | match_key("a"), 218 | match_instance("b"), 219 | ) 220 | 221 | class B: 222 | pass 223 | 224 | class BB(B): 225 | pass 226 | 227 | class Unknown: 228 | pass 229 | 230 | r.register(("A", B), "foo") 231 | 232 | assert r.component(("A", B)) == "foo" 233 | assert r.component(("A", BB)) == "foo" 234 | assert r.component(("A", Unknown)) is None 235 | assert r.component(("unknown", Unknown)) is None 236 | 237 | assert list(r.all(("A", B))) == ["foo"] 238 | assert list(r.all(("A", BB))) == ["foo"] 239 | assert list(r.all(("A", Unknown))) == [] 240 | assert list(r.all(("unknown", Unknown))) == [] 241 | 242 | 243 | def test_single_predicate_get_key(): 244 | def get_key(foo): 245 | return foo 246 | 247 | p = match_key("a", get_key) 248 | 249 | assert p.get_key({"foo": "value"}) == "value" 250 | 251 | 252 | def test_multi_predicate_get_key(): 253 | def a_key(**d): 254 | return d["a"] 255 | 256 | def b_key(**d): 257 | return d["b"] 258 | 259 | p = PredicateRegistry(match_key("a", a_key), match_key("b", b_key)) 260 | 261 | assert p.key(a="A", b="B") == ("A", "B") 262 | 263 | 264 | def test_single_predicate_fallback(): 265 | r = PredicateRegistry(match_key("a", fallback="fallback")) 266 | 267 | r.register(("A",), "A value") 268 | 269 | assert r.component(("A",)) == "A value" 270 | assert r.component(("B",)) is None 271 | assert r.fallback(("B",)) == "fallback" 272 | 273 | 274 | def test_multi_predicate_fallback(): 275 | r = PredicateRegistry( 276 | match_key("a", fallback="fallback1"), 277 | match_key("b", fallback="fallback2"), 278 | ) 279 | 280 | r.register(("A", "B"), "value") 281 | 282 | assert r.component(("A", "B")) == "value" 283 | assert r.component(("A", "C")) is None 284 | assert r.fallback(("A", "C")) == "fallback2" 285 | assert r.component(("C", "B")) is None 286 | assert r.fallback(("C", "B")) == "fallback1" 287 | 288 | assert list(r.all(("A", "B"))) == ["value"] 289 | assert list(r.all(("A", "C"))) == [] 290 | assert list(r.all(("C", "B"))) == [] 291 | 292 | 293 | def test_predicate_self_request(): 294 | m = PredicateRegistry(match_key("a"), match_key("b", fallback="registered for all")) 295 | m.register(("foo", "POST"), "registered for post") 296 | 297 | assert m.component(("foo", "GET")) is None 298 | assert m.fallback(("foo", "GET")) == "registered for all" 299 | assert m.component(("foo", "POST")) == "registered for post" 300 | assert m.fallback(("foo", "POST")) is None 301 | assert m.component(("bar", "GET")) is None 302 | 303 | 304 | # XXX using an incomplete key returns undefined results 305 | 306 | 307 | def test_predicate_duplicate_key(): 308 | m = PredicateRegistry(match_key("a"), match_key("b", fallback="registered for all")) 309 | m.register(("foo", "POST"), "registered for post") 310 | with pytest.raises(RegistrationError): 311 | m.register(("foo", "POST"), "registered again") 312 | 313 | 314 | def test_name_request_method_body_model_registered_for_base(): 315 | m = PredicateRegistry( 316 | match_key("name", fallback="name fallback"), 317 | match_key("request_method", fallback="request_method fallback"), 318 | match_instance("body_model", fallback="body_model fallback"), 319 | ) 320 | 321 | class Foo: 322 | pass 323 | 324 | class Bar(Foo): 325 | pass 326 | 327 | m.register(("foo", "POST", Foo), "post foo") 328 | 329 | assert m.component(("bar", "GET", object)) is None 330 | assert m.fallback(("bar", "GET", object)) == "name fallback" 331 | assert m.component(("foo", "GET", object)) is None 332 | assert m.fallback(("foo", "GET", object)) == "request_method fallback" 333 | assert m.component(("foo", "POST", object)) is None 334 | assert m.fallback(("foo", "POST", object)) == "body_model fallback" 335 | assert m.component(("foo", "POST", Foo)) == "post foo" 336 | assert m.component(("foo", "POST", Bar)) == "post foo" 337 | 338 | 339 | def test_name_request_method_body_model_registered_for_base_and_sub(): 340 | m = PredicateRegistry( 341 | match_key("name", fallback="name fallback"), 342 | match_key("request", fallback="request_method fallback"), 343 | match_instance("body_model", fallback="body_model fallback"), 344 | ) 345 | 346 | class Foo: 347 | pass 348 | 349 | class Bar(Foo): 350 | pass 351 | 352 | m.register(("foo", "POST", Foo), "post foo") 353 | m.register(("foo", "POST", Bar), "post bar") 354 | 355 | assert m.component(("bar", "GET", object)) is None 356 | assert m.fallback(("bar", "GET", object)) == "name fallback" 357 | 358 | assert m.component(("foo", "GET", object)) is None 359 | assert m.fallback(("foo", "GET", object)) == "request_method fallback" 360 | 361 | assert m.component(("foo", "POST", object)) is None 362 | assert m.fallback(("foo", "POST", object)) == "body_model fallback" 363 | 364 | assert m.component(("foo", "POST", Foo)) == "post foo" 365 | assert m.component(("foo", "POST", Bar)) == "post bar" 366 | 367 | 368 | def test_key_by_predicate_name(): 369 | p = match_key("foo", default="default") 370 | 371 | assert p.key_by_predicate_name({"foo": "value"}) == "value" 372 | assert p.key_by_predicate_name({}) == "default" 373 | 374 | 375 | def test_multi_key_by_predicate_name(): 376 | p = PredicateRegistry( 377 | match_key("foo", default="default foo"), 378 | match_key("bar", default="default bar"), 379 | ) 380 | assert p.key_dict_to_predicate_key({"foo": "FOO", "bar": "BAR"}) == ( 381 | "FOO", 382 | "BAR", 383 | ) 384 | assert p.key_dict_to_predicate_key({}) == ("default foo", "default bar") 385 | 386 | 387 | def test_nameless_predicate_key(): 388 | p = match_key("a") 389 | 390 | assert p.key_by_predicate_name({}) is None 391 | -------------------------------------------------------------------------------- /reg/tests/test_registry.py: -------------------------------------------------------------------------------- 1 | from ..predicate import PredicateRegistry, match_instance, match_key 2 | from ..cache import DictCachingKeyLookup, LruCachingKeyLookup 3 | from ..error import RegistrationError 4 | from ..dispatch import dispatch 5 | import pytest 6 | 7 | 8 | def register_value(generic, key, value): 9 | """Low-level function that directly uses the internal registry of the 10 | generic function to register an implementation. 11 | """ 12 | generic.register.__self__.registry.register(key, value) 13 | 14 | 15 | def test_registry(): 16 | class Foo: 17 | pass 18 | 19 | class FooSub(Foo): 20 | pass 21 | 22 | @dispatch() 23 | def view(self, request): 24 | raise NotImplementedError() 25 | 26 | def get_model(self, request): 27 | return self 28 | 29 | def get_name(self, request): 30 | return request.name 31 | 32 | def get_request_method(self, request): 33 | return request.request_method 34 | 35 | def model_fallback(self, request): 36 | return "Model fallback" 37 | 38 | def name_fallback(self, request): 39 | return "Name fallback" 40 | 41 | def request_method_fallback(self, request): 42 | return "Request method fallback" 43 | 44 | view.add_predicates( 45 | [ 46 | match_instance("model", get_model, model_fallback), 47 | match_key("name", get_name, name_fallback), 48 | match_key("request_method", get_request_method, request_method_fallback), 49 | ] 50 | ) 51 | 52 | def foo_default(self, request): 53 | return "foo default" 54 | 55 | def foo_post(self, request): 56 | return "foo default post" 57 | 58 | def foo_edit(self, request): 59 | return "foo edit" 60 | 61 | register_value(view, (Foo, "", "GET"), foo_default) 62 | register_value(view, (Foo, "", "POST"), foo_post) 63 | register_value(view, (Foo, "edit", "POST"), foo_edit) 64 | 65 | key_lookup = view.key_lookup 66 | assert key_lookup.component((Foo, "", "GET")) is foo_default 67 | assert key_lookup.component((Foo, "", "POST")) is foo_post 68 | assert key_lookup.component((Foo, "edit", "POST")) is foo_edit 69 | assert key_lookup.component((FooSub, "", "GET")) is foo_default 70 | assert key_lookup.component((FooSub, "", "POST")) is foo_post 71 | 72 | class Request: 73 | def __init__(self, name, request_method): 74 | self.name = name 75 | self.request_method = request_method 76 | 77 | assert view(Foo(), Request("", "GET")) == "foo default" 78 | assert view(FooSub(), Request("", "GET")) == "foo default" 79 | assert view(FooSub(), Request("edit", "POST")) == "foo edit" 80 | 81 | class Bar: 82 | pass 83 | 84 | assert view(Bar(), Request("", "GET")) == "Model fallback" 85 | assert view(Foo(), Request("dummy", "GET")) == "Name fallback" 86 | assert view(Foo(), Request("", "PUT")) == "Request method fallback" 87 | assert view(FooSub(), Request("dummy", "GET")) == "Name fallback" 88 | 89 | 90 | def test_predicate_registry_class_lookup(): 91 | reg = PredicateRegistry(match_instance("obj")) 92 | 93 | class Document: 94 | pass 95 | 96 | class SpecialDocument(Document): 97 | pass 98 | 99 | reg.register((Document,), "document line count") 100 | reg.register((SpecialDocument,), "special document line count") 101 | 102 | assert reg.component((Document,)) == "document line count" 103 | 104 | assert reg.component((SpecialDocument,)) == "special document line count" 105 | 106 | class AnotherDocument(Document): 107 | pass 108 | 109 | assert reg.component((AnotherDocument,)) == "document line count" 110 | 111 | class Other: 112 | pass 113 | 114 | assert reg.component((Other,)) is None 115 | 116 | 117 | def test_predicate_registry_target_find_specific(): 118 | reg = PredicateRegistry(match_instance("obj")) 119 | reg2 = PredicateRegistry(match_instance("obj")) 120 | 121 | class Document: 122 | pass 123 | 124 | class SpecialDocument(Document): 125 | pass 126 | 127 | def linecount(obj): 128 | pass 129 | 130 | def special_linecount(obj): 131 | pass 132 | 133 | reg.register((Document,), "line count") 134 | reg2.register((Document,), "special line count") 135 | 136 | assert reg.component((Document,)) == "line count" 137 | assert reg2.component((Document,)) == "special line count" 138 | 139 | assert reg.component((SpecialDocument,)) == "line count" 140 | assert reg2.component((SpecialDocument,)) == "special line count" 141 | 142 | 143 | def test_registry_no_sources(): 144 | reg = PredicateRegistry() 145 | 146 | class Animal: 147 | pass 148 | 149 | reg.register((), "elephant") 150 | assert reg.component(()) == "elephant" 151 | 152 | 153 | def test_register_twice_with_predicate(): 154 | reg = PredicateRegistry(match_instance("obj")) 155 | 156 | class Document: 157 | pass 158 | 159 | reg.register((Document,), "document line count") 160 | with pytest.raises(RegistrationError): 161 | reg.register((Document,), "another line count") 162 | 163 | 164 | def test_register_twice_without_predicates(): 165 | reg = PredicateRegistry() 166 | 167 | reg.register((), "once") 168 | with pytest.raises(RegistrationError): 169 | reg.register((), "twice") 170 | 171 | 172 | def test_dict_caching_registry(): 173 | class Foo: 174 | pass 175 | 176 | class FooSub(Foo): 177 | pass 178 | 179 | def get_model(self, request): 180 | return self 181 | 182 | def get_name(self, request): 183 | return request.name 184 | 185 | def get_request_method(self, request): 186 | return request.request_method 187 | 188 | def model_fallback(self, request): 189 | return "Model fallback" 190 | 191 | def name_fallback(self, request): 192 | return "Name fallback" 193 | 194 | def request_method_fallback(self, request): 195 | return "Request method fallback" 196 | 197 | def get_caching_key_lookup(r): 198 | return DictCachingKeyLookup(r) 199 | 200 | @dispatch( 201 | match_instance("model", get_model, model_fallback), 202 | match_key("name", get_name, name_fallback), 203 | match_key("request_method", get_request_method, request_method_fallback), 204 | get_key_lookup=get_caching_key_lookup, 205 | ) 206 | def view(self, request): 207 | raise NotImplementedError() 208 | 209 | def foo_default(self, request): 210 | return "foo default" 211 | 212 | def foo_post(self, request): 213 | return "foo default post" 214 | 215 | def foo_edit(self, request): 216 | return "foo edit" 217 | 218 | register_value(view, (Foo, "", "GET"), foo_default) 219 | register_value(view, (Foo, "", "POST"), foo_post) 220 | register_value(view, (Foo, "edit", "POST"), foo_edit) 221 | 222 | class Request: 223 | def __init__(self, name, request_method): 224 | self.name = name 225 | self.request_method = request_method 226 | 227 | assert view(Foo(), Request("", "GET")) == "foo default" 228 | assert view(FooSub(), Request("", "GET")) == "foo default" 229 | assert view(FooSub(), Request("edit", "POST")) == "foo edit" 230 | assert view.by_predicates(model=Foo, name="", request_method="GET").key == ( 231 | Foo, 232 | "", 233 | "GET", 234 | ) 235 | 236 | # use a bit of inside knowledge to check the cache is filled 237 | assert view.key_lookup.component.__self__.get((Foo, "", "GET")) is not None 238 | assert view.key_lookup.component.__self__.get((FooSub, "", "GET")) is not None 239 | assert view.key_lookup.component.__self__.get((FooSub, "edit", "POST")) is not None 240 | 241 | # now let's do this again. this time things come from the component cache 242 | assert view(Foo(), Request("", "GET")) == "foo default" 243 | assert view(FooSub(), Request("", "GET")) == "foo default" 244 | assert view(FooSub(), Request("edit", "POST")) == "foo edit" 245 | 246 | key_lookup = view.key_lookup 247 | # prime and check the all cache 248 | assert view.by_args(Foo(), Request("", "GET")).all_matches == [foo_default] 249 | assert key_lookup.all.__self__.get((Foo, "", "GET")) is not None 250 | # should be coming from cache now 251 | assert view.by_args(Foo(), Request("", "GET")).all_matches == [foo_default] 252 | 253 | class Bar: 254 | pass 255 | 256 | assert view(Bar(), Request("", "GET")) == "Model fallback" 257 | assert view(Foo(), Request("dummy", "GET")) == "Name fallback" 258 | assert view(Foo(), Request("", "PUT")) == "Request method fallback" 259 | assert view(FooSub(), Request("dummy", "GET")) == "Name fallback" 260 | 261 | # fallbacks get cached too 262 | assert key_lookup.fallback.__self__.get((Bar, "", "GET")) is model_fallback 263 | 264 | # these come from the fallback cache now 265 | assert view(Bar(), Request("", "GET")) == "Model fallback" 266 | assert view(Foo(), Request("dummy", "GET")) == "Name fallback" 267 | assert view(Foo(), Request("", "PUT")) == "Request method fallback" 268 | assert view(FooSub(), Request("dummy", "GET")) == "Name fallback" 269 | 270 | 271 | def test_lru_caching_registry(): 272 | class Foo: 273 | pass 274 | 275 | class FooSub(Foo): 276 | pass 277 | 278 | def get_model(self, request): 279 | return self 280 | 281 | def get_name(self, request): 282 | return request.name 283 | 284 | def get_request_method(self, request): 285 | return request.request_method 286 | 287 | def model_fallback(self, request): 288 | return "Model fallback" 289 | 290 | def name_fallback(self, request): 291 | return "Name fallback" 292 | 293 | def request_method_fallback(self, request): 294 | return "Request method fallback" 295 | 296 | def get_caching_key_lookup(r): 297 | return LruCachingKeyLookup(r, 100, 100, 100) 298 | 299 | @dispatch( 300 | match_instance("model", get_model, model_fallback), 301 | match_key("name", get_name, name_fallback), 302 | match_key("request_method", get_request_method, request_method_fallback), 303 | get_key_lookup=get_caching_key_lookup, 304 | ) 305 | def view(self, request): 306 | raise NotImplementedError() 307 | 308 | def foo_default(self, request): 309 | return "foo default" 310 | 311 | def foo_post(self, request): 312 | return "foo default post" 313 | 314 | def foo_edit(self, request): 315 | return "foo edit" 316 | 317 | register_value(view, (Foo, "", "GET"), foo_default) 318 | register_value(view, (Foo, "", "POST"), foo_post) 319 | register_value(view, (Foo, "edit", "POST"), foo_edit) 320 | 321 | class Request: 322 | def __init__(self, name, request_method): 323 | self.name = name 324 | self.request_method = request_method 325 | 326 | assert view(Foo(), Request("", "GET")) == "foo default" 327 | assert view(FooSub(), Request("", "GET")) == "foo default" 328 | assert view(FooSub(), Request("edit", "POST")) == "foo edit" 329 | assert view.by_predicates(model=Foo, name="", request_method="GET").key == ( 330 | Foo, 331 | "", 332 | "GET", 333 | ) 334 | 335 | # use a bit of inside knowledge to check the cache is filled 336 | component_cache = view.key_lookup.component.__closure__[0].cell_contents 337 | assert component_cache.get(((Foo, "", "GET"),)) is not None 338 | assert component_cache.get(((FooSub, "", "GET"),)) is not None 339 | assert component_cache.get(((FooSub, "edit", "POST"),)) is not None 340 | 341 | # now let's do this again. this time things come from the component cache 342 | assert view(Foo(), Request("", "GET")) == "foo default" 343 | assert view(FooSub(), Request("", "GET")) == "foo default" 344 | assert view(FooSub(), Request("edit", "POST")) == "foo edit" 345 | 346 | all_cache = view.key_lookup.all.__closure__[0].cell_contents 347 | # prime and check the all cache 348 | assert view.by_args(Foo(), Request("", "GET")).all_matches == [foo_default] 349 | assert all_cache.get(((Foo, "", "GET"),)) is not None 350 | # should be coming from cache now 351 | assert view.by_args(Foo(), Request("", "GET")).all_matches == [foo_default] 352 | 353 | class Bar: 354 | pass 355 | 356 | assert view(Bar(), Request("", "GET")) == "Model fallback" 357 | assert view(Foo(), Request("dummy", "GET")) == "Name fallback" 358 | assert view(Foo(), Request("", "PUT")) == "Request method fallback" 359 | assert view(FooSub(), Request("dummy", "GET")) == "Name fallback" 360 | 361 | # fallbacks get cached too 362 | fallback_cache = view.key_lookup.fallback.__closure__[0].cell_contents 363 | assert fallback_cache.get(((Bar, "", "GET"),)) is model_fallback 364 | 365 | # these come from the fallback cache now 366 | assert view(Bar(), Request("", "GET")) == "Model fallback" 367 | assert view(Foo(), Request("dummy", "GET")) == "Name fallback" 368 | assert view(Foo(), Request("", "PUT")) == "Request method fallback" 369 | assert view(FooSub(), Request("dummy", "GET")) == "Name fallback" 370 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ******* 3 | 4 | 0.13 (unreleased) 5 | ================= 6 | 7 | - Drop support for Python below 3.6 8 | 9 | - Use GitHub Actions for CI 10 | 11 | 12 | 0.12 (2020-01-29) 13 | ================= 14 | 15 | - **Breaking change** 16 | 17 | reg.arginfo(callable) returns now a FullArgSpec tuple 18 | instead of the deprecated ArgSpec tuple. FullArgSpec has 19 | full support for annotations and keyword-only parameters. 20 | 21 | Fixes `#55`_. 22 | 23 | .. _#55: https://github.com/morepath/reg/issues/55 24 | 25 | - **Removed**: Removed support for Python 2. 26 | 27 | You have to upgrade to Python 3 if you want to use this version. 28 | 29 | - Dropped support for Python 3.3. 30 | 31 | - Added support for Python 3.5, 3.6, 3.7, 3.8 and PyPy 3.6. 32 | 33 | - Make Python 3.7 the default testing environment. 34 | 35 | - Add integration for the Black code formatter. 36 | 37 | 38 | 0.11 (2016-12-23) 39 | ================= 40 | 41 | - **Breaking change** 42 | 43 | The ``key_predicate`` function is gone. You can now use 44 | ``Predicate(..., index=KeyIndex)`` or ``match_key`` instead. 45 | 46 | - **Breaking change** 47 | 48 | The ``class_predicate`` function is gone. You can now use 49 | ``Predicate(..., index=ClassIndex)``, ``match_instance`` or 50 | ``match_class`` instead. 51 | 52 | - **Breaking change** 53 | 54 | The undocumented ``Sentinel`` class and ``NOT_FOUND`` object are 55 | gone. 56 | 57 | - **Breaking change** 58 | 59 | The class ``PredicateRegistry`` is not longer part of the API. 60 | Internally, the classes ``MultiPredicate``, ``MultiIndex``, 61 | ``SingleValueRegistry`` have all been merged into 62 | ``PredicateRegistry``, which should now considered an implementation 63 | detail. 64 | 65 | - The second argument for ``match_key`` is now optional; if you 66 | don't supply it ``match_key`` will generate a predicate function 67 | that extracts that name by default. 68 | 69 | - The documentation now includes a section describing the internals of 70 | Reg. 71 | 72 | - Upload universal wheels to pypi during release. 73 | 74 | 75 | 0.10 (2016-10-04) 76 | ================= 77 | 78 | - **Breaking change** 79 | 80 | Reg has undergone another API breaking change. The goals of this 81 | change were: 82 | 83 | * Make everything explicit. 84 | 85 | * A simpler implementation structure -- dispatch functions maintain 86 | their own registries, which allows for less interacting objects. 87 | 88 | * Make the advanced context-dependent dispatch more Pythonic by 89 | using classes with special dispatch methods. 90 | 91 | Detailed changes: 92 | 93 | * ``reg.Registry`` is gone. Instead you register directly on the 94 | dispatch function:: 95 | 96 | @reg.dispatch('a') 97 | def foo(a): 98 | ... 99 | 100 | def foo_implementation(a): 101 | ... 102 | 103 | foo.register(foo_implementation, a=Document) 104 | 105 | * Caching is now per dispatch function, not globally per lookup. You 106 | can pass a ``get_key_lookup`` function that wraps 107 | ``reg.PredicateRegistry`` instance inside a 108 | ``reg.DictCachingKeyLookup`` cache. You can also use a 109 | ``reg.LruCachingKeyLookup`` if you expect a dispatch to be called 110 | with a large amount of possible predicate combinations, to 111 | preserve memory. 112 | 113 | * The whole concept of a "lookup" is gone: 114 | 115 | * ``reg.implicit`` is gone: everything is explicit. There is no more 116 | implicit lookup. 117 | 118 | * ``reg.Lookup`` itself is gone -- its now implemented directly in the 119 | dispatch object, but was already how you accessed it. 120 | 121 | * The special ``lookup`` argument to pass through the current 122 | ``Lookup`` is gone. If you need context-dependent dispatch, you 123 | use dispatch methods. 124 | 125 | * If you need context dependent dispatch, where the functions 126 | being dispatched to depend on application context (such as 127 | Morepath's application mounting), you use 128 | ``reg.dispatch_method`` to create a dispatch method. A dispatch 129 | method maintains an entirely separate dispatch registry for each 130 | subclass. You use ``reg.methodify`` to register a dispatch 131 | function that takes an optional context first argument. 132 | 133 | If you do not use the context-dependent dispatch feature, then to 134 | upgrade your code: 135 | 136 | * remove any ``reg.set_implicit`` from your code, setup of 137 | ``Lookup`` and the like. 138 | 139 | * If you use an explicit ``lookup`` argument you can just remove them. 140 | 141 | * You also need to change your registration code: no more 142 | ``reg.Registry`` setup. 143 | 144 | * Change your registrations to be on the dispatch objects itself 145 | using ``Dispatch.register``. 146 | 147 | * To enable caching you need to set up ``get_key_lookup`` on the 148 | dispatch functions. You can create a partially applied version of 149 | ``dispatch`` to make this less verbose:: 150 | 151 | import reg 152 | from functools import partial 153 | 154 | def get_caching_key_lookup(r): 155 | return reg.CachingKeyLookup(r, 5000, 5000, 5000) 156 | 157 | dispatch = partial(reg.dispatch, get_key_lookup=get_caching_key_lookup) 158 | 159 | * ``dispatch_external_predicates`` is gone. Just use ``dispatch`` 160 | directly. You can add predicates to an existing Dispatch object 161 | using the ``add_predicates`` method. 162 | 163 | If you do use the context-dependent dispatch feature, then you also 164 | need to: 165 | 166 | * identify the context class in your application (or create one). 167 | 168 | * move the dispatch functions to this class, marking them with 169 | ``@reg.dispatch_method`` instead of ``@reg.dispatch``. 170 | 171 | * Registration is now using 172 | ``..register``. Functions you register this 173 | way behave as methods to ``context_class``, so get an instance of 174 | this class as the first argument. 175 | 176 | * You can also use ``reg.methodify`` to register implementation 177 | functions that do not take the context as the first argument -- 178 | this is useful when upgrading existing code. 179 | 180 | * Call your context-dependent methods as methods on the context 181 | instance. This way you can indicate what context you are calling 182 | your dispatch methods in, instead of using the `lookup`` argument. 183 | 184 | In some cases you want a context-dependent method that actually does 185 | not dispatch on any of its arguments. To support this use case you 186 | can simply set function (that takes an app argument) as a the method 187 | on the context class directly:: 188 | 189 | Context.my_method = some_function 190 | 191 | If you want to set up a function that doesn't take a reference to a 192 | ``Context`` instance as its first argument, you can use 193 | ``reg.methodify`` to turn it into a method that ignores its first 194 | argument:: 195 | 196 | Context.my_method = reg.methodify(some_function) 197 | 198 | If you want to register a function that might or might not have a 199 | reference to a ``Context`` instance as its first argument, called, 200 | e.g., ``app``, you can use the following:: 201 | 202 | Context.my_method = reg.methodify(some_function, selfname='app') 203 | 204 | - **Breaking change** 205 | 206 | Removed the helper function ``mapply`` from the API. 207 | 208 | - **Breaking change** 209 | 210 | Removed the exception class ``KeyExtractorError`` from the API. 211 | When passing the wrong number of arguments to a dispatch function, 212 | or when using the wrong argument names, you will now get a 213 | TypeError, in conformity with standard Python behaviour. 214 | 215 | - **Breaking change** 216 | 217 | Removed the ``KeyExtractor`` class from the API. Callables used in 218 | predicate construction now expect the same arguments as the dispatch 219 | function. 220 | 221 | - **Breaking change** 222 | 223 | Removed the ``argnames`` attribute from ``Predicate`` and its 224 | descendant. 225 | 226 | - **Breaking change** 227 | 228 | Remove the ``match_argname`` predicate. You can now use 229 | ``match_instance`` with no callable instead. 230 | 231 | - The second argument for ``match_class`` is now optional; if you 232 | don't supply it ``match_class`` will generate a predicate function 233 | that extracts that name by default. 234 | 235 | - The second argument for ``match_instance`` is now optional; if you 236 | don't supply it ``match_instance`` will generate a predicate function 237 | that extracts that name by default. 238 | 239 | - Include doctests in Tox and Travis. 240 | 241 | - We now use virtualenv and pip instead of buildout to set up the 242 | development environment. The development documentation has been 243 | updated accordingly. 244 | 245 | - As we reached 100% code coverage for pytest, coveralls integration 246 | was replaced by the ``--fail-under=100`` argument of ``coverage 247 | report`` in the tox coverage test. 248 | 249 | 0.9.3 (2016-07-18) 250 | ================== 251 | 252 | - Minor fixes to documentation. 253 | 254 | - Add tox test environments for Python 3.4 and 3.5, PyPy 3 and PEP 8. 255 | 256 | - Make Python 3.5 the default Python environment. 257 | 258 | - Changed location ``NoImplicitLookupError`` was imported from in 259 | ``__init__.py``. 260 | 261 | 0.9.2 (2014-11-13) 262 | ================== 263 | 264 | - Reg was a bit too strict; when you had multiple (but not single) 265 | predicates, Reg would raise KeyError when you put in an unknown 266 | key. Now they're just being silently ignored, as they don't do any 267 | harm. 268 | 269 | - Eliminated a check in ``ArgExtractor`` that could never take place. 270 | 271 | - Bring test coverage back up to 100%. 272 | 273 | - Add converage configuration to ignore test files in coverage 274 | reporting. 275 | 276 | 0.9.1 (2014-11-11) 277 | ================== 278 | 279 | - A bugfix in the behavior of the fallback logic. In situations with 280 | multiple predicates of which one is a class predicate it was 281 | possible for a fallback not to be found even though a fallback was 282 | available. 283 | 284 | 0.9 (2014-11-11) 285 | ================ 286 | 287 | Total rewrite of Reg! This includes a range of changes that can break 288 | code. The primary motivations for this rewrite: 289 | 290 | * unify predicate system with class-based lookup system. 291 | 292 | * extract dispatch information from specific arguments instead of all 293 | arguments. 294 | 295 | Some specific changes: 296 | 297 | * Replaced ``@reg.generic`` decorator with ``@reg.dispatch()`` 298 | decorator. This decorator can be configured with predicates that 299 | extract information from the arguments. Rewrite this:: 300 | 301 | @reg.generic 302 | def foo(obj): 303 | pass 304 | 305 | to this:: 306 | 307 | @reg.dispatch('obj') 308 | def foo(obj): 309 | pass 310 | 311 | And this:: 312 | 313 | @reg.generic 314 | def bar(a, b): 315 | pass 316 | 317 | To this:: 318 | 319 | @reg.dispatch('a', 'b') 320 | def bar(a, b): 321 | pass 322 | 323 | This is to get dispatch on the classes of these instance 324 | arguments. If you want to match on the class of an attribute of 325 | an argument (for instance) you can use ``match_instance`` 326 | with a function:: 327 | 328 | @reg.dispatch(match_instance('a', lambda a: a.attr)) 329 | 330 | The first argument to ``match_instance`` is the name of the 331 | predicate by which you refer to it in ``register_function``. 332 | 333 | You can also use ``match_class`` to have direct dispatch on classes 334 | (useful for replicating classmethods), and ``match_key`` to have 335 | dispatch on the (immutable) value of the argument (useful for a view 336 | predicate system). Like for ``match_instance``, you supply functions 337 | to these match functions that extract the exact information to 338 | dispatch on from the argument. 339 | 340 | * The ``register_function`` API replaces the ``register`` API to 341 | register a function. Replace this:: 342 | 343 | r.register(foo, (SomeClass,), dispatched_to) 344 | 345 | with:: 346 | 347 | r.register_function(foo, dispatched_to, obj=SomeClass) 348 | 349 | You now use keyword parameters to indicate exactly those arguments 350 | specified by ``reg.dispatch()`` are actually predicate 351 | arguments. You don't need to worry about the order of predicates 352 | anymore when you register a function for it. 353 | 354 | * The new ``classgeneric`` functionality is part of the predicate 355 | system now; you can use ``reg.match_class`` instead. Replace:: 356 | 357 | @reg.classgeneric 358 | def foo(cls): 359 | pass 360 | 361 | with:: 362 | 363 | @reg.dispatch(reg.match_class('cls', lambda cls: cls)) 364 | def foo(cls): 365 | pass 366 | 367 | You can do this with any argument now, not just the first one. 368 | 369 | * pep443 support is gone. Reg is focused on its own dispatch system. 370 | 371 | * Compose functionality is gone -- it turns out Morepath doesn't use 372 | lookup composition to support App inheritance. The cached lookup 373 | functionality has moved into ``registry.py`` and now also supports 374 | caching of predicate-based lookups. 375 | 376 | * Dependency on the future module is gone in favor of a small amount 377 | of compatibility code. 378 | 379 | 0.8 (2014-08-28) 380 | ================ 381 | 382 | - Added a ``@reg.classgeneric``. This is like ``@reg.generic``, but 383 | the first argument is treated as a class, not as an instance. This 384 | makes it possible to replace ``@classmethod`` with a generic 385 | function too. 386 | 387 | - Fix documentation on running documentation tests. For some reason 388 | this did not work properly anymore without running sphinxpython 389 | explicitly. 390 | 391 | - Optimization: improve performance of generic function calls by 392 | employing ``lookup_mapply`` instead of general ``mapply``, as we 393 | only care about passing in the lookup argument when it's defined, 394 | and any other arguments should work as before. Also added a 395 | ``perf.py`` which is a simple generic function timing script. 396 | 397 | 0.7 (2014-06-17) 398 | ================ 399 | 400 | - Python 2.6 compatibility. (Ivo van der Wijk) 401 | 402 | - Class maps (and thus generic function lookup) now works with old 403 | style classes as well. 404 | 405 | - Marked as production/stable now in ``setup.py``. 406 | 407 | 0.6 (2014-04-08) 408 | ================ 409 | 410 | - Removed unused code from mapply.py. 411 | 412 | - Typo fix in API docs. 413 | 414 | 0.5 (2014-01-21) 415 | ================ 416 | 417 | - Make ``reg.ANY`` public. Used for predicates that match any value. 418 | 419 | 0.4 (2014-01-14) 420 | ================ 421 | 422 | - arginfo has been totally rewritten and is now part of the public API of reg. 423 | 424 | 0.3 (2014-01-06) 425 | ================ 426 | 427 | - Experimental Python 3.3 support thanks to the future module. 428 | 429 | 0.2 (2013-12-19) 430 | ================ 431 | 432 | - If a generic function implementation defines a ``lookup`` argument 433 | that argument will be the lookup used to call it. 434 | 435 | - Added ``reg.mapply()``. This allows you to call things with more 436 | keyword arguments than it accepts, ignoring those extra keyword 437 | args. 438 | 439 | - A function that returns ``None`` is not assumed to fail, so no fallback 440 | to the original generic function is triggered anymore. 441 | 442 | - An optional ``precalc`` facility is made available on ``Matcher`` to 443 | avoid some recalculation. 444 | 445 | - Implement a specific ``PredicateMatcher`` that matches a value on 446 | predicate. 447 | 448 | 0.1 (2013-10-28) 449 | ================ 450 | 451 | - Initial public release. 452 | -------------------------------------------------------------------------------- /reg/tests/test_dispatch_method.py: -------------------------------------------------------------------------------- 1 | from types import FunctionType 2 | import pytest 3 | from ..context import ( 4 | dispatch, 5 | dispatch_method, 6 | methodify, 7 | clean_dispatch_methods, 8 | ) 9 | from ..predicate import match_instance 10 | from ..error import RegistrationError 11 | 12 | 13 | def test_dispatch_method_explicit_fallback(): 14 | def obj_fallback(self, obj): 15 | return "Obj fallback" 16 | 17 | class Foo: 18 | @dispatch_method(match_instance("obj", fallback=obj_fallback)) 19 | def bar(self, obj): 20 | return "default" 21 | 22 | class Alpha: 23 | pass 24 | 25 | class Beta: 26 | pass 27 | 28 | foo = Foo() 29 | 30 | assert foo.bar(Alpha()) == "Obj fallback" 31 | 32 | Foo.bar.register(lambda self, obj: "Alpha", obj=Alpha) 33 | Foo.bar.register(lambda self, obj: "Beta", obj=Beta) 34 | 35 | assert foo.bar(Alpha()) == "Alpha" 36 | assert foo.bar(Beta()) == "Beta" 37 | assert foo.bar(None) == "Obj fallback" 38 | 39 | 40 | def test_dispatch_method_without_fallback(): 41 | class Foo: 42 | @dispatch_method(match_instance("obj")) 43 | def bar(self, obj): 44 | return "default" 45 | 46 | class Alpha: 47 | pass 48 | 49 | class Beta: 50 | pass 51 | 52 | foo = Foo() 53 | 54 | assert foo.bar(Alpha()) == "default" 55 | 56 | Foo.bar.register(lambda self, obj: "Alpha", obj=Alpha) 57 | Foo.bar.register(lambda self, obj: "Beta", obj=Beta) 58 | 59 | assert foo.bar(Alpha()) == "Alpha" 60 | assert foo.bar(Beta()) == "Beta" 61 | assert foo.bar(None) == "default" 62 | 63 | 64 | def test_dispatch_method_string_predicates(): 65 | class Foo: 66 | @dispatch_method("obj") 67 | def bar(self, obj): 68 | return "default" 69 | 70 | class Alpha: 71 | pass 72 | 73 | class Beta: 74 | pass 75 | 76 | foo = Foo() 77 | 78 | assert foo.bar(Alpha()) == "default" 79 | 80 | Foo.bar.register(lambda self, obj: "Alpha", obj=Alpha) 81 | Foo.bar.register(lambda self, obj: "Beta", obj=Beta) 82 | 83 | assert foo.bar(Alpha()) == "Alpha" 84 | assert foo.bar(Beta()) == "Beta" 85 | assert foo.bar(None) == "default" 86 | 87 | 88 | def test_dispatch_method_add_predicates(): 89 | class Foo: 90 | @dispatch_method() 91 | def bar(self, obj): 92 | return "default" 93 | 94 | Foo.bar.add_predicates([match_instance("obj")]) 95 | 96 | class Alpha: 97 | pass 98 | 99 | class Beta: 100 | pass 101 | 102 | foo = Foo() 103 | 104 | assert foo.bar(Alpha()) == "default" 105 | 106 | Foo.bar.register(lambda self, obj: "Alpha", obj=Alpha) 107 | Foo.bar.register(lambda self, obj: "Beta", obj=Beta) 108 | 109 | assert foo.bar(Alpha()) == "Alpha" 110 | assert foo.bar(Beta()) == "Beta" 111 | assert foo.bar(None) == "default" 112 | 113 | 114 | def test_dispatch_method_register_function(): 115 | class Foo: 116 | @dispatch_method(match_instance("obj")) 117 | def bar(self, obj): 118 | return "default" 119 | 120 | class Alpha: 121 | pass 122 | 123 | class Beta: 124 | pass 125 | 126 | foo = Foo() 127 | 128 | assert foo.bar(Alpha()) == "default" 129 | 130 | Foo.bar.register(methodify(lambda obj: "Alpha"), obj=Alpha) 131 | Foo.bar.register(methodify(lambda obj: "Beta"), obj=Beta) 132 | 133 | assert foo.bar(Alpha()) == "Alpha" 134 | assert foo.bar(Beta()) == "Beta" 135 | assert foo.bar(None) == "default" 136 | 137 | 138 | def test_dispatch_method_register_function_wrong_signature_too_long(): 139 | class Foo: 140 | @dispatch_method("obj") 141 | def bar(self, obj): 142 | return "default" 143 | 144 | class Alpha: 145 | pass 146 | 147 | with pytest.raises(RegistrationError): 148 | Foo.bar.register(methodify(lambda obj, extra: "Alpha"), obj=Alpha) 149 | 150 | 151 | def test_dispatch_method_register_function_wrong_signature_too_short(): 152 | class Foo: 153 | @dispatch_method("obj") 154 | def bar(self, obj): 155 | return "default" 156 | 157 | class Alpha: 158 | pass 159 | 160 | with pytest.raises(RegistrationError): 161 | Foo.bar.register(methodify(lambda: "Alpha"), obj=Alpha) 162 | 163 | 164 | def test_dispatch_method_register_non_callable(): 165 | class Foo: 166 | @dispatch_method("obj") 167 | def bar(self, obj): 168 | return "default" 169 | 170 | class Alpha: 171 | pass 172 | 173 | with pytest.raises(RegistrationError): 174 | Foo.bar.register("cannot call this", obj=Alpha) 175 | 176 | 177 | def test_dispatch_method_methodify_non_callable(): 178 | with pytest.raises(TypeError): 179 | methodify("cannot call this") 180 | 181 | 182 | def test_dispatch_method_register_auto(): 183 | class Foo: 184 | x = "X" 185 | 186 | @dispatch_method(match_instance("obj")) 187 | def bar(self, obj): 188 | return "default" 189 | 190 | class Alpha: 191 | pass 192 | 193 | class Beta: 194 | pass 195 | 196 | foo = Foo() 197 | 198 | assert foo.bar(Alpha()) == "default" 199 | 200 | Foo.bar.register(methodify(lambda obj: "Alpha", "app"), obj=Alpha) 201 | Foo.bar.register(methodify(lambda app, obj: "Beta %s" % app.x, "app"), obj=Beta) 202 | 203 | assert foo.bar(Alpha()) == "Alpha" 204 | assert foo.bar(Beta()) == "Beta X" 205 | assert foo.bar(None) == "default" 206 | 207 | 208 | def test_dispatch_method_class_method_accessed_first(): 209 | class Foo: 210 | @dispatch_method(match_instance("obj")) 211 | def bar(self, obj): 212 | return "default" 213 | 214 | class Alpha: 215 | pass 216 | 217 | class Beta: 218 | pass 219 | 220 | Foo.bar.register(lambda self, obj: "Alpha", obj=Alpha) 221 | Foo.bar.register(lambda self, obj: "Beta", obj=Beta) 222 | 223 | foo = Foo() 224 | 225 | assert foo.bar(Alpha()) == "Alpha" 226 | assert foo.bar(Beta()) == "Beta" 227 | assert foo.bar(None) == "default" 228 | 229 | 230 | def test_dispatch_method_accesses_instance(): 231 | class Foo: 232 | def __init__(self, x): 233 | self.x = x 234 | 235 | @dispatch_method(match_instance("obj")) 236 | def bar(self, obj): 237 | return "default %s" % self.x 238 | 239 | class Alpha: 240 | pass 241 | 242 | class Beta: 243 | pass 244 | 245 | Foo.bar.register(lambda self, obj: "Alpha %s" % self.x, obj=Alpha) 246 | Foo.bar.register(lambda self, obj: "Beta %s" % self.x, obj=Beta) 247 | 248 | foo = Foo("hello") 249 | 250 | assert foo.bar(Alpha()) == "Alpha hello" 251 | assert foo.bar(Beta()) == "Beta hello" 252 | assert foo.bar(None) == "default hello" 253 | 254 | 255 | def test_dispatch_method_inheritance_register_on_subclass(): 256 | class Foo: 257 | @dispatch_method(match_instance("obj")) 258 | def bar(self, obj): 259 | return "default" 260 | 261 | class Sub(Foo): 262 | pass 263 | 264 | class Alpha: 265 | pass 266 | 267 | class Beta: 268 | pass 269 | 270 | sub = Sub() 271 | 272 | assert sub.bar(Alpha()) == "default" 273 | 274 | Sub.bar.register(lambda self, obj: "Alpha", obj=Alpha) 275 | Sub.bar.register(lambda self, obj: "Beta", obj=Beta) 276 | 277 | assert sub.bar(Alpha()) == "Alpha" 278 | assert sub.bar(Beta()) == "Beta" 279 | assert sub.bar(None) == "default" 280 | 281 | 282 | def test_dispatch_method_inheritance_separation(): 283 | class Foo: 284 | @dispatch_method(match_instance("obj")) 285 | def bar(self, obj): 286 | return "default" 287 | 288 | class Sub(Foo): 289 | pass 290 | 291 | class Alpha: 292 | pass 293 | 294 | class Beta: 295 | pass 296 | 297 | # programmatic style: 298 | Foo.bar.register(lambda self, obj: "Foo Alpha", obj=Alpha) 299 | # decorator style: 300 | Foo.bar.register(obj=Beta)(lambda self, obj: "Foo Beta") 301 | # programmatic style: 302 | Sub.bar.register(lambda self, obj: "Sub Alpha", obj=Alpha) 303 | # decorator style: 304 | Sub.bar.register(obj=Beta)(lambda self, obj: "Sub Beta") 305 | 306 | foo = Foo() 307 | sub = Sub() 308 | 309 | assert foo.bar(Alpha()) == "Foo Alpha" 310 | assert foo.bar(Beta()) == "Foo Beta" 311 | assert foo.bar(None) == "default" 312 | 313 | assert sub.bar(Alpha()) == "Sub Alpha" 314 | assert sub.bar(Beta()) == "Sub Beta" 315 | assert sub.bar(None) == "default" 316 | 317 | 318 | def test_dispatch_method_inheritance_separation_multiple(): 319 | class Foo: 320 | @dispatch_method(match_instance("obj")) 321 | def bar(self, obj): 322 | return "bar default" 323 | 324 | @dispatch_method(match_instance("obj")) 325 | def qux(self, obj): 326 | return "qux default" 327 | 328 | class Sub(Foo): 329 | pass 330 | 331 | class Alpha: 332 | pass 333 | 334 | class Beta: 335 | pass 336 | 337 | Foo.bar.register(lambda self, obj: "Bar Foo Alpha", obj=Alpha) 338 | Foo.bar.register(lambda self, obj: "Bar Foo Beta", obj=Beta) 339 | Sub.bar.register(lambda self, obj: "Bar Sub Alpha", obj=Alpha) 340 | Sub.bar.register(lambda self, obj: "Bar Sub Beta", obj=Beta) 341 | 342 | Foo.qux.register(lambda self, obj: "Qux Foo Alpha", obj=Alpha) 343 | Foo.qux.register(lambda self, obj: "Qux Foo Beta", obj=Beta) 344 | Sub.qux.register(lambda self, obj: "Qux Sub Alpha", obj=Alpha) 345 | Sub.qux.register(lambda self, obj: "Qux Sub Beta", obj=Beta) 346 | 347 | foo = Foo() 348 | sub = Sub() 349 | 350 | assert foo.bar(Alpha()) == "Bar Foo Alpha" 351 | assert foo.bar(Beta()) == "Bar Foo Beta" 352 | assert foo.bar(None) == "bar default" 353 | 354 | assert sub.bar(Alpha()) == "Bar Sub Alpha" 355 | assert sub.bar(Beta()) == "Bar Sub Beta" 356 | assert sub.bar(None) == "bar default" 357 | 358 | assert foo.qux(Alpha()) == "Qux Foo Alpha" 359 | assert foo.qux(Beta()) == "Qux Foo Beta" 360 | assert foo.qux(None) == "qux default" 361 | 362 | assert sub.qux(Alpha()) == "Qux Sub Alpha" 363 | assert sub.qux(Beta()) == "Qux Sub Beta" 364 | assert sub.qux(None) == "qux default" 365 | 366 | 367 | def test_dispatch_method_api_available(): 368 | def obj_fallback(self, obj): 369 | return "Obj fallback" 370 | 371 | class Foo: 372 | @dispatch_method(match_instance("obj", fallback=obj_fallback)) 373 | def bar(self, obj): 374 | return "default" 375 | 376 | class Alpha: 377 | pass 378 | 379 | class Beta: 380 | pass 381 | 382 | foo = Foo() 383 | 384 | def alpha_func(self, obj): 385 | return "Alpha" 386 | 387 | def beta_func(self, obj): 388 | return "Beta" 389 | 390 | Foo.bar.register(alpha_func, obj=Alpha) 391 | Foo.bar.register(beta_func, obj=Beta) 392 | 393 | assert foo.bar(Alpha()) == "Alpha" 394 | assert Foo.bar.by_args(Alpha()).component == alpha_func 395 | assert foo.bar.by_args(Alpha()).component == alpha_func 396 | assert foo.bar.by_args(Alpha()).all_matches == [alpha_func] 397 | assert foo.bar.by_args(Beta()).component == beta_func 398 | assert foo.bar.by_args(None).component is None 399 | assert foo.bar.by_args(None).fallback is obj_fallback 400 | assert foo.bar.by_args(None).all_matches == [] 401 | 402 | 403 | def test_dispatch_method_with_register_function_value(): 404 | class Foo: 405 | @dispatch_method(match_instance("obj")) 406 | def bar(self, obj): 407 | return "default" 408 | 409 | class Alpha: 410 | pass 411 | 412 | class Beta: 413 | pass 414 | 415 | foo = Foo() 416 | 417 | assert foo.bar(Alpha()) == "default" 418 | 419 | def alpha_func(obj): 420 | return "Alpha" 421 | 422 | def beta_func(obj): 423 | return "Beta" 424 | 425 | Foo.bar.register(methodify(alpha_func), obj=Alpha) 426 | Foo.bar.register(methodify(beta_func), obj=Beta) 427 | 428 | assert unmethodify(foo.bar.by_args(Alpha()).component) is alpha_func 429 | 430 | 431 | def test_dispatch_method_with_register_auto_value(): 432 | class Foo: 433 | @dispatch_method(match_instance("obj")) 434 | def bar(self, obj): 435 | return "default" 436 | 437 | class Alpha: 438 | pass 439 | 440 | class Beta: 441 | pass 442 | 443 | foo = Foo() 444 | 445 | assert foo.bar(Alpha()) == "default" 446 | 447 | def alpha_func(obj): 448 | return "Alpha" 449 | 450 | def beta_func(app, obj): 451 | return "Beta" 452 | 453 | Foo.bar.register(methodify(alpha_func, "app"), obj=Alpha) 454 | Foo.bar.register(methodify(beta_func, "app"), obj=Beta) 455 | 456 | assert unmethodify(foo.bar.by_args(Alpha()).component) is alpha_func 457 | assert unmethodify(foo.bar.by_args(Beta()).component) is beta_func 458 | # actually since this is a method this is also unwrapped 459 | assert foo.bar.by_args(Beta()).component is beta_func 460 | 461 | 462 | def test_install_method(): 463 | class Target: 464 | pass 465 | 466 | def f(self, a): 467 | return a 468 | 469 | Target.m = f 470 | 471 | t = Target() 472 | 473 | assert t.m("A") == "A" 474 | 475 | 476 | def test_install_auto_method_function_no_app_arg(): 477 | class Target: 478 | pass 479 | 480 | def f(a): 481 | return a 482 | 483 | Target.m = methodify(f, "app") 484 | 485 | t = Target() 486 | 487 | assert t.m("A") == "A" 488 | assert unmethodify(t.m) is f 489 | 490 | 491 | def test_install_auto_method_function_app_arg(): 492 | class Target: 493 | pass 494 | 495 | def g(app, a): 496 | assert isinstance(app, Target) 497 | return a 498 | 499 | Target.m = methodify(g, "app") 500 | 501 | t = Target() 502 | assert t.m("A") == "A" 503 | assert unmethodify(t.m) is g 504 | 505 | 506 | def test_install_auto_method_method_no_app_arg(): 507 | class Target: 508 | pass 509 | 510 | class Foo: 511 | def f(self, a): 512 | return a 513 | 514 | f = Foo().f 515 | 516 | Target.m = methodify(f, "app") 517 | 518 | t = Target() 519 | 520 | assert t.m("A") == "A" 521 | assert unmethodify(t.m) is f 522 | 523 | 524 | def test_install_auto_method_method_app_arg(): 525 | class Target: 526 | pass 527 | 528 | class Bar: 529 | def g(self, app, a): 530 | assert isinstance(app, Target) 531 | return a 532 | 533 | g = Bar().g 534 | 535 | Target.m = methodify(g, "app") 536 | 537 | t = Target() 538 | 539 | assert t.m("A") == "A" 540 | assert unmethodify(t.m) is g 541 | 542 | 543 | def test_install_instance_method(): 544 | class Target: 545 | pass 546 | 547 | class Bar: 548 | def g(self, a): 549 | assert isinstance(self, Bar) 550 | return a 551 | 552 | g = Bar().g 553 | 554 | Target.m = methodify(g) 555 | 556 | t = Target() 557 | 558 | assert t.m("A") == "A" 559 | assert unmethodify(t.m) is g 560 | 561 | 562 | def test_dispatch_method_introspection(): 563 | class Foo: 564 | @dispatch_method("obj") 565 | def bar(self, obj): 566 | "Return the bar of an object." 567 | return "default" 568 | 569 | assert Foo.bar.__name__ == "bar" 570 | assert Foo.bar.__doc__ == "Return the bar of an object." 571 | assert Foo.bar.__module__ == __name__ 572 | 573 | 574 | def test_dispatch_method_clean(): 575 | class Foo: 576 | @dispatch_method(match_instance("obj")) 577 | def bar(self, obj): 578 | return "default" 579 | 580 | class Qux(Foo): 581 | pass 582 | 583 | class Alpha: 584 | pass 585 | 586 | class Beta: 587 | pass 588 | 589 | foo = Foo() 590 | qux = Qux() 591 | 592 | Foo.bar.register(lambda self, obj: "Alpha", obj=Alpha) 593 | Foo.bar.register(lambda self, obj: "Beta", obj=Beta) 594 | Qux.bar.register(lambda self, obj: "Qux Alpha", obj=Alpha) 595 | Qux.bar.register(lambda self, obj: "Qux Beta", obj=Beta) 596 | 597 | assert foo.bar(Alpha()) == "Alpha" 598 | assert foo.bar(Beta()) == "Beta" 599 | assert foo.bar(None) == "default" 600 | assert qux.bar(Alpha()) == "Qux Alpha" 601 | assert qux.bar(Beta()) == "Qux Beta" 602 | assert qux.bar(None) == "default" 603 | 604 | Foo.bar.clean() 605 | 606 | assert foo.bar(Alpha()) == "default" 607 | 608 | # but hasn't affected qux registry 609 | assert qux.bar(Alpha()) == "Qux Alpha" 610 | 611 | 612 | def test_clean_dispatch_methods(): 613 | class Foo: 614 | @dispatch_method(match_instance("obj")) 615 | def bar(self, obj): 616 | return "default" 617 | 618 | class Qux(Foo): 619 | pass 620 | 621 | class Alpha: 622 | pass 623 | 624 | class Beta: 625 | pass 626 | 627 | foo = Foo() 628 | qux = Qux() 629 | 630 | Foo.bar.register(lambda self, obj: "Alpha", obj=Alpha) 631 | Foo.bar.register(lambda self, obj: "Beta", obj=Beta) 632 | Qux.bar.register(lambda self, obj: "Qux Alpha", obj=Alpha) 633 | Qux.bar.register(lambda self, obj: "Qux Beta", obj=Beta) 634 | 635 | assert foo.bar(Alpha()) == "Alpha" 636 | assert foo.bar(Beta()) == "Beta" 637 | assert foo.bar(None) == "default" 638 | assert qux.bar(Alpha()) == "Qux Alpha" 639 | assert qux.bar(Beta()) == "Qux Beta" 640 | assert qux.bar(None) == "default" 641 | 642 | clean_dispatch_methods(Foo) 643 | 644 | assert foo.bar(Alpha()) == "default" 645 | # but hasn't affected qux registry 646 | assert qux.bar(Alpha()) == "Qux Alpha" 647 | 648 | 649 | def test_replacing_with_normal_method(): 650 | class Foo: 651 | @dispatch_method("obj") 652 | def bar(self, obj): 653 | return "default" 654 | 655 | class Alpha: 656 | pass 657 | 658 | class Beta: 659 | pass 660 | 661 | # At this moment Foo.bar is still a descriptor, even though it is 662 | # not easy to see that: 663 | assert isinstance(vars(Foo)["bar"], dispatch_method) 664 | 665 | # Simply using Foo.bar wouldn't have worked here, as it would 666 | # invoke the descriptor: 667 | assert isinstance(Foo.bar, FunctionType) 668 | 669 | # We now replace the descriptor with the actual unbound method: 670 | Foo.bar = Foo.bar 671 | 672 | # Now the descriptor is gone 673 | assert isinstance(vars(Foo)["bar"], FunctionType) 674 | 675 | # But we can still use the generic function as usual, and even 676 | # register new implementations: 677 | Foo.bar.register(obj=Alpha)(lambda self, obj: "Alpha") 678 | Foo.bar.register(obj=Beta)(lambda self, obj: "Beta") 679 | foo = Foo() 680 | assert foo.bar(Alpha()) == "Alpha" 681 | assert foo.bar(Beta()) == "Beta" 682 | assert foo.bar(None) == "default" 683 | 684 | 685 | def test_replacing_with_normal_method_and_its_effect_on_inheritance(): 686 | class Foo: 687 | @dispatch_method("obj") 688 | def bar(self, obj): 689 | return "default" 690 | 691 | class SubFoo(Foo): 692 | pass 693 | 694 | class Alpha: 695 | pass 696 | 697 | class Beta: 698 | pass 699 | 700 | Foo.bar.register(obj=Alpha)(lambda self, obj: "Alpha") 701 | Foo.bar.register(obj=Beta)(lambda self, obj: "Beta") 702 | 703 | foo = Foo() 704 | assert foo.bar(Alpha()) == "Alpha" 705 | assert foo.bar(Beta()) == "Beta" 706 | assert foo.bar(None) == "default" 707 | 708 | # SubFoo has different dispatching from Foo 709 | subfoo = SubFoo() 710 | assert subfoo.bar(Alpha()) == "default" 711 | assert subfoo.bar(Beta()) == "default" 712 | assert subfoo.bar(None) == "default" 713 | 714 | # We now replace the descriptor with the actual unbound method: 715 | Foo.bar = Foo.bar 716 | 717 | # Now the descriptor is gone 718 | assert isinstance(vars(Foo)["bar"], FunctionType) 719 | 720 | # Foo.bar works as before: 721 | foo = Foo() 722 | assert foo.bar(Alpha()) == "Alpha" 723 | assert foo.bar(Beta()) == "Beta" 724 | assert foo.bar(None) == "default" 725 | 726 | # But now SubFoo.bar shares the dispatch registry with Foo: 727 | subfoo = SubFoo() 728 | assert subfoo.bar(Alpha()) == "Alpha" 729 | assert subfoo.bar(Beta()) == "Beta" 730 | assert subfoo.bar(None) == "default" 731 | 732 | # This is exactly the same behavior we'd get by using dispatch 733 | # instead of dispatch_method: 734 | del Foo, SubFoo 735 | 736 | class Foo: 737 | @dispatch("obj") 738 | def bar(self, obj): 739 | return "default" 740 | 741 | class SubFoo(Foo): 742 | pass 743 | 744 | # Foo and SubFoo share the same registry: 745 | Foo.bar.register(obj=Alpha)(lambda self, obj: "Alpha") 746 | SubFoo.bar.register(obj=Beta)(lambda self, obj: "Beta") 747 | 748 | foo = Foo() 749 | assert foo.bar(Alpha()) == "Alpha" 750 | assert foo.bar(Beta()) == "Beta" 751 | assert foo.bar(None) == "default" 752 | 753 | subfoo = SubFoo() 754 | assert subfoo.bar(Alpha()) == "Alpha" 755 | assert subfoo.bar(Beta()) == "Beta" 756 | assert subfoo.bar(None) == "default" 757 | 758 | # Now we start again, and do the replacement for both subclass and 759 | # parent class, in this order: 760 | del Foo, SubFoo 761 | 762 | class Foo: 763 | @dispatch_method("obj") 764 | def bar(self, obj): 765 | return "default" 766 | 767 | class SubFoo(Foo): 768 | pass 769 | 770 | Foo.bar.register(obj=Alpha)(lambda self, obj: "Alpha") 771 | Foo.bar.register(obj=Beta)(lambda self, obj: "Beta") 772 | 773 | SubFoo.bar = SubFoo.bar 774 | Foo.bar = Foo.bar 775 | 776 | # This has kept two separate registries: 777 | foo = Foo() 778 | assert foo.bar(Alpha()) == "Alpha" 779 | assert foo.bar(Beta()) == "Beta" 780 | assert foo.bar(None) == "default" 781 | 782 | subfoo = SubFoo() 783 | assert subfoo.bar(Alpha()) == "default" 784 | assert subfoo.bar(Beta()) == "default" 785 | assert subfoo.bar(None) == "default" 786 | 787 | 788 | def unmethodify(func): 789 | """Reverses methodify operation. 790 | 791 | Given an object that is returned from a call to 792 | :func:`reg.methodify` return the original object. This can be used to 793 | discover the original object that was registered. You can apply 794 | this to a function after it was attached as a method. 795 | 796 | :param func: the methodified function. 797 | :returns: the original function. 798 | """ 799 | func = getattr(func, "__func__", func) 800 | return func.__globals__.get("_func", func) 801 | -------------------------------------------------------------------------------- /doc/usage.rst: -------------------------------------------------------------------------------- 1 | Using Reg 2 | ========= 3 | 4 | .. testsetup:: * 5 | 6 | pass 7 | 8 | Introduction 9 | ------------ 10 | 11 | Reg implements *predicate dispatch* and *multiple registries*: 12 | 13 | Predicate dispatch 14 | 15 | We all know about `dynamic dispatch`_: when you call a method on an 16 | instance it is dispatched to the implementation in its class, and 17 | the class is determined from the first argument (``self``). This is 18 | known as *single dispatch*. 19 | 20 | Reg implements `multiple dispatch`_. This is a generalization of single 21 | dispatch: multiple dispatch allows you to dispatch on the class of 22 | *other* arguments besides the first one. 23 | 24 | Reg actually implements `predicate dispatch`_, which is a further 25 | generalization that allows dispatch on *arbitrary properties* of 26 | arguments, instead of just their class. 27 | 28 | The Morepath_ web framework is built with Reg. It uses Reg's 29 | predicate dispatch system. Its full power can be seen in its view 30 | lookup system. 31 | 32 | This document explains how to use Reg. Various specific patterns are 33 | documented in :doc:`patterns`. 34 | 35 | .. _`dynamic dispatch`: https://en.wikipedia.org/wiki/Dynamic_dispatch 36 | 37 | .. _`multiple dispatch`: http://en.wikipedia.org/wiki/Multiple_dispatch 38 | 39 | .. _`predicate dispatch`: https://en.wikipedia.org/wiki/Predicate_dispatch 40 | 41 | Multiple registries 42 | 43 | Reg supports an advanced application architecture pattern where you 44 | have multiple predicate dispatch registries in the same 45 | runtime. This means that dispatch can behave differently depending 46 | on runtime context. You do this by using dispatch *methods* that you 47 | associate with a class that represents the application context. When 48 | you switch the context class, you switch the behavior. 49 | 50 | Morepath_ uses context-based dispatch to support its application 51 | composition system, where one application can be mounted into 52 | another. 53 | 54 | See :doc:`context` for this advanced application pattern. 55 | 56 | Reg is designed with a caching layer that allows it to support these 57 | features efficiently. 58 | 59 | .. _`Morepath`: http://morepath.readthedocs.io 60 | 61 | Example 62 | ------- 63 | 64 | Let's examine a short example. First we use the :meth:`reg.dispatch` 65 | decorator to define a function that dispatches based on the 66 | class of its ``obj`` argument: 67 | 68 | .. testcode:: 69 | 70 | import reg 71 | 72 | @reg.dispatch('obj') 73 | def title(obj): 74 | return "we don't know the title" 75 | 76 | We want this function to return the title of its ``obj`` argument. 77 | 78 | Now we create a few example classes for which we want to be able to use 79 | the ``title`` fuction we defined above. 80 | 81 | .. testcode:: 82 | 83 | class TitledReport(object): 84 | def __init__(self, title): 85 | self.title = title 86 | 87 | class LabeledReport(object): 88 | def __init__(self, label): 89 | self.label = label 90 | 91 | If we call ``title`` with a ``TitledReport`` instance, we want it to return 92 | its ``title`` attribute: 93 | 94 | .. testcode:: 95 | 96 | @title.register(obj=TitledReport) 97 | def titled_report_title(obj): 98 | return obj.title 99 | 100 | The ``title.register`` decorator registers the function 101 | ``titled_report_title`` as an implementation of ``title`` when ``obj`` 102 | is an instance of ``TitleReport``. 103 | 104 | There is also a more programmatic way to register implementations. 105 | Take for example, the implementation of ``title`` with a ``LabeledReport`` 106 | instance, where we want it to return its ``label`` attribute: 107 | 108 | .. testcode:: 109 | 110 | def labeled_report_title(obj): 111 | return obj.label 112 | 113 | We can register it by explicitely invoking ``title.register``: 114 | 115 | .. testcode:: 116 | 117 | title.register(labeled_report_title, obj=LabeledReport) 118 | 119 | Now the generic ``title`` function works on both titled and labeled 120 | objects: 121 | 122 | .. doctest:: 123 | 124 | >>> titled = TitledReport('This is a report') 125 | >>> labeled = LabeledReport('This is also a report') 126 | >>> title(titled) 127 | 'This is a report' 128 | >>> title(labeled) 129 | 'This is also a report' 130 | 131 | What is going on and why is this useful at all? We present a worked 132 | out example next. 133 | 134 | Dispatch functions 135 | ------------------ 136 | 137 | A Hypothetical CMS 138 | ~~~~~~~~~~~~~~~~~~ 139 | 140 | Let's look at how Reg works in the context of a hypothetical content 141 | management system (CMS). 142 | 143 | This hypothetical CMS has two kinds of content item (we'll add more 144 | later): 145 | 146 | * a ``Document`` which contains some text. 147 | 148 | * a ``Folder`` which contains a bunch of content entries, for instance 149 | ``Document`` instances. 150 | 151 | This is the implementation of our CMS: 152 | 153 | .. testcode:: 154 | 155 | class Document(object): 156 | def __init__(self, text): 157 | self.text = text 158 | 159 | class Folder(object): 160 | def __init__(self, entries): 161 | self.entries = entries 162 | 163 | ``size`` methods 164 | ~~~~~~~~~~~~~~~~ 165 | 166 | Now we want to add a feature to our CMS: we want the ability to 167 | calculate the size (in bytes) of any content item. The size of the 168 | document is defined as the length of its text, and the size of the 169 | folder is defined as the sum of the size of everything in it. 170 | 171 | .. sidebar:: ``len(text)`` is not in bytes! 172 | 173 | Yeah, we're lying here. ``len(text)`` is not in bytes if text is in 174 | unicode. Just pretend that text is in ASCII for the sake of this 175 | example. 176 | 177 | If we have control over the implementation of ``Document`` and 178 | ``Folder`` we can implement this feature easily by adding a ``size`` 179 | method to both classes: 180 | 181 | .. testcode:: 182 | 183 | class Document(object): 184 | def __init__(self, text): 185 | self.text = text 186 | 187 | def size(self): 188 | return len(self.text) 189 | 190 | class Folder(object): 191 | def __init__(self, entries): 192 | self.entries = entries 193 | 194 | def size(self): 195 | return sum([entry.size() for entry in self.entries]) 196 | 197 | And then we can simply call the ``.size()`` method to get the size: 198 | 199 | .. doctest:: 200 | 201 | >>> doc = Document('Hello world!') 202 | >>> doc.size() 203 | 12 204 | >>> doc2 = Document('Bye world!') 205 | >>> doc2.size() 206 | 10 207 | >>> folder = Folder([doc, doc2]) 208 | >>> folder.size() 209 | 22 210 | 211 | The ``Folder`` size code is generic; it doesn't care what the entries 212 | inside it are; if they have a ``size`` method that gives the right 213 | result, it will work. If a new content item ``Image`` is defined and 214 | we provide a ``size`` method for this, a ``Folder`` instance that 215 | contains ``Image`` instances will still be able to calculate its 216 | size. Let's try this: 217 | 218 | .. testcode:: 219 | 220 | class Image(object): 221 | def __init__(self, bytes): 222 | self.bytes = bytes 223 | 224 | def size(self): 225 | return len(self.bytes) 226 | 227 | When we add an ``Image`` instance to the folder, the size of the folder 228 | can still be calculated: 229 | 230 | .. doctest:: 231 | 232 | >>> image = Image('abc') 233 | >>> folder.entries.append(image) 234 | >>> folder.size() 235 | 25 236 | 237 | Cool! So we're done, right? 238 | 239 | Adding ``size`` from outside 240 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 241 | 242 | .. sidebar:: Open/Closed Principle 243 | 244 | The `Open/Closed principle`_ states software entities should be open 245 | for extension, but closed for modification. The idea is that you may 246 | have a piece of software that you cannot or do not want to change, 247 | for instance because it's being developed by a third party, or 248 | because the feature you want to add is outside of the scope of that 249 | software (separation of concerns). By extending the software without 250 | modifying its source code, you can benefit from the stability of the 251 | core software and still add new functionality. 252 | 253 | .. _`Open/Closed principle`: https://en.wikipedia.org/wiki/Open/closed_principle 254 | 255 | So far we didn't need Reg at all. But in a real world CMS we aren't 256 | always in the position to change the content classes themselves. We 257 | may be dealing with a content management system core where we *cannot* 258 | control the implementation of ``Document`` and ``Folder``. Or perhaps 259 | we can, but we want to keep our code modular, in independent 260 | packages. So how would we add a size calculation feature in an 261 | extension package? 262 | 263 | We can fall back on good-old Python functions instead. We separate out 264 | the size logic from our classes: 265 | 266 | .. testcode:: 267 | 268 | def document_size(item): 269 | return len(item.text) 270 | 271 | def folder_size(item): 272 | return sum([document_size(entry) for entry in item.entries]) 273 | 274 | Generic size 275 | ~~~~~~~~~~~~ 276 | 277 | .. sidebar:: What about monkey patching? 278 | 279 | We *could* `monkey patch`_ a ``size`` method into all our content 280 | classes. This would work. But doing this can be risky -- what if the 281 | original CMS's implementers change it so it *does* gain a size 282 | method or attribute, for instance? Multiple monkey patches 283 | interacting can also lead to trouble. In addition, monkey-patched 284 | classes become harder to read: where is this ``size`` method coming 285 | from? It isn't there in the ``class`` statement, or in any of its 286 | superclasses! And how would we document such a construction? 287 | 288 | In short, monkey patching does not make for very maintainable code. 289 | 290 | .. _`monkey patch`: https://en.wikipedia.org/wiki/Monkey_patch 291 | 292 | There is a problem with the above function-based implementation 293 | however: ``folder_size`` is not generic anymore, but now depends on 294 | ``document_size``. It fails when presented with a folder with an 295 | ``Image`` in it: 296 | 297 | .. doctest:: 298 | 299 | >>> folder_size(folder) 300 | Traceback (most recent call last): 301 | ... 302 | AttributeError: ... 303 | 304 | To support ``Image`` we first need an ``image_size`` function: 305 | 306 | .. testcode:: 307 | 308 | def image_size(item): 309 | return len(item.bytes) 310 | 311 | We can now write a generic ``size`` function to get the size for any 312 | item we give it: 313 | 314 | .. testcode:: 315 | 316 | def size(item): 317 | if isinstance(item, Document): 318 | return document_size(item) 319 | elif isinstance(item, Image): 320 | return image_size(item) 321 | elif isinstance(item, Folder): 322 | return folder_size(item) 323 | assert False, "Unknown item: %s" % item 324 | 325 | With this, we can rewrite ``folder_size`` to use the generic ``size``: 326 | 327 | .. testcode:: 328 | 329 | def folder_size(item): 330 | return sum([size(entry) for entry in item.entries]) 331 | 332 | Now our generic ``size`` function works: 333 | 334 | .. doctest:: 335 | 336 | >>> size(doc) 337 | 12 338 | >>> size(image) 339 | 3 340 | >>> size(folder) 341 | 25 342 | 343 | All a bit complicated and hard-coded, but it works! 344 | 345 | New ``File`` content 346 | ~~~~~~~~~~~~~~~~~~~~ 347 | 348 | What if we want to write a new extension to our CMS that adds a new 349 | kind of folder item, the ``File``, with a ``file_size`` function? 350 | 351 | .. testcode:: 352 | 353 | class File(object): 354 | def __init__(self, bytes): 355 | self.bytes = bytes 356 | 357 | def file_size(item): 358 | return len(item.bytes) 359 | 360 | We need to remember to adjust the generic ``size`` function so we can 361 | teach it about ``file_size`` as well. Annoying, tightly coupled, but 362 | sometimes doable. 363 | 364 | But what if we are actually another party, and we have control of 365 | neither the basic CMS *nor* its size extension? We cannot adjust 366 | ``generic_size`` to teach it about ``File`` now! Uh oh! 367 | 368 | Perhaps the implementers of the size extension anticipated this use 369 | case. They could have implemented ``size`` like this: 370 | 371 | .. testcode:: 372 | 373 | size_function_registry = { 374 | Document: document_size, 375 | Image: image_size, 376 | Folder: folder_size 377 | } 378 | 379 | def register_size(class_, function): 380 | size_function_registry[class_] = function 381 | 382 | def size(item): 383 | return size_function_registry[item.__class__](item) 384 | 385 | We can now use ``register_size`` to teach ``size`` how to get 386 | the size of a ``File`` instance: 387 | 388 | .. testcode:: 389 | 390 | register_size(File, file_size) 391 | 392 | And it works: 393 | 394 | .. doctest:: 395 | 396 | >>> size(File('xyz')) 397 | 3 398 | 399 | But this is quite a bit of custom work that the implementers need to 400 | do, and it involves a new API (``register_size``) to manipulate the 401 | ``size_function_registry``. But it can be done. 402 | 403 | New ``HtmlDocument`` content 404 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 405 | 406 | What if we introduce a new ``HtmlDocument`` item that is a subclass of 407 | ``Document``? 408 | 409 | .. testcode:: 410 | 411 | class HtmlDocument(Document): 412 | pass # imagine new html functionality here 413 | 414 | Let's try to get its size: 415 | 416 | .. doctest:: 417 | 418 | >>> htmldoc = HtmlDocument('

Hello world!

') 419 | >>> size(htmldoc) 420 | Traceback (most recent call last): 421 | ... 422 | KeyError: ... 423 | 424 | That doesn't work! There's nothing registered for the ``HtmlDocument`` 425 | class. 426 | 427 | We need to remember to also call ``register_size`` for 428 | ``HtmlDocument``. We can reuse ``document_size``: 429 | 430 | .. doctest:: 431 | 432 | >>> register_size(HtmlDocument, document_size) 433 | 434 | Now ``size`` will work: 435 | 436 | .. doctest:: 437 | 438 | >>> size(htmldoc) 439 | 19 440 | 441 | This is getting rather complicated, requiring not only foresight and 442 | extra implementation work for the developers of ``size`` but also 443 | extra work for the person who wants to subclass a content item. 444 | 445 | Hey, we should write a system that automates a lot of this, and gives 446 | us a universal registration API, making our life easier! And what if 447 | we want to switch behavior based on more than just one argument? Maybe 448 | you even want different dispatch behavior depending on application 449 | context? This is what Reg is for. 450 | 451 | Doing this with Reg 452 | ~~~~~~~~~~~~~~~~~~~ 453 | 454 | Let's see how we can implement ``size`` using Reg: 455 | 456 | First we need our generic ``size`` function: 457 | 458 | .. testcode:: 459 | 460 | def size(item): 461 | raise NotImplementedError 462 | 463 | This function raises ``NotImplementedError`` as we don't know how to 464 | get the size for an arbitrary Python object. Not very useful yet. We need 465 | to be able to hook the actual implementations into it. To do this, we first 466 | need to transform the ``size`` function to a generic one: 467 | 468 | .. testcode:: 469 | 470 | import reg 471 | 472 | size = reg.dispatch('item')(size) 473 | 474 | We can actually spell these two steps in a single step, as 475 | :func:`reg.dispatch` can be used as decorator: 476 | 477 | .. testcode:: 478 | 479 | @reg.dispatch('item') 480 | def size(item): 481 | raise NotImplementedError 482 | 483 | What this says that when we call ``size``, we want to dispatch based 484 | on the class of its ``item`` argument. 485 | 486 | We can now register the various size functions for the various content 487 | items as implementations of ``size``: 488 | 489 | .. testcode:: 490 | 491 | size.register(document_size, item=Document) 492 | size.register(folder_size, item=Folder) 493 | size.register(image_size, item=Image) 494 | size.register(file_size, item=File) 495 | 496 | ``size`` now works: 497 | 498 | .. doctest:: 499 | 500 | >>> size(doc) 501 | 12 502 | 503 | It works for folder too: 504 | 505 | .. doctest:: 506 | 507 | >>> size(folder) 508 | 25 509 | 510 | It works for subclasses too: 511 | 512 | .. doctest:: 513 | 514 | >>> size(htmldoc) 515 | 19 516 | 517 | Reg knows that ``HtmlDocument`` is a subclass of ``Document`` and will 518 | find ``document_size`` automatically for you. We only have to register 519 | something for ``HtmlDocument`` if we want to use a special, different 520 | size function for ``HtmlDocument``. 521 | 522 | Multiple and predicate dispatch 523 | ------------------------------- 524 | 525 | Let's look at an example where dispatching on multiple arguments is 526 | useful: a web view lookup system. Given a request object that 527 | represents a HTTP request, and a model instance ( document, icon, 528 | etc), we want to find a view function that knows how to make a 529 | representation of the model given the request. Information in the 530 | request can influence the representation. In this example we use a 531 | ``request_method`` attribute, which can be ``GET``, ``POST``, ``PUT``, 532 | etc. 533 | 534 | Let's first define a ``Request`` class with a ``request_method`` 535 | attribute: 536 | 537 | .. testcode:: 538 | 539 | class Request(object): 540 | def __init__(self, request_method, body=''): 541 | self.request_method = request_method 542 | self.body = body 543 | 544 | We've also defined a ``body`` attribute which contains text in case 545 | the request is a ``POST`` request. 546 | 547 | We use the previously defined ``Document`` as the model class. 548 | 549 | Now we define a view function that dispatches on the class of the 550 | model instance, and the ``request_method`` attribute of the request: 551 | 552 | .. testcode:: 553 | 554 | @reg.dispatch( 555 | reg.match_instance('obj'), 556 | reg.match_key('request_method', 557 | lambda obj, request: request.request_method)) 558 | def view(obj, request): 559 | raise NotImplementedError 560 | 561 | As you can see here we use ``match_instance`` and ``match_key`` 562 | instead of strings to specify how to dispatch. 563 | 564 | If you use a string argument, this string names an argument and 565 | dispatch is based on the class of the instance you pass in. Here we 566 | use ``match_instance``, which is equivalent to this: we have a ``obj`` 567 | predicate which uses the class of the ``obj`` argument for dispatch. 568 | 569 | We also use ``match_key``, which dispatches on the ``request_method`` 570 | attribute of the request; this attribute is a string, so dispatch is 571 | on string matching, not ``isinstance`` as with ``match_instance``. You 572 | can use any Python immutable with ``match_key``, not just strings. 573 | 574 | We now define concrete views for ``Document`` and ``Image``: 575 | 576 | .. testcode:: 577 | 578 | @view.register(request_method='GET', obj=Document) 579 | def document_get(obj, request): 580 | return "Document text is: " + obj.text 581 | 582 | @view.register(request_method='POST', obj=Document) 583 | def document_post(obj, request): 584 | obj.text = request.body 585 | return "We changed the document" 586 | 587 | Let's also define them for ``Image``: 588 | 589 | .. testcode:: 590 | 591 | @view.register(request_method='GET', obj=Image) 592 | def image_get(obj, request): 593 | return obj.bytes 594 | 595 | @view.register(request_method='POST', obj=Image) 596 | def image_post(obj, request): 597 | obj.bytes = request.body 598 | return "We changed the image" 599 | 600 | Let's try it out: 601 | 602 | .. doctest:: 603 | 604 | >>> view(doc, Request('GET')) 605 | 'Document text is: Hello world!' 606 | >>> view(doc, Request('POST', 'New content')) 607 | 'We changed the document' 608 | >>> doc.text 609 | 'New content' 610 | >>> view(image, Request('GET')) 611 | 'abc' 612 | >>> view(image, Request('POST', "new data")) 613 | 'We changed the image' 614 | >>> image.bytes 615 | 'new data' 616 | 617 | Dispatch methods 618 | ---------------- 619 | 620 | Rather than having a ``size`` function and a ``view`` function, we can 621 | also have a context class with ``size`` and ``view`` as methods. We 622 | need to use :class:`reg.dispatch_method` instead of 623 | :class:`reg.dispatch` to do this. 624 | 625 | .. testcode:: 626 | 627 | class CMS(object): 628 | 629 | @reg.dispatch_method('item') 630 | def size(self, item): 631 | raise NotImplementedError 632 | 633 | @reg.dispatch_method( 634 | reg.match_instance('obj'), 635 | reg.match_key('request_method', 636 | lambda self, obj, request: request.request_method)) 637 | def view(self, obj, request): 638 | return "Generic content of {} bytes.".format(self.size(obj)) 639 | 640 | We can now register an implementation of ``CMS.size`` for a 641 | ``Document`` object: 642 | 643 | .. testcode:: 644 | 645 | @CMS.size.register(item=Document) 646 | def document_size_as_method(self, item): 647 | return len(item.text) 648 | 649 | Note that this is almost the same as the function ``document_size`` we 650 | defined before: the only difference is the signature, with the 651 | additional ``self`` as the first argument. We can in fact use 652 | :func:`reg.methodify` to reuse such functions without an initial 653 | context argument: 654 | 655 | .. testcode:: 656 | 657 | from reg import methodify 658 | 659 | CMS.size.register(methodify(folder_size), item=Folder) 660 | CMS.size.register(methodify(image_size), item=Image) 661 | CMS.size.register(methodify(file_size), item=File) 662 | 663 | ``CMS.size`` now behaves as expected: 664 | 665 | .. doctest:: 666 | 667 | >>> cms = CMS() 668 | >>> cms.size(Image("123")) 669 | 3 670 | >>> cms.size(Document("12345")) 671 | 5 672 | 673 | Similarly for the ``view`` method we can define: 674 | 675 | .. testcode:: 676 | 677 | @CMS.view.register(request_method='GET', obj=Document) 678 | def document_get(self, obj, request): 679 | return "{}-byte-long text is: {}".format( 680 | self.size(obj), obj.text) 681 | 682 | This works as expected as well: 683 | 684 | .. doctest:: 685 | 686 | >>> cms.view(Document("12345"), Request("GET")) 687 | '5-byte-long text is: 12345' 688 | >>> cms.view(Image("123"), Request("GET")) 689 | 'Generic content of 3 bytes.' 690 | 691 | For more about how you can use dispatch methods and class-based context, 692 | see :doc:`context`. 693 | 694 | Lower level API 695 | --------------- 696 | 697 | Component lookup 698 | ~~~~~~~~~~~~~~~~ 699 | 700 | You can look up the implementation that a generic function would 701 | dispatch to without calling it. You can look that up by invocation 702 | arguments using the :meth:`reg.Dispatch.by_args` method on the 703 | dispatch function or by predicate values using the 704 | :meth:`reg.Dispatch.by_predicates` method: 705 | 706 | >>> size.by_args(doc).component 707 | 708 | 709 | >>> size.by_predicates(item=Document).component 710 | 711 | 712 | Both methods return a :class:`reg.LookupEntry` instance whose 713 | attributes, as we've just seen, include the dispatched implementation 714 | under the name ``component``. Another interesting attribute is the 715 | actual key used for dispatching: 716 | 717 | >>> view.by_predicates(request_method='GET', obj=Document).key 718 | (, 'GET') 719 | >>> view.by_predicates(obj=Image, request_method='POST').key 720 | (, 'POST') 721 | 722 | 723 | Getting all compatible implementations 724 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 725 | 726 | As Reg supports inheritance, if a function like ``size`` has an 727 | implementation registered for a class, say ``Document``, the same 728 | implementation will be available for any if its subclasses, like 729 | ``HtmlDocument``: 730 | 731 | >>> size.by_args(doc).component is size.by_args(htmldoc).component 732 | True 733 | 734 | The ``matches`` and ``all_matches`` attributes of 735 | :class:`reg.LookupEntry` are an interator and the list, respectively, 736 | of *all* the registered components that are compatible with a 737 | particular instance, including those of base classes. Right now this 738 | is pretty boring as there's only one of them: 739 | 740 | >>> size.by_args(doc).all_matches 741 | [] 742 | >>> size.by_args(htmldoc).all_matches 743 | [] 744 | 745 | We can make this more interesting by registering a special 746 | ``htmldocument_size`` to handle ``HtmlDocument`` instances: 747 | 748 | .. testcode:: 749 | 750 | def htmldocument_size(doc): 751 | return len(doc.text) + 1 # 1 so we can see a difference 752 | 753 | size.register(htmldocument_size, item=HtmlDocument) 754 | 755 | ``size.all()`` for ``htmldoc`` now also gives back the more specific 756 | ``htmldocument_size``: 757 | 758 | >>> size.by_args(htmldoc).all_matches 759 | [, ] 760 | 761 | The implementation are listed in order of decreasing specificity, with 762 | the first one as the one returned by the ``component`` attribute: 763 | 764 | >>> size.by_args(htmldoc).component 765 | 766 | --------------------------------------------------------------------------------