├── .nojeklyll ├── fastdispatch ├── __init__.py ├── _modidx.py └── core.py ├── MANIFEST.in ├── styles.css ├── settings.ini ├── _quarto.yml ├── .github └── workflows │ ├── test.yaml │ └── deploy.yaml ├── README.md ├── .gitignore ├── setup.py ├── index.ipynb ├── LICENSE └── 00_core.ipynb /.nojeklyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastdispatch/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import * 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include settings.ini 2 | include LICENSE 3 | include CONTRIBUTING.md 4 | include README.md 5 | recursive-exclude * __pycache__ 6 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .cell-output pre { 2 | margin-left: 0.8rem; 3 | margin-top: 0; 4 | background: none; 5 | border-left: 2px solid lightsalmon; 6 | border-top-left-radius: 0; 7 | border-top-right-radius: 0; 8 | } 9 | 10 | .cell-output .sourceCode { 11 | background: none; 12 | margin-top: 0; 13 | } 14 | 15 | .cell > .sourceCode { 16 | margin-bottom: 0; 17 | } 18 | -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | host = github 3 | lib_name = fastdispatch 4 | description = Wrapper for plum dispatch to make it more compatible with fastcore's typedispatch 5 | copyright = 2022 onwards, Wasim Lorgat 6 | keywords = multiple-dispatch plum fastai 7 | user = fastai 8 | author = seem 9 | author_email = mwlorgat@gmail.com 10 | branch = master 11 | version = 0.0.1 12 | min_python = 3.7 13 | audience = Developers 14 | language = English 15 | custom_sidebar = False 16 | license = apache2 17 | status = 2 18 | nbs_path = . 19 | doc_path = _docs 20 | recursive = False 21 | tst_flags = notest 22 | doc_host = https://%(user)s.github.io 23 | doc_baseurl = /%(lib_name)s/ 24 | git_url = https://github.com/%(user)s/%(lib_name)s/tree/%(branch)s/ 25 | lib_path = %(lib_name)s 26 | title = %(lib_name)s 27 | requirements = fastcore plum-dispatch 28 | dev_requirements = numpy 29 | # console_scripts = 30 | -------------------------------------------------------------------------------- /_quarto.yml: -------------------------------------------------------------------------------- 1 | ipynb-filters: [nbprocess_filter] 2 | 3 | project: 4 | type: website 5 | output-dir: _docs 6 | preview: 7 | port: 3000 8 | browser: false 9 | 10 | format: 11 | html: 12 | theme: cosmo 13 | css: styles.css 14 | toc: true 15 | 16 | website: 17 | title: "fastdispatch" 18 | description: "Wrapper for plum dispatch to make it more compatible with fastcore's typedispatch" 19 | execute: 20 | enabled: false 21 | twitter-card: true 22 | open-graph: true 23 | reader-mode: true 24 | repo-branch: master 25 | repo-url: https://github.com/fastai/fastdispatch/tree/master/ 26 | repo-actions: [issue] 27 | navbar: 28 | background: primary 29 | search: true 30 | right: 31 | - icon: github 32 | href: https://github.com/fastai/fastdispatch/tree/master/ 33 | sidebar: 34 | style: "floating" 35 | 36 | metadata-files: 37 | - sidebar.yml 38 | - custom.yml 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [workflow_dispatch, pull_request, push] 3 | 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-python@v3 11 | with: 12 | python-version: '3.10' 13 | architecture: 'x64' 14 | - name: Install Dependencies 15 | run: | 16 | python -m pip install --upgrade pip 17 | python -m pip install git+https://github.com/fastai/nbprocess.git@master 18 | pip install -qe ".[dev]" 19 | - name: Check if all notebooks are cleaned 20 | run: | 21 | echo "Check we are starting with clean git checkout" 22 | if [ -n "$(git status -uno -s)" ]; then echo "git status is not clean"; false; fi 23 | echo "Trying to strip out notebooks" 24 | nbprocess_clean 25 | echo "Check that strip out was unnecessary" 26 | git status -s # display the status to see which nbs need cleaning up 27 | if [ -n "$(git status -uno -s)" ]; then echo -e "!!! Detected unstripped out notebooks\n!!!Remember to run nbprocess_install_hooks"; false; fi 28 | - name: Test Notebooks 29 | run: nbprocess_test 30 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | workflow_dispatch: 9 | 10 | jobs: 11 | deploy: 12 | name: Deploy to GitHub Pages 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-python@v3 17 | with: 18 | python-version: "3.10" 19 | architecture: "x64" 20 | - name: Install Dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install git+https://github.com/fastai/nbprocess.git 24 | - name: Build website 25 | run: nbprocess_docs 26 | - name: Deploy to GitHub Pages 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | force_orphan: true 31 | publish_dir: ./_docs 32 | # The following lines assign commit authorship to the official 33 | # GH-Actions bot for deploys to `gh-pages` branch: 34 | # https://github.com/actions/checkout/issues/13#issuecomment-724415212 35 | # The GH actions bot is used by default if you didn't specify the two fields. 36 | # You can swap them out with your own user credentials. 37 | user_name: github-actions[bot] 38 | user_email: 41898282+github-actions[bot]@users.noreply.github.com 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # fastdispatch 3 | 4 | 5 | 6 | [![CI](https://github.com/fastai/fastdispatch/actions/workflows/test.yaml/badge.svg)](https://github.com/fastai/fastdispatch/actions/workflows/test.yaml) 7 | [![Deploy to GitHub 8 | Pages](https://github.com/fastai/fastdispatch/actions/workflows/deploy.yaml/badge.svg)](https://github.com/fastai/fastdispatch/actions/workflows/deploy.yaml) 9 | 10 | Wrapper for plum dispatch to make it more compatible with fastcore’s 11 | typedispatch. Hopefully this is just temporary, and instead the 12 | functionality here will be moved into plum. 13 | 14 | ## Install 15 | 16 | `pip install fastdispatch` 17 | 18 | ## How to use 19 | 20 | `fastdispatch` works just like plum, with a few extensions. We recommend 21 | reading through their [very informative 22 | docs](https://github.com/wesselb/plum), however, here’s a quick example 23 | to get started: 24 | 25 | ``` python 26 | from fastcore.test import test_fail 27 | from fastdispatch import * 28 | ``` 29 | 30 | Decorate type annotated Python functions with `fastdispatch.dispatch` to 31 | add them as *methods* to a dispatched *function* (following [Julia’s 32 | terminology](https://docs.julialang.org/en/v1/manual/methods/)): 33 | 34 | ``` python 35 | @dispatch 36 | def f(x: str): return "This is a string!" 37 | 38 | @dispatch 39 | def f(x: int): return "This is an integer!" 40 | ``` 41 | 42 | ``` python 43 | f('1') 44 | ``` 45 | 46 | 'This is a string!' 47 | 48 | ``` python 49 | f(1) 50 | ``` 51 | 52 | 'This is an integer!' 53 | 54 | If there’s no matching method, `plum.NotFoundLookupError` is raised: 55 | 56 | ``` python 57 | test_fail(lambda: f(1.0), contains='For function "f", signature Signature(builtins.float) could not be resolved.') 58 | ``` 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | /.quarto/ 132 | sidebar.yml 133 | _docs/ 134 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import parse_version 2 | from configparser import ConfigParser 3 | import setuptools 4 | assert parse_version(setuptools.__version__)>=parse_version('36.2') 5 | 6 | # note: all settings are in settings.ini; edit there, not here 7 | config = ConfigParser(delimiters=['=']) 8 | config.read('settings.ini') 9 | cfg = config['DEFAULT'] 10 | 11 | cfg_keys = 'version description keywords author author_email'.split() 12 | expected = cfg_keys + "lib_name user branch license status min_python audience language".split() 13 | for o in expected: assert o in cfg, "missing expected setting: {}".format(o) 14 | setup_cfg = {o:cfg[o] for o in cfg_keys} 15 | 16 | licenses = { 17 | 'apache2': ('Apache Software License 2.0','OSI Approved :: Apache Software License'), 18 | 'mit': ('MIT License', 'OSI Approved :: MIT License'), 19 | 'gpl2': ('GNU General Public License v2', 'OSI Approved :: GNU General Public License v2 (GPLv2)'), 20 | 'gpl3': ('GNU General Public License v3', 'OSI Approved :: GNU General Public License v3 (GPLv3)'), 21 | 'bsd3': ('BSD License', 'OSI Approved :: BSD License'), 22 | } 23 | statuses = [ '1 - Planning', '2 - Pre-Alpha', '3 - Alpha', 24 | '4 - Beta', '5 - Production/Stable', '6 - Mature', '7 - Inactive' ] 25 | py_versions = '3.6 3.7 3.8 3.9 3.10'.split() 26 | 27 | requirements = cfg.get('requirements','').split() 28 | min_python = cfg['min_python'] 29 | lic = licenses.get(cfg['license'].lower(), (cfg['license'], None)) 30 | dev_requirements = (cfg.get('dev_requirements') or '').split() 31 | 32 | setuptools.setup( 33 | name = cfg['lib_name'], 34 | license = lic[0], 35 | classifiers = [ 36 | 'Development Status :: ' + statuses[int(cfg['status'])], 37 | 'Intended Audience :: ' + cfg['audience'].title(), 38 | 'Natural Language :: ' + cfg['language'].title(), 39 | ] + ['Programming Language :: Python :: '+o for o in py_versions[py_versions.index(min_python):]] + (['License :: ' + lic[1] ] if lic[1] else []), 40 | url = cfg['git_url'], 41 | packages = setuptools.find_packages(), 42 | include_package_data = True, 43 | install_requires = requirements, 44 | extras_require={ 'dev': dev_requirements }, 45 | dependency_links = cfg.get('dep_links','').split(), 46 | python_requires = '>=' + cfg['min_python'], 47 | long_description = open('README.md').read(), 48 | long_description_content_type = 'text/markdown', 49 | zip_safe = False, 50 | entry_points = { 51 | 'console_scripts': cfg.get('console_scripts','').split(), 52 | 'mkdocs.plugins': [ 'rm_num_prefix = nbprocess.mkdocs:RmNumPrefix' ], 53 | 'nbdev': [f'{cfg.get("lib_path")}={cfg.get("lib_path")}._modidx:d'] 54 | }, 55 | **setup_cfg) 56 | 57 | 58 | -------------------------------------------------------------------------------- /fastdispatch/_modidx.py: -------------------------------------------------------------------------------- 1 | # Autogenerated by nbprocess 2 | 3 | d = { 'settings': { 'audience': 'Developers', 4 | 'author': 'seem', 5 | 'author_email': 'mwlorgat@gmail.com', 6 | 'branch': 'master', 7 | 'copyright': '2022 onwards, Wasim Lorgat', 8 | 'custom_sidebar': 'False', 9 | 'description': "Wrapper for plum dispatch to make it more compatible with fastcore's typedispatch", 10 | 'dev_requirements': 'numpy', 11 | 'doc_baseurl': '/fastdispatch/', 12 | 'doc_host': 'https://fastai.github.io', 13 | 'doc_path': '_docs', 14 | 'git_url': 'https://github.com/fastai/fastdispatch/tree/master/', 15 | 'host': 'github', 16 | 'keywords': 'multiple-dispatch plum fastai', 17 | 'language': 'English', 18 | 'lib_name': 'fastdispatch', 19 | 'lib_path': 'fastdispatch', 20 | 'license': 'apache2', 21 | 'min_python': '3.7', 22 | 'nbs_path': '.', 23 | 'recursive': 'False', 24 | 'requirements': 'fastcore plum-dispatch', 25 | 'status': '2', 26 | 'title': 'fastdispatch', 27 | 'tst_flags': 'notest', 28 | 'user': 'fastai', 29 | 'version': '0.0.1'}, 30 | 'syms': { 'fastdispatch.core': { 'fastdispatch.core.FastDispatcher': 'https://fastai.github.io/fastdispatch/core#FastDispatcher', 31 | 'fastdispatch.core.FastDispatcher._to': 'https://fastai.github.io/fastdispatch/core#FastDispatcher._to', 32 | 'fastdispatch.core.FastDispatcher.to': 'https://fastai.github.io/fastdispatch/core#FastDispatcher.to', 33 | 'fastdispatch.core.FastFunction': 'https://fastai.github.io/fastdispatch/core#FastFunction', 34 | 'fastdispatch.core.FastFunction.dispatch': 'https://fastai.github.io/fastdispatch/core#FastFunction.dispatch', 35 | 'fastdispatch.core.FastFunction.register': 'https://fastai.github.io/fastdispatch/core#FastFunction.register', 36 | 'fastdispatch.core.cast': 'https://fastai.github.io/fastdispatch/core#cast', 37 | 'fastdispatch.core.dispatch': 'https://fastai.github.io/fastdispatch/core#dispatch', 38 | 'fastdispatch.core.explode_types': 'https://fastai.github.io/fastdispatch/core#explode_types', 39 | 'fastdispatch.core.retain_meta': 'https://fastai.github.io/fastdispatch/core#retain_meta', 40 | 'fastdispatch.core.retain_type': 'https://fastai.github.io/fastdispatch/core#retain_type', 41 | 'fastdispatch.core.retain_types': 'https://fastai.github.io/fastdispatch/core#retain_types'}}} -------------------------------------------------------------------------------- /index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# fastdispatch" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "[![CI](https://github.com/fastai/fastdispatch/actions/workflows/test.yaml/badge.svg)](https://github.com/fastai/fastdispatch/actions/workflows/test.yaml) [![Deploy to GitHub Pages](https://github.com/fastai/fastdispatch/actions/workflows/deploy.yaml/badge.svg)](https://github.com/fastai/fastdispatch/actions/workflows/deploy.yaml)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "Wrapper for plum dispatch to make it more compatible with fastcore's typedispatch. Hopefully this is just temporary, and instead the functionality here will be moved into plum." 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "## Install" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "`pip install fastdispatch`" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "## How to use" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "`fastdispatch` works just like plum, with a few extensions. We recommend reading through their [very informative docs](https://github.com/wesselb/plum), however, here's a quick example to get started:" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "from fastcore.test import *\n", 59 | "from fastdispatch import *" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "metadata": {}, 65 | "source": [ 66 | "Decorate type annotated Python functions with `fastdispatch.dispatch` to add them as _methods_ to a dispatched _function_ (following [Julia's terminology](https://docs.julialang.org/en/v1/manual/methods/)):" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "@dispatch\n", 76 | "def f(x: str): return \"This is a string!\"\n", 77 | "\n", 78 | "@dispatch\n", 79 | "def f(x: int): return \"This is an integer!\"" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": {}, 86 | "outputs": [ 87 | { 88 | "data": { 89 | "text/plain": [ 90 | "'This is a string!'" 91 | ] 92 | }, 93 | "execution_count": null, 94 | "metadata": {}, 95 | "output_type": "execute_result" 96 | } 97 | ], 98 | "source": [ 99 | "f('1')" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [ 107 | { 108 | "data": { 109 | "text/plain": [ 110 | "'This is an integer!'" 111 | ] 112 | }, 113 | "execution_count": null, 114 | "metadata": {}, 115 | "output_type": "execute_result" 116 | } 117 | ], 118 | "source": [ 119 | "f(1)" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "If there's no matching method, `plum.NotFoundLookupError` is raised:" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "test_fail(lambda: f(1.0), contains='For function \"f\", signature Signature(builtins.float) could not be resolved.')" 136 | ] 137 | } 138 | ], 139 | "metadata": { 140 | "kernelspec": { 141 | "display_name": "Python 3 (ipykernel)", 142 | "language": "python", 143 | "name": "python3" 144 | } 145 | }, 146 | "nbformat": 4, 147 | "nbformat_minor": 4 148 | } 149 | -------------------------------------------------------------------------------- /fastdispatch/core.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../00_core.ipynb. 2 | 3 | # %% ../00_core.ipynb 3 4 | from __future__ import annotations 5 | import inspect 6 | from fastcore.meta import * 7 | from fastcore.utils import * 8 | from plum import Function, Dispatcher 9 | 10 | # %% auto 0 11 | __all__ = ['dispatch', 'FastFunction', 'FastDispatcher', 'retain_meta', 'cast', 'retain_type', 'retain_types', 'explode_types'] 12 | 13 | # %% ../00_core.ipynb 7 14 | def _eval_annotations(f): 15 | "Evaluate future annotations before passing to plum to support backported union operator `|`" 16 | f = copy_func(f) 17 | for k, v in type_hints(f).items(): f.__annotations__[k] = Union[v] if isinstance(v, tuple) else v 18 | return f 19 | 20 | # %% ../00_core.ipynb 9 21 | def _pt_repr(o): 22 | "Concise repr of plum types" 23 | n = type(o).__name__ 24 | if n == 'Tuple': return f"{n.lower()}[{','.join(_pt_repr(t) for t in o._el_types)}]" 25 | if n == 'List': return f'{n.lower()}[{_pt_repr(o._el_type)}]' 26 | if n == 'Dict': return f'{n.lower()}[{_pt_repr(o._key_type)},{_pt_repr(o._value_type)}]' 27 | if n in ('Sequence','Iterable'): return f'{n}[{_pt_repr(o._el_type)}]' 28 | if n == 'VarArgs': return f'{n}[{_pt_repr(o.type)}]' 29 | if n == 'Union': return '|'.join(sorted(t.__name__ for t in (o.get_types()))) 30 | assert len(o.get_types()) == 1 31 | return o.get_types()[0].__name__ 32 | 33 | # %% ../00_core.ipynb 12 34 | class FastFunction(Function): 35 | "Multiple dispatched function; extends `plum.Function`" 36 | def __repr__(self): 37 | return '\n'.join(f"{f.__name__}({','.join(_pt_repr(t) for t in s.types)}) -> {_pt_repr(r)}" 38 | for s, (f, r) in self.methods.items()) 39 | 40 | def dispatch(self, f=None, precedence=0): 41 | return super().dispatch(_eval_annotations(f), precedence) 42 | 43 | def register(self, signature, f, precedence=0, return_type=object, delayed=None): 44 | self.__signature__ = inspect.signature(f) 45 | return super().register(signature, f, precedence, return_type, delayed) 46 | 47 | def __getitem__(self, ts): 48 | "Return the most-specific matching method with fewest parameters" 49 | ts = L(ts) 50 | nargs = min(len(o) for o in self.methods.keys()) 51 | while len(ts) < nargs: ts.append(object) 52 | return self.invoke(*ts) 53 | 54 | # %% ../00_core.ipynb 20 55 | class FastDispatcher(Dispatcher): 56 | "Namespace for multiple dispatched functions; extends `plum.Dispatcher`" 57 | def _get_function(self, method, owner): 58 | "Adapted from `Dispatcher._get_function` to use `FastFunction`" 59 | name = method.__name__ 60 | if owner: 61 | if owner not in self._classes: self._classes[owner] = {} 62 | namespace = self._classes[owner] 63 | else: namespace = self._functions 64 | if name not in namespace: namespace[name] = FastFunction(method, owner=owner) 65 | return namespace[name] 66 | 67 | def __call__(self, f, precedence=0): 68 | "Decorator for a particular signature" 69 | return super().__call__(_eval_annotations(f), precedence) 70 | 71 | dispatch = FastDispatcher() 72 | 73 | # %% ../00_core.ipynb 29 74 | @patch 75 | def _to(self:FastDispatcher, cls, nm, f, **kwargs): 76 | nf = copy_func(f) 77 | nf.__qualname__ = f'{cls.__name__}.{nm}' # plum uses __qualname__ to infer f's owner 78 | pf = self(nf, **kwargs) 79 | # plum uses __set_name__ to resolve a plum.Function's owner 80 | # since we assign after class creation, __set_name__ must be called directly 81 | # source: https://docs.python.org/3/reference/datamodel.html#object.__set_name__ 82 | pf.__set_name__(cls, nm) 83 | pf = pf.resolve() 84 | setattr(cls, nm, pf) 85 | return pf 86 | 87 | @patch 88 | def to(self:FastDispatcher, cls): 89 | "Decorator: dispatch `f` to `cls.f`" 90 | def _inner(f, **kwargs): 91 | nm = f.__name__ 92 | # check __dict__ to avoid inherited methods but use getattr so pf.__get__ is called, which plum relies on 93 | if nm in cls.__dict__: 94 | pf = getattr(cls, nm) 95 | if not hasattr(pf, 'dispatch'): pf = self._to(cls, nm, pf, **kwargs) 96 | pf.dispatch(f) 97 | else: pf = self._to(cls, nm, f, **kwargs) 98 | return pf 99 | return _inner 100 | 101 | # %% ../00_core.ipynb 38 102 | _all_=['cast'] 103 | 104 | # %% ../00_core.ipynb 39 105 | def retain_meta(x, res, as_copy=False): 106 | "Call `res.set_meta(x)`, if it exists" 107 | if hasattr(res,'set_meta'): res.set_meta(x, as_copy=as_copy) 108 | return res 109 | 110 | # %% ../00_core.ipynb 40 111 | @dispatch 112 | def cast(x, typ): 113 | "Cast `x` to `typ` (may change `x` inplace)" 114 | res = typ._before_cast(x) if hasattr(typ,'_before_cast') else x 115 | if risinstance('ndarray',res): res = res.view(typ) 116 | elif hasattr(res,'as_subclass'): res = res.as_subclass(typ) 117 | else: 118 | try: res.__class__ = typ 119 | except: res = typ(res) 120 | return retain_meta(x, res) 121 | 122 | # %% ../00_core.ipynb 46 123 | def retain_type(new, old=None, typ=None, as_copy=False): 124 | "Cast `new` to type of `old` or `typ` if it's a superclass" 125 | # e.g. old is TensorImage, new is Tensor - if not subclass then do nothing 126 | if new is None: return 127 | assert old is not None or typ is not None 128 | if typ is None: 129 | if not isinstance(old, type(new)): return new 130 | typ = old if isinstance(old,type) else type(old) 131 | # Do nothing the new type is already an instance of requested type (i.e. same type) 132 | if typ==NoneType or isinstance(new, typ): return new 133 | return retain_meta(old, cast(new, typ), as_copy=as_copy) 134 | 135 | # %% ../00_core.ipynb 51 136 | def retain_types(new, old=None, typs=None): 137 | "Cast each item of `new` to type of matching item in `old` if it's a superclass" 138 | if not is_listy(new): return retain_type(new, old, typs) 139 | if typs is not None: 140 | if isinstance(typs, dict): 141 | t = first(typs.keys()) 142 | typs = typs[t] 143 | else: t,typs = typs,None 144 | else: t = type(old) if old is not None and isinstance(old,type(new)) else type(new) 145 | return t(L(new, old, typs).map_zip(retain_types, cycled=True)) 146 | 147 | # %% ../00_core.ipynb 53 148 | def explode_types(o): 149 | "Return the type of `o`, potentially in nested dictionaries for thing that are listy" 150 | if not is_listy(o): return type(o) 151 | return {type(o): [explode_types(o_) for o_ in o]} 152 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /00_core.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "#| default_exp core" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# core\n", 17 | "\n", 18 | "> plum-dispatch extensions" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "`fastdispatch` extends the wonderful [`plum`](https://app.reviewnb.com/fastai/fastcore/pull/415/#:~:text=extends%20the%20wonderful-,plum,-library%27s%20implementation%20of)'s Julia-inspired implementation of multiple dispatch for Python." 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "#|export\n", 35 | "from __future__ import annotations\n", 36 | "import inspect\n", 37 | "from fastcore.meta import *\n", 38 | "from fastcore.utils import *\n", 39 | "from plum import Function, Dispatcher" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "import numpy as np\n", 49 | "from fastcore.test import *" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "#|hide\n", 59 | "from nbprocess.showdoc import *" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "#|export\n", 69 | "def _eval_annotations(f):\n", 70 | " \"Evaluate future annotations before passing to plum to support backported union operator `|`\"\n", 71 | " f = copy_func(f)\n", 72 | " for k, v in type_hints(f).items(): f.__annotations__[k] = Union[v] if isinstance(v, tuple) else v\n", 73 | " return f" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "#|hide\n", 83 | "def f(x:int|str) -> float: pass\n", 84 | "test_eq(_eval_annotations(f).__annotations__, {'x': typing.Union[int, str], 'return': float})\n", 85 | "def f(x:(int,str)) -> float: pass\n", 86 | "test_eq(_eval_annotations(f).__annotations__, {'x': typing.Union[int, str], 'return': float})\n", 87 | "def f(x): pass\n", 88 | "test_eq(_eval_annotations(f).__annotations__, {})" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "#|export\n", 98 | "def _pt_repr(o):\n", 99 | " \"Concise repr of plum types\"\n", 100 | " n = type(o).__name__\n", 101 | " if n == 'Tuple': return f\"{n.lower()}[{','.join(_pt_repr(t) for t in o._el_types)}]\"\n", 102 | " if n == 'List': return f'{n.lower()}[{_pt_repr(o._el_type)}]'\n", 103 | " if n == 'Dict': return f'{n.lower()}[{_pt_repr(o._key_type)},{_pt_repr(o._value_type)}]'\n", 104 | " if n in ('Sequence','Iterable'): return f'{n}[{_pt_repr(o._el_type)}]'\n", 105 | " if n == 'VarArgs': return f'{n}[{_pt_repr(o.type)}]'\n", 106 | " if n == 'Union': return '|'.join(sorted(t.__name__ for t in (o.get_types())))\n", 107 | " assert len(o.get_types()) == 1\n", 108 | " return o.get_types()[0].__name__" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "metadata": {}, 115 | "outputs": [], 116 | "source": [ 117 | "#|hide\n", 118 | "from typing import Dict, List, Iterable, Sequence, Tuple\n", 119 | "from plum.type import VarArgs, ptype\n", 120 | "\n", 121 | "test_eq(_pt_repr(ptype(int)), 'int')\n", 122 | "test_eq(_pt_repr(ptype(Union[int, str])), 'int|str')\n", 123 | "test_eq(_pt_repr(ptype(Tuple[int, str])), 'tuple[int,str]')\n", 124 | "test_eq(_pt_repr(ptype(List[int])), 'list[int]')\n", 125 | "test_eq(_pt_repr(ptype(Sequence[int])), 'Sequence[int]')\n", 126 | "test_eq(_pt_repr(ptype(Iterable[int])), 'Iterable[int]')\n", 127 | "test_eq(_pt_repr(ptype(Dict[str, int])), 'dict[str,int]')\n", 128 | "test_eq(_pt_repr(ptype(VarArgs[str])), 'VarArgs[str]')\n", 129 | "test_eq(_pt_repr(ptype(Dict[Tuple[Union[int,str],float], List[Tuple[object]]])),\n", 130 | " 'dict[tuple[int|str,float],list[tuple[object]]]')" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": {}, 136 | "source": [ 137 | "### FastFunction -" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "#|export\n", 147 | "class FastFunction(Function):\n", 148 | " \"Multiple dispatched function; extends `plum.Function`\"\n", 149 | " def __repr__(self):\n", 150 | " return '\\n'.join(f\"{f.__name__}({','.join(_pt_repr(t) for t in s.types)}) -> {_pt_repr(r)}\"\n", 151 | " for s, (f, r) in self.methods.items())\n", 152 | "\n", 153 | " def dispatch(self, f=None, precedence=0):\n", 154 | " return super().dispatch(_eval_annotations(f), precedence)\n", 155 | "\n", 156 | " def register(self, signature, f, precedence=0, return_type=object, delayed=None):\n", 157 | " self.__signature__ = inspect.signature(f)\n", 158 | " return super().register(signature, f, precedence, return_type, delayed)\n", 159 | "\n", 160 | " def __getitem__(self, ts):\n", 161 | " \"Return the most-specific matching method with fewest parameters\"\n", 162 | " ts = L(ts)\n", 163 | " nargs = min(len(o) for o in self.methods.keys())\n", 164 | " while len(ts) < nargs: ts.append(object)\n", 165 | " return self.invoke(*ts)" 166 | ] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "metadata": {}, 171 | "source": [ 172 | "It has a concise `repr`:" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "metadata": {}, 179 | "outputs": [ 180 | { 181 | "data": { 182 | "text/plain": [ 183 | "f(int) -> float" 184 | ] 185 | }, 186 | "execution_count": null, 187 | "metadata": {}, 188 | "output_type": "execute_result" 189 | } 190 | ], 191 | "source": [ 192 | "def f(x:int) -> float: pass\n", 193 | "f = FastFunction(f).dispatch(f)\n", 194 | "f" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "It supports `fastcore`'s backport of the `|` operator on types:" 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": null, 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [ 210 | "def f1(x): return 'obj'\n", 211 | "def f2(x:int|str): return 'int|str'\n", 212 | "f = FastFunction(f1).dispatch(f1).dispatch(f2)\n", 213 | "\n", 214 | "test_eq(f(0), 'int|str')\n", 215 | "test_eq(f(''), 'int|str')\n", 216 | "test_eq(f(0.0), 'obj')" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": {}, 222 | "source": [ 223 | "Indexing a `FastFunction` works like [`plum.Function.invoke`](https://github.com/wesselb/plum#directly-invoke-a-method) but returns the most-specific matching method with the fewest parameters:" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": null, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "def f1(a:int, b, c): return 'int, 3 args'\n", 233 | "def f2(a:int, b, c, d): return 'int, 4 args'\n", 234 | "def f3(a:float, b, c): return 'float, 3 args'\n", 235 | "def f4(a:float, b:str, c): return 'float, str, 3 args'\n", 236 | "f = FastFunction(f1).dispatch(f1).dispatch(f2).dispatch(f3).dispatch(f4)\n", 237 | "\n", 238 | "test_eq(f[int](0,0,0), 'int, 3 args')\n", 239 | "test_eq(f[float](0,0,0), 'float, 3 args')\n", 240 | "test_eq(f[float](0,0,0), 'float, 3 args')\n", 241 | "test_eq(f[float, str](0,0,0), 'float, str, 3 args')" 242 | ] 243 | }, 244 | { 245 | "cell_type": "markdown", 246 | "metadata": {}, 247 | "source": [ 248 | "### FastDispatcher -" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": null, 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "#|export\n", 258 | "class FastDispatcher(Dispatcher):\n", 259 | " \"Namespace for multiple dispatched functions; extends `plum.Dispatcher`\"\n", 260 | " def _get_function(self, method, owner):\n", 261 | " \"Adapted from `Dispatcher._get_function` to use `FastFunction`\"\n", 262 | " name = method.__name__\n", 263 | " if owner:\n", 264 | " if owner not in self._classes: self._classes[owner] = {}\n", 265 | " namespace = self._classes[owner]\n", 266 | " else: namespace = self._functions\n", 267 | " if name not in namespace: namespace[name] = FastFunction(method, owner=owner)\n", 268 | " return namespace[name]\n", 269 | "\n", 270 | " def __call__(self, f, precedence=0):\n", 271 | " \"Decorator for a particular signature\"\n", 272 | " return super().__call__(_eval_annotations(f), precedence)\n", 273 | "\n", 274 | "dispatch = FastDispatcher()" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": null, 280 | "metadata": {}, 281 | "outputs": [], 282 | "source": [ 283 | "dispatch = FastDispatcher()" 284 | ] 285 | }, 286 | { 287 | "cell_type": "markdown", 288 | "metadata": {}, 289 | "source": [ 290 | "Dispatching with `FastDispatcher` returns a `FastFunction`:" 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": null, 296 | "metadata": {}, 297 | "outputs": [], 298 | "source": [ 299 | "@dispatch\n", 300 | "def f(x): return 'obj'\n", 301 | "\n", 302 | "assert isinstance(f, FastFunction)" 303 | ] 304 | }, 305 | { 306 | "cell_type": "markdown", 307 | "metadata": {}, 308 | "source": [ 309 | "It supports fastcore's backport of the `|` operator on types:" 310 | ] 311 | }, 312 | { 313 | "cell_type": "code", 314 | "execution_count": null, 315 | "metadata": {}, 316 | "outputs": [], 317 | "source": [ 318 | "@dispatch\n", 319 | "def f(x:int|str): return 'int|str'\n", 320 | "\n", 321 | "test_eq(f(0), 'int|str')\n", 322 | "test_eq(f(''), 'int|str')\n", 323 | "test_eq(f(0.0), 'obj')" 324 | ] 325 | }, 326 | { 327 | "cell_type": "markdown", 328 | "metadata": {}, 329 | "source": [ 330 | "... `FastDispatcher.multi` works too:" 331 | ] 332 | }, 333 | { 334 | "cell_type": "code", 335 | "execution_count": null, 336 | "metadata": {}, 337 | "outputs": [], 338 | "source": [ 339 | "@dispatch.multi([bool],[list])\n", 340 | "def f(x:bool|list): return 'bool|list'\n", 341 | "@dispatch\n", 342 | "def f(x:int): return 'int'\n", 343 | "\n", 344 | "test_eq(f(True), 'bool|list')\n", 345 | "test_eq(f([]), 'bool|list')\n", 346 | "test_eq(f(0), 'int')" 347 | ] 348 | }, 349 | { 350 | "cell_type": "markdown", 351 | "metadata": {}, 352 | "source": [ 353 | "#### FastDispatcher.to -" 354 | ] 355 | }, 356 | { 357 | "cell_type": "code", 358 | "execution_count": null, 359 | "metadata": {}, 360 | "outputs": [], 361 | "source": [ 362 | "#|export\n", 363 | "@patch\n", 364 | "def _to(self:FastDispatcher, cls, nm, f, **kwargs):\n", 365 | " nf = copy_func(f)\n", 366 | " nf.__qualname__ = f'{cls.__name__}.{nm}' # plum uses __qualname__ to infer f's owner\n", 367 | " pf = self(nf, **kwargs)\n", 368 | " # plum uses __set_name__ to resolve a plum.Function's owner\n", 369 | " # since we assign after class creation, __set_name__ must be called directly\n", 370 | " # source: https://docs.python.org/3/reference/datamodel.html#object.__set_name__\n", 371 | " pf.__set_name__(cls, nm)\n", 372 | " pf = pf.resolve()\n", 373 | " setattr(cls, nm, pf)\n", 374 | " return pf\n", 375 | "\n", 376 | "@patch\n", 377 | "def to(self:FastDispatcher, cls):\n", 378 | " \"Decorator: dispatch `f` to `cls.f`\"\n", 379 | " def _inner(f, **kwargs):\n", 380 | " nm = f.__name__\n", 381 | " # check __dict__ to avoid inherited methods but use getattr so pf.__get__ is called, which plum relies on\n", 382 | " if nm in cls.__dict__:\n", 383 | " pf = getattr(cls, nm)\n", 384 | " if not hasattr(pf, 'dispatch'): pf = self._to(cls, nm, pf, **kwargs)\n", 385 | " pf.dispatch(f)\n", 386 | " else: pf = self._to(cls, nm, f, **kwargs)\n", 387 | " return pf\n", 388 | " return _inner" 389 | ] 390 | }, 391 | { 392 | "cell_type": "markdown", 393 | "metadata": {}, 394 | "source": [ 395 | "This lets you dynamically extend dispatched methods:" 396 | ] 397 | }, 398 | { 399 | "cell_type": "code", 400 | "execution_count": null, 401 | "metadata": {}, 402 | "outputs": [], 403 | "source": [ 404 | "class A:\n", 405 | " @dispatch\n", 406 | " def f(self, x): return 'obj'\n", 407 | "\n", 408 | "@dispatch.to(A)\n", 409 | "def f(self, x:int): return 'int'\n", 410 | "\n", 411 | "a = A()\n", 412 | "test_eq(a.f(0), 'int')\n", 413 | "test_eq(a.f(''), 'obj')" 414 | ] 415 | }, 416 | { 417 | "cell_type": "markdown", 418 | "metadata": {}, 419 | "source": [ 420 | "#### Tests -" 421 | ] 422 | }, 423 | { 424 | "cell_type": "code", 425 | "execution_count": null, 426 | "metadata": {}, 427 | "outputs": [], 428 | "source": [ 429 | "#|hide\n", 430 | "#Call `to` twice consecutively\n", 431 | "class A: pass\n", 432 | "\n", 433 | "@dispatch.to(A)\n", 434 | "def f(self, x:int): return 'int'\n", 435 | "\n", 436 | "a = A()\n", 437 | "test_eq(a.f(0), 'int')\n", 438 | "\n", 439 | "@dispatch.to(A)\n", 440 | "def f(self, x:str): return 'str'\n", 441 | "\n", 442 | "test_eq(a.f(''), 'str')" 443 | ] 444 | }, 445 | { 446 | "cell_type": "code", 447 | "execution_count": null, 448 | "metadata": {}, 449 | "outputs": [], 450 | "source": [ 451 | "#|hide\n", 452 | "#Call `to` on an ordinary method (not a `FastFunction`)\n", 453 | "class A:\n", 454 | " def f(self, x): return 'obj'\n", 455 | "\n", 456 | "@dispatch.to(A)\n", 457 | "def f(self, x:int): return 'int'\n", 458 | "\n", 459 | "a = A()\n", 460 | "test_eq(a.f(0), 'int')\n", 461 | "test_eq(a.f(''), 'obj')" 462 | ] 463 | }, 464 | { 465 | "cell_type": "code", 466 | "execution_count": null, 467 | "metadata": {}, 468 | "outputs": [], 469 | "source": [ 470 | "#|hide\n", 471 | "#Calling `to` when there is a matching inherited method doesn't alter the base class\n", 472 | "#but still dispatches to it\n", 473 | "class A:\n", 474 | " def f(self, x): return 'A'\n", 475 | "Af = A.f\n", 476 | "class B(A):\n", 477 | " @dispatch\n", 478 | " def f(self, x:int): return 'B'\n", 479 | "test_is(Af, A.f)\n", 480 | "b = B()\n", 481 | "test_eq(b.f(0), 'B')\n", 482 | "test_eq(b.f(''), 'A')" 483 | ] 484 | }, 485 | { 486 | "cell_type": "markdown", 487 | "metadata": {}, 488 | "source": [ 489 | "## Casting" 490 | ] 491 | }, 492 | { 493 | "cell_type": "markdown", 494 | "metadata": {}, 495 | "source": [ 496 | "Now that we can dispatch on types, let's make it easier to cast objects to a different type." 497 | ] 498 | }, 499 | { 500 | "cell_type": "code", 501 | "execution_count": null, 502 | "metadata": {}, 503 | "outputs": [], 504 | "source": [ 505 | "#|export\n", 506 | "_all_=['cast']" 507 | ] 508 | }, 509 | { 510 | "cell_type": "code", 511 | "execution_count": null, 512 | "metadata": {}, 513 | "outputs": [], 514 | "source": [ 515 | "#|export\n", 516 | "def retain_meta(x, res, as_copy=False):\n", 517 | " \"Call `res.set_meta(x)`, if it exists\"\n", 518 | " if hasattr(res,'set_meta'): res.set_meta(x, as_copy=as_copy)\n", 519 | " return res" 520 | ] 521 | }, 522 | { 523 | "cell_type": "code", 524 | "execution_count": null, 525 | "metadata": {}, 526 | "outputs": [], 527 | "source": [ 528 | "#|export\n", 529 | "@dispatch\n", 530 | "def cast(x, typ):\n", 531 | " \"Cast `x` to `typ` (may change `x` inplace)\"\n", 532 | " res = typ._before_cast(x) if hasattr(typ,'_before_cast') else x\n", 533 | " if risinstance('ndarray',res): res = res.view(typ)\n", 534 | " elif hasattr(res,'as_subclass'): res = res.as_subclass(typ)\n", 535 | " else:\n", 536 | " try: res.__class__ = typ\n", 537 | " except: res = typ(res)\n", 538 | " return retain_meta(x, res)" 539 | ] 540 | }, 541 | { 542 | "cell_type": "markdown", 543 | "metadata": {}, 544 | "source": [ 545 | "This works both for plain python classes:..." 546 | ] 547 | }, 548 | { 549 | "cell_type": "code", 550 | "execution_count": null, 551 | "metadata": {}, 552 | "outputs": [], 553 | "source": [ 554 | "mk_class('_T1', 'a') # mk_class is a fastcore utility that constructs a class\n", 555 | "class _T2(_T1): pass\n", 556 | "\n", 557 | "t = _T1(a=1)\n", 558 | "t2 = cast(t, _T2) \n", 559 | "assert t2 is t # t2 refers to the same object as t\n", 560 | "assert isinstance(t,_T2) # t also changed in-place\n", 561 | "assert isinstance(t2,_T2)\n", 562 | "\n", 563 | "test_eq_type(_T2(a=1), t2) " 564 | ] 565 | }, 566 | { 567 | "cell_type": "markdown", 568 | "metadata": {}, 569 | "source": [ 570 | "...as well as for arrays and tensors." 571 | ] 572 | }, 573 | { 574 | "cell_type": "code", 575 | "execution_count": null, 576 | "metadata": {}, 577 | "outputs": [], 578 | "source": [ 579 | "class _T1(np.ndarray): pass\n", 580 | "\n", 581 | "t = np.array([1])\n", 582 | "t2 = cast(t, _T1)\n", 583 | "test_eq(np.array([1]), t2)\n", 584 | "test_eq(_T1, type(t2))" 585 | ] 586 | }, 587 | { 588 | "cell_type": "markdown", 589 | "metadata": {}, 590 | "source": [ 591 | "To customize casting for other types, define a separate `cast` function with `dispatch` for your type." 592 | ] 593 | }, 594 | { 595 | "cell_type": "code", 596 | "execution_count": null, 597 | "metadata": {}, 598 | "outputs": [], 599 | "source": [ 600 | "#|export\n", 601 | "def retain_type(new, old=None, typ=None, as_copy=False):\n", 602 | " \"Cast `new` to type of `old` or `typ` if it's a superclass\"\n", 603 | " # e.g. old is TensorImage, new is Tensor - if not subclass then do nothing\n", 604 | " if new is None: return\n", 605 | " assert old is not None or typ is not None\n", 606 | " if typ is None:\n", 607 | " if not isinstance(old, type(new)): return new\n", 608 | " typ = old if isinstance(old,type) else type(old)\n", 609 | " # Do nothing the new type is already an instance of requested type (i.e. same type)\n", 610 | " if typ==NoneType or isinstance(new, typ): return new\n", 611 | " return retain_meta(old, cast(new, typ), as_copy=as_copy)" 612 | ] 613 | }, 614 | { 615 | "cell_type": "code", 616 | "execution_count": null, 617 | "metadata": {}, 618 | "outputs": [], 619 | "source": [ 620 | "class _T(tuple): pass\n", 621 | "a = _T((1,2))\n", 622 | "b = tuple((1,2))\n", 623 | "c = retain_type(b, typ=_T)\n", 624 | "test_eq_type(c, a)" 625 | ] 626 | }, 627 | { 628 | "cell_type": "markdown", 629 | "metadata": {}, 630 | "source": [ 631 | "If `old` has a `_meta` attribute, its content is passed when casting `new` to the type of `old`. In the below example, only the attribute `a`, but not `other_attr` is kept, because `other_attr` is not in `_meta`:" 632 | ] 633 | }, 634 | { 635 | "cell_type": "code", 636 | "execution_count": null, 637 | "metadata": {}, 638 | "outputs": [], 639 | "source": [ 640 | "def default_set_meta(self, x, as_copy=False):\n", 641 | " \"Copy over `_meta` from `x` to `res`, if it's missing\"\n", 642 | " if hasattr(x, '_meta') and not hasattr(self, '_meta'):\n", 643 | " meta = x._meta\n", 644 | " if as_copy: meta = copy(meta)\n", 645 | " self._meta = meta\n", 646 | " return self" 647 | ] 648 | }, 649 | { 650 | "cell_type": "code", 651 | "execution_count": null, 652 | "metadata": {}, 653 | "outputs": [], 654 | "source": [ 655 | "class _A():\n", 656 | " set_meta = default_set_meta\n", 657 | " def __init__(self, t): self.t=t\n", 658 | "\n", 659 | "class _B1(_A):\n", 660 | " def __init__(self, t, a=1):\n", 661 | " super().__init__(t)\n", 662 | " self._meta = {'a':a}\n", 663 | " self.other_attr = 'Hello' # will not be kept after casting.\n", 664 | " \n", 665 | "x = _B1(1, a=2)\n", 666 | "b = _A(1)\n", 667 | "c = retain_type(b, old=x)\n", 668 | "test_eq(c._meta, {'a': 2})\n", 669 | "assert not getattr(c, 'other_attr', None)" 670 | ] 671 | }, 672 | { 673 | "cell_type": "code", 674 | "execution_count": null, 675 | "metadata": {}, 676 | "outputs": [], 677 | "source": [ 678 | "#|export\n", 679 | "def retain_types(new, old=None, typs=None):\n", 680 | " \"Cast each item of `new` to type of matching item in `old` if it's a superclass\"\n", 681 | " if not is_listy(new): return retain_type(new, old, typs)\n", 682 | " if typs is not None:\n", 683 | " if isinstance(typs, dict):\n", 684 | " t = first(typs.keys())\n", 685 | " typs = typs[t]\n", 686 | " else: t,typs = typs,None\n", 687 | " else: t = type(old) if old is not None and isinstance(old,type(new)) else type(new)\n", 688 | " return t(L(new, old, typs).map_zip(retain_types, cycled=True))" 689 | ] 690 | }, 691 | { 692 | "cell_type": "code", 693 | "execution_count": null, 694 | "metadata": {}, 695 | "outputs": [], 696 | "source": [ 697 | "class T(tuple): pass\n", 698 | "\n", 699 | "t1,t2 = retain_types((1,(1,(1,1))), (2,T((2,T((3,4))))))\n", 700 | "test_eq_type(t1, 1)\n", 701 | "test_eq_type(t2, T((1,T((1,1)))))\n", 702 | "\n", 703 | "t1,t2 = retain_types((1,(1,(1,1))), typs = {tuple: [int, {T: [int, {T: [int,int]}]}]})\n", 704 | "test_eq_type(t1, 1)\n", 705 | "test_eq_type(t2, T((1,T((1,1)))))" 706 | ] 707 | }, 708 | { 709 | "cell_type": "code", 710 | "execution_count": null, 711 | "metadata": {}, 712 | "outputs": [], 713 | "source": [ 714 | "#|export\n", 715 | "def explode_types(o):\n", 716 | " \"Return the type of `o`, potentially in nested dictionaries for thing that are listy\"\n", 717 | " if not is_listy(o): return type(o)\n", 718 | " return {type(o): [explode_types(o_) for o_ in o]}" 719 | ] 720 | }, 721 | { 722 | "cell_type": "code", 723 | "execution_count": null, 724 | "metadata": {}, 725 | "outputs": [], 726 | "source": [ 727 | "test_eq(explode_types((2,T((2,T((3,4)))))), {tuple: [int, {T: [int, {T: [int,int]}]}]})" 728 | ] 729 | }, 730 | { 731 | "cell_type": "code", 732 | "execution_count": null, 733 | "metadata": {}, 734 | "outputs": [], 735 | "source": [ 736 | "#|hide\n", 737 | "#|eval: false\n", 738 | "from nbprocess.doclinks import nbprocess_export\n", 739 | "nbprocess_export()" 740 | ] 741 | } 742 | ], 743 | "metadata": { 744 | "kernelspec": { 745 | "display_name": "Python 3 (ipykernel)", 746 | "language": "python", 747 | "name": "python3" 748 | } 749 | }, 750 | "nbformat": 4, 751 | "nbformat_minor": 4 752 | } 753 | --------------------------------------------------------------------------------