├── .clang-format ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── build.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── _templates │ └── autosummary │ │ ├── base.rst │ │ ├── class.rst │ │ └── module.rst ├── conf.py ├── enduser_docs.rst ├── generated │ ├── uarray.BackendNotImplementedError.rst │ ├── uarray.Dispatchable.__init__.rst │ ├── uarray.Dispatchable.rst │ ├── uarray.all_of_type.rst │ ├── uarray.clear_backends.rst │ ├── uarray.create_multimethod.rst │ ├── uarray.generate_multimethod.rst │ ├── uarray.get_state.rst │ ├── uarray.mark_as.rst │ ├── uarray.register_backend.rst │ ├── uarray.reset_state.rst │ ├── uarray.rst │ ├── uarray.set_backend.rst │ ├── uarray.set_global_backend.rst │ ├── uarray.set_state.rst │ ├── uarray.skip_backend.rst │ └── uarray.wrap_single_convertor.rst ├── glossary.rst ├── gsoc │ └── 2020 │ │ └── ideas.rst ├── index.rst ├── libauthor_docs.rst ├── logo.png └── multimethod_docs.rst ├── notebooks ├── 01_user_facing.ipynb ├── 02_basic_dev_tutorial.ipynb └── fruit-puzzle.jpg ├── pixi.lock ├── pyproject.toml ├── readthedocs.yml ├── requirements.txt ├── requirements ├── all.txt ├── docs.txt ├── optional.txt └── tests.txt ├── setup.py └── src ├── CMakeLists.txt ├── _uarray_dispatch.cxx ├── small_dynamic_array.h ├── uarray ├── .coveragerc ├── __init__.py ├── _backend.py ├── _typing.pyi ├── _uarray.pyi ├── conftest.py ├── py.typed ├── pytest.ini └── tests │ ├── __init__.py │ ├── example_helpers.py │ └── test_uarray.py ├── vectorcall.cxx └── vectorcall.h /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: LLVM 3 | SpaceBeforeCtorInitializerColon: false 4 | DerivePointerAlignment: false 5 | PointerAlignment: Middle 6 | ColumnLimit: 80 7 | AlwaysBreakTemplateDeclarations: true 8 | AlignAfterOpenBracket: AlwaysBreak 9 | AlwaysBreakAfterReturnType: None 10 | AllowAllParametersOfDeclarationOnNextLine: true 11 | BinPackParameters: true 12 | PenaltyReturnTypeOnItsOwnLine: 1000 13 | IndentPPDirectives: AfterHash 14 | MaxEmptyLinesToKeep: 2 15 | 16 | # Macros that aren't followed by a semi-colon 17 | StatementMacros: 18 | - PyObject_HEAD 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | uarray/_version.py export-subst 2 | # GitHub syntax highlighting 3 | pixi.lock linguist-language=YAML linguist-generated=true 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | # This opens a PR when actions in workflows need an update 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | # Check for updates to GitHub Actions every week 10 | interval: "weekly" 11 | commit-message: 12 | prefix: "skip changelog" # So this PR will not be added to release-drafter 13 | include: "scope" # List of the updated dependencies in the commit will be added 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the main branch 6 | push: 7 | branches: 8 | - main 9 | - master 10 | tags: 11 | - '*' 12 | pull_request: 13 | branches: 14 | - main 15 | - master 16 | 17 | jobs: 18 | build_wheels: 19 | name: Build wheels on ${{ matrix.os }} 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, windows-latest, macos-latest] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Build wheels 31 | uses: pypa/cibuildwheel@v2.23.3 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} 35 | path: ./wheelhouse/*.whl 36 | 37 | make_sdist: 38 | name: Make SDist 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | with: 43 | fetch-depth: 0 44 | 45 | - name: Build SDist 46 | run: pipx run build --sdist 47 | 48 | - uses: actions/upload-artifact@v4 49 | with: 50 | name: cibw-sdist 51 | path: dist/*.tar.gz 52 | 53 | upload_all: 54 | needs: [build_wheels, make_sdist] 55 | permissions: 56 | id-token: write 57 | runs-on: ubuntu-latest 58 | if: github.ref_type == 'tag' 59 | steps: 60 | - uses: actions/download-artifact@v4 61 | with: 62 | pattern: cibw-* 63 | path: dist 64 | merge-multiple: true 65 | 66 | - uses: pypa/gh-action-pypi-publish@release/v1 67 | 68 | doc_lint_cov: 69 | name: Documentation, Linting and Coverage 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | with: 74 | fetch-depth: 0 75 | - uses: actions/setup-python@v5 76 | with: 77 | # Update according to NEP-29 78 | python-version: '3.10' 79 | cache: 'pip' 80 | - name: Install package 81 | run: | 82 | pip install -v .[all] 83 | - name: Build documentation 84 | run: | 85 | sphinx-build -W -b html docs/ _build/html 86 | - name: Upload documentation 87 | uses: actions/upload-artifact@v4 88 | with: 89 | name: Documentation 90 | path: _build/html 91 | - name: Code style and tests 92 | run: | 93 | pytest --pyargs uarray 94 | - name: mypy 95 | run: | 96 | mypy src/uarray 97 | - name: Run clang-format style check for C/C++ code. 98 | uses: jidicula/clang-format-action@v4.15.0 99 | with: 100 | clang-format-version: '19' 101 | check-path: 'src' 102 | - name: Upload coverage to Codecov 103 | uses: codecov/codecov-action@v5 104 | with: 105 | directory: 'coverage/coverage*.xml' 106 | env_vars: OS,PYTHON 107 | fail_ci_if_error: false 108 | verbose: true 109 | 110 | pypy3: 111 | name: Tests for PyPy3 112 | runs-on: ubuntu-latest 113 | steps: 114 | - uses: actions/checkout@v4 115 | with: 116 | fetch-depth: 0 117 | - uses: actions/setup-python@v5 118 | with: 119 | python-version: 'pypy-3.10' 120 | cache: 'pip' 121 | - name: Install package 122 | run: | 123 | pip install -v .[tests] 124 | - name: Run tests 125 | run: | 126 | pytest --pyargs uarray 127 | 128 | scipy_fft: 129 | name: Run SciPy FFT tests 130 | runs-on: ubuntu-latest 131 | steps: 132 | - uses: actions/checkout@v4 133 | - uses: actions/setup-python@v5 134 | with: 135 | # Update according to NEP-29 136 | python-version: '3.10' 137 | cache: 'pip' 138 | - name: Install package 139 | run: | 140 | pip install -v .[tests] pytest"<7" 141 | pip install scipy==1.7.2 142 | - name: SciPy tests 143 | run: | 144 | import scipy.fft 145 | assert scipy.fft.test() 146 | shell: python 147 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.code-workspace 3 | .pytest_cache 4 | .mypy_cache 5 | .hypothesis 6 | __pycache__ 7 | Digraph* 8 | .ipynb_checkpoints 9 | junit 10 | test_readme.py 11 | junit 12 | coverage.xml 13 | **/.coverage 14 | dist 15 | **/*.pyc 16 | _build/ 17 | docs/_build 18 | .idea/ 19 | pytype_output 20 | htmlcov/ 21 | *.egg-info/ 22 | sandbox.py 23 | *.so 24 | build/ 25 | default.profraw 26 | src/uarray/_version.py 27 | *.pyd 28 | # pixi environments 29 | .pixi 30 | *.egg-info 31 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This repository is governed by the Quansight Repository Code of Conduct. It 2 | can be found here: 3 | https://github.com/Quansight/.github/blob/master/CODE_OF_CONDUCT.md. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contribution to `uarray` are welcome and appreciated. Contributions can take the form of bug reports, documentation, code, and more. 4 | 5 | ## Getting the code 6 | 7 | Make a fork of the main [uarray repository](https://github.com/Quansight-Labs/uarray) and clone the fork: 8 | 9 | ``` 10 | git clone https://github.com//uarray 11 | ``` 12 | 13 | ## Install 14 | 15 | `uarray` and all development dependencies can be installed via: 16 | 17 | ``` 18 | pip install -e ".[all]" 19 | ``` 20 | 21 | Note that uarray supports Python versions >= 3.5. If you're running `conda` and would prefer to have dependencies 22 | pulled from there, use 23 | 24 | ``` 25 | conda env create -f .conda/environment.yml 26 | ``` 27 | 28 | This will create an environment named `uarray` which you can use for development. 29 | 30 | ## Testing 31 | 32 | Tests can be run from the main uarray directory as follows: 33 | 34 | ``` 35 | pytest --pyargs uarray 36 | ``` 37 | 38 | To run a subset of tests: 39 | 40 | ``` 41 | pytest uarray.tests.test_backend 42 | ``` 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Quansight-Labs 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `uarray` - A back-end mechanism geared towards array computing 2 | 3 | [![Join the chat at https://gitter.im/Plures/uarray](https://badges.gitter.im/Plures/uarray.svg)](https://gitter.im/Plures/uarray?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://github.com/Quansight-Labs/uarray/actions/workflows/build.yaml/badge.svg)](https://github.com/Quansight-Labs/uarray/actions) 5 | [![PyPI](https://img.shields.io/pypi/v/uarray.svg?style=flat-square)](https://pypi.org/project/uarray/) 6 | 7 | uarray logo 8 | 9 | - [Documentation](https://uarray.org) 10 | - [Road Map](https://github.com/orgs/Quansight-Labs/projects/1) 11 | - [Future Meetings](https://calendar.google.com/calendar/embed?src=quansight.com_cg7sf4usbcn18gdhdb3l2c6v1g%40group.calendar.google.com&ctz=America%2FNew_York) 12 | - [Meeting Notes](https://github.com/Quansight-Labs/uarray/wiki/Meeting-Notes) 13 | - [References](https://github.com/Quansight-Labs/uarray/wiki/References) 14 | - [Papers](https://paperpile.com/shared/fHftX5) 15 | 16 | ## Contributing 17 | 18 | See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more information on how to contribute to `uarray`. 19 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/base.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. auto{{ objtype }}:: {{ objname }} 6 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | 7 | {% block attributes %} 8 | {% if attributes %} 9 | .. rubric:: Attributes 10 | .. autosummary:: 11 | :toctree: 12 | {% for item in attributes %} 13 | {{ name }}.{{ item }} 14 | {% endfor %} 15 | {% endif %} 16 | {% endblock %} 17 | 18 | {% block methods %} 19 | {% if methods %} 20 | .. rubric:: Methods 21 | .. autosummary:: 22 | :toctree: 23 | {% for item in methods %} 24 | {{ name }}.{{ item }} 25 | {% endfor %} 26 | {% endif %} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline }} 2 | 3 | .. rubric:: Description 4 | .. automodule:: {{ fullname }} 5 | .. currentmodule:: {{ fullname }} 6 | 7 | {% if classes %} 8 | .. rubric:: Classes 9 | 10 | .. autosummary:: 11 | :toctree: 12 | 13 | {% for class in classes %} 14 | {{ class }} 15 | {% endfor %} 16 | 17 | {% endif %} 18 | 19 | {% if functions %} 20 | .. rubric:: Functions 21 | 22 | .. autosummary:: 23 | :toctree: 24 | 25 | {% for function in functions %} 26 | {{ function }} 27 | {% endfor %} 28 | 29 | {% endif %} 30 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | from importlib.metadata import version 18 | 19 | 20 | # Mock Extension modules 21 | try: 22 | import uarray._uarray 23 | except ImportError: 24 | from unittest.mock import MagicMock 25 | 26 | class Mock(MagicMock): 27 | @classmethod 28 | def __getattr__(cls, name): 29 | return MagicMock() 30 | 31 | MOCK_MODULES = ["uarray._uarray"] 32 | sys.modules.update((mod, Mock()) for mod in MOCK_MODULES) 33 | 34 | 35 | # sys.path.insert(0, os.path.abspath('.')) 36 | from typing import List, Dict 37 | 38 | 39 | # -- Project information ----------------------------------------------------- 40 | 41 | project = "uarray" 42 | copyright = "2019, Quansight-Labs" 43 | author = "Quansight-Labs" 44 | 45 | # The full version, including alpha/beta/rc tags 46 | release = version("uarray") 47 | # The short X.Y version 48 | version = ".".join(release.split(".")[:2]) 49 | 50 | # -- General configuration --------------------------------------------------- 51 | 52 | # If your documentation needs a minimal Sphinx version, state it here. 53 | # 54 | # needs_sphinx = '1.0' 55 | 56 | # Add any Sphinx extension module names here, as strings. They can be 57 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 58 | # ones. 59 | extensions: List[str] = [ 60 | "sphinx.ext.autodoc", 61 | "sphinx.ext.viewcode", 62 | "sphinx.ext.napoleon", 63 | "sphinx.ext.intersphinx", 64 | "sphinx.ext.autosummary", 65 | "sphinx.ext.doctest", 66 | ] 67 | 68 | # Add any paths that contain templates here, relative to this directory. 69 | templates_path = ["_templates"] 70 | 71 | # The suffix(es) of source filenames. 72 | # You can specify multiple suffix as a list of string: 73 | # 74 | # source_suffix = ['.rst', '.md'] 75 | source_suffix = ".rst" 76 | 77 | # The master toctree document. 78 | master_doc = "index" 79 | 80 | # The language for content autogenerated by Sphinx. Refer to documentation 81 | # for a list of supported languages. 82 | # 83 | # This is also used if you do content translation via gettext catalogs. 84 | # Usually you set "language" from the command line for these cases. 85 | language = 'en' 86 | 87 | # List of patterns, relative to source directory, that match files and 88 | # directories to ignore when looking for source files. 89 | # This pattern also affects html_static_path and html_extra_path. 90 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = None 94 | 95 | 96 | # -- Options for HTML output ------------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | # 101 | html_theme = "sphinx_rtd_theme" 102 | html_logo = "logo.png" 103 | html_favicon = "logo.png" 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | # 109 | # html_theme_options = {} 110 | 111 | # Add any paths that contain custom static files (such as style sheets) here, 112 | # relative to this directory. They are copied after the builtin static files, 113 | # so a file named "default.css" will overwrite the builtin "default.css". 114 | html_static_path = [] # type: List[str] 115 | 116 | # Custom sidebar templates, must be a dictionary that maps document names 117 | # to template names. 118 | # 119 | # The default sidebars (for documents that don't match any pattern) are 120 | # defined by theme itself. Builtin themes are using these templates by 121 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 122 | # 'searchbox.html']``. 123 | # 124 | # html_sidebars = {} 125 | 126 | 127 | # -- Options for HTMLHelp output --------------------------------------------- 128 | 129 | # Output file base name for HTML help builder. 130 | htmlhelp_basename = "uarraydoc" 131 | 132 | 133 | # -- Options for LaTeX output ------------------------------------------------ 134 | 135 | latex_elements = { 136 | # The paper size ('letterpaper' or 'a4paper'). 137 | # 138 | # 'papersize': 'letterpaper', 139 | # The font size ('10pt', '11pt' or '12pt'). 140 | # 141 | # 'pointsize': '10pt', 142 | # Additional stuff for the LaTeX preamble. 143 | # 144 | # 'preamble': '', 145 | # Latex figure (float) alignment 146 | # 147 | # 'figure_align': 'htbp', 148 | } # type: Dict[str, str] 149 | 150 | # Grouping the document tree into LaTeX files. List of tuples 151 | # (source start file, target name, title, 152 | # author, documentclass [howto, manual, or own class]). 153 | latex_documents = [ 154 | (master_doc, "uarray.tex", "uarray Documentation", "Quansight-Labs", "manual") 155 | ] 156 | 157 | 158 | # -- Options for manual page output ------------------------------------------ 159 | 160 | # One entry per manual page. List of tuples 161 | # (source start file, name, description, authors, manual section). 162 | man_pages = [(master_doc, "uarray", "uarray Documentation", [author], 1)] 163 | 164 | 165 | # -- Options for Texinfo output ---------------------------------------------- 166 | 167 | # Grouping the document tree into Texinfo files. List of tuples 168 | # (source start file, target name, title, author, 169 | # dir menu entry, description, category) 170 | texinfo_documents = [ 171 | ( 172 | master_doc, 173 | "uarray", 174 | "uarray Documentation", 175 | author, 176 | "uarray", 177 | "One line description of project.", 178 | "Miscellaneous", 179 | ) 180 | ] 181 | 182 | 183 | # -- Options for Epub output ------------------------------------------------- 184 | 185 | # Bibliographic Dublin Core info. 186 | epub_title = project 187 | 188 | # The unique identifier of the text. This can be a ISBN number 189 | # or the project homepage. 190 | # 191 | # epub_identifier = '' 192 | 193 | # A unique identification for the text. 194 | # 195 | # epub_uid = '' 196 | 197 | # A list of files that should not be packed into the epub file. 198 | epub_exclude_files = ["search.html"] 199 | 200 | autosummary_generate = True 201 | autoclass_content = "both" 202 | 203 | intersphinx_mapping = { 204 | "python": ("https://docs.python.org/3/", None), 205 | "unumpy": ("https://unumpy.uarray.org/en/latest/", None), 206 | } 207 | 208 | doctest_global_setup = """ 209 | import uarray as ua 210 | """ 211 | -------------------------------------------------------------------------------- /docs/enduser_docs.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: uarray 2 | 3 | .. _userdocs: 4 | 5 | End-user quickstart 6 | =================== 7 | 8 | Ideally, the only thing an end-user should have to do is set the backend 9 | and its options. Given a backend, you (as the end-user) can decide to 10 | do one of two things: 11 | 12 | * Set the backend permanently (use the :obj:`set_global_backend` function). 13 | * Set the backend temporarily (use the :obj:`set_backend` context manager). 14 | 15 | .. note:: 16 | API authors may want to wrap these methods and provide their own methods. 17 | 18 | Also of a note may be the :obj:`BackendNotImplementedError`, which is raised 19 | when none of the selected backends have an implementation for a multimethod. 20 | 21 | Setting the backend temporarily 22 | ------------------------------- 23 | 24 | To set the backend temporarily, use the :obj:`set_backend` context manager. 25 | 26 | .. code:: python3 27 | 28 | import uarray as ua 29 | 30 | with ua.set_backend(mybackend): 31 | # Use multimethods (or code dependent on them) here. 32 | 33 | Setting the backend permanently 34 | ------------------------------- 35 | 36 | To set the backend permanently, use the :obj:`set_global_backend` 37 | method. It is a recommendation that the global backend should not 38 | depend on any other backend, as it is not guaranteed that another 39 | backend will be available. 40 | 41 | You can also register backends other than the global backend for permanent 42 | use, but the global backend will be tried first outside of a :obj:`set_backend` 43 | context. This can be done via :obj:`register_backend`. 44 | 45 | 46 | .. code:: python3 47 | 48 | import uarray as ua 49 | 50 | ua.set_global_backend(mybackend) 51 | 52 | # Use relevant multimethods here. 53 | -------------------------------------------------------------------------------- /docs/generated/uarray.BackendNotImplementedError.rst: -------------------------------------------------------------------------------- 1 | BackendNotImplementedError 2 | ========================== 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autoexception:: BackendNotImplementedError -------------------------------------------------------------------------------- /docs/generated/uarray.Dispatchable.__init__.rst: -------------------------------------------------------------------------------- 1 | Dispatchable.\_\_init\_\_ 2 | ========================= 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. automethod:: Dispatchable.__init__ -------------------------------------------------------------------------------- /docs/generated/uarray.Dispatchable.rst: -------------------------------------------------------------------------------- 1 | Dispatchable 2 | ============ 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autoclass:: Dispatchable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | .. rubric:: Methods 15 | .. autosummary:: 16 | :toctree: 17 | 18 | Dispatchable.__init__ 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/generated/uarray.all_of_type.rst: -------------------------------------------------------------------------------- 1 | all\_of\_type 2 | ============= 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: all_of_type -------------------------------------------------------------------------------- /docs/generated/uarray.clear_backends.rst: -------------------------------------------------------------------------------- 1 | clear\_backends 2 | =============== 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: clear_backends -------------------------------------------------------------------------------- /docs/generated/uarray.create_multimethod.rst: -------------------------------------------------------------------------------- 1 | create\_multimethod 2 | =================== 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: create_multimethod -------------------------------------------------------------------------------- /docs/generated/uarray.generate_multimethod.rst: -------------------------------------------------------------------------------- 1 | generate\_multimethod 2 | ===================== 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: generate_multimethod -------------------------------------------------------------------------------- /docs/generated/uarray.get_state.rst: -------------------------------------------------------------------------------- 1 | get\_state 2 | ========== 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: get_state -------------------------------------------------------------------------------- /docs/generated/uarray.mark_as.rst: -------------------------------------------------------------------------------- 1 | mark\_as 2 | ======== 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: mark_as -------------------------------------------------------------------------------- /docs/generated/uarray.register_backend.rst: -------------------------------------------------------------------------------- 1 | register\_backend 2 | ================= 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: register_backend -------------------------------------------------------------------------------- /docs/generated/uarray.reset_state.rst: -------------------------------------------------------------------------------- 1 | reset\_state 2 | ============ 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: reset_state -------------------------------------------------------------------------------- /docs/generated/uarray.rst: -------------------------------------------------------------------------------- 1 | uarray 2 | ====== 3 | 4 | .. automodule:: uarray 5 | 6 | 7 | 8 | .. rubric:: Functions 9 | 10 | .. autosummary:: 11 | :toctree: 12 | 13 | all_of_type 14 | create_multimethod 15 | generate_multimethod 16 | mark_as 17 | set_backend 18 | set_global_backend 19 | register_backend 20 | clear_backends 21 | skip_backend 22 | wrap_single_convertor 23 | get_state 24 | set_state 25 | reset_state 26 | determine_backend 27 | determine_backend_multi 28 | 29 | 30 | 31 | 32 | 33 | .. rubric:: Classes 34 | 35 | .. autosummary:: 36 | :toctree: 37 | 38 | Dispatchable 39 | 40 | 41 | 42 | 43 | 44 | .. rubric:: Exceptions 45 | 46 | .. autosummary:: 47 | :toctree: 48 | 49 | BackendNotImplementedError 50 | 51 | 52 | Design Philosophies 53 | ------------------- 54 | 55 | The following section discusses the design philosophies of :obj:`uarray`, and the 56 | reasoning behind some of these philosophies. 57 | 58 | Modularity 59 | ^^^^^^^^^^ 60 | 61 | :obj:`uarray` (and its sister modules :obj:`unumpy` and others to come) were designed 62 | from the ground-up to be modular. This is part of why :obj:`uarray` itself holds 63 | the core backend and dispatch machinery, and :obj:`unumpy` holds the actual 64 | multimethods. Also, :obj:`unumpy` can be developed completely separately to 65 | :obj:`uarray`, although the ideal place to have it would be NumPy itself. 66 | 67 | However, the benefit of having it separate is that it could span multiple 68 | NumPy versions, even before NEP-18 (or even NEP-13) was available. Another 69 | benefit is that it can have a faster release cycle to help it achieve this. 70 | 71 | Separate Imports 72 | ^^^^^^^^^^^^^^^^ 73 | 74 | Code wishing to use the backend machinery for NumPy (as an example) will 75 | use the statement ``import unumpy as np`` instead of the usual 76 | ``import numpy as np``. This is deliberate: it makes dispatching opt-in 77 | instead of being forced to use it, and the overhead associated with it. 78 | However, a package is free to define its main methods as the dispatchable 79 | versions, thereby allowing dispatch on the default implementation. 80 | 81 | Extensibility *and* Choice 82 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 83 | 84 | If some effort is put into the dispatch machinery, it's possible to 85 | dispatch over arbitrary objects --- including arrays, dtypes, and 86 | so on. A method defines the type of each dispatchable argument, and 87 | backends are *only* passed types they know how to dispatch over when 88 | deciding whether or not to use that backend. For example, if a backend 89 | doesn't know how to dispatch over dtypes, it won't be asked to decide 90 | based on that front. 91 | 92 | Methods can have a default implementation in terms of other methods, 93 | but they're still overridable. 94 | 95 | This means that only one framework is needed to, for example, dispatch 96 | over ``ufunc`` s, arrays, dtypes and all other primitive objects in NumPy, 97 | while keeping the core :obj:`uarray` code independent of NumPy and even 98 | :obj:`unumpy`. 99 | 100 | Backends can span modules, so SciPy could jump in and define its own methods 101 | on NumPy objects and make them overridable within the NumPy backend. 102 | 103 | User Choice 104 | ^^^^^^^^^^^ 105 | 106 | The users of :obj:`unumpy` or :obj:`uarray` can choose which backend they want 107 | to prefer with a simple context manager. They also have the ability to 108 | force a backend, and to skip a backend. This is useful for array-like 109 | objects that provide other array-like objects by composing them. For 110 | example, Dask could perform all its blockwise function calls with the 111 | following psuedocode (obviously, this is simplified): 112 | 113 | .. code:: python 114 | 115 | in_arrays = extract_inner_arrays(input_arrays) 116 | out_arrays = [] 117 | for input_arrays_single in in_arrays: 118 | args, kwargs = blockwise_function.replace_args_kwargs( 119 | args, kwargs, input_arrays_single) 120 | with ua.skip_backend(DaskBackend): 121 | out_arrays_single = blockwise_function(*args, **kwargs) 122 | out_arrays.append(out_arrays_single) 123 | 124 | return combine_arrays(out_arrays) 125 | 126 | A user would simply do the following: 127 | 128 | .. code:: python 129 | 130 | with ua.use_backend(DaskBackend): 131 | # Write all your code here 132 | # It will prefer the Dask backend 133 | 134 | There is no default backend, to :obj:`unumpy`, NumPy is just another backend. One 135 | can register backends, which will all be tried in indeterminate order when no 136 | backend is selected. 137 | 138 | Addressing past flaws 139 | ^^^^^^^^^^^^^^^^^^^^^ 140 | 141 | The progress on NumPy's side for defining an override mechanism has been slow, with 142 | NEP-13 being first introduced in 2013, and with the wealth of dispatchable objects 143 | (including arrays, ufuns, and dtypes), and with the advent of libraries like Dask, 144 | CuPy, Xarray, PyData/Sparse, and XND, it has become clear that the need for alternative 145 | array-like implementations is growing. There are even other libraries like PyTorch, and 146 | TensorFlow that'd be possible to express in NumPy API-like terms. Another example 147 | includes the Keras API, for which an overridable ``ukeras`` could be created, similar 148 | to :obj:`unumpy`. 149 | 150 | :obj:`uarray` is intended to have fast development to fill the need posed by these 151 | communities, while keeping itself as general as possible, and quickly reach maturity, 152 | after which backward compatibility will be guaranteed. 153 | 154 | Performance considerations will come only after such a state has been reached. 155 | -------------------------------------------------------------------------------- /docs/generated/uarray.set_backend.rst: -------------------------------------------------------------------------------- 1 | set\_backend 2 | ============ 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: set_backend -------------------------------------------------------------------------------- /docs/generated/uarray.set_global_backend.rst: -------------------------------------------------------------------------------- 1 | set\_global\_backend 2 | ==================== 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: set_global_backend -------------------------------------------------------------------------------- /docs/generated/uarray.set_state.rst: -------------------------------------------------------------------------------- 1 | set\_state 2 | ========== 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: set_state -------------------------------------------------------------------------------- /docs/generated/uarray.skip_backend.rst: -------------------------------------------------------------------------------- 1 | skip\_backend 2 | ============= 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: skip_backend -------------------------------------------------------------------------------- /docs/generated/uarray.wrap_single_convertor.rst: -------------------------------------------------------------------------------- 1 | wrap\_single\_convertor 2 | ======================= 3 | 4 | .. currentmodule:: uarray 5 | 6 | .. autofunction:: wrap_single_convertor -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: uarray 2 | 3 | Glossary 4 | ======== 5 | 6 | Multimethod 7 | ----------- 8 | 9 | A method, possibly with a default/reference implementation, that can 10 | have other implementations provided by different backends. 11 | 12 | If a multimethod does not have an implementation, a 13 | :obj:`BackendNotImplementedError` is raised. 14 | 15 | Backend 16 | ------- 17 | 18 | A backend is an entity that can provide implementations for different 19 | functions. It can also (optionally) receive some options from the user 20 | about how to process the implementations. A backend can be set permanently 21 | or temporarily. 22 | 23 | .. _DomainGlossary: 24 | 25 | Domain 26 | ------ 27 | 28 | A domain defines the hierarchical grouping of multimethods. The domain string 29 | is, by convention, the name of the module that provides the multimethods. 30 | 31 | Sub-domains are denoted with a separating ``.``. For example, a multimethod in 32 | ``"numpy.fft"`` is also considered to be in the domain ``"numpy"``. When calling 33 | a multimethod, the backends for the most specific sub-domain are always tried first, 34 | followed by the next domain up the hierarchy. 35 | 36 | Dispatching 37 | ----------- 38 | 39 | Dispatching is the process of forwarding a function call to an implementation 40 | in a backend. 41 | 42 | Conversion 43 | ---------- 44 | 45 | A backend might have different object types compared to the reference 46 | implementation, or it might require some other conversions of objects. 47 | Conversion is the process of converting any given object into a 48 | library's native form. 49 | 50 | Coercion 51 | ^^^^^^^^ 52 | 53 | Coercions are conversions that may take a long time, usually those 54 | involving copying or moving of data. As a rule of thumb, conversions 55 | longer than ``O(log n)`` (where ``n`` is the size of the object in 56 | memory) should be made into coercions. 57 | 58 | .. _MarkingGlossary: 59 | 60 | Marking 61 | ------- 62 | 63 | Marking is the process of telling the backend what convertor to use for 64 | a given argument. 65 | -------------------------------------------------------------------------------- /docs/gsoc/2020/ideas.rst: -------------------------------------------------------------------------------- 1 | GSoC 2020 project ideas 2 | ======================= 3 | 4 | Introduction 5 | ------------ 6 | 7 | This is the Google Summer of Code 2020 (GSoC'20) ideas page for ``uarray``, 8 | ``unumpy`` and ``udiff``. The ``uarray`` library is is a backend mechanism 9 | geared towards array computing, but intended for general use. ``unumpy`` is an 10 | incomplete stub of the NumPy API that can be dispatched by ``uarray``. 11 | ``udiff`` is a general-purpose automatic differentiation library built 12 | on top of ``unumpy`` and ``uarray``. 13 | 14 | This page lists a number of ideas for Google Summer of Code projects for 15 | ``uarray``, plus gives some pointers for potential GSoC students on how to get 16 | started with contributing and putting together their application. 17 | 18 | Guidelines & requirements 19 | ------------------------- 20 | 21 | ``uarray`` plans to participate in GSoC'20 under the `umbrella of Python Software Foundation `_. 22 | 23 | We expect from students that they're at least comfortable with Python 24 | (intermediate level). Some projects may also require C++ or C skills. 25 | Knowing how to use Git is also important; this can be learned before the 26 | official start of GSoC if needed though. 27 | 28 | If you have an idea of what you would like to work on (see below for ideas) 29 | and are considering participating: 30 | 31 | 32 | 1. Read the `PSF page `_ carefully, it contains 33 | important advice on the process. 34 | 2. Read `advice on writing a proposal `_ 35 | (written with the Mailman project in mind, but generally applicable) 36 | 3. Make a enhancement/bugfix/documentation fix -- it does not have to be big, 37 | and it does not need to be related to your proposal. Doing so before 38 | applying for the GSoC is a hard requirement for ``uarray``. It helps 39 | everyone you get some idea how things would work during GSoC. 40 | 4. Start writing your proposal early, post a draft to the issue tracker and 41 | iterate based on the feedback you receive. This will both improve the 42 | quality of your proposal and help you find a suitable mentor. 43 | 44 | Contact 45 | ------- 46 | 47 | If you have a question *after checking all guideline pages above*, you can 48 | open an issue in the issue tracker, but feel free to 49 | `chat with us on Gitter `_ if you need 50 | clarification regarding any of the projects. Keep in mind that you might not 51 | get a response right away, but we will endeavour to respond as early as possible. 52 | 53 | 54 | ``uarray`` project ideas 55 | ------------------------ 56 | 57 | ``uarray``: Add querying for state 58 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 59 | 60 | Adding querying for the `uarray._BackendState `_ 61 | object will allow users of ``uarray`` to see what's inside the opaque object. 62 | Some parts can be re-used from the `pickling machinery `_. 63 | 64 | It can also help downstream users to access the parameters of the currently 65 | set backend, which is a planned feature of ``uarray``. Here is a list of goals 66 | for this project: 67 | 68 | * Allow downstream projects to query the list of backends. 69 | * Allow downstream projects to query the list of parameters for a backend. 70 | 71 | This would enable, for example, the following use-cases: 72 | 73 | * Allow a downstream library to detect a backend and run specialised code for 74 | it. 75 | * Allow a downstream library to fail-fast on a known-unsupported backend. 76 | 77 | This project has a straightforward design and needs some implementation work, 78 | and will require interacting with the mentors to implement and polish. The accepted 79 | student will get an outline of the desired API, along with some failing tests and 80 | doctests. The student will make a pull request to implement the desired functionality 81 | so that the tests pass. 82 | 83 | * Required knowledge: Python C-API and C++ 84 | * Difficulty level: medium 85 | * Potential mentors: Peter Bell and Hameer Abbasi 86 | 87 | ``uarray``: Allow subdomains 88 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 89 | This idea would allow a backend to encompass functions from more than one 90 | domain. 91 | 92 | The primary goal of this project would be: 93 | 94 | * Develop a system that allows, via some kind of matching mechanism, to select 95 | which domains it supports, **while maintaining backward compatibility**. 96 | 97 | This would allow a backend targeting NumPy to also target, for example, the 98 | ``numpy.random`` submodule. Since the domain for functions in 99 | ``numpy.random`` will be just that: ``numpy.random``, it won't match 100 | backends defined with the ``numpy`` domain, since it's an exact string 101 | match. 102 | 103 | The second objective here would be to allow backends to target submodules 104 | of projects rather than the whole project. For example, targeting just 105 | ``numpy.random`` or ``numpy.fft`` without targeting all of NumPy. 106 | 107 | For more detail see `this issue `_. 108 | 109 | This project has a somewhat complicated design and needs some involved 110 | implementation work, and will require interacting with the mentors to flesh 111 | out and work through. 112 | 113 | * Required knowledge: Python C-API and C++ 114 | * Difficulty level: hard 115 | * Potential mentors: Peter Bell and Hameer Abbasi 116 | 117 | ``unumpy``: Expand overall coverage 118 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 119 | 120 | This project is split into two parts: 121 | 122 | * Adding further coverage of the NumPy API. 123 | * Adding more backends to ``unumpy``. 124 | 125 | We realise this is a large (possibly open-ended) undertaking, and so there 126 | will need to be a minimum amount of work done in order to pass (~150 function stubs, 127 | if time allows a `JAX `_ backend). You may 128 | see the existing methods and figure out how they are written using a combination 129 | of the `documentation for writing multimethods `_ 130 | and the `already existing multimethods in this file `_. 131 | For writing backends, you can see the `documentation for backends `_ 132 | in combination with the already existing backends in `this directory `_. 133 | 134 | * Required knowledge: Python (intermediate level) 135 | * Difficulty level: easy 136 | * Potential mentors: Prasun Anand and Hameer Abbasi 137 | 138 | ``udiff``: Completion and Packaging 139 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 140 | This requires completion and packaging of the `udiff `_ library. Potential 141 | goals include: 142 | 143 | 1. Publishing an initial version to PyPI. Here's a `guide `_ 144 | on how to do that. 145 | 2. Adding matrix/tensor calculus support. 146 | 147 | * For this, you can see the `matrix cookbook `_. 148 | Don't be intimidated! There will only be five or so equations you have to 149 | pull out of the matrix cookbook and implement, most prominently, the 150 | equation for matrix multiplication. 151 | * `Here `_ 152 | is how derivatives are registered. 153 | * The second task here will be to add the "separation" between the data 154 | dimensions and the differentiation dimensions. For example, the input 155 | could be a vector, or an array of scalars, and this might need to be 156 | taken into account when doing the differentiation. That will require 157 | some work in `this file `_, 158 | and possibly `this one as well `_. 159 | 160 | 3. Adding tests. 161 | 162 | * This will require calculating a few derivatives by hand and making sure 163 | they match up with what ``udiff`` computes. 164 | * We will use the `PyTest framework `_. 165 | 166 | 4. Adding documentation on use, which will be fairly minimal. We will learn to 167 | set up `Sphinx `_, and add some documentation. 168 | 5. Publishing a final version to PyPI. 169 | 170 | This project has a somewhat some minimal design and needs some involved 171 | implementation work. It will allow the accepted student to get an idea of 172 | what it's like to actually publish, test and document a small Python package. 173 | 174 | * Required knowledge: Python (intermediate level) and calculus 175 | * Difficulty level: medium 176 | * Potential mentors: Prasun Anand and Hameer Abbasi 177 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ``uarray`` 2 | ========== 3 | 4 | .. warning:: 5 | :obj:`uarray` is a developer tool, it is not meant to be used directly by end-users. 6 | 7 | .. warning:: 8 | This document is meant to elicit discussion from the broader community and to help 9 | drive the direction that :obj:`uarray` goes towards. Examples provided here may not be 10 | immediately stable. 11 | 12 | .. note:: 13 | This page describes the overall philosophy behind :obj:`uarray`. For usage instructions, 14 | see the :obj:`uarray` API documentation page. If you are interested in augmentation 15 | for NEP-22, please see the :obj:`unumpy` page. 16 | 17 | `uarray` is a backend system for Python that allows you to separately define an API, 18 | along with backends that contain separate implementations of that API. 19 | 20 | `unumpy` builds on top of `uarray`. It is an effort to specify the core NumPy API, and 21 | provide backends for the API. 22 | 23 | What's new in ``uarray``? 24 | ------------------------- 25 | 26 | :obj:`uarray` is, to our knowledge, the first backend-system for Python that's generic 27 | enough to cater to the use-cases of many libraries, while at the same time, being 28 | library independent. 29 | 30 | :obj:`unumpy` is the first approach to leverage :obj:`uarray` in order to build a 31 | generic backend system for (what we hope will be) the core NumPy API. It will be 32 | possible to create a backend object and use that to perform operations. In addition, 33 | it will be possible to change the used backend via a context manager. 34 | 35 | Benefits for end-users 36 | ---------------------- 37 | 38 | End-users can easily take their code written for one backend and use it on another 39 | backend with a simple switch (using a Python context manager). This can have any number 40 | of effects, depending on the functionality of the library. For example: 41 | 42 | * For Matplotlib, changing styles of plots or producing different windows or image 43 | formats. 44 | * For Tensorly, providing a different computation backend that can be distributed or 45 | target the GPU or sparse arrays. 46 | * For :obj:`unumpy`, it can do a similar thing: provide users with code they already 47 | wrote for `numpy` and easily switch to a different backend. 48 | 49 | Benefits for library authors 50 | ---------------------------- 51 | 52 | To library authors, the benefits come in two forms: First, it allows them to build their 53 | libraries to be implementation independent. In code that builds itself on top of 54 | :obj:`unumpy`, it would be very easy to target the GPU, use sparse arrays or do any kind 55 | of distributed computing. 56 | 57 | The second is to allow a way to separate the interface from implementation, and easily 58 | allow a way to switch an implementation. 59 | 60 | Relation to the NumPy duck-array ecosystem 61 | ------------------------------------------ 62 | 63 | :obj:`uarray` is a backend/dispatch mechanism with a focus on array computing and the 64 | needs of the wider array community, by allowing a clean way to register an 65 | implementation for any Python object (functions, classes, class methods, properties, 66 | dtypes, ...), it also provides an important building block for 67 | `NEP-22 `_. 68 | It is meant to address the shortcomings of `NEP-18 69 | `_ and `NEP-13 70 | `_; 71 | while still holding nothing in :obj:`uarray` itself that's specific to array computing 72 | or the NumPy API. 73 | 74 | Where to from here? 75 | ------------------- 76 | 77 | Choose the documentation page relevant to you: 78 | 79 | * :ref:`mmauthordocs` 80 | * :ref:`libauthordocs` 81 | * :ref:`userdocs` 82 | 83 | .. toctree:: 84 | :hidden: 85 | :maxdepth: 3 86 | 87 | enduser_docs 88 | 89 | libauthor_docs 90 | 91 | multimethod_docs 92 | 93 | glossary 94 | 95 | generated/uarray 96 | 97 | gsoc/2020/ideas 98 | 99 | 100 | Indices and tables 101 | ================== 102 | 103 | * :ref:`genindex` 104 | * :ref:`modindex` 105 | * :ref:`search` 106 | -------------------------------------------------------------------------------- /docs/libauthor_docs.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: uarray 2 | 3 | .. _libauthordocs: 4 | 5 | Documentation for backend providers 6 | =================================== 7 | 8 | Backend providers can provide a back-end for a defined API within 9 | the :obj:`uarray` ecosystem. To find out how to define your own 10 | API with :obj:`uarray`, see :ref:`mmauthordocs`. To find out how 11 | your backend will be provided, use :ref:`userdocs`. 12 | 13 | Backend providers need to be aware of three protocols: ``__ua_domain__``, 14 | ``__ua_function__`` and ``__ua_convert__``. The first two are mandatory and 15 | the last is optional. 16 | 17 | ``__ua_domain__`` 18 | ----------------- 19 | 20 | ``__ua_domain__`` is a string containing the domain of the backend. This is, 21 | by convention, the name of the module (or one of its dependencies or parents) 22 | that contains the multimethods. For example, ``scipy`` and ``numpy.fft`` could 23 | both be in the ``numpy`` domain or one of its subdomains. 24 | 25 | Additionally, ``__ua_domain__`` can be a sequence of domains, such as a tuple or 26 | list of strings. This allows a single backend to implement functions from more 27 | than one domain. 28 | 29 | ``__ua_function__`` 30 | ------------------- 31 | 32 | This is the most important protocol, one that defines the implementation of a 33 | multimethod. It has the signature ``(method, args, kwargs)``. 34 | Note that it is called in this form, so if your backend is an object instead of 35 | a module, you should add ``self``. ``method`` is the multimethod being called, 36 | and it is guaranteed that it is in the same domain as the backend. ``args`` and 37 | ``kwargs`` are the arguments to the function, possibly after conversion 38 | (explained below) 39 | 40 | Returning :obj:`NotImplemented` signals that the backend does not support this 41 | operation. 42 | 43 | ``__ua_convert__`` 44 | ------------------ 45 | 46 | All dispatchable arguments are passed through ``__ua_convert__`` before being 47 | passed into ``__ua_function__``. This protocol has the signature 48 | ``(dispatchables, coerce)``, where ``dispatchables`` is iterable of 49 | :obj:`Dispatchable` and ``coerce`` is whether or not to coerce forcefully. 50 | ``dispatch_type`` is the mark of the object to be converted, and ``coerce`` 51 | specifies whether or not to "force" the conversion. By convention, operations 52 | larger than ``O(log n)`` (where ``n`` is the size of the object in memory) 53 | should only be done if ``coerce`` is ``True``. In addition, there are arguments 54 | wrapped as non-coercible via the ``coercible`` attribute, if these *must* be 55 | coerced, then one should return ``NotImplemented``. 56 | 57 | A convenience wrapper for converting a single object, 58 | :obj:`wrap_single_convertor` is provided. 59 | 60 | Returning :obj:`NotImplemented` signals that the backend does not support the 61 | conversion of the given object. 62 | 63 | :obj:`skip_backend` 64 | ------------------- 65 | 66 | If a backend consumes multimethods from a domain and provides multimethods 67 | for that same domain, it may wish to have the ability to use multimethods while 68 | excluding itself from the list of tried backends in order to avoid infinite 69 | recursion. This allows the backend to implement its functions in terms of 70 | functions provided by other backends. This is the purpose of the 71 | :obj:`skip_backend` decorator. 72 | 73 | The process that takes place when the backend is tried 74 | ------------------------------------------------------ 75 | 76 | First of all, the backend's ``__ua_convert__`` method is tried. If this returns 77 | :obj:`NotImplemented`, then the backend is skipped, otherwise, its 78 | ``__ua_function__`` protocol is tried. If a value other than 79 | :obj:`NotImplemented` is returned, it is assumed to be the final 80 | return value. Any exceptions raised are propagated up the call stack, except a 81 | :obj:`BackendNotImplementedError`, which signals a skip of the backend. If all 82 | backends are exhausted, or if a backend with its ``only`` flag set to ``True`` 83 | is encountered, a :obj:`BackendNotImplementedError` is raised. 84 | 85 | Examples 86 | -------- 87 | 88 | Examples for library authors can be found `in the source of unumpy.numpy_backend `_ 89 | and other ``*_backend.py`` files in `this directory `_. 90 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quansight-Labs/uarray/42bc38bcc607fb9b01b41bd1a203d680b3a25a25/docs/logo.png -------------------------------------------------------------------------------- /docs/multimethod_docs.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: uarray 2 | 3 | .. _mmauthordocs: 4 | 5 | Documentation for API authors 6 | ============================= 7 | 8 | .. testsetup:: mmtutorial 9 | import uarray as ua 10 | 11 | Multimethods are the most important part of :obj:`uarray`. They 12 | are created via the :obj:`generate_multimethod` function. Multimethods 13 | define the API of a project, and backends have to be written against 14 | this API. You should see :ref:`libauthordocs` for how to define a 15 | backend against the multimethods you write, or :ref:`userdocs` for 16 | how to switch backends for a given API. 17 | 18 | A multimethod has the following parts: 19 | 20 | * Domain 21 | * Argument extractor 22 | * Argument replacer 23 | * Default implementation 24 | 25 | We will go through each of these in detail now. 26 | 27 | Domain 28 | ------ 29 | 30 | See the :ref:`glossary for domain `. 31 | 32 | Argument extractor 33 | ------------------ 34 | 35 | An argument extractor extracts arguments :ref:`marked ` as a 36 | given type from the list of given arguments. Note that the objects extracted 37 | don't necessarily have to be in the list of arguments, they can be arbitrarily 38 | nested within the arguments. For example, extracting each argument from a list 39 | is a possibility. Note that the order is important, as it will come into play 40 | later. This function should return an iterable of :obj:`Dispatchable`. 41 | 42 | This function has the same signature as the multimethod itself, and 43 | the documentation, name and so on are copied from the argument extractor 44 | via :obj:`functools.wraps`. 45 | 46 | Argument replacer 47 | ----------------- 48 | 49 | The argument replacer takes in the arguments and dispatchable arguments, and 50 | its job is to replace the arguments previously extracted by the argument 51 | extractor by other arguments provided in the list. Therefore, the 52 | signature of this function is ``(args, kwargs, dispatchable_args)``, 53 | and it returns an ``args``/``kwargs`` pair. We realise this is a hard problem 54 | in general, so we have provided a few simplifications, such as that the 55 | default-valued keyword arguments will be removed from the list. 56 | 57 | We recommend following the pattern in `here `_ 58 | for optimal operation: passing the ``args``/``kwargs`` into a function with a 59 | similar signature and then return the modified ``args``/``kwargs``. 60 | 61 | Default implementation 62 | ---------------------- 63 | 64 | This is a default implementation for the multimethod, ideally with the same 65 | signature as the original multimethod. It can also be used to provide one 66 | multimethod in terms of others, even if the default implementation for the. 67 | downstream multimethods is not defined. 68 | 69 | Examples 70 | -------- 71 | 72 | Examples of writing multimethods are found in `this file `_. 73 | It also teaches some advanced techniques, such as overriding instance methods, 74 | including ``__call__``. The same philosophy may be used to override properties, 75 | static methods, and class methods. 76 | -------------------------------------------------------------------------------- /notebooks/01_user_facing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# What is `uarray`?\n", 8 | "At its core, `uarray` is a dispatch and back-end mechanism specifically geared towards array computing. Combined with its sister packages `unumpy` (and others currently in development), it allows NumPy functions to be overridden by their counterparts in other libraries (such as Dask, Xnd, and so on) while using the exact same code everywhere. Backends can be changed using just a context manager.\n", 9 | "\n", 10 | "Please note that only a small subset of the NumPy API is implemented, and not every backend implements every API method." 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 1, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import uarray as ua\n", 20 | "import unumpy as np # Note the changed import statement" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "import unumpy.xnd_backend as be_xnd\n", 30 | "import unumpy.numpy_backend as be_np\n", 31 | "import unumpy.dask_backend as be_da\n", 32 | "\n", 33 | "ua.set_global_backend(be_np)\n", 34 | "ua.register_backend(be_da)\n", 35 | "ua.register_backend(be_xnd)\n" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "## Computing on different back-ends\n", 43 | "`unumpy` allows you to compute with different back-ends. Here are examples of creating arrays via `unumpy` (something not currently possible with NEP-18, the `__array_function__` protocol)." 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 3, 49 | "metadata": {}, 50 | "outputs": [ 51 | { 52 | "name": "stdout", 53 | "output_type": "stream", 54 | "text": [ 55 | "With the XND backend set, the type of the array is: \n", 56 | "With NumPy backend set, the type of the array is: \n", 57 | "With Dask Backend set, the type of the array is: \n" 58 | ] 59 | } 60 | ], 61 | "source": [ 62 | "my_list = [0, 1, 2, 3, 4]\n", 63 | "\n", 64 | "with ua.set_backend(be_xnd):\n", 65 | " x = np.asarray(my_list)\n", 66 | "print('With the XND backend set, the type of the array is: {}'.format(type(x)))\n", 67 | "\n", 68 | "with ua.set_backend(be_np):\n", 69 | " y = np.asarray(my_list)\n", 70 | "print('With NumPy backend set, the type of the array is: {}'.format(type(y)))\n", 71 | "\n", 72 | "with ua.set_backend(be_da):\n", 73 | " z = np.asarray(my_list)\n", 74 | "print('With Dask Backend set, the type of the array is: {}'.format(type(z)))" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "## Computing based on the type of array passed in\n", 82 | "`unumpy` allows you to compute on arrays based on the type, in a fashion similar to NEP-18." 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 4, 88 | "metadata": {}, 89 | "outputs": [ 90 | { 91 | "name": "stdout", 92 | "output_type": "stream", 93 | "text": [ 94 | "With np.sum(xnd_array) we get: \n", 95 | "With np.sum(torch_array) we get: \n", 96 | "With np.sum(numpy_array) we get: \n" 97 | ] 98 | } 99 | ], 100 | "source": [ 101 | "print('With np.sum(xnd_array) we get: {}'.format(type(np.sum(x))))\n", 102 | "print('With np.sum(torch_array) we get: {}'.format(type(np.sum(y))))\n", 103 | "print('With np.sum(numpy_array) we get: {}'.format(type(np.sum(z))))" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "## Forcing a particular backend\n", 111 | "You can even force a particular back-end, if you want to pipe all possible computations through that back-end." 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 5, 117 | "metadata": {}, 118 | "outputs": [ 119 | { 120 | "name": "stdout", 121 | "output_type": "stream", 122 | "text": [ 123 | "Using the Dask backend with coerce=True on a NumPy array: \n" 124 | ] 125 | } 126 | ], 127 | "source": [ 128 | "with ua.set_backend(be_da, coerce=True):\n", 129 | " print('Using the Dask backend with coerce=True on a NumPy array: {}'.format(type(np.sum(z))))" 130 | ] 131 | } 132 | ], 133 | "metadata": { 134 | "kernelspec": { 135 | "display_name": "Python 3", 136 | "language": "python", 137 | "name": "python3" 138 | }, 139 | "language_info": { 140 | "codemirror_mode": { 141 | "name": "ipython", 142 | "version": 3 143 | }, 144 | "file_extension": ".py", 145 | "mimetype": "text/x-python", 146 | "name": "python", 147 | "nbconvert_exporter": "python", 148 | "pygments_lexer": "ipython3", 149 | "version": "3.7.4" 150 | } 151 | }, 152 | "nbformat": 4, 153 | "nbformat_minor": 4 154 | } 155 | -------------------------------------------------------------------------------- /notebooks/02_basic_dev_tutorial.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Inside `ulinalg`\n", 8 | "Inside `ulinalg`, you would define a method consisting of an argument replacer and argument extractor.\n", 9 | "\n", 10 | "- The argument extractor is the simpler of the two: It \"extracts\" the array arguments from the method as a `tuple`.\n", 11 | "- The argument replacer \"replaces\" the array arguments inside args, kwargs with the supplied arrays." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import uarray as ua\n", 21 | "from uarray import all_of_type\n", 22 | "import unumpy as unp" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 2, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "def solve_argreplacer(args, kwargs, arrays):\n", 32 | " out_args = arrays + args[2:]\n", 33 | " return out_args, kwargs\n", 34 | "\n", 35 | "@unp.create_numpy(solve_argreplacer)\n", 36 | "@all_of_type(unp.ndarray)\n", 37 | "def solve(a, b, sym_pos=False, lower=False, overwrite_a=False, overwrite_b=False, debug=None, check_finite=True, assume_a='gen', transposed=False):\n", 38 | " return (a, b)" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "# Inside a Numpy backend for `ulinalg` (ideally, `scipy.linalg` itself)\n", 46 | "Here, you register the implementation for the backend itself." 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 3, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "import unumpy.numpy_backend as NumpyBackend\n", 56 | "import scipy.linalg as linalg\n", 57 | "ua.set_global_backend(NumpyBackend)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 4, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "NumpyBackend._implementations[solve] = linalg.solve" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "# Inside PyTorch or a PyTorch backend for `ulinalg` (ideally, PyTorch itself)\n", 74 | "Here, you have to perform some translation, because the PyTorch API isn't 1:1 with the NumPy API. See [this documentation page](https://pytorch.org/docs/stable/torch.html#torch.solve). In practice, you would select the function based on the input arguments, but let's ignore that for now." 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 5, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "import unumpy.torch_backend as TorchBackend\n", 84 | "import torch\n", 85 | "\n", 86 | "ua.register_backend(TorchBackend)" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 6, 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "def solve_impl(a, b, sym_pos=False, lower=False, overwrite_a=False, overwrite_b=False, debug=None, check_finite=True, assume_a='gen', transposed=False):\n", 96 | " return torch.solve(b, a)[0]" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 7, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "TorchBackend._implementations[solve] = solve_impl" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "# User Experience\n", 113 | "The user simply imports the right library and uses everything as normal. Let's try solving this fruit puzzle:\n", 114 | "\n", 115 | "" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 8, 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "a = [[3.0, 0.0, 0.0], [1.0, 2.0, 0.0], [0.0, 1.0, -2.0]]\n", 125 | "b = [[30.0], [18.0], [2.0]]" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 9, 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [ 134 | "ta = torch.tensor(a, dtype=torch.float32); tb = torch.tensor(b, dtype=torch.float32)\n", 135 | "\n", 136 | "tx = solve(ta, tb)\n", 137 | "tsol = tx[0] + tx[1] + tx[2]" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": 10, 143 | "metadata": {}, 144 | "outputs": [ 145 | { 146 | "name": "stdout", 147 | "output_type": "stream", 148 | "text": [ 149 | "[15.]\n", 150 | "\n" 151 | ] 152 | } 153 | ], 154 | "source": [ 155 | "print(tsol)\n", 156 | "print(type(tsol))" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": 11, 162 | "metadata": {}, 163 | "outputs": [], 164 | "source": [ 165 | "import numpy as np\n", 166 | "\n", 167 | "na = np.array(a); nb = np.array(b)\n", 168 | "\n", 169 | "nx = solve(na, nb)\n", 170 | "nsol = nx[0] + nx[1] + nx[2]" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": 12, 176 | "metadata": {}, 177 | "outputs": [ 178 | { 179 | "name": "stdout", 180 | "output_type": "stream", 181 | "text": [ 182 | "[15.]\n", 183 | "\n" 184 | ] 185 | } 186 | ], 187 | "source": [ 188 | "print(nsol)\n", 189 | "print(type(nsol))" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "## Manually choosing the back-end\n", 197 | "It's also possible to use context managers to safely set the back-end." 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": 13, 203 | "metadata": {}, 204 | "outputs": [ 205 | { 206 | "name": "stdout", 207 | "output_type": "stream", 208 | "text": [ 209 | "\n", 210 | "\n" 211 | ] 212 | } 213 | ], 214 | "source": [ 215 | "import uarray as ua\n", 216 | "with ua.set_backend(NumpyBackend, coerce=True):\n", 217 | " print(type(solve(a, b)))\n", 218 | " \n", 219 | "with ua.set_backend(TorchBackend, coerce=True):\n", 220 | " print(type(solve(ta, tb)))" 221 | ] 222 | } 223 | ], 224 | "metadata": { 225 | "kernelspec": { 226 | "display_name": "Python 3", 227 | "language": "python", 228 | "name": "python3" 229 | }, 230 | "language_info": { 231 | "codemirror_mode": { 232 | "name": "ipython", 233 | "version": 3 234 | }, 235 | "file_extension": ".py", 236 | "mimetype": "text/x-python", 237 | "name": "python", 238 | "nbconvert_exporter": "python", 239 | "pygments_lexer": "ipython3", 240 | "version": "3.7.4" 241 | } 242 | }, 243 | "nbformat": 4, 244 | "nbformat_minor": 4 245 | } 246 | -------------------------------------------------------------------------------- /notebooks/fruit-puzzle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quansight-Labs/uarray/42bc38bcc607fb9b01b41bd1a203d680b3a25a25/notebooks/fruit-puzzle.jpg -------------------------------------------------------------------------------- /pixi.lock: -------------------------------------------------------------------------------- 1 | version: 5 2 | environments: 3 | all: 4 | channels: 5 | - url: https://conda.anaconda.org/conda-forge/ 6 | indexes: 7 | - https://pypi.org/simple 8 | packages: 9 | osx-arm64: 10 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda 11 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda 12 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.3-hf9b8971_0.conda 13 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 14 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda 15 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda 16 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda 17 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda 18 | - conda: https://conda.anaconda.org/conda-forge/noarch/pip-24.2-pyh8b19718_1.conda 19 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.5-h30c5eda_0_cpython.conda 20 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda 21 | - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-73.0.1-pyhd8ed1ab_0.conda 22 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda 23 | - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda 24 | - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.44.0-pyhd8ed1ab_0.conda 25 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 26 | - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl 27 | - pypi: https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl 28 | - pypi: https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl 29 | - pypi: https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl 30 | - pypi: https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl 31 | - pypi: https://files.pythonhosted.org/packages/0c/f1/6ffd5d76578e98a8f21ae7216b88a7212c778f665f1a8f4f8ce6f9605da4/doc8-1.1.2-py3-none-any.whl 32 | - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl 33 | - pypi: https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl 34 | - pypi: https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl 35 | - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl 36 | - pypi: https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl 37 | - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl 38 | - pypi: https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl 39 | - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl 40 | - pypi: https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl 41 | - pypi: https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl 42 | - pypi: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl 43 | - pypi: https://files.pythonhosted.org/packages/1d/44/6a65ecd630393d47ad3e7d5354768cb7f9a10b3a0eb2cd8c6f52b28211ee/pbr-6.1.0-py2.py3-none-any.whl 44 | - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl 45 | - pypi: https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl 46 | - pypi: https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl 47 | - pypi: https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl 48 | - pypi: https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl 49 | - pypi: https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl 50 | - pypi: https://files.pythonhosted.org/packages/6e/47/27f24919d9ac5989e067043a739e27ba20d6f9dd5d6acd7598a1e4aebed3/pytest_flake8-1.2.2-py3-none-any.whl 51 | - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl 52 | - pypi: https://files.pythonhosted.org/packages/48/9c/6d8035cafa2d2d314f34e6cd9313a299de095b26e96f1c7312878f988eec/restructuredtext_lint-1.4.0.tar.gz 53 | - pypi: https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl 54 | - pypi: https://files.pythonhosted.org/packages/4d/61/2ad169c6ff1226b46e50da0e44671592dbc6d840a52034a0193a99b28579/sphinx-8.0.2-py3-none-any.whl 55 | - pypi: https://files.pythonhosted.org/packages/76/81/d5af3a50a45ee4311ac2dac5b599d69f68388401c7a4ca902e0e450a9f94/sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl 56 | - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl 57 | - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl 58 | - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl 59 | - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl 60 | - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl 61 | - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl 62 | - pypi: https://files.pythonhosted.org/packages/ec/50/70762bdb23f6c2b746b90661f461d33c4913a22a46bb5265b10947e85ffb/stevedore-5.3.0-py3-none-any.whl 63 | - pypi: https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl 64 | - pypi: https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl 65 | - pypi: . 66 | default: 67 | channels: 68 | - url: https://conda.anaconda.org/conda-forge/ 69 | indexes: 70 | - https://pypi.org/simple 71 | packages: 72 | osx-arm64: 73 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda 74 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda 75 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.3-hf9b8971_0.conda 76 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 77 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda 78 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda 79 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda 80 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda 81 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.5-h30c5eda_0_cpython.conda 82 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda 83 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda 84 | - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda 85 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 86 | - pypi: . 87 | docs: 88 | channels: 89 | - url: https://conda.anaconda.org/conda-forge/ 90 | indexes: 91 | - https://pypi.org/simple 92 | packages: 93 | osx-arm64: 94 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda 95 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda 96 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.3-hf9b8971_0.conda 97 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 98 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda 99 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda 100 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda 101 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda 102 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.5-h30c5eda_0_cpython.conda 103 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda 104 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda 105 | - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda 106 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 107 | - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl 108 | - pypi: https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl 109 | - pypi: https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl 110 | - pypi: https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl 111 | - pypi: https://files.pythonhosted.org/packages/0c/f1/6ffd5d76578e98a8f21ae7216b88a7212c778f665f1a8f4f8ce6f9605da4/doc8-1.1.2-py3-none-any.whl 112 | - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl 113 | - pypi: https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl 114 | - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl 115 | - pypi: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl 116 | - pypi: https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl 117 | - pypi: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl 118 | - pypi: https://files.pythonhosted.org/packages/1d/44/6a65ecd630393d47ad3e7d5354768cb7f9a10b3a0eb2cd8c6f52b28211ee/pbr-6.1.0-py2.py3-none-any.whl 119 | - pypi: https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl 120 | - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl 121 | - pypi: https://files.pythonhosted.org/packages/48/9c/6d8035cafa2d2d314f34e6cd9313a299de095b26e96f1c7312878f988eec/restructuredtext_lint-1.4.0.tar.gz 122 | - pypi: https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl 123 | - pypi: https://files.pythonhosted.org/packages/4d/61/2ad169c6ff1226b46e50da0e44671592dbc6d840a52034a0193a99b28579/sphinx-8.0.2-py3-none-any.whl 124 | - pypi: https://files.pythonhosted.org/packages/76/81/d5af3a50a45ee4311ac2dac5b599d69f68388401c7a4ca902e0e450a9f94/sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl 125 | - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl 126 | - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl 127 | - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl 128 | - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl 129 | - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl 130 | - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl 131 | - pypi: https://files.pythonhosted.org/packages/ec/50/70762bdb23f6c2b746b90661f461d33c4913a22a46bb5265b10947e85ffb/stevedore-5.3.0-py3-none-any.whl 132 | - pypi: https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl 133 | - pypi: . 134 | optional: 135 | channels: 136 | - url: https://conda.anaconda.org/conda-forge/ 137 | indexes: 138 | - https://pypi.org/simple 139 | packages: 140 | osx-arm64: 141 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda 142 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda 143 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.3-hf9b8971_0.conda 144 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 145 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda 146 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda 147 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda 148 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda 149 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.5-h30c5eda_0_cpython.conda 150 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda 151 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda 152 | - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda 153 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 154 | - pypi: . 155 | tests: 156 | channels: 157 | - url: https://conda.anaconda.org/conda-forge/ 158 | indexes: 159 | - https://pypi.org/simple 160 | packages: 161 | osx-arm64: 162 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda 163 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda 164 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.3-hf9b8971_0.conda 165 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 166 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda 167 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda 168 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda 169 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda 170 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.5-h30c5eda_0_cpython.conda 171 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda 172 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda 173 | - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda 174 | - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 175 | - pypi: https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl 176 | - pypi: https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl 177 | - pypi: https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl 178 | - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl 179 | - pypi: https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl 180 | - pypi: https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl 181 | - pypi: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl 182 | - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl 183 | - pypi: https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl 184 | - pypi: https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl 185 | - pypi: https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl 186 | - pypi: https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl 187 | - pypi: https://files.pythonhosted.org/packages/6e/47/27f24919d9ac5989e067043a739e27ba20d6f9dd5d6acd7598a1e4aebed3/pytest_flake8-1.2.2-py3-none-any.whl 188 | - pypi: https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl 189 | - pypi: . 190 | packages: 191 | - kind: pypi 192 | name: alabaster 193 | version: 1.0.0 194 | url: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl 195 | sha256: fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b 196 | requires_python: '>=3.10' 197 | - kind: pypi 198 | name: babel 199 | version: 2.16.0 200 | url: https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl 201 | sha256: 368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b 202 | requires_dist: 203 | - pytz>=2015.7 ; python_full_version < '3.9' 204 | - pytest>=6.0 ; extra == 'dev' 205 | - pytest-cov ; extra == 'dev' 206 | - freezegun~=1.0 ; extra == 'dev' 207 | requires_python: '>=3.8' 208 | - kind: conda 209 | name: bzip2 210 | version: 1.0.8 211 | build: h99b78c6_7 212 | build_number: 7 213 | subdir: osx-arm64 214 | url: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda 215 | sha256: adfa71f158cbd872a36394c56c3568e6034aa55c623634b37a4836bd036e6b91 216 | md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab 217 | depends: 218 | - __osx >=11.0 219 | license: bzip2-1.0.6 220 | license_family: BSD 221 | purls: [] 222 | size: 122909 223 | timestamp: 1720974522888 224 | - kind: conda 225 | name: ca-certificates 226 | version: 2024.8.30 227 | build: hf0a4a13_0 228 | subdir: osx-arm64 229 | url: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda 230 | sha256: 2db1733f4b644575dbbdd7994a8f338e6ef937f5ebdb74acd557e9dda0211709 231 | md5: 40dec13fd8348dbe303e57be74bd3d35 232 | license: ISC 233 | purls: [] 234 | size: 158482 235 | timestamp: 1725019034582 236 | - kind: pypi 237 | name: certifi 238 | version: 2024.8.30 239 | url: https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl 240 | sha256: 922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 241 | requires_python: '>=3.6' 242 | - kind: pypi 243 | name: charset-normalizer 244 | version: 3.3.2 245 | url: https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl 246 | sha256: 55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 247 | requires_python: '>=3.7.0' 248 | - kind: pypi 249 | name: coverage 250 | version: 7.6.1 251 | url: https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl 252 | sha256: 5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391 253 | requires_dist: 254 | - tomli ; python_full_version <= '3.11' and extra == 'toml' 255 | requires_python: '>=3.8' 256 | - kind: pypi 257 | name: doc8 258 | version: 1.1.2 259 | url: https://files.pythonhosted.org/packages/0c/f1/6ffd5d76578e98a8f21ae7216b88a7212c778f665f1a8f4f8ce6f9605da4/doc8-1.1.2-py3-none-any.whl 260 | sha256: e787b3076b391b8b49400da5d018bacafe592dfc0a04f35a9be22d0122b82b59 261 | requires_dist: 262 | - docutils<=0.21.2,>=0.19 263 | - restructuredtext-lint>=0.7 264 | - stevedore 265 | - pygments 266 | - tomli ; python_full_version < '3.11' 267 | requires_python: '>=3.8' 268 | - kind: pypi 269 | name: docutils 270 | version: 0.21.2 271 | url: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl 272 | sha256: dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 273 | requires_python: '>=3.9' 274 | - kind: pypi 275 | name: flake8 276 | version: 5.0.4 277 | url: https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl 278 | sha256: 7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248 279 | requires_dist: 280 | - mccabe<0.8.0,>=0.7.0 281 | - pycodestyle<2.10.0,>=2.9.0 282 | - pyflakes<2.6.0,>=2.5.0 283 | - importlib-metadata<4.3,>=1.1.0 ; python_full_version < '3.8' 284 | requires_python: '>=3.6.1' 285 | - kind: pypi 286 | name: idna 287 | version: '3.8' 288 | url: https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl 289 | sha256: 050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac 290 | requires_python: '>=3.6' 291 | - kind: pypi 292 | name: imagesize 293 | version: 1.4.1 294 | url: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl 295 | sha256: 0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b 296 | requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' 297 | - kind: pypi 298 | name: iniconfig 299 | version: 2.0.0 300 | url: https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl 301 | sha256: b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 302 | requires_python: '>=3.7' 303 | - kind: pypi 304 | name: jinja2 305 | version: 3.1.4 306 | url: https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl 307 | sha256: bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d 308 | requires_dist: 309 | - markupsafe>=2.0 310 | - babel>=2.7 ; extra == 'i18n' 311 | requires_python: '>=3.7' 312 | - kind: conda 313 | name: libexpat 314 | version: 2.6.3 315 | build: hf9b8971_0 316 | subdir: osx-arm64 317 | url: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.3-hf9b8971_0.conda 318 | sha256: 5cbe5a199fba14ade55457a468ce663aac0b54832c39aa54470b3889b4c75c4a 319 | md5: 5f22f07c2ab2dea8c66fe9585a062c96 320 | depends: 321 | - __osx >=11.0 322 | constrains: 323 | - expat 2.6.3.* 324 | license: MIT 325 | license_family: MIT 326 | purls: [] 327 | size: 63895 328 | timestamp: 1725568783033 329 | - kind: conda 330 | name: libffi 331 | version: 3.4.2 332 | build: h3422bc3_5 333 | build_number: 5 334 | subdir: osx-arm64 335 | url: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 336 | sha256: 41b3d13efb775e340e4dba549ab5c029611ea6918703096b2eaa9c015c0750ca 337 | md5: 086914b672be056eb70fd4285b6783b6 338 | license: MIT 339 | license_family: MIT 340 | purls: [] 341 | size: 39020 342 | timestamp: 1636488587153 343 | - kind: conda 344 | name: libsqlite 345 | version: 3.46.1 346 | build: hc14010f_0 347 | subdir: osx-arm64 348 | url: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda 349 | sha256: 3725f962f490c5d44dae326d5f5b2e3c97f71a6322d914ccc85b5ddc2e50d120 350 | md5: 58050ec1724e58668d0126a1615553fa 351 | depends: 352 | - __osx >=11.0 353 | - libzlib >=1.3.1,<2.0a0 354 | license: Unlicense 355 | purls: [] 356 | size: 829500 357 | timestamp: 1725353720793 358 | - kind: conda 359 | name: libzlib 360 | version: 1.3.1 361 | build: hfb2fe0b_1 362 | build_number: 1 363 | subdir: osx-arm64 364 | url: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda 365 | sha256: c34365dd37b0eab27b9693af32a1f7f284955517c2cc91f1b88a7ef4738ff03e 366 | md5: 636077128927cf79fd933276dc3aed47 367 | depends: 368 | - __osx >=11.0 369 | constrains: 370 | - zlib 1.3.1 *_1 371 | license: Zlib 372 | license_family: Other 373 | purls: [] 374 | size: 46921 375 | timestamp: 1716874262512 376 | - kind: pypi 377 | name: markupsafe 378 | version: 2.1.5 379 | url: https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl 380 | sha256: 8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 381 | requires_python: '>=3.7' 382 | - kind: pypi 383 | name: mccabe 384 | version: 0.7.0 385 | url: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl 386 | sha256: 6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e 387 | requires_python: '>=3.6' 388 | - kind: pypi 389 | name: mypy 390 | version: 1.11.2 391 | url: https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl 392 | sha256: 06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36 393 | requires_dist: 394 | - typing-extensions>=4.6.0 395 | - mypy-extensions>=1.0.0 396 | - tomli>=1.1.0 ; python_full_version < '3.11' 397 | - psutil>=4.0 ; extra == 'dmypy' 398 | - pip ; extra == 'install-types' 399 | - setuptools>=50 ; extra == 'mypyc' 400 | - lxml ; extra == 'reports' 401 | requires_python: '>=3.8' 402 | - kind: pypi 403 | name: mypy-extensions 404 | version: 1.0.0 405 | url: https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl 406 | sha256: 4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d 407 | requires_python: '>=3.5' 408 | - kind: conda 409 | name: ncurses 410 | version: '6.5' 411 | build: h7bae524_1 412 | build_number: 1 413 | subdir: osx-arm64 414 | url: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda 415 | sha256: 27d0b9ff78ad46e1f3a6c96c479ab44beda5f96def88e2fe626e0a49429d8afc 416 | md5: cb2b0ea909b97b3d70cd3921d1445e1a 417 | depends: 418 | - __osx >=11.0 419 | license: X11 AND BSD-3-Clause 420 | purls: [] 421 | size: 802321 422 | timestamp: 1724658775723 423 | - kind: conda 424 | name: openssl 425 | version: 3.3.2 426 | build: h8359307_0 427 | subdir: osx-arm64 428 | url: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda 429 | sha256: 940fa01c4dc6152158fe8943e05e55a1544cab639df0994e3b35937839e4f4d1 430 | md5: 1773ebccdc13ec603356e8ff1db9e958 431 | depends: 432 | - __osx >=11.0 433 | - ca-certificates 434 | license: Apache-2.0 435 | license_family: Apache 436 | purls: [] 437 | size: 2882450 438 | timestamp: 1725410638874 439 | - kind: pypi 440 | name: packaging 441 | version: '24.1' 442 | url: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl 443 | sha256: 5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 444 | requires_python: '>=3.8' 445 | - kind: pypi 446 | name: pbr 447 | version: 6.1.0 448 | url: https://files.pythonhosted.org/packages/1d/44/6a65ecd630393d47ad3e7d5354768cb7f9a10b3a0eb2cd8c6f52b28211ee/pbr-6.1.0-py2.py3-none-any.whl 449 | sha256: a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a 450 | requires_python: '>=2.6' 451 | - kind: conda 452 | name: pip 453 | version: '24.2' 454 | build: pyh8b19718_1 455 | build_number: 1 456 | subdir: noarch 457 | noarch: python 458 | url: https://conda.anaconda.org/conda-forge/noarch/pip-24.2-pyh8b19718_1.conda 459 | sha256: d820e5358bcb117fa6286e55d4550c60b0332443df62121df839eab2d11c890b 460 | md5: 6c78fbb8ddfd64bcb55b5cbafd2d2c43 461 | depends: 462 | - python >=3.8,<3.13.0a0 463 | - setuptools 464 | - wheel 465 | license: MIT 466 | license_family: MIT 467 | purls: 468 | - pkg:pypi/pip?source=hash-mapping 469 | size: 1237976 470 | timestamp: 1724954490262 471 | - kind: pypi 472 | name: pluggy 473 | version: 1.5.0 474 | url: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl 475 | sha256: 44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 476 | requires_dist: 477 | - pre-commit ; extra == 'dev' 478 | - tox ; extra == 'dev' 479 | - pytest ; extra == 'testing' 480 | - pytest-benchmark ; extra == 'testing' 481 | requires_python: '>=3.8' 482 | - kind: pypi 483 | name: pycodestyle 484 | version: 2.9.1 485 | url: https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl 486 | sha256: d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b 487 | requires_python: '>=3.6' 488 | - kind: pypi 489 | name: pyflakes 490 | version: 2.5.0 491 | url: https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl 492 | sha256: 4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2 493 | requires_python: '>=3.6' 494 | - kind: pypi 495 | name: pygments 496 | version: 2.18.0 497 | url: https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl 498 | sha256: b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a 499 | requires_dist: 500 | - colorama>=0.4.6 ; extra == 'windows-terminal' 501 | requires_python: '>=3.8' 502 | - kind: pypi 503 | name: pytest 504 | version: 8.3.2 505 | url: https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl 506 | sha256: 4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5 507 | requires_dist: 508 | - iniconfig 509 | - packaging 510 | - pluggy<2,>=1.5 511 | - exceptiongroup>=1.0.0rc8 ; python_full_version < '3.11' 512 | - tomli>=1 ; python_full_version < '3.11' 513 | - colorama ; sys_platform == 'win32' 514 | - argcomplete ; extra == 'dev' 515 | - attrs>=19.2 ; extra == 'dev' 516 | - hypothesis>=3.56 ; extra == 'dev' 517 | - mock ; extra == 'dev' 518 | - pygments>=2.7.2 ; extra == 'dev' 519 | - requests ; extra == 'dev' 520 | - setuptools ; extra == 'dev' 521 | - xmlschema ; extra == 'dev' 522 | requires_python: '>=3.8' 523 | - kind: pypi 524 | name: pytest-cov 525 | version: 5.0.0 526 | url: https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl 527 | sha256: 4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 528 | requires_dist: 529 | - pytest>=4.6 530 | - coverage[toml]>=5.2.1 531 | - fields ; extra == 'testing' 532 | - hunter ; extra == 'testing' 533 | - process-tests ; extra == 'testing' 534 | - pytest-xdist ; extra == 'testing' 535 | - virtualenv ; extra == 'testing' 536 | requires_python: '>=3.8' 537 | - kind: pypi 538 | name: pytest-flake8 539 | version: 1.2.2 540 | url: https://files.pythonhosted.org/packages/6e/47/27f24919d9ac5989e067043a739e27ba20d6f9dd5d6acd7598a1e4aebed3/pytest_flake8-1.2.2-py3-none-any.whl 541 | sha256: 7b8dfa77be42b5cec901b6b67eafb18d47ae6a05c4deac89399dba4c3591ce4d 542 | requires_dist: 543 | - flake8<6,>=5 544 | - pytest>=7.0 545 | - sphinx>=3.5 ; extra == 'doc' 546 | - jaraco-packaging>=9.3 ; extra == 'doc' 547 | - rst-linker>=1.9 ; extra == 'doc' 548 | - furo ; extra == 'doc' 549 | - sphinx-lint ; extra == 'doc' 550 | - pytest!=8.1.*,>=6 ; extra == 'test' 551 | - pytest-checkdocs>=2.4 ; extra == 'test' 552 | - pytest-cov ; extra == 'test' 553 | - pytest-mypy ; extra == 'test' 554 | - pytest-enabler>=2.2 ; extra == 'test' 555 | - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'test' 556 | requires_python: '>=3.8' 557 | - kind: conda 558 | name: python 559 | version: 3.12.5 560 | build: h30c5eda_0_cpython 561 | subdir: osx-arm64 562 | url: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.5-h30c5eda_0_cpython.conda 563 | sha256: 1319e918fb54c9491832a9731cad00235a76f61c6f9b23fc0f70cdfb74c950ea 564 | md5: 5e315581e2948dfe3bcac306540e9803 565 | depends: 566 | - __osx >=11.0 567 | - bzip2 >=1.0.8,<2.0a0 568 | - libexpat >=2.6.2,<3.0a0 569 | - libffi >=3.4,<4.0a0 570 | - libsqlite >=3.46.0,<4.0a0 571 | - libzlib >=1.3.1,<2.0a0 572 | - ncurses >=6.5,<7.0a0 573 | - openssl >=3.3.1,<4.0a0 574 | - readline >=8.2,<9.0a0 575 | - tk >=8.6.13,<8.7.0a0 576 | - tzdata 577 | - xz >=5.2.6,<6.0a0 578 | constrains: 579 | - python_abi 3.12.* *_cp312 580 | license: Python-2.0 581 | purls: [] 582 | size: 12926356 583 | timestamp: 1723142203193 584 | - kind: conda 585 | name: readline 586 | version: '8.2' 587 | build: h92ec313_1 588 | build_number: 1 589 | subdir: osx-arm64 590 | url: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda 591 | sha256: a1dfa679ac3f6007362386576a704ad2d0d7a02e98f5d0b115f207a2da63e884 592 | md5: 8cbb776a2f641b943d413b3e19df71f4 593 | depends: 594 | - ncurses >=6.3,<7.0a0 595 | license: GPL-3.0-only 596 | license_family: GPL 597 | purls: [] 598 | size: 250351 599 | timestamp: 1679532511311 600 | - kind: pypi 601 | name: requests 602 | version: 2.32.3 603 | url: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl 604 | sha256: 70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 605 | requires_dist: 606 | - charset-normalizer<4,>=2 607 | - idna<4,>=2.5 608 | - urllib3<3,>=1.21.1 609 | - certifi>=2017.4.17 610 | - pysocks!=1.5.7,>=1.5.6 ; extra == 'socks' 611 | - chardet<6,>=3.0.2 ; extra == 'use-chardet-on-py3' 612 | requires_python: '>=3.8' 613 | - kind: pypi 614 | name: restructuredtext-lint 615 | version: 1.4.0 616 | url: https://files.pythonhosted.org/packages/48/9c/6d8035cafa2d2d314f34e6cd9313a299de095b26e96f1c7312878f988eec/restructuredtext_lint-1.4.0.tar.gz 617 | sha256: 1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45 618 | requires_dist: 619 | - docutils<1.0,>=0.11 620 | - kind: conda 621 | name: setuptools 622 | version: 73.0.1 623 | build: pyhd8ed1ab_0 624 | subdir: noarch 625 | noarch: python 626 | url: https://conda.anaconda.org/conda-forge/noarch/setuptools-73.0.1-pyhd8ed1ab_0.conda 627 | sha256: c9f5e110e3fe5a7c4cd5b9da445c05a1fae000b43ab3a97cb6a501f4267515fc 628 | md5: f0b618d7673d1b2464f600b34d912f6f 629 | depends: 630 | - python >=3.8 631 | license: MIT 632 | license_family: MIT 633 | purls: 634 | - pkg:pypi/setuptools?source=hash-mapping 635 | size: 1460460 636 | timestamp: 1725348602179 637 | - kind: pypi 638 | name: snowballstemmer 639 | version: 2.2.0 640 | url: https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl 641 | sha256: c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a 642 | - kind: pypi 643 | name: sphinx 644 | version: 8.0.2 645 | url: https://files.pythonhosted.org/packages/4d/61/2ad169c6ff1226b46e50da0e44671592dbc6d840a52034a0193a99b28579/sphinx-8.0.2-py3-none-any.whl 646 | sha256: 56173572ae6c1b9a38911786e206a110c9749116745873feae4f9ce88e59391d 647 | requires_dist: 648 | - sphinxcontrib-applehelp 649 | - sphinxcontrib-devhelp 650 | - sphinxcontrib-jsmath 651 | - sphinxcontrib-htmlhelp>=2.0.0 652 | - sphinxcontrib-serializinghtml>=1.1.9 653 | - sphinxcontrib-qthelp 654 | - jinja2>=3.1 655 | - pygments>=2.17 656 | - docutils>=0.20,<0.22 657 | - snowballstemmer>=2.2 658 | - babel>=2.13 659 | - alabaster>=0.7.14 660 | - imagesize>=1.3 661 | - requests>=2.30.0 662 | - packaging>=23.0 663 | - tomli>=2 ; python_full_version < '3.11' 664 | - colorama>=0.4.6 ; sys_platform == 'win32' 665 | - sphinxcontrib-websupport ; extra == 'docs' 666 | - flake8>=6.0 ; extra == 'lint' 667 | - ruff==0.5.5 ; extra == 'lint' 668 | - mypy==1.11.0 ; extra == 'lint' 669 | - sphinx-lint>=0.9 ; extra == 'lint' 670 | - types-colorama==0.4.15.20240311 ; extra == 'lint' 671 | - types-defusedxml==0.7.0.20240218 ; extra == 'lint' 672 | - types-docutils==0.21.0.20240724 ; extra == 'lint' 673 | - types-pillow==10.2.0.20240520 ; extra == 'lint' 674 | - types-pygments==2.18.0.20240506 ; extra == 'lint' 675 | - types-requests>=2.30.0 ; extra == 'lint' 676 | - tomli>=2 ; extra == 'lint' 677 | - pytest>=6.0 ; extra == 'lint' 678 | - pytest>=8.0 ; extra == 'test' 679 | - defusedxml>=0.7.1 ; extra == 'test' 680 | - cython>=3.0 ; extra == 'test' 681 | - setuptools>=70.0 ; extra == 'test' 682 | - typing-extensions>=4.9 ; extra == 'test' 683 | requires_python: '>=3.10' 684 | - kind: pypi 685 | name: sphinx-rtd-theme 686 | version: 0.5.1 687 | url: https://files.pythonhosted.org/packages/76/81/d5af3a50a45ee4311ac2dac5b599d69f68388401c7a4ca902e0e450a9f94/sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl 688 | sha256: fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113 689 | requires_dist: 690 | - sphinx 691 | - transifex-client ; extra == 'dev' 692 | - sphinxcontrib-httpdomain ; extra == 'dev' 693 | - bump2version ; extra == 'dev' 694 | - kind: pypi 695 | name: sphinxcontrib-applehelp 696 | version: 2.0.0 697 | url: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl 698 | sha256: 4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 699 | requires_dist: 700 | - ruff==0.5.5 ; extra == 'lint' 701 | - mypy ; extra == 'lint' 702 | - types-docutils ; extra == 'lint' 703 | - sphinx>=5 ; extra == 'standalone' 704 | - pytest ; extra == 'test' 705 | requires_python: '>=3.9' 706 | - kind: pypi 707 | name: sphinxcontrib-devhelp 708 | version: 2.0.0 709 | url: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl 710 | sha256: aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2 711 | requires_dist: 712 | - ruff==0.5.5 ; extra == 'lint' 713 | - mypy ; extra == 'lint' 714 | - types-docutils ; extra == 'lint' 715 | - sphinx>=5 ; extra == 'standalone' 716 | - pytest ; extra == 'test' 717 | requires_python: '>=3.9' 718 | - kind: pypi 719 | name: sphinxcontrib-htmlhelp 720 | version: 2.1.0 721 | url: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl 722 | sha256: 166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8 723 | requires_dist: 724 | - ruff==0.5.5 ; extra == 'lint' 725 | - mypy ; extra == 'lint' 726 | - types-docutils ; extra == 'lint' 727 | - sphinx>=5 ; extra == 'standalone' 728 | - pytest ; extra == 'test' 729 | - html5lib ; extra == 'test' 730 | requires_python: '>=3.9' 731 | - kind: pypi 732 | name: sphinxcontrib-jsmath 733 | version: 1.0.1 734 | url: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl 735 | sha256: 2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 736 | requires_dist: 737 | - pytest ; extra == 'test' 738 | - flake8 ; extra == 'test' 739 | - mypy ; extra == 'test' 740 | requires_python: '>=3.5' 741 | - kind: pypi 742 | name: sphinxcontrib-qthelp 743 | version: 2.0.0 744 | url: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl 745 | sha256: b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb 746 | requires_dist: 747 | - ruff==0.5.5 ; extra == 'lint' 748 | - mypy ; extra == 'lint' 749 | - types-docutils ; extra == 'lint' 750 | - sphinx>=5 ; extra == 'standalone' 751 | - pytest ; extra == 'test' 752 | - defusedxml>=0.7.1 ; extra == 'test' 753 | requires_python: '>=3.9' 754 | - kind: pypi 755 | name: sphinxcontrib-serializinghtml 756 | version: 2.0.0 757 | url: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl 758 | sha256: 6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 759 | requires_dist: 760 | - ruff==0.5.5 ; extra == 'lint' 761 | - mypy ; extra == 'lint' 762 | - types-docutils ; extra == 'lint' 763 | - sphinx>=5 ; extra == 'standalone' 764 | - pytest ; extra == 'test' 765 | requires_python: '>=3.9' 766 | - kind: pypi 767 | name: stevedore 768 | version: 5.3.0 769 | url: https://files.pythonhosted.org/packages/ec/50/70762bdb23f6c2b746b90661f461d33c4913a22a46bb5265b10947e85ffb/stevedore-5.3.0-py3-none-any.whl 770 | sha256: 1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78 771 | requires_dist: 772 | - pbr>=2.0.0 773 | requires_python: '>=3.8' 774 | - kind: conda 775 | name: tk 776 | version: 8.6.13 777 | build: h5083fa2_1 778 | build_number: 1 779 | subdir: osx-arm64 780 | url: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda 781 | sha256: 72457ad031b4c048e5891f3f6cb27a53cb479db68a52d965f796910e71a403a8 782 | md5: b50a57ba89c32b62428b71a875291c9b 783 | depends: 784 | - libzlib >=1.2.13,<2.0.0a0 785 | license: TCL 786 | license_family: BSD 787 | purls: [] 788 | size: 3145523 789 | timestamp: 1699202432999 790 | - kind: pypi 791 | name: typing-extensions 792 | version: 4.12.2 793 | url: https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl 794 | sha256: 04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d 795 | requires_python: '>=3.8' 796 | - kind: conda 797 | name: tzdata 798 | version: 2024a 799 | build: h8827d51_1 800 | build_number: 1 801 | subdir: noarch 802 | noarch: generic 803 | url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda 804 | sha256: 7d21c95f61319dba9209ca17d1935e6128af4235a67ee4e57a00908a1450081e 805 | md5: 8bfdead4e0fff0383ae4c9c50d0531bd 806 | license: LicenseRef-Public-Domain 807 | purls: [] 808 | size: 124164 809 | timestamp: 1724736371498 810 | - kind: pypi 811 | name: uarray 812 | version: 0.8.8.dev25+gc6136a7.d20240906 813 | path: . 814 | sha256: 2b63b122546f0e235d38a0b6c4e0a8c45b64183fb6ff0b82a791484ca361f35f 815 | requires_dist: 816 | - uarray[docs] ; extra == 'all' 817 | - uarray[tests] ; extra == 'all' 818 | - uarray[optional] ; extra == 'all' 819 | - sphinx ; extra == 'docs' 820 | - sphinx-rtd-theme ; extra == 'docs' 821 | - doc8 ; extra == 'docs' 822 | - pytest>=3.5 ; extra == 'tests' 823 | - pytest-flake8 ; extra == 'tests' 824 | - pytest-cov ; extra == 'tests' 825 | - mypy>=0.930 ; extra == 'tests' 826 | requires_python: '>=3.10' 827 | - kind: pypi 828 | name: urllib3 829 | version: 2.2.2 830 | url: https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl 831 | sha256: a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 832 | requires_dist: 833 | - brotli>=1.0.9 ; platform_python_implementation == 'CPython' and extra == 'brotli' 834 | - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' 835 | - h2<5,>=4 ; extra == 'h2' 836 | - pysocks!=1.5.7,<2.0,>=1.5.6 ; extra == 'socks' 837 | - zstandard>=0.18.0 ; extra == 'zstd' 838 | requires_python: '>=3.8' 839 | - kind: conda 840 | name: wheel 841 | version: 0.44.0 842 | build: pyhd8ed1ab_0 843 | subdir: noarch 844 | noarch: python 845 | url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.44.0-pyhd8ed1ab_0.conda 846 | sha256: d828764736babb4322b8102094de38074dedfc71f5ff405c9dfee89191c14ebc 847 | md5: d44e3b085abcaef02983c6305b84b584 848 | depends: 849 | - python >=3.8 850 | license: MIT 851 | license_family: MIT 852 | purls: 853 | - pkg:pypi/wheel?source=hash-mapping 854 | size: 58585 855 | timestamp: 1722797131787 856 | - kind: conda 857 | name: xz 858 | version: 5.2.6 859 | build: h57fd34a_0 860 | subdir: osx-arm64 861 | url: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 862 | sha256: 59d78af0c3e071021cfe82dc40134c19dab8cdf804324b62940f5c8cd71803ec 863 | md5: 39c6b54e94014701dd157f4f576ed211 864 | license: LGPL-2.1 and GPL-2.0 865 | purls: [] 866 | size: 235693 867 | timestamp: 1660346961024 868 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["scikit-build-core", "setuptools_scm>=8"] 3 | build-backend = "scikit_build_core.setuptools.build_meta" 4 | 5 | [project] 6 | name = "uarray" 7 | dynamic = ["version"] 8 | description = "Array interface object for Python with pluggable backends and a multiple-dispatch mechanism for defining down-stream functions" 9 | readme = "README.md" 10 | dependencies = [] 11 | maintainers = [{ name = "Hameer Abbasi", email = "habbasi@quansight.com" }] 12 | requires-python = ">=3.10" 13 | license = { file = "LICENSE" } 14 | keywords = ["uarray", "scipy", "multiple-dispatch"] 15 | classifiers = [ 16 | "Development Status :: 2 - Pre-Alpha", 17 | "Operating System :: OS Independent", 18 | "License :: OSI Approved :: BSD License", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Programming Language :: Python :: 3.13", 25 | "Programming Language :: Python :: 3 :: Only", 26 | "Intended Audience :: Developers", 27 | "Intended Audience :: Science/Research", 28 | "Typing :: Typed", 29 | ] 30 | 31 | [tool.setuptools_scm] 32 | version_file = "src/uarray/_version.py" 33 | 34 | [tool.scikit-build] 35 | wheel.packages = ["src/uarray"] 36 | metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" 37 | sdist.include = ["src/package/_version.py"] 38 | 39 | [project.urls] 40 | Documentation = "https://uarray.org/" 41 | Source = "https://github.com/Quansight-Labs/uarray/" 42 | Repository = "https://github.com/Quansight-Labs/uarray.git" 43 | "Issue Tracker" = "https://github.com/Quansight-Labs/uarray/issues" 44 | 45 | [project.optional-dependencies] 46 | docs = [ 47 | "sphinx", 48 | "sphinx_rtd_theme", 49 | ] 50 | tests = [ 51 | "pytest>=3.5", 52 | "pytest-flake8", 53 | "pytest-cov", 54 | "mypy>=0.930", 55 | ] 56 | optional = [] 57 | all = [ 58 | "uarray[docs]", 59 | "uarray[tests]", 60 | "uarray[optional]", 61 | ] 62 | 63 | [tool.mypy] 64 | show_error_codes = true 65 | no_implicit_reexport = true 66 | disallow_any_unimported = true 67 | 68 | [[tool.mypy.overrides]] 69 | ignore_missing_imports = true 70 | disallow_any_unimported = false 71 | 72 | [tool.cibuildwheel] 73 | build = "{cp3{10..13}-*,pp310-*}" 74 | build-frontend = "build" 75 | free-threaded-support = true 76 | before-test = "pip install -r {project}/requirements/tests.txt" 77 | test-command = "pytest --pyargs uarray" 78 | 79 | [tool.cibuildwheel.macos] 80 | archs = ["universal2"] 81 | 82 | [[tool.cibuildwheel.overrides]] 83 | select = "*-macosx_*" 84 | inherit.environment = "append" 85 | environment.MACOSX_DEPLOYMENT_TARGET = "10.13" 86 | 87 | [tool.cibuildwheel.config-settings] 88 | "cmake.verbose" = true 89 | "logging.level" = "INFO" 90 | 91 | [tool.pixi.project] 92 | channels = ["conda-forge"] 93 | platforms = ["osx-arm64"] 94 | 95 | [tool.pixi.feature.all.dependencies] 96 | pip = "*" 97 | 98 | [tool.pixi.pypi-dependencies] 99 | uarray = { path = "." } 100 | 101 | [tool.pixi.environments] 102 | default = { solve-group = "default" } 103 | all = { features = ["all", "docs", "tests", "optional"], solve-group = "default" } 104 | docs = { features = ["docs"], solve-group = "default" } 105 | optional = { features = ["optional"], solve-group = "default" } 106 | tests = { features = ["tests"], solve-group = "default" } 107 | 108 | [tool.pixi.tasks] 109 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | fail_on_warning: true 6 | 7 | formats: all 8 | 9 | build: 10 | image: latest 11 | 12 | python: 13 | version: 3.9 14 | install: 15 | - method: pip 16 | path: . 17 | extra_requirements: 18 | - docs 19 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quansight-Labs/uarray/42bc38bcc607fb9b01b41bd1a203d680b3a25a25/requirements.txt -------------------------------------------------------------------------------- /requirements/all.txt: -------------------------------------------------------------------------------- 1 | -r optional.txt 2 | -r tests.txt 3 | -r docs.txt 4 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /requirements/optional.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quansight-Labs/uarray/42bc38bcc607fb9b01b41bd1a203d680b3a25a25/requirements/optional.txt -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | pytest>=3.5 2 | pytest-flake8 3 | pytest-cov 4 | mypy>=0.930 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(cmake_source_dir="./src") -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15...3.30) 2 | 3 | project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX) 4 | 5 | find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) 6 | 7 | Python_add_library(_uarray MODULE 8 | small_dynamic_array.h 9 | vectorcall.h 10 | vectorcall.cxx 11 | _uarray_dispatch.cxx 12 | WITH_SOABI) 13 | set_property(TARGET _uarray PROPERTY CXX_STANDARD 17) 14 | install(TARGETS _uarray LIBRARY DESTINATION uarray) 15 | -------------------------------------------------------------------------------- /src/small_dynamic_array.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | /** Fixed size dynamic array with small buffer optimisation */ 11 | template 12 | class SmallDynamicArray { 13 | ptrdiff_t size_; 14 | union { 15 | T elements[SmallCapacity]; 16 | T * array; 17 | } storage_; 18 | 19 | bool is_small() const { return size_ <= SmallCapacity; } 20 | 21 | void destroy_buffer(T * first, T * last) noexcept { 22 | for (; first < last; ++first) { 23 | first->~T(); 24 | } 25 | } 26 | 27 | void default_construct_buffer(T * first, T * last) noexcept( 28 | std::is_nothrow_destructible::value) { 29 | auto cur = first; 30 | try { 31 | for (; cur < last; ++cur) { 32 | new (cur) T(); // Placement new 33 | } 34 | } catch (...) { 35 | // If construction failed, destroy already constructed values 36 | destroy_buffer(first, cur); 37 | throw; 38 | } 39 | } 40 | 41 | void move_construct_buffer(T * first, T * last, T * d_first) noexcept( 42 | std::is_nothrow_move_constructible::value) { 43 | T * d_cur = d_first; 44 | 45 | try { 46 | for (; first < last; ++first, ++d_cur) { 47 | new (d_cur) T(std::move(*first)); // Placement new 48 | } 49 | } catch (...) { 50 | destroy_buffer(d_first, d_cur); 51 | throw; 52 | } 53 | } 54 | 55 | void allocate() { 56 | assert(size_ >= 0); 57 | if (is_small()) 58 | return; 59 | 60 | storage_.array = (T *)malloc(size_ * sizeof(T)); 61 | if (!storage_.array) { 62 | throw std::bad_alloc(); 63 | } 64 | } 65 | 66 | void deallocate() noexcept { 67 | if (!is_small()) { 68 | free(storage_.array); 69 | } 70 | } 71 | 72 | public: 73 | using value_type = T; 74 | using iterator = value_type *; 75 | using const_iterator = const value_type *; 76 | using reference = value_type &; 77 | using const_reference = const value_type &; 78 | using size_type = ptrdiff_t; 79 | 80 | SmallDynamicArray(): size_(0) {} 81 | 82 | explicit SmallDynamicArray(size_t size): size_(size) { 83 | allocate(); 84 | auto first = begin(); 85 | try { 86 | default_construct_buffer(first, first + size_); 87 | } catch (...) { 88 | deallocate(); 89 | throw; 90 | } 91 | } 92 | 93 | SmallDynamicArray(size_type size, const T & fill_value): size_(size) { 94 | allocate(); 95 | try { 96 | std::uninitialized_fill_n(begin(), size_, fill_value); 97 | } catch (...) { 98 | deallocate(); 99 | throw; 100 | } 101 | } 102 | 103 | SmallDynamicArray(const SmallDynamicArray & copy): size_(copy.size_) { 104 | allocate(); 105 | try { 106 | std::uninitialized_copy_n(copy.begin(), size_, begin()); 107 | } catch (...) { 108 | deallocate(); 109 | throw; 110 | } 111 | } 112 | 113 | SmallDynamicArray(SmallDynamicArray && move) noexcept( 114 | std::is_nothrow_move_constructible::value) 115 | : size_(move.size_) { 116 | if (!move.is_small()) { 117 | storage_.array = move.storage_.array; 118 | move.storage_.array = nullptr; 119 | move.storage_.size = 0; 120 | return; 121 | } 122 | 123 | auto m_first = move.begin(); 124 | try { 125 | move_construct_buffer(m_first, m_first + size_, begin()); 126 | } catch (...) { 127 | destroy_buffer(m_first, m_first + size_); 128 | move.size_ = 0; 129 | size_ = 0; 130 | throw; 131 | } 132 | 133 | destroy_buffer(&move.storage_.elements[0], &move.storage_.elements[size_]); 134 | move.size_ = 0; 135 | } 136 | 137 | ~SmallDynamicArray() { clear(); } 138 | 139 | SmallDynamicArray & operator=(const SmallDynamicArray & copy) { 140 | if (© == this) 141 | return *this; 142 | 143 | clear(); 144 | 145 | size_ = copy.size_; 146 | try { 147 | allocate(); 148 | } catch (...) { 149 | size_ = 0; 150 | throw; 151 | } 152 | 153 | try { 154 | std::uninitialized_copy_n(copy.begin(), size_, begin()); 155 | } catch (...) { 156 | deallocate(); 157 | size_ = 0; 158 | throw; 159 | } 160 | return *this; 161 | } 162 | 163 | SmallDynamicArray & operator=(SmallDynamicArray && move) noexcept( 164 | std::is_nothrow_move_constructible::value) { 165 | if (&move == this) 166 | return *this; 167 | 168 | if (!move.is_small()) { 169 | storage_.array = move.storage_.array; 170 | size_ = move.size_; 171 | move.storage_.array = nullptr; 172 | move.size_ = 0; 173 | return *this; 174 | } 175 | 176 | clear(); 177 | 178 | size_ = move.size_; 179 | auto m_first = move.begin(); 180 | try { 181 | move_construct_buffer(m_first, m_first + size_, begin()); 182 | } catch (...) { 183 | destroy_buffer(m_first, m_first + size_); 184 | move.size_ = 0; 185 | size_ = 0; 186 | throw; 187 | } 188 | 189 | destroy_buffer(m_first, m_first + size_); 190 | move.size_ = 0; 191 | return *this; 192 | } 193 | 194 | void clear() noexcept(std::is_nothrow_destructible::value) { 195 | auto first = begin(); 196 | destroy_buffer(first, first + size_); 197 | deallocate(); 198 | size_ = 0; 199 | } 200 | 201 | iterator begin() { 202 | return is_small() ? &storage_.elements[0] : storage_.array; 203 | } 204 | 205 | const_iterator cbegin() const { 206 | return is_small() ? &storage_.elements[0] : storage_.array; 207 | } 208 | 209 | iterator end() { return begin() + size_; } 210 | 211 | const_iterator cend() const { return cbegin() + size_; } 212 | 213 | const_iterator begin() const { return cbegin(); } 214 | 215 | const_iterator end() const { return cend(); } 216 | 217 | size_type size() const { return size_; } 218 | 219 | const_reference operator[](size_type idx) const { 220 | assert(0 <= idx && idx < size_); 221 | return begin()[idx]; 222 | } 223 | 224 | reference operator[](size_type idx) { 225 | assert(0 <= idx && idx < size_); 226 | return begin()[idx]; 227 | } 228 | }; 229 | -------------------------------------------------------------------------------- /src/uarray/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = 4 | uarray 5 | [report] 6 | omit = 7 | **/tests/ 8 | docs/ 9 | uarray/_version.py 10 | -------------------------------------------------------------------------------- /src/uarray/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. note: 3 | If you are looking for overrides for NumPy-specific methods, see the 4 | documentation for :obj:`unumpy`. This page explains how to write 5 | back-ends and multimethods. 6 | 7 | ``uarray`` is built around a back-end protocol, and overridable multimethods. 8 | It is necessary to define multimethods for back-ends to be able to override them. 9 | See the documentation of :obj:`generate_multimethod` on how to write multimethods. 10 | 11 | 12 | 13 | Let's start with the simplest: 14 | 15 | ``__ua_domain__`` defines the back-end *domain*. The domain consists of period- 16 | separated string consisting of the modules you extend plus the submodule. For 17 | example, if a submodule ``module2.submodule`` extends ``module1`` 18 | (i.e., it exposes dispatchables marked as types available in ``module1``), 19 | then the domain string should be ``"module1.module2.submodule"``. 20 | 21 | 22 | For the purpose of this demonstration, we'll be creating an object and setting 23 | its attributes directly. However, note that you can use a module or your own type 24 | as a backend as well. 25 | 26 | >>> class Backend: pass 27 | >>> be = Backend() 28 | >>> be.__ua_domain__ = "ua_examples" 29 | 30 | It might be useful at this point to sidetrack to the documentation of 31 | :obj:`generate_multimethod` to find out how to generate a multimethod 32 | overridable by :obj:`uarray`. Needless to say, writing a backend and 33 | creating multimethods are mostly orthogonal activities, and knowing 34 | one doesn't necessarily require knowledge of the other, although it 35 | is certainly helpful. We expect core API designers/specifiers to write the 36 | multimethods, and implementors to override them. But, as is often the case, 37 | similar people write both. 38 | 39 | Without further ado, here's an example multimethod: 40 | 41 | >>> import uarray as ua 42 | >>> from uarray import Dispatchable 43 | >>> def override_me(a, b): 44 | ... return Dispatchable(a, int), 45 | >>> def override_replacer(args, kwargs, dispatchables): 46 | ... return (dispatchables[0], args[1]), {} 47 | >>> overridden_me = ua.generate_multimethod( 48 | ... override_me, override_replacer, "ua_examples" 49 | ... ) 50 | 51 | Next comes the part about overriding the multimethod. This requires 52 | the ``__ua_function__`` protocol, and the ``__ua_convert__`` 53 | protocol. The ``__ua_function__`` protocol has the signature 54 | ``(method, args, kwargs)`` where ``method`` is the passed 55 | multimethod, ``args``/``kwargs`` specify the arguments and ``dispatchables`` 56 | is the list of converted dispatchables passed in. 57 | 58 | >>> def __ua_function__(method, args, kwargs): 59 | ... return method.__name__, args, kwargs 60 | >>> be.__ua_function__ = __ua_function__ 61 | 62 | The other protocol of interest is the ``__ua_convert__`` protocol. It has the 63 | signature ``(dispatchables, coerce)``. When ``coerce`` is ``False``, conversion 64 | between the formats should ideally be an ``O(1)`` operation, but it means that 65 | no memory copying should be involved, only views of the existing data. 66 | 67 | >>> def __ua_convert__(dispatchables, coerce): 68 | ... for d in dispatchables: 69 | ... if d.type is int: 70 | ... if coerce and d.coercible: 71 | ... yield str(d.value) 72 | ... else: 73 | ... yield d.value 74 | >>> be.__ua_convert__ = __ua_convert__ 75 | 76 | Now that we have defined the backend, the next thing to do is to call the multimethod. 77 | 78 | >>> with ua.set_backend(be): 79 | ... overridden_me(1, "2") 80 | ('override_me', (1, '2'), {}) 81 | 82 | Note that the marked type has no effect on the actual type of the passed object. 83 | We can also coerce the type of the input. 84 | 85 | >>> with ua.set_backend(be, coerce=True): 86 | ... overridden_me(1, "2") 87 | ... overridden_me(1.0, "2") 88 | ('override_me', ('1', '2'), {}) 89 | ('override_me', ('1.0', '2'), {}) 90 | 91 | Another feature is that if you remove ``__ua_convert__``, the arguments are not 92 | converted at all and it's up to the backend to handle that. 93 | 94 | >>> del be.__ua_convert__ 95 | >>> with ua.set_backend(be): 96 | ... overridden_me(1, "2") 97 | ('override_me', (1, '2'), {}) 98 | 99 | You also have the option to return ``NotImplemented``, in which case processing moves on 100 | to the next back-end, which in this case, doesn't exist. The same applies to 101 | ``__ua_convert__``. 102 | 103 | >>> be.__ua_function__ = lambda *a, **kw: NotImplemented 104 | >>> with ua.set_backend(be): 105 | ... overridden_me(1, "2") 106 | Traceback (most recent call last): 107 | ... 108 | uarray.BackendNotImplementedError: ... 109 | 110 | The last possibility is if we don't have ``__ua_convert__``, in which case the job is left 111 | up to ``__ua_function__``, but putting things back into arrays after conversion will not be 112 | possible. 113 | """ 114 | 115 | # Explicitly re-export `__all__` so type checkers consider it a public member 116 | from ._backend import __all__ as __all__ 117 | from ._backend import * 118 | from ._version import version as __version__ 119 | -------------------------------------------------------------------------------- /src/uarray/_backend.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import types 4 | import inspect 5 | import functools 6 | from . import _uarray 7 | import copyreg 8 | import pickle 9 | import contextlib 10 | import warnings 11 | 12 | from collections.abc import Callable, Generator, Iterable 13 | from typing import TYPE_CHECKING, Any, Generic, TypeVar, Literal, overload, no_type_check 14 | 15 | from ._uarray import ( 16 | BackendNotImplementedError, 17 | _Function, 18 | _SkipBackendContext, 19 | _SetBackendContext, 20 | _BackendState, 21 | ) 22 | 23 | if TYPE_CHECKING: 24 | from typing_extensions import ParamSpec 25 | from ._typing import ( 26 | _SupportsUA, 27 | _PartialDispatchable, 28 | _ReplacerFunc, 29 | ) 30 | 31 | _P = ParamSpec("_P") 32 | 33 | _T = TypeVar("_T") 34 | _T2 = TypeVar("_T2") 35 | _Self = TypeVar("_Self") 36 | _TT = TypeVar("_TT", bound=type) 37 | 38 | __all__ = [ 39 | "set_backend", 40 | "set_global_backend", 41 | "skip_backend", 42 | "register_backend", 43 | "determine_backend", 44 | "determine_backend_multi", 45 | "clear_backends", 46 | "create_multimethod", 47 | "generate_multimethod", 48 | "_Function", 49 | "BackendNotImplementedError", 50 | "Dispatchable", 51 | "wrap_single_convertor", 52 | "wrap_single_convertor_instance", 53 | "all_of_type", 54 | "mark_as", 55 | "set_state", 56 | "get_state", 57 | "reset_state", 58 | "_BackendState", 59 | "_SkipBackendContext", 60 | "_SetBackendContext", 61 | ] 62 | 63 | 64 | @no_type_check 65 | def unpickle_function( 66 | mod_name: str, 67 | qname: str, 68 | self_: object, 69 | ) -> Callable[..., Any]: 70 | import importlib 71 | 72 | try: 73 | module = importlib.import_module(mod_name) 74 | qname = qname.split(".") 75 | func = module 76 | for q in qname: 77 | func = getattr(func, q) 78 | 79 | if self_ is not None: 80 | func = types.MethodType(func, self_) 81 | 82 | return func 83 | except (ImportError, AttributeError) as e: 84 | from pickle import UnpicklingError 85 | 86 | raise UnpicklingError from e 87 | 88 | 89 | @no_type_check 90 | def pickle_function( 91 | func: Callable[..., Any], 92 | ) -> tuple[ 93 | Callable[[str, str, object], Callable[..., Any]], 94 | tuple[str, str, Any | None], 95 | ]: 96 | mod_name = getattr(func, "__module__", None) 97 | qname = getattr(func, "__qualname__", None) 98 | self_ = getattr(func, "__self__", None) 99 | 100 | try: 101 | test = unpickle_function(mod_name, qname, self_) 102 | except pickle.UnpicklingError: 103 | test = None 104 | 105 | if test is not func: 106 | raise pickle.PicklingError( 107 | "Can't pickle {}: it's not the same object as {}".format(func, test) 108 | ) 109 | 110 | return unpickle_function, (mod_name, qname, self_) 111 | 112 | 113 | def pickle_state( 114 | state: _BackendState, 115 | ) -> tuple[ 116 | Callable[[dict[str, Any], dict[str, Any], bool], _BackendState], 117 | tuple[dict[str, Any], dict[str, Any], bool], 118 | ]: 119 | return _uarray._BackendState._unpickle, state._pickle() 120 | 121 | 122 | def pickle_set_backend_context( 123 | ctx: _SetBackendContext, 124 | ) -> tuple[type[_SetBackendContext], tuple[_SupportsUA, bool, bool],]: 125 | return _SetBackendContext, ctx._pickle() 126 | 127 | 128 | def pickle_skip_backend_context( 129 | ctx: _SkipBackendContext, 130 | ) -> tuple[type[_SkipBackendContext], tuple[_SupportsUA],]: 131 | return _SkipBackendContext, ctx._pickle() 132 | 133 | 134 | # TODO: Remove the `if` block once python/typeshed#7415 135 | # has been integrated into mypy 136 | if not TYPE_CHECKING: 137 | copyreg.pickle(_Function, pickle_function) 138 | copyreg.pickle(_uarray._BackendState, pickle_state) 139 | copyreg.pickle(_SetBackendContext, pickle_set_backend_context) 140 | copyreg.pickle(_SkipBackendContext, pickle_skip_backend_context) 141 | 142 | 143 | def get_state() -> _BackendState: 144 | """ 145 | Returns an opaque object containing the current state of all the backends. 146 | 147 | Can be used for synchronization between threads/processes. 148 | 149 | See Also 150 | -------- 151 | set_state 152 | Sets the state returned by this function. 153 | """ 154 | return _uarray.get_state() 155 | 156 | 157 | @contextlib.contextmanager 158 | def reset_state() -> Generator[None, None, None]: 159 | """ 160 | Returns a context manager that resets all state once exited. 161 | 162 | See Also 163 | -------- 164 | set_state 165 | Context manager that sets the backend state. 166 | get_state 167 | Gets a state to be set by this context manager. 168 | """ 169 | with set_state(get_state()): 170 | yield 171 | 172 | 173 | @contextlib.contextmanager 174 | def set_state(state: _BackendState) -> Generator[None, None, None]: 175 | """ 176 | A context manager that sets the state of the backends to one returned by :obj:`get_state`. 177 | 178 | See Also 179 | -------- 180 | get_state 181 | Gets a state to be set by this context manager. 182 | """ 183 | old_state = get_state() 184 | _uarray.set_state(state) 185 | try: 186 | yield 187 | finally: 188 | _uarray.set_state(old_state, True) 189 | 190 | 191 | def create_multimethod( 192 | *args: Any, 193 | **kwargs: Any, 194 | ) -> Callable[[Callable[_P, tuple[Dispatchable[Any, Any], ...]]], _Function[_P]]: 195 | """ 196 | Creates a decorator for generating multimethods. 197 | 198 | This function creates a decorator that can be used with an argument 199 | extractor in order to generate a multimethod. Other than for the 200 | argument extractor, all arguments are passed on to 201 | :obj:`generate_multimethod`. 202 | 203 | See Also 204 | -------- 205 | generate_multimethod 206 | Generates a multimethod. 207 | """ 208 | 209 | def wrapper(a): 210 | return generate_multimethod(a, *args, **kwargs) 211 | 212 | return wrapper 213 | 214 | 215 | def generate_multimethod( 216 | argument_extractor: Callable[_P, tuple[Dispatchable[Any, Any], ...]], 217 | argument_replacer: _ReplacerFunc, 218 | domain: str, 219 | default: None | Callable[..., Any] = None, 220 | ) -> _Function[_P]: 221 | """ 222 | Generates a multimethod. 223 | 224 | Parameters 225 | ---------- 226 | argument_extractor : ArgumentExtractorType 227 | A callable which extracts the dispatchable arguments. Extracted arguments 228 | should be marked by the :obj:`Dispatchable` class. It has the same signature 229 | as the desired multimethod. 230 | argument_replacer : ArgumentReplacerType 231 | A callable with the signature (args, kwargs, dispatchables), which should also 232 | return an (args, kwargs) pair with the dispatchables replaced inside the args/kwargs. 233 | domain : str 234 | A string value indicating the domain of this multimethod. 235 | default: Optional[Callable], optional 236 | The default implementation of this multimethod, where ``None`` (the default) specifies 237 | there is no default implementation. 238 | 239 | Examples 240 | -------- 241 | In this example, ``a`` is to be dispatched over, so we return it, while marking it as an ``int``. 242 | The trailing comma is needed because the args have to be returned as an iterable. 243 | 244 | >>> def override_me(a, b): 245 | ... return Dispatchable(a, int), 246 | 247 | Next, we define the argument replacer that replaces the dispatchables inside args/kwargs with the 248 | supplied ones. 249 | 250 | >>> def override_replacer(args, kwargs, dispatchables): 251 | ... return (dispatchables[0], args[1]), {} 252 | 253 | Next, we define the multimethod. 254 | 255 | >>> overridden_me = generate_multimethod( 256 | ... override_me, override_replacer, "ua_examples" 257 | ... ) 258 | 259 | Notice that there's no default implementation, unless you supply one. 260 | 261 | >>> overridden_me(1, "a") 262 | Traceback (most recent call last): 263 | ... 264 | uarray.BackendNotImplementedError: ... 265 | 266 | >>> overridden_me2 = generate_multimethod( 267 | ... override_me, override_replacer, "ua_examples", default=lambda x, y: (x, y) 268 | ... ) 269 | >>> overridden_me2(1, "a") 270 | (1, 'a') 271 | 272 | See Also 273 | -------- 274 | uarray 275 | See the module documentation for how to override the method by creating backends. 276 | """ 277 | kw_defaults, arg_defaults, opts = get_defaults(argument_extractor) 278 | ua_func = _Function( 279 | argument_extractor, 280 | argument_replacer, 281 | domain, 282 | arg_defaults, 283 | kw_defaults, 284 | default, 285 | ) 286 | 287 | return functools.update_wrapper(ua_func, argument_extractor) # type: ignore[return-value] 288 | 289 | 290 | def set_backend( 291 | backend: _SupportsUA, 292 | coerce: bool = False, 293 | only: bool = False, 294 | ) -> _SetBackendContext: 295 | """ 296 | A context manager that sets the preferred backend. 297 | 298 | Parameters 299 | ---------- 300 | backend 301 | The backend to set. 302 | coerce 303 | Whether or not to coerce to a specific backend's types. Implies ``only``. 304 | only 305 | Whether or not this should be the last backend to try. 306 | 307 | See Also 308 | -------- 309 | skip_backend: A context manager that allows skipping of backends. 310 | set_global_backend: Set a single, global backend for a domain. 311 | """ 312 | # Deprecated: 2022-08-17, To be removed: 2023-08-17 313 | # See gh-237 and https://discuss.scientific-python.org/t/requirements-and-discussion-of-a-type-dispatcher-for-the-ecosystem/157/40 314 | warnings.warn("uarray.skip_backend is deprecated, please migrate to scoped backends.", category=DeprecationWarning) 315 | try: 316 | return backend.__ua_cache__["set", coerce, only] 317 | except AttributeError: 318 | backend.__ua_cache__ = {} # type: ignore[misc] 319 | except KeyError: 320 | pass 321 | 322 | ctx = _SetBackendContext(backend, coerce, only) 323 | backend.__ua_cache__["set", coerce, only] = ctx 324 | return ctx 325 | 326 | 327 | def skip_backend(backend: _SupportsUA) -> _SkipBackendContext: 328 | """ 329 | A context manager that allows one to skip a given backend from processing 330 | entirely. This allows one to use another backend's code in a library that 331 | is also a consumer of the same backend. 332 | 333 | Parameters 334 | ---------- 335 | backend 336 | The backend to skip. 337 | 338 | See Also 339 | -------- 340 | set_backend: A context manager that allows setting of backends. 341 | set_global_backend: Set a single, global backend for a domain. 342 | """ 343 | try: 344 | return backend.__ua_cache__["skip"] 345 | except AttributeError: 346 | backend.__ua_cache__ = {} # type: ignore[misc] 347 | except KeyError: 348 | pass 349 | 350 | ctx = _SkipBackendContext(backend) 351 | backend.__ua_cache__["skip"] = ctx 352 | return ctx 353 | 354 | 355 | def get_defaults( 356 | f: Callable[..., Any], 357 | ) -> tuple[dict[str, Any], tuple[Any, ...], set[str]]: 358 | sig = inspect.signature(f) 359 | kw_defaults = {} 360 | arg_defaults = [] 361 | opts = set() 362 | for k, v in sig.parameters.items(): 363 | if v.default is not inspect.Parameter.empty: 364 | kw_defaults[k] = v.default 365 | if v.kind in ( 366 | inspect.Parameter.POSITIONAL_ONLY, 367 | inspect.Parameter.POSITIONAL_OR_KEYWORD, 368 | ): 369 | arg_defaults.append(v.default) 370 | opts.add(k) 371 | 372 | return kw_defaults, tuple(arg_defaults), opts 373 | 374 | 375 | def set_global_backend( 376 | backend: _SupportsUA, 377 | coerce: bool = False, 378 | only: bool = False, 379 | *, 380 | try_last: bool = False, 381 | ) -> None: 382 | """ 383 | This utility method replaces the default backend for permanent use. It 384 | will be tried in the list of backends automatically, unless the 385 | ``only`` flag is set on a backend. This will be the first tried 386 | backend outside the :obj:`set_backend` context manager. 387 | 388 | Note that this method is not thread-safe. 389 | 390 | .. warning:: 391 | We caution library authors against using this function in 392 | their code. We do *not* support this use-case. This function 393 | is meant to be used only by users themselves, or by a reference 394 | implementation, if one exists. 395 | 396 | Parameters 397 | ---------- 398 | backend 399 | The backend to register. 400 | coerce : bool 401 | Whether to coerce input types when trying this backend. 402 | only : bool 403 | If ``True``, no more backends will be tried if this fails. 404 | Implied by ``coerce=True``. 405 | try_last : bool 406 | If ``True``, the global backend is tried after registered backends. 407 | 408 | See Also 409 | -------- 410 | set_backend: A context manager that allows setting of backends. 411 | skip_backend: A context manager that allows skipping of backends. 412 | """ 413 | _uarray.set_global_backend(backend, coerce, only, try_last) 414 | 415 | 416 | def register_backend(backend: _SupportsUA) -> None: 417 | """ 418 | This utility method sets registers backend for permanent use. It 419 | will be tried in the list of backends automatically, unless the 420 | ``only`` flag is set on a backend. 421 | 422 | Note that this method is not thread-safe. 423 | 424 | Parameters 425 | ---------- 426 | backend 427 | The backend to register. 428 | """ 429 | _uarray.register_backend(backend) 430 | 431 | 432 | def clear_backends( 433 | domain: None | str, 434 | registered: bool = True, 435 | globals: bool = False, 436 | ) -> None: 437 | """ 438 | This utility method clears registered backends. 439 | 440 | .. warning:: 441 | We caution library authors against using this function in 442 | their code. We do *not* support this use-case. This function 443 | is meant to be used only by users themselves. 444 | 445 | .. warning:: 446 | Do NOT use this method inside a multimethod call, or the 447 | program is likely to crash. 448 | 449 | Parameters 450 | ---------- 451 | domain : Optional[str] 452 | The domain for which to de-register backends. ``None`` means 453 | de-register for all domains. 454 | registered : bool 455 | Whether or not to clear registered backends. See :obj:`register_backend`. 456 | globals : bool 457 | Whether or not to clear global backends. See :obj:`set_global_backend`. 458 | 459 | See Also 460 | -------- 461 | register_backend : Register a backend globally. 462 | set_global_backend : Set a global backend. 463 | """ 464 | _uarray.clear_backends(domain, registered, globals) 465 | 466 | 467 | class Dispatchable(Generic[_T, _TT]): 468 | """ 469 | A utility class which marks an argument with a specific dispatch type. 470 | 471 | 472 | Attributes 473 | ---------- 474 | value 475 | The value of the Dispatchable. 476 | 477 | type 478 | The type of the Dispatchable. 479 | 480 | Examples 481 | -------- 482 | >>> x = Dispatchable(1, str) 483 | >>> x 484 | , value=1> 485 | 486 | See Also 487 | -------- 488 | all_of_type 489 | Marks all unmarked parameters of a function. 490 | 491 | mark_as 492 | Allows one to create a utility function to mark as a given type. 493 | """ 494 | 495 | def __init__( 496 | self, 497 | value: _T, 498 | dispatch_type: _TT, 499 | coercible: bool = True, 500 | ) -> None: 501 | self.value = value 502 | self.type = dispatch_type 503 | self.coercible = coercible 504 | 505 | @overload 506 | def __getitem__(self, index: Literal[0]) -> _TT: 507 | ... 508 | 509 | @overload 510 | def __getitem__(self, index: Literal[1]) -> _T: 511 | ... 512 | 513 | def __getitem__(self, index: Literal[0, 1]) -> _TT | _T: 514 | return (self.type, self.value)[index] 515 | 516 | def __str__(self) -> str: 517 | return "<{0}: type={1!r}, value={2!r}>".format( 518 | type(self).__name__, self.type, self.value 519 | ) 520 | 521 | __repr__ = __str__ 522 | 523 | 524 | def mark_as(dispatch_type: _TT) -> _PartialDispatchable[_TT]: 525 | """ 526 | Creates a utility function to mark something as a specific type. 527 | 528 | Examples 529 | -------- 530 | >>> mark_int = mark_as(int) 531 | >>> mark_int(1) 532 | , value=1> 533 | """ 534 | # Pretend a more specific `functools.partial` sub-type is returned 535 | return functools.partial( # type: ignore[return-value] 536 | Dispatchable, dispatch_type=dispatch_type, 537 | ) 538 | 539 | 540 | def all_of_type( 541 | arg_type: _TT, 542 | ) -> Callable[ 543 | [Callable[_P, Iterable[_T]]], 544 | Callable[_P, tuple[Dispatchable[_T, _TT], ...]], 545 | ]: 546 | """ 547 | Marks all unmarked arguments as a given type. 548 | 549 | Examples 550 | -------- 551 | >>> @all_of_type(str) 552 | ... def f(a, b): 553 | ... return a, Dispatchable(b, int) 554 | >>> f('a', 1) 555 | (, value='a'>, , value=1>) 556 | """ 557 | 558 | def outer(func): 559 | @functools.wraps(func) 560 | def inner(*args, **kwargs): 561 | extracted_args = func(*args, **kwargs) 562 | return tuple( 563 | Dispatchable(arg, arg_type) 564 | if not isinstance(arg, Dispatchable) 565 | else arg 566 | for arg in extracted_args 567 | ) 568 | 569 | return inner 570 | 571 | return outer 572 | 573 | 574 | def wrap_single_convertor( 575 | convert_single: Callable[[_T, _TT, bool], _T2], 576 | ) -> Callable[[Iterable[Dispatchable[_T, _TT]], bool], list[_T2]]: 577 | """ 578 | Wraps a ``__ua_convert__`` defined for a single element to all elements. 579 | If any of them return ``NotImplemented``, the operation is assumed to be 580 | undefined. 581 | 582 | Accepts a signature of (value, type, coerce). 583 | """ 584 | 585 | @functools.wraps(convert_single) 586 | def __ua_convert__(dispatchables, coerce): 587 | converted = [] 588 | for d in dispatchables: 589 | c = convert_single(d.value, d.type, coerce and d.coercible) 590 | 591 | if c is NotImplemented: 592 | return NotImplemented 593 | 594 | converted.append(c) 595 | 596 | return converted 597 | 598 | return __ua_convert__ 599 | 600 | 601 | def wrap_single_convertor_instance( 602 | convert_single: Callable[[_Self, _T, _TT, bool], _T2], 603 | ) -> Callable[[_Self, Iterable[Dispatchable[_T, _TT]], bool], list[_T2]]: 604 | """ 605 | Wraps a ``__ua_convert__`` defined for a single element to all elements. 606 | If any of them return ``NotImplemented``, the operation is assumed to be 607 | undefined. 608 | 609 | Accepts a signature of (value, type, coerce). 610 | """ 611 | 612 | @functools.wraps(convert_single) 613 | def __ua_convert__(self, dispatchables, coerce): 614 | converted = [] 615 | for d in dispatchables: 616 | c = convert_single(self, d.value, d.type, coerce and d.coercible) 617 | 618 | if c is NotImplemented: 619 | return NotImplemented 620 | 621 | converted.append(c) 622 | 623 | return converted 624 | 625 | return __ua_convert__ 626 | 627 | 628 | def determine_backend( 629 | value: object, 630 | dispatch_type: type[Any], 631 | *, 632 | domain: str, 633 | only: bool = True, 634 | coerce: bool = False, 635 | ) -> _SetBackendContext: 636 | """Set the backend to the first active backend that supports ``value`` 637 | 638 | This is useful for functions that call multimethods without any dispatchable 639 | arguments. You can use :func:`determine_backend` to ensure the same backend 640 | is used everywhere in a block of multimethod calls. 641 | 642 | Parameters 643 | ---------- 644 | value 645 | The value being tested 646 | dispatch_type 647 | The dispatch type associated with ``value``, aka 648 | ":ref:`marking `". 649 | domain: string 650 | The domain to query for backends and set. 651 | coerce: bool 652 | Whether or not to allow coercion to the backend's types. Implies ``only``. 653 | only: bool 654 | Whether or not this should be the last backend to try. 655 | 656 | See Also 657 | -------- 658 | set_backend: For when you know which backend to set 659 | 660 | Notes 661 | ----- 662 | 663 | Support is determined by the ``__ua_convert__`` protocol. Backends not 664 | supporting the type must return ``NotImplemented`` from their 665 | ``__ua_convert__`` if they don't support input of that type. 666 | 667 | Examples 668 | -------- 669 | 670 | Suppose we have two backends ``BackendA`` and ``BackendB`` each supporting 671 | different types, ``TypeA`` and ``TypeB``. Neither supporting the other type: 672 | 673 | >>> with ua.set_backend(ex.BackendA): 674 | ... ex.call_multimethod(ex.TypeB(), ex.TypeB()) 675 | Traceback (most recent call last): 676 | ... 677 | uarray.BackendNotImplementedError: ... 678 | 679 | Now consider a multimethod that creates a new object of ``TypeA``, or 680 | ``TypeB`` depending on the active backend. 681 | 682 | >>> with ua.set_backend(ex.BackendA), ua.set_backend(ex.BackendB): 683 | ... res = ex.creation_multimethod() 684 | ... ex.call_multimethod(res, ex.TypeA()) 685 | Traceback (most recent call last): 686 | ... 687 | uarray.BackendNotImplementedError: ... 688 | 689 | ``res`` is an object of ``TypeB`` because ``BackendB`` is set in the 690 | innermost with statement. So, ``call_multimethod`` fails since the types 691 | don't match. 692 | 693 | Instead, we need to first find a backend suitable for all of our objects. 694 | 695 | >>> with ua.set_backend(ex.BackendA), ua.set_backend(ex.BackendB): 696 | ... x = ex.TypeA() 697 | ... with ua.determine_backend(x, "mark", domain="ua_examples"): 698 | ... res = ex.creation_multimethod() 699 | ... ex.call_multimethod(res, x) 700 | TypeA 701 | 702 | """ 703 | dispatchables = (Dispatchable(value, dispatch_type, coerce),) 704 | backend = _uarray.determine_backend(domain, dispatchables, coerce) 705 | 706 | return set_backend(backend, coerce=coerce, only=only) 707 | 708 | 709 | def determine_backend_multi( 710 | dispatchables: Iterable[Any], 711 | *, 712 | domain: str, 713 | only: bool = True, 714 | coerce: bool = False, 715 | **kwargs: type[Any], 716 | ) -> _SetBackendContext: 717 | """Set a backend supporting all ``dispatchables`` 718 | 719 | This is useful for functions that call multimethods without any dispatchable 720 | arguments. You can use :func:`determine_backend_multi` to ensure the same 721 | backend is used everywhere in a block of multimethod calls involving 722 | multiple arrays. 723 | 724 | Parameters 725 | ---------- 726 | dispatchables: Sequence[Union[uarray.Dispatchable, Any]] 727 | The dispatchables that must be supported 728 | domain: string 729 | The domain to query for backends and set. 730 | coerce: bool 731 | Whether or not to allow coercion to the backend's types. Implies ``only``. 732 | only: bool 733 | Whether or not this should be the last backend to try. 734 | dispatch_type: Optional[Any] 735 | The default dispatch type associated with ``dispatchables``, aka 736 | ":ref:`marking `". 737 | 738 | See Also 739 | -------- 740 | determine_backend: For a single dispatch value 741 | set_backend: For when you know which backend to set 742 | 743 | Notes 744 | ----- 745 | 746 | Support is determined by the ``__ua_convert__`` protocol. Backends not 747 | supporting the type must return ``NotImplemented`` from their 748 | ``__ua_convert__`` if they don't support input of that type. 749 | 750 | Examples 751 | -------- 752 | 753 | :func:`determine_backend` allows the backend to be set from a single 754 | object. :func:`determine_backend_multi` allows multiple objects to be 755 | checked simultaneously for support in the backend. Suppose we have a 756 | ``BackendAB`` which supports ``TypeA`` and ``TypeB`` in the same call, 757 | and a ``BackendBC`` that doesn't support ``TypeA``. 758 | 759 | >>> with ua.set_backend(ex.BackendAB), ua.set_backend(ex.BackendBC): 760 | ... a, b = ex.TypeA(), ex.TypeB() 761 | ... with ua.determine_backend_multi( 762 | ... [ua.Dispatchable(a, "mark"), ua.Dispatchable(b, "mark")], 763 | ... domain="ua_examples" 764 | ... ): 765 | ... res = ex.creation_multimethod() 766 | ... ex.call_multimethod(res, a, b) 767 | TypeA 768 | 769 | This won't call ``BackendBC`` because it doesn't support ``TypeA``. 770 | 771 | We can also use leave out the ``ua.Dispatchable`` if we specify the 772 | default ``dispatch_type`` for the ``dispatchables`` argument. 773 | 774 | >>> with ua.set_backend(ex.BackendAB), ua.set_backend(ex.BackendBC): 775 | ... a, b = ex.TypeA(), ex.TypeB() 776 | ... with ua.determine_backend_multi( 777 | ... [a, b], dispatch_type="mark", domain="ua_examples" 778 | ... ): 779 | ... res = ex.creation_multimethod() 780 | ... ex.call_multimethod(res, a, b) 781 | TypeA 782 | 783 | """ 784 | if "dispatch_type" in kwargs: 785 | disp_type = kwargs.pop("dispatch_type") 786 | dispatchables = tuple( 787 | d if isinstance(d, Dispatchable) else Dispatchable(d, disp_type) 788 | for d in dispatchables 789 | ) 790 | else: 791 | dispatchables = tuple(dispatchables) 792 | if not all(isinstance(d, Dispatchable) for d in dispatchables): 793 | raise TypeError("dispatchables must be instances of uarray.Dispatchable") 794 | 795 | if len(kwargs) != 0: 796 | raise TypeError("Received unexpected keyword arguments: {}".format(kwargs)) 797 | 798 | backend = _uarray.determine_backend(domain, dispatchables, coerce) 799 | 800 | return set_backend(backend, coerce=coerce, only=only) 801 | -------------------------------------------------------------------------------- /src/uarray/_typing.pyi: -------------------------------------------------------------------------------- 1 | """Helper module with various typing-related utilities.""" 2 | 3 | import functools 4 | from collections.abc import Callable 5 | from typing import Any, Protocol, TypeVar, Iterable, type_check_only 6 | 7 | import uarray 8 | 9 | _T = TypeVar("_T") 10 | _TT = TypeVar("_TT", bound=type[Any]) 11 | _T_co = TypeVar("_T_co", covariant=True) 12 | 13 | @type_check_only 14 | class _PySequence(Protocol[_T_co]): 15 | def __len__(self) -> int: ... 16 | def __getitem__(self, key: int, /) -> _T_co: ... 17 | 18 | @type_check_only 19 | class _SupportsUA(Protocol): 20 | @property 21 | def __ua_domain__(self) -> str | _PySequence[str]: ... 22 | @property 23 | def __ua_cache__(self) -> dict[Any, Any]: ... 24 | def __ua_convert__( 25 | self, 26 | dispatchables: tuple[uarray.Dispatchable[Any, Any], ...], 27 | coerce: bool, 28 | /, 29 | ) -> Iterable[Any]: ... 30 | 31 | @type_check_only 32 | class _PartialDispatchable(functools.partial[uarray.Dispatchable[Any, _TT]]): 33 | func: type[uarray.Dispatchable[Any, _TT]] 34 | args: tuple[_TT] 35 | def __call__( # type: ignore[override] 36 | self, 37 | value: _T, 38 | coercible: bool = ..., 39 | ) -> uarray.Dispatchable[_T, _TT]: ... 40 | 41 | _ReplacerFunc = Callable[ 42 | [ 43 | tuple[Any, ...], 44 | dict[str, Any], 45 | tuple[uarray.Dispatchable[Any, Any], ...], 46 | ], 47 | tuple[tuple[Any, ...], dict[str, Any]], 48 | ] 49 | 50 | _PyGlobalDict = dict[ 51 | str, 52 | tuple[ 53 | tuple[_T | None, bool, bool], 54 | list[_T], 55 | bool, 56 | ], 57 | ] 58 | 59 | _PyLocalDict = dict[ 60 | str, 61 | tuple[ 62 | list[_T], 63 | list[tuple[_T, bool, bool]], 64 | ], 65 | ] 66 | -------------------------------------------------------------------------------- /src/uarray/_uarray.pyi: -------------------------------------------------------------------------------- 1 | """Annotations for the ``uarray._uarray`` extension module.""" 2 | 3 | import types 4 | from collections.abc import Callable, Iterable 5 | from typing import Any, final, overload, Generic 6 | 7 | # TODO: Import from `typing` once `uarray` requires Python >= 3.10 8 | from typing_extensions import ParamSpec 9 | 10 | import uarray 11 | from uarray._typing import ( 12 | _PyGlobalDict, 13 | _PyLocalDict, 14 | _ReplacerFunc, 15 | _SupportsUA, 16 | ) 17 | 18 | _P = ParamSpec("_P") 19 | 20 | class BackendNotImplementedError(NotImplementedError): ... 21 | 22 | @final 23 | class _SkipBackendContext: 24 | def __init__(self, backend: _SupportsUA) -> None: ... 25 | def __enter__(self) -> None: ... 26 | def __exit__( 27 | self, 28 | exc_type: type[BaseException] | None, 29 | exc_value: BaseException | None, 30 | traceback: types.TracebackType | None, 31 | /, 32 | ) -> None: ... 33 | def _pickle(self) -> tuple[_SupportsUA]: ... 34 | 35 | @final 36 | class _SetBackendContext: 37 | def __init__( 38 | self, 39 | backend: _SupportsUA, 40 | coerce: bool = ..., 41 | only: bool = ..., 42 | ) -> None: ... 43 | def __enter__(self) -> None: ... 44 | def __exit__( 45 | self, 46 | exc_type: type[BaseException] | None, 47 | exc_value: BaseException | None, 48 | traceback: types.TracebackType | None, 49 | /, 50 | ) -> None: ... 51 | def _pickle(self) -> tuple[_SupportsUA, bool, bool]: ... 52 | 53 | # NOTE: Parametrize w.r.t. `__ua_domain__` when returning, but use `Any` 54 | # when used as argument type. Due to lists being invariant the `__ua_domain__` 55 | # protocol will likelly be disruptivelly strict in the latter case, hence the 56 | # usage of `Any` as an escape hatch. 57 | @final 58 | class _BackendState: 59 | def _pickle(self) -> tuple[ 60 | _PyGlobalDict[_SupportsUA], 61 | _PyLocalDict[_SupportsUA], 62 | bool, 63 | ]: ... 64 | @classmethod 65 | def _unpickle( 66 | cls, 67 | py_global: _PyGlobalDict[Any], 68 | py_locals: _PyLocalDict[Any], 69 | use_thread_local_globals: bool, 70 | /, 71 | ) -> _BackendState: ... 72 | 73 | # TODO: Remove the `type: ignore` once python/mypy#12033 has been bug fixed 74 | @final # type: ignore[arg-type] 75 | class _Function(Generic[_P]): 76 | def __init__( 77 | self, 78 | extractor: Callable[_P, tuple[uarray.Dispatchable[Any, Any], ...]], 79 | replacer: None | _ReplacerFunc, 80 | domain: str, 81 | def_args: tuple[Any, ...], 82 | def_kwargs: dict[str, Any], 83 | def_impl: None | Callable[..., Any], 84 | ) -> None: ... 85 | def __repr__(self) -> str: ... 86 | def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> Any: ... 87 | @overload 88 | def __get__(self, obj: None, type: type[Any]) -> _Function[_P]: ... 89 | @overload 90 | def __get__( 91 | self, obj: object, type: None | type[Any] = ... 92 | ) -> types.MethodType: ... 93 | @property 94 | def arg_extractor(self) -> Callable[_P, tuple[uarray.Dispatchable[Any, Any], ...]]: ... 95 | @property 96 | def arg_replacer(self) -> None | _ReplacerFunc: ... 97 | @property 98 | def default(self) -> None | Callable[..., Any]: ... 99 | @property 100 | def domain(self) -> str: ... 101 | # NOTE: These attributes are dynamically inserted by 102 | # `uarray.generate_multimethod` via a `functools.update_wrapper` call 103 | __module__: str 104 | __name__: str 105 | __qualname__: str 106 | __doc__: None | str 107 | __wrapped__: Callable[_P, tuple[uarray.Dispatchable[Any, Any], ...]] 108 | __annotations__: dict[str, Any] 109 | 110 | def set_global_backend( 111 | backend: _SupportsUA, 112 | coerce: bool = ..., 113 | only: bool = ..., 114 | try_last: bool = ..., 115 | /, 116 | ) -> None: ... 117 | def clear_backends( 118 | domain: None | str, 119 | registered: bool = ..., 120 | global_: bool = ..., 121 | /, 122 | ) -> None: ... 123 | def determine_backend( 124 | domain_object: str, 125 | dispatchables: Iterable[uarray.Dispatchable[Any, Any]], 126 | coerce: bool, 127 | /, 128 | ) -> _SupportsUA: ... 129 | def register_backend(backend: _SupportsUA, /) -> None: ... 130 | def get_state() -> _BackendState: ... 131 | def set_state(arg: _BackendState, reset_allowed: bool = ..., /) -> None: ... 132 | -------------------------------------------------------------------------------- /src/uarray/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import uarray 3 | import pytest # type: ignore 4 | 5 | from .tests import example_helpers 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def add_namespaces(doctest_namespace): 10 | doctest_namespace["ua"] = uarray 11 | doctest_namespace["ex"] = example_helpers 12 | -------------------------------------------------------------------------------- /src/uarray/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quansight-Labs/uarray/42bc38bcc607fb9b01b41bd1a203d680b3a25a25/src/uarray/py.typed -------------------------------------------------------------------------------- /src/uarray/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --doctest-modules --junitxml=junit/test-results.xml --cov-report=xml:./coverage/coverage.xml --cov-report=term --cov . --cov-report html --cov-config .coveragerc 3 | doctest_optionflags= IGNORE_EXCEPTION_DETAIL 4 | -------------------------------------------------------------------------------- /src/uarray/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quansight-Labs/uarray/42bc38bcc607fb9b01b41bd1a203d680b3a25a25/src/uarray/tests/__init__.py -------------------------------------------------------------------------------- /src/uarray/tests/example_helpers.py: -------------------------------------------------------------------------------- 1 | import uarray as ua 2 | 3 | 4 | class _TypedBackend: 5 | __ua_domain__ = "ua_examples" 6 | 7 | def __init__(self, *my_types): 8 | self.my_types = my_types 9 | 10 | def __ua_convert__(self, dispatchables, coerce): 11 | if not all(type(d.value) in self.my_types for d in dispatchables): 12 | return NotImplemented 13 | return tuple(d.value for d in dispatchables) 14 | 15 | def __ua_function__(self, func, args, kwargs): 16 | return self.my_types[0]() 17 | 18 | 19 | class TypeA: 20 | @classmethod 21 | def __repr__(cls): 22 | return cls.__name__ 23 | 24 | 25 | class TypeB(TypeA): 26 | pass 27 | 28 | 29 | class TypeC(TypeA): 30 | pass 31 | 32 | 33 | BackendA = _TypedBackend(TypeA) 34 | BackendB = _TypedBackend(TypeB) 35 | BackendC = _TypedBackend(TypeC) 36 | BackendAB = _TypedBackend(TypeA, TypeB) 37 | BackendBC = _TypedBackend(TypeB, TypeC) 38 | 39 | creation_multimethod = ua.generate_multimethod( 40 | lambda: (), lambda a, kw, d: (a, kw), "ua_examples" 41 | ) 42 | call_multimethod = ua.generate_multimethod( 43 | lambda *a: tuple(ua.Dispatchable(x, "mark") for x in a), 44 | lambda a, kw, d: (a, kw), 45 | "ua_examples", 46 | ) 47 | -------------------------------------------------------------------------------- /src/uarray/tests/test_uarray.py: -------------------------------------------------------------------------------- 1 | import uarray as ua 2 | import pickle 3 | 4 | import pytest # type: ignore 5 | 6 | 7 | @pytest.fixture(scope="function", autouse=True) 8 | def cleanup_backends(): 9 | with ua.reset_state(): 10 | yield 11 | 12 | 13 | class Backend: 14 | __ua_domain__ = "ua_tests" 15 | 16 | 17 | class DisableBackend: 18 | def __init__(self, domain="ua_tests"): 19 | self.__ua_domain__ = domain 20 | self.active = True 21 | self.ret = object() 22 | 23 | def __ua_function__(self, f, a, kw): 24 | if self.active: 25 | return self.ret 26 | 27 | raise ua.BackendNotImplementedError(self.__ua_domain__) 28 | 29 | 30 | @pytest.fixture() 31 | def nullary_mm(): 32 | return ua.generate_multimethod(lambda: (), lambda a, kw, d: (a, kw), "ua_tests") 33 | 34 | 35 | def test_nestedbackend(nullary_mm): 36 | obj = object() 37 | be_outer = Backend() 38 | be_outer.__ua_function__ = lambda f, a, kw: obj 39 | 40 | def default(*a, **kw): 41 | return nullary_mm(*a, **kw) 42 | 43 | mm2 = ua.generate_multimethod( 44 | lambda: (), lambda a, kw, d: (a, kw), "ua_tests", default=default 45 | ) 46 | be_inner = Backend() 47 | 48 | def be2_ua_func(f, a, kw): 49 | with ua.skip_backend(be_inner): 50 | return f(*a, **kw) 51 | 52 | be_inner.__ua_function__ = be2_ua_func 53 | with ua.set_backend(be_outer), ua.set_backend(be_inner): 54 | assert mm2() is obj 55 | 56 | 57 | def _replacer(args, kwargs, dispatchables): 58 | return (args, kwargs) 59 | 60 | 61 | @ua.create_multimethod(_replacer, "ua_tests") 62 | def pickle_mm(): 63 | return () 64 | 65 | 66 | def test_pickle_support(): 67 | unpickle_mm = pickle.loads(pickle.dumps(pickle_mm)) 68 | 69 | assert unpickle_mm is pickle_mm 70 | 71 | 72 | def test_registration(nullary_mm): 73 | obj = object() 74 | be = Backend() 75 | be.__ua_function__ = lambda f, a, kw: obj 76 | 77 | ua.register_backend(be) 78 | assert nullary_mm() is obj 79 | 80 | 81 | def test_global(nullary_mm): 82 | obj = object() 83 | be = Backend() 84 | be.__ua_function__ = lambda f, a, kw: obj 85 | 86 | ua.set_global_backend(be) 87 | assert nullary_mm() is obj 88 | 89 | 90 | def ctx_before_global(nullary_mm): 91 | obj = object() 92 | obj2 = object() 93 | be = Backend() 94 | be.__ua_function__ = lambda f, a, kw: obj 95 | 96 | be2 = Backend() 97 | be2.__ua_function__ = lambda f, a, kw: obj2 98 | 99 | ua.set_global_backend(be) 100 | 101 | with ua.set_backend(be2): 102 | assert nullary_mm() is obj2 103 | 104 | 105 | def test_global_before_registered(nullary_mm): 106 | obj = object() 107 | obj2 = object() 108 | be = Backend() 109 | be.__ua_function__ = lambda f, a, kw: obj 110 | 111 | be2 = Backend() 112 | be2.__ua_function__ = lambda f, a, kw: obj2 113 | 114 | ua.set_global_backend(be) 115 | ua.register_backend(be2) 116 | assert nullary_mm() is obj 117 | 118 | 119 | def test_global_try_last(nullary_mm): 120 | obj = object() 121 | obj2 = object() 122 | be = Backend() 123 | be.__ua_function__ = lambda f, a, kw: obj 124 | 125 | be2 = Backend() 126 | be2.__ua_function__ = lambda f, a, kw: obj2 127 | 128 | ua.set_global_backend(be, try_last=True) 129 | ua.register_backend(be2) 130 | assert nullary_mm() is obj2 131 | 132 | 133 | def test_global_only(nullary_mm): 134 | obj = object() 135 | be = Backend() 136 | be.__ua_function__ = lambda f, a, kw: NotImplemented 137 | 138 | be2 = Backend() 139 | be2.__ua_function__ = lambda f, a, kw: obj 140 | 141 | ua.set_global_backend(be, only=True) 142 | ua.register_backend(be2) 143 | 144 | with pytest.raises(ua.BackendNotImplementedError): 145 | nullary_mm() 146 | 147 | 148 | def test_clear_backends(nullary_mm): 149 | obj = object() 150 | obj2 = object() 151 | be = Backend() 152 | be.__ua_function__ = lambda f, a, kw: obj 153 | 154 | be2 = Backend() 155 | be2.__ua_function__ = lambda f, a, kw: obj2 156 | 157 | ua.set_global_backend(be) 158 | ua.register_backend(be2) 159 | 160 | ua.clear_backends(Backend.__ua_domain__, registered=True, globals=True) 161 | with pytest.raises(ua.BackendNotImplementedError): 162 | nullary_mm() 163 | 164 | 165 | def test_function_attrs(): 166 | def extractor(): 167 | return () 168 | 169 | def replacer(a, kw, d): 170 | return a, kw 171 | 172 | def default(): 173 | return NotImplemented 174 | 175 | mm = ua.generate_multimethod(extractor, replacer, "ua_tests", default=default) 176 | 177 | assert mm.arg_extractor is extractor 178 | assert mm.arg_replacer is replacer 179 | assert mm.default is default 180 | assert mm.domain == "ua_tests" 181 | 182 | 183 | def test_raising_from_backend(nullary_mm): 184 | def raise_(foo): 185 | raise foo 186 | 187 | Foo = ua.BackendNotImplementedError("Foo") 188 | be = Backend() 189 | be.__ua_function__ = lambda f, a, kw: raise_(Foo) 190 | 191 | # BackendNotImplementedErrors are nested 192 | with ua.set_backend(be): 193 | with pytest.raises(ua.BackendNotImplementedError) as e: 194 | nullary_mm() 195 | 196 | assert ( 197 | e.value.args[0] 198 | == "No selected backends had an implementation for this function." 199 | ) 200 | assert type(e.value.args[1]) == tuple 201 | assert e.value.args[1] == (be, Foo) 202 | 203 | Bar = ua.BackendNotImplementedError("Bar") 204 | be2 = Backend() 205 | be2.__ua_function__ = lambda f, a, kw: raise_(Bar) 206 | # Errors are in the order the backends were tried 207 | with ua.set_backend(be), ua.set_backend(be2): 208 | with pytest.raises(ua.BackendNotImplementedError) as e: 209 | nullary_mm() 210 | 211 | assert e.value.args[1] == (be2, Bar) 212 | assert e.value.args[2] == (be, Foo) 213 | 214 | be3 = Backend() 215 | be3.__ua_function__ = lambda f, a, kw: "Success" 216 | # Can succeed after a backend has raised BackendNotImplementedError 217 | with ua.set_backend(be3), ua.set_backend(be): 218 | assert nullary_mm() == "Success" 219 | 220 | 221 | def test_nested(): 222 | be = Backend() 223 | be.__ua_function__ = lambda f, a, kw: None 224 | 225 | ctx = ua.set_backend(be) 226 | 227 | with ctx, ctx: 228 | pass 229 | 230 | 231 | def test_invalid(): 232 | be1 = Backend() 233 | be1.__ua_function__ = lambda f, a, kw: None 234 | 235 | be2 = Backend() 236 | be2.__ua_function__ = lambda f, a, kw: None 237 | 238 | ctx1 = ua.set_backend(be1) 239 | ctx2 = ua.set_backend(be2) 240 | 241 | with pytest.raises(RuntimeError): 242 | try: 243 | ctx1.__enter__() 244 | try: 245 | ctx2.__enter__() 246 | finally: 247 | ctx1.__exit__(None, None, None) 248 | finally: 249 | ctx2.__exit__(None, None, None) 250 | 251 | 252 | def test_skip_comparison(nullary_mm): 253 | be1 = Backend() 254 | be1.__ua_function__ = lambda f, a, kw: None 255 | 256 | class Backend2(Backend): 257 | @staticmethod 258 | def __ua_function__(f, a, kw): 259 | pass 260 | 261 | def __eq__(self, other): 262 | return other is self or other is be1 263 | 264 | with pytest.raises(ua.BackendNotImplementedError): 265 | with ua.set_backend(be1), ua.skip_backend(Backend2()): 266 | nullary_mm() 267 | 268 | 269 | def test_skip_raises(nullary_mm): 270 | be1 = Backend() 271 | be1.__ua_function__ = lambda f, a, kw: None 272 | 273 | foo = Exception("Foo") 274 | 275 | class Backend2(Backend): 276 | @staticmethod 277 | def __ua_function__(f, a, kw): 278 | pass 279 | 280 | def __eq__(self, other): 281 | raise foo 282 | 283 | with pytest.raises(Exception) as e: 284 | with ua.set_backend(be1), ua.skip_backend(Backend2()): 285 | nullary_mm() 286 | 287 | assert e.value is foo 288 | 289 | 290 | def test_getset_state(cleanup_backends): 291 | ua.set_global_backend(Backend()) 292 | ua.register_backend(Backend()) 293 | 294 | with ua.set_backend(Backend()), ua.skip_backend(Backend()): 295 | state = ua.get_state() 296 | 297 | pstate = state._pickle() 298 | 299 | assert pstate != ua.get_state()._pickle() 300 | 301 | with ua.set_state(state): 302 | assert pstate[:2] == ua.get_state()._pickle()[:2] 303 | 304 | 305 | class ComparableBackend(Backend): 306 | def __init__(self, obj): 307 | super().__init__() 308 | self.obj = obj 309 | 310 | def __eq__(self, other): 311 | return isinstance(other, ComparableBackend) and self.obj == other.obj 312 | 313 | def __ne__(self, other): 314 | return not (self == other) 315 | 316 | 317 | def test_pickle_state(): 318 | ua.set_global_backend(ComparableBackend("a")) 319 | ua.register_backend(ComparableBackend("b")) 320 | 321 | with ua.set_backend(ComparableBackend("c")), ua.skip_backend( 322 | ComparableBackend("d") 323 | ): 324 | state = ua.get_state() 325 | 326 | state_loaded = pickle.loads(pickle.dumps(state)) 327 | 328 | assert state._pickle() == state_loaded._pickle() 329 | 330 | 331 | def test_hierarchical_backends(): 332 | mm = ua.generate_multimethod( 333 | lambda: (), lambda a, kw, d: (a, kw), "ua_tests.foo.bar" 334 | ) 335 | subdomains = "ua_tests.foo.bar".split(".") 336 | depth = len(subdomains) 337 | 338 | mms = [ 339 | ua.generate_multimethod( 340 | lambda: (), lambda a, kw, d: (a, kw), ".".join(subdomains[: i + 1]) 341 | ) 342 | for i in range(depth) 343 | ] 344 | 345 | be = [DisableBackend(".".join(subdomains[: i + 1])) for i in range(depth)] 346 | 347 | ua.set_global_backend(be[1]) 348 | with pytest.raises(ua.BackendNotImplementedError): 349 | mms[0]() 350 | 351 | for i in range(1, depth): 352 | assert mms[i]() is be[1].ret 353 | 354 | ua.set_global_backend(be[0]) 355 | 356 | for i in range(depth): 357 | assert mms[i]() is be[min(i, 1)].ret 358 | 359 | ua.set_global_backend(be[2]) 360 | 361 | for i in range(depth): 362 | assert mms[i]() is be[i].ret 363 | 364 | be[2].active = False 365 | for i in range(depth): 366 | print(i) 367 | assert mms[i]() is be[min(i, 1)].ret 368 | 369 | be[1].active = False 370 | for i in range(depth): 371 | assert mms[i]() is be[0].ret 372 | 373 | be[0].active = False 374 | for i in range(depth): 375 | with pytest.raises(ua.BackendNotImplementedError): 376 | mms[i]() 377 | 378 | # only=True prevents all further domain checking 379 | be[0].active = True 380 | be[1].active = True 381 | with ua.set_backend(be[2], only=True), pytest.raises(ua.BackendNotImplementedError): 382 | mms[2]() 383 | 384 | 385 | def test_multidomain_backends(): 386 | n_domains = 2 387 | be = DisableBackend(domain=["ua_tests" + str(i) for i in range(n_domains)]) 388 | 389 | mms = [ 390 | ua.generate_multimethod( 391 | lambda: (), lambda a, kw, d: (a, kw), "ua_tests" + str(i) 392 | ) 393 | for i in range(n_domains) 394 | ] 395 | 396 | def assert_no_backends(): 397 | for i in range(len(mms)): 398 | with pytest.raises(ua.BackendNotImplementedError): 399 | mms[i]() 400 | 401 | def assert_backend_active(backend): 402 | assert all(mms[i]() is backend.ret for i in range(len(mms))) 403 | 404 | assert_no_backends() 405 | 406 | with ua.set_backend(be): 407 | assert_backend_active(be) 408 | 409 | ua.set_global_backend(be) 410 | assert_backend_active(be) 411 | 412 | with ua.skip_backend(be): 413 | assert_no_backends() 414 | 415 | assert_backend_active(be) 416 | 417 | for i in range(len(mms)): 418 | ua.clear_backends(mms[i].domain, globals=True) 419 | 420 | with pytest.raises(ua.BackendNotImplementedError): 421 | mms[i]() 422 | 423 | for j in range(i + 1, len(mms)): 424 | assert mms[j]() is be.ret 425 | 426 | assert_no_backends() 427 | 428 | ua.register_backend(be) 429 | assert_backend_active(be) 430 | 431 | 432 | def test_determine_backend(nullary_mm): 433 | class TypeA: 434 | pass 435 | 436 | class TypeB: 437 | pass 438 | 439 | mark = "determine_backend_test" 440 | 441 | class TypeBackend: 442 | __ua_domain__ = "ua_tests" 443 | 444 | def __init__(self, my_type): 445 | self.my_type = my_type 446 | 447 | def __ua_convert__(self, dispatchables, coerce): 448 | if not all( 449 | type(d.value) is self.my_type and d.type is mark for d in dispatchables 450 | ): 451 | return NotImplemented 452 | return tuple(d.value for d in dispatchables) 453 | 454 | def __ua_function__(self, func, args, kwargs): 455 | return self.my_type 456 | 457 | BackendA = TypeBackend(TypeA) 458 | BackendB = TypeBackend(TypeB) 459 | 460 | with ua.set_backend(BackendA), pytest.raises(ua.BackendNotImplementedError): 461 | with ua.determine_backend(TypeB(), mark, domain="ua_tests"): 462 | pass 463 | 464 | with ua.set_backend(BackendA), ua.set_backend(BackendB): 465 | with ua.determine_backend(TypeA(), mark, domain="ua_tests"): 466 | assert nullary_mm() is TypeA 467 | 468 | with ua.determine_backend(TypeB(), mark, domain="ua_tests"): 469 | assert nullary_mm() is TypeB 470 | 471 | # Has no __ua_convert__, so assumed to not accept the type 472 | with ua.set_backend(DisableBackend()), pytest.raises(ua.BackendNotImplementedError): 473 | with ua.determine_backend(TypeB(), mark, domain="ua_tests"): 474 | pass 475 | 476 | with ua.set_backend(BackendA), ua.set_backend(BackendB): 477 | with pytest.raises(ua.BackendNotImplementedError): 478 | with ua.determine_backend_multi( 479 | [ua.Dispatchable(TypeA(), mark), ua.Dispatchable(TypeB(), mark)], 480 | domain="ua_tests", 481 | ): 482 | pass 483 | 484 | with ua.determine_backend_multi( 485 | [ua.Dispatchable(TypeA(), mark), ua.Dispatchable(TypeA(), mark)], 486 | domain="ua_tests", 487 | ): 488 | assert nullary_mm() is TypeA 489 | 490 | 491 | def test_determine_backend_coerce(nullary_mm): 492 | class TypeA: 493 | pass 494 | 495 | class TypeB: 496 | pass 497 | 498 | mark = "determine_backend_test" 499 | 500 | class TypeBackend: 501 | __ua_domain__ = "ua_tests" 502 | 503 | def __init__(self, my_type): 504 | self.my_type = my_type 505 | 506 | def __ua_convert__(self, dispatchables, coerce): 507 | if len(dispatchables) > 0: 508 | print(dispatchables[0], coerce) 509 | if coerce and all(d.coercible for d in dispatchables): 510 | return tuple(self.my_type() for _ in dispatchables) 511 | 512 | if not all( 513 | type(d.value) is self.my_type and d.type is mark for d in dispatchables 514 | ): 515 | return NotImplemented 516 | return tuple(d.value for d in dispatchables) 517 | 518 | def __ua_function__(self, func, args, kwargs): 519 | return self.my_type 520 | 521 | BackendA = TypeBackend(TypeA) 522 | BackendB = TypeBackend(TypeB) 523 | unary_mm = ua.generate_multimethod( 524 | lambda a: (ua.Dispatchable(a, mark),), lambda a, kw, d: (d, kw), "ua_tests" 525 | ) 526 | 527 | # coercion is not forced on the existing set backend 528 | with ua.set_backend(BackendA), ua.set_backend(BackendB): 529 | with ua.determine_backend(TypeA(), mark, domain="ua_tests", coerce=True): 530 | assert nullary_mm() is TypeA 531 | assert unary_mm(TypeB()) is TypeA 532 | 533 | # But is allowed if the backend was set with coerce in the first place 534 | with ua.set_backend(BackendA), ua.set_backend(BackendB, coerce=True): 535 | with ua.determine_backend(TypeA(), mark, domain="ua_tests", coerce=True): 536 | assert nullary_mm() is TypeB 537 | assert unary_mm(TypeA()) is TypeB 538 | 539 | 540 | def test_default(nullary_mm): 541 | obj = object() 542 | be = Backend() 543 | be.__ua_function__ = lambda f, a, kw: NotImplemented 544 | 545 | # If a backend returns NotImplemented, the default is called 546 | def default1(*a, **kw): 547 | return obj 548 | 549 | mm1 = ua.generate_multimethod( 550 | lambda: (), lambda a, kw, d: (a, kw), "ua_tests", default=default1 551 | ) 552 | 553 | with ua.set_backend(be): 554 | assert mm1() is obj 555 | 556 | # If all backends fail, the default is called again without a specific backend 557 | num_calls = [0] 558 | 559 | def default2(*a, **kw): 560 | num_calls[0] = num_calls[0] + 1 561 | raise ua.BackendNotImplementedError() 562 | 563 | mm2 = ua.generate_multimethod( 564 | lambda: (), lambda a, kw, d: (a, kw), "ua_tests", default=default2 565 | ) 566 | 567 | with ua.set_backend(be), pytest.raises(ua.BackendNotImplementedError): 568 | mm2() 569 | 570 | assert num_calls[0] == 2 571 | 572 | # If the last backend is set as only or coerce, the last default call is skipped 573 | num_calls[0] = 0 574 | with ua.set_backend(be, only=True), pytest.raises(ua.BackendNotImplementedError): 575 | mm2() 576 | assert num_calls[0] == 1 577 | num_calls[0] = 0 578 | with ua.set_backend(be, coerce=True), pytest.raises(ua.BackendNotImplementedError): 579 | mm2() 580 | assert num_calls[0] == 1 581 | -------------------------------------------------------------------------------- /src/vectorcall.cxx: -------------------------------------------------------------------------------- 1 | #include "vectorcall.h" 2 | 3 | #ifdef PYPY_VERSION 4 | 5 | /* PyPy doesn't have any support for Vectorcall/FastCall. 6 | * These helpers are for translating to PyObject_Call. */ 7 | 8 | static PyObject * build_arg_tuple(PyObject * const * args, Py_ssize_t nargs) { 9 | PyObject * tuple = PyTuple_New(nargs); 10 | if (!tuple) { 11 | return NULL; 12 | } 13 | 14 | for (Py_ssize_t i = 0; i < nargs; ++i) { 15 | Py_INCREF(args[i]); /* SET_ITEM steals a reference */ 16 | PyTuple_SET_ITEM(tuple, i, args[i]); 17 | } 18 | return tuple; 19 | } 20 | 21 | static PyObject * build_kwarg_dict( 22 | PyObject * const * args, PyObject * names, Py_ssize_t nargs) { 23 | PyObject * dict = PyDict_New(); 24 | if (!dict) { 25 | return NULL; 26 | } 27 | 28 | for (Py_ssize_t i = 0; i < nargs; ++i) { 29 | PyObject * key = PyTuple_GET_ITEM(names, i); 30 | int success = PyDict_SetItem(dict, key, args[i]); 31 | if (success == -1) { 32 | Py_DECREF(dict); 33 | return NULL; 34 | } 35 | } 36 | return dict; 37 | } 38 | #elif (PY_VERSION_HEX < 0x03090000) 39 | // clang-format off 40 | 41 | static int is_method_descr(PyTypeObject* descr_tp) { 42 | return ( 43 | (descr_tp->tp_flags & Q_Py_TPFLAGS_METHOD_DESCRIPTOR) != 0 || 44 | (descr_tp == &PyFunction_Type) || 45 | (descr_tp == &PyMethodDescr_Type)); 46 | } 47 | 48 | /* The below code is derivative of CPython source, and is taken because 49 | _PyObject_GetMethod is not exported from shared objects. 50 | 51 | Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 52 | 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation; 53 | All Rights Reserved 54 | */ 55 | 56 | /* Specialized version of _PyObject_GenericGetAttrWithDict 57 | specifically for the LOAD_METHOD opcode. 58 | 59 | Return 1 if a method is found, 0 if it's a regular attribute 60 | from __dict__ or something returned by using a descriptor 61 | protocol. 62 | 63 | `method` will point to the resolved attribute or NULL. In the 64 | latter case, an error will be set. 65 | */ 66 | static int _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) 67 | { 68 | PyTypeObject *tp = Py_TYPE(obj); 69 | PyObject *descr; 70 | descrgetfunc f = NULL; 71 | PyObject **dictptr, *dict; 72 | PyObject *attr; 73 | int meth_found = 0; 74 | 75 | assert(*method == NULL); 76 | 77 | if (Py_TYPE(obj)->tp_getattro != PyObject_GenericGetAttr 78 | || !PyUnicode_Check(name)) { 79 | *method = PyObject_GetAttr(obj, name); 80 | return 0; 81 | } 82 | 83 | if (tp->tp_dict == NULL && PyType_Ready(tp) < 0) 84 | return 0; 85 | 86 | descr = _PyType_Lookup(tp, name); 87 | if (descr != NULL) { 88 | Py_INCREF(descr); 89 | if (is_method_descr(Py_TYPE(descr))) { 90 | meth_found = 1; 91 | } else { 92 | f = Py_TYPE(descr)->tp_descr_get; 93 | if (f != NULL && PyDescr_IsData(descr)) { 94 | *method = f(descr, obj, (PyObject *)Py_TYPE(obj)); 95 | Py_DECREF(descr); 96 | return 0; 97 | } 98 | } 99 | } 100 | 101 | dictptr = _PyObject_GetDictPtr(obj); 102 | if (dictptr != NULL && (dict = *dictptr) != NULL) { 103 | Py_INCREF(dict); 104 | attr = PyDict_GetItemWithError(dict, name); 105 | if (attr != NULL) { 106 | Py_INCREF(attr); 107 | *method = attr; 108 | Py_DECREF(dict); 109 | Py_XDECREF(descr); 110 | return 0; 111 | } 112 | else { 113 | Py_DECREF(dict); 114 | if (PyErr_Occurred()) { 115 | Py_XDECREF(descr); 116 | return 0; 117 | } 118 | } 119 | } 120 | 121 | if (meth_found) { 122 | *method = descr; 123 | return 1; 124 | } 125 | 126 | if (f != NULL) { 127 | *method = f(descr, obj, (PyObject *)Py_TYPE(obj)); 128 | Py_DECREF(descr); 129 | return 0; 130 | } 131 | 132 | if (descr != NULL) { 133 | *method = descr; 134 | return 0; 135 | } 136 | 137 | PyErr_Format(PyExc_AttributeError, 138 | "'%.50s' object has no attribute '%U'", 139 | tp->tp_name, name); 140 | return 0; 141 | } 142 | 143 | // clang-format on 144 | #endif /* PYPY_VERSION */ 145 | 146 | 147 | Py_ssize_t Q_PyVectorcall_NARGS(size_t n) { 148 | return n & (~Q_PY_VECTORCALL_ARGUMENTS_OFFSET); 149 | } 150 | 151 | PyObject * Q_PyObject_Vectorcall( 152 | PyObject * callable, PyObject * const * args, size_t nargsf, 153 | PyObject * kwnames) { 154 | #ifdef PYPY_VERSION 155 | PyObject * dict = NULL; 156 | Py_ssize_t nargs = Q_PyVectorcall_NARGS(nargsf); 157 | if (kwnames) { 158 | Py_ssize_t nkwargs = PyTuple_GET_SIZE(kwnames); 159 | dict = build_kwarg_dict(&args[nargs - nkwargs], kwnames, nkwargs); 160 | if (!dict) { 161 | return NULL; 162 | } 163 | nargs -= nkwargs; 164 | } 165 | PyObject * ret = Q_PyObject_VectorcallDict(callable, args, nargs, dict); 166 | Py_XDECREF(dict); 167 | return ret; 168 | #elif (PY_VERSION_HEX >= 0x03090000) 169 | return PyObject_Vectorcall(callable, args, nargsf, kwnames); 170 | #elif (PY_VERSION_HEX >= 0x03080000) 171 | return _PyObject_Vectorcall(callable, args, nargsf, kwnames); 172 | #else 173 | Py_ssize_t nargs = Q_PyVectorcall_NARGS(nargsf); 174 | return _PyObject_FastCallKeywords( 175 | callable, (PyObject **)args, nargs, kwnames); 176 | #endif 177 | } 178 | 179 | PyObject * Q_PyObject_VectorcallDict( 180 | PyObject * callable, PyObject * const * args, size_t nargsf, 181 | PyObject * kwdict) { 182 | #ifdef PYPY_VERSION 183 | Py_ssize_t nargs = Q_PyVectorcall_NARGS(nargsf); 184 | PyObject * tuple = build_arg_tuple(args, nargs); 185 | if (!tuple) { 186 | return NULL; 187 | } 188 | PyObject * ret = PyObject_Call(callable, tuple, kwdict); 189 | Py_DECREF(tuple); 190 | return ret; 191 | #elif (PY_VERSION_HEX >= 0x03090000) 192 | return PyObject_VectorcallDict(callable, args, nargsf, kwdict); 193 | #else 194 | Py_ssize_t nargs = Q_PyVectorcall_NARGS(nargsf); 195 | return _PyObject_FastCallDict(callable, (PyObject **)args, nargs, kwdict); 196 | #endif 197 | } 198 | 199 | PyObject * Q_PyObject_VectorcallMethod( 200 | PyObject * name, PyObject * const * args, size_t nargsf, 201 | PyObject * kwnames) { 202 | #ifdef PYPY_VERSION 203 | PyObject * callable = PyObject_GetAttr(args[0], name); 204 | if (!callable) { 205 | return NULL; 206 | } 207 | PyObject * result = 208 | Q_PyObject_Vectorcall(callable, &args[1], nargsf - 1, kwnames); 209 | Py_DECREF(callable); 210 | return result; 211 | #elif (PY_VERSION_HEX >= 0x03090000) 212 | return PyObject_VectorcallMethod(name, args, nargsf, kwnames); 213 | #else 214 | /* Private CPython code for CALL_METHOD opcode */ 215 | PyObject * callable = NULL; 216 | int unbound = _PyObject_GetMethod(args[0], name, &callable); 217 | if (callable == NULL) { 218 | return NULL; 219 | } 220 | 221 | if (unbound) { 222 | /* We must remove PY_VECTORCALL_ARGUMENTS_OFFSET since 223 | * that would be interpreted as allowing to change args[-1] */ 224 | nargsf &= ~Q_PY_VECTORCALL_ARGUMENTS_OFFSET; 225 | } else { 226 | /* Skip "self". We can keep PY_VECTORCALL_ARGUMENTS_OFFSET since 227 | * args[-1] in the onward call is args[0] here. */ 228 | args++; 229 | nargsf--; 230 | } 231 | PyObject * result = Q_PyObject_Vectorcall(callable, args, nargsf, kwnames); 232 | Py_DECREF(callable); 233 | return result; 234 | #endif 235 | } 236 | -------------------------------------------------------------------------------- /src/vectorcall.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | // True if python supports vectorcall on custom classes 9 | #if (!defined(PYPY_VERSION) && (PY_VERSION_HEX >= 0x03080000)) 10 | # define Q_HAS_VECTORCALL 1 11 | #else 12 | # define Q_HAS_VECTORCALL 0 13 | #endif 14 | 15 | #if !Q_HAS_VECTORCALL 16 | # define Q_Py_TPFLAGS_HAVE_VECTORCALL 0 17 | #elif (PY_VERSION_HEX >= 0x03090000) 18 | # define Q_Py_TPFLAGS_HAVE_VECTORCALL Py_TPFLAGS_HAVE_VECTORCALL 19 | #else 20 | # define Q_Py_TPFLAGS_HAVE_VECTORCALL _Py_TPFLAGS_HAVE_VECTORCALL 21 | #endif 22 | 23 | #if !Q_HAS_VECTORCALL 24 | # define Q_Py_TPFLAGS_METHOD_DESCRIPTOR 0 25 | #else 26 | # define Q_Py_TPFLAGS_METHOD_DESCRIPTOR Py_TPFLAGS_METHOD_DESCRIPTOR 27 | #endif 28 | 29 | #if !Q_HAS_VECTORCALL 30 | # define Q_PY_VECTORCALL_ARGUMENTS_OFFSET \ 31 | ((size_t)1 << (8 * sizeof(size_t) - 1)) 32 | #else 33 | # define Q_PY_VECTORCALL_ARGUMENTS_OFFSET PY_VECTORCALL_ARGUMENTS_OFFSET 34 | #endif 35 | 36 | 37 | Py_ssize_t Q_PyVectorcall_NARGS(size_t n); 38 | 39 | PyObject * Q_PyObject_Vectorcall( 40 | PyObject * callable, PyObject * const * args, size_t nargsf, 41 | PyObject * kwnames); 42 | PyObject * Q_PyObject_VectorcallDict( 43 | PyObject * callable, PyObject * const * args, size_t nargsf, 44 | PyObject * kwdict); 45 | PyObject * Q_PyObject_VectorcallMethod( 46 | PyObject * name, PyObject * const * args, size_t nargsf, PyObject * kwdict); 47 | 48 | 49 | #ifdef __cplusplus 50 | } // extern "C" 51 | #endif 52 | --------------------------------------------------------------------------------