pax_global_header00006660000000000000000000000064147334057240014523gustar00rootroot0000000000000052 comment=5a9b962a6fbb5e722fedbbccb66c4993de474178 multimethod-2.0/000077500000000000000000000000001473340572400137175ustar00rootroot00000000000000multimethod-2.0/.github/000077500000000000000000000000001473340572400152575ustar00rootroot00000000000000multimethod-2.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001473340572400174425ustar00rootroot00000000000000multimethod-2.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000015011473340572400221310ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. Please open separate issues if there are multiple topics. **To Reproduce** Please isolate the issue to the smallest reproducible example, with no dependencies. Most dispatch errors are caused by an unsupported type - one that does not implement `issubclass` correctly. **Expected behavior** For relevant types, what is the output and expectation of: ```python issubclass(..., MyType) issubclass(MyType, ...) ``` If those error, `subtype` can be used to check if `multitmethod` has custom support. What is the the output and expectation of: ```python from multimethod import subtype issubclass(..., subtype(MyType)) issubclass(subtype(MyType), ...) ``` multimethod-2.0/.github/dependabot.yml000066400000000000000000000003171473340572400201100ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" multimethod-2.0/.github/workflows/000077500000000000000000000000001473340572400173145ustar00rootroot00000000000000multimethod-2.0/.github/workflows/build.yml000066400000000000000000000020011473340572400211270ustar00rootroot00000000000000name: build on: workflow_dispatch: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14.0-alpha - 3.14'] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: pip install -r tests/requirements.in - run: make check - run: coverage xml - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install ruff mypy - run: make lint docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install -r docs/requirements.in - run: make html multimethod-2.0/.github/workflows/codspeed.yml000066400000000000000000000006771473340572400216370ustar00rootroot00000000000000name: codspeed-benchmarks on: workflow_dispatch: push: branches: [main] pull_request: branches: [main] jobs: benchmarks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install -r tests/requirements.in - uses: CodSpeedHQ/action@v3 with: token: ${{ secrets.CODSPEED_TOKEN }} run: make bench multimethod-2.0/.github/workflows/release.yml000066400000000000000000000006501473340572400214600ustar00rootroot00000000000000name: release on: push: tags: - 'v*' jobs: publish: runs-on: ubuntu-latest permissions: write-all steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install build -r docs/requirements.in - run: python -m build - run: PYTHONPATH=$PWD mkdocs gh-deploy --force - uses: pypa/gh-action-pypi-publish@release/v1 multimethod-2.0/.gitignore000066400000000000000000000000351473340572400157050ustar00rootroot00000000000000__pycache__/ .coverage site/ multimethod-2.0/CHANGELOG.md000066400000000000000000000102001473340572400155210ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## Unreleased ## [2.0](https://pypi.org/project/multimethod/2.0/) - 2024-12-26 ### Removed * Resolving ambiguity using positional distance * `overload` is redundant with instance checking ## [1.12](https://pypi.org/project/multimethod/1.12/) - 2024-07-04 ### Changed * `multidispatch` supports variable keyword arguments * `multidispatch` base implementation always matches * Type and Annotated aliases supported ## [1.11.2](https://pypi.org/project/multimethod/1.11.2/) - 2024-02-27 ### Fixed * Subclassed generics allowed ## [1.11.1](https://pypi.org/project/multimethod/1.11.1/) - 2024-02-19 ### Fixed * Unions with custom metaclasses supported * Concrete protocol classes supported ### Deprecated * Dispatch on base implementation of `multidispatch` ## [1.11](https://pypi.org/project/multimethod/1.11/) - 2024-01-28 ### Changed * Python >=3.9 required * `isinstance` and generic dispatch optimized ### Deprecated * Resolving ambiguity using positional distance * `overload` is redundant with instance checking ### Added * Custom instance checks * `subtype` and `parametric` utilities ## [1.10](https://pypi.org/project/multimethod/1.10/) - 2023-09-21 ### Changed * Python >=3.8 required ### Added * `Type[...]` dispatches on class arguments * `|` syntax for union types * `overload` supports generics and forward references * Dispatch on optional parameters ## [1.9.1](https://pypi.org/project/multimethod/1.9.1/) - 2022-12-21 ### Fixed * Dispatch is thread-safe ## [1.9](https://pypi.org/project/multimethod/1.9/) - 2022-09-14 ### Changed * Python 3.11 supported ### Fixed * Fixes for `Callable` and `object` annotations ## [1.8](https://pypi.org/project/multimethod/1.8/) - 2022-04-07 * `Callable` checks parameters and return type * Support for `NewType` ## [1.7](https://pypi.org/project/multimethod/1.7/) - 2022-01-28 * `overload` allows types and converts them to an `isa` check * Only functions with docstrings combine signatures * Fixes for subscripted union and literal checks ## [1.6](https://pypi.org/project/multimethod/1.6/) - 2021-09-12 * Python >=3.7 required * Improved checking for TypeErrors * `multidispatch` has provisional support for dispatching on keyword arguments * `multidispatch` supports static analysis of return type * Fix for forward references and subscripts * Checking type subscripts is done minimally based on each parameter * Provisionally dispatch on `Literal` type * Provisionally empty iterables match subscript ## [1.5](https://pypi.org/project/multimethod/1.5/) - 2021-01-29 * Postponed evaluation of nested annotations * Variable-length tuples of homogeneous type * Ignore default and keyword-only parameters * Resolved ambiguous `Union` types * Fixed an issue with name collision when defining a multimethod * Resolved dispatch errors when annotating parameters with meta-types such as `type` ## [1.4](https://pypi.org/project/multimethod/1.4/) - 2020-08-05 * Python >=3.6 required * Expanded support for subscripted type hints ## [1.3](https://pypi.org/project/multimethod/1.3/) - 2022-02-19 * Python 3 required * Support for subscripted ABCs ## [1.2](https://pypi.org/project/multimethod/1.2/) - 2019-12-07 * Support for typing generics * Stricter dispatching consistent with singledispatch ## [1.1](https://pypi.org/project/multimethod/1.1/) - 2019-06-17 * Fix for Python 2 typing backport * Metaclass for automatic multimethods ## [1.0](https://pypi.org/project/multimethod/1.0/) - 2018-12-07 * Missing annotations default to object * Removed deprecated dispatch stacking ## [0.7](https://pypi.org/project/multimethod/0.7/) - 2017-12-07 * Forward references allowed in type hints * Register method * Overloads with predicate dispatch ## [0.6](https://pypi.org/project/multimethod/0.6/) - 2017-01-02 * Multimethods can be defined inside a class ## [0.5](https://pypi.org/project/multimethod/0.5/) - 2015-09-03 * Optimized dispatching * Support for `functools.singledispatch` syntax ## [0.4](https://pypi.org/project/multimethod/0.4/) - 2013-11-10 * Dispatch on Python 3 annotations multimethod-2.0/CONTRIBUTING.md000066400000000000000000000014301473340572400161460ustar00rootroot00000000000000# Contributing to `multimethod` First off, thanks for taking the time to contribute. ## Reporting bugs The project uses GitHub's [issue tracker](https://github.com/coady/multimethod/issues). Please isolate the issue to the smallest reproducible example, with no dependencies. Most dispatch errors are caused by an unsupported type - one that does not implement `issubclass` correctly. The bug report template has specific suggestions on how to check if a type is compatible. ## Feature Requests Feel free to open a new [discussion](https://github.com/coady/multimethod/discussions) for ideas, or an issue for concrete proposals. ## Contributing to code Contributions are welcome, though note that the code base is small and typically feature-complete. Starting with an issue is helpful. multimethod-2.0/LICENSE.txt000066400000000000000000000011001473340572400155320ustar00rootroot00000000000000Copyright 2022 Aric Coady Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. multimethod-2.0/Makefile000066400000000000000000000003361473340572400153610ustar00rootroot00000000000000check: python -m pytest -s --cov bench: python -m pytest --codspeed lint: ruff check . ruff format --check . mypy -p multimethod mypy tests/static.py | grep -qv Any html: PYTHONPATH=$(PWD) python -m mkdocs build multimethod-2.0/README.md000066400000000000000000000160671473340572400152100ustar00rootroot00000000000000[![image](https://img.shields.io/pypi/v/multimethod.svg)](https://pypi.org/project/multimethod/) ![image](https://img.shields.io/pypi/pyversions/multimethod.svg) [![image](https://pepy.tech/badge/multimethod)](https://pepy.tech/project/multimethod) ![image](https://img.shields.io/pypi/status/multimethod.svg) [![build](https://github.com/coady/multimethod/actions/workflows/build.yml/badge.svg)](https://github.com/coady/multimethod/actions/workflows/build.yml) [![image](https://codecov.io/gh/coady/multimethod/branch/main/graph/badge.svg)](https://codecov.io/gh/coady/multimethod/) [![CodeQL](https://github.com/coady/multimethod/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/coady/multimethod/actions/workflows/github-code-scanning/codeql) [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/coady/multimethod) [![image](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![image](https://mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) Multimethod provides a decorator for adding multiple argument dispatching to functions. The decorator creates a multimethod object as needed, and registers the function with its annotations. There are several multiple dispatch libraries on PyPI. This one aims for simplicity and speed. With caching of argument types, it should be the fastest pure Python implementation possible. ## Usage There are a couple options which trade-off dispatch speed for flexibility. Decorator | Speed | Dispatch | Arguments --------- | ----- | -------- | --------- [multimethod](#multimethod) | faster | cached lookup | positional only [multidispatch](#multidispatch) | slower | binds to first signature + cached lookup | positional + keywords Dispatching on simple types which use `issubclass` is cached. Advanced types which use `isinstance` require a linear scan. ### multimethod ```python from multimethod import multimethod @multimethod def func(x: int, y: float): ... ``` `func` is now a `multimethod` which will delegate to the above function, when called with arguments of the specified types. Subsequent usage will register new types and functions to the existing multimethod of the same name. ```python @multimethod def func(x: float, y: int): ... ``` Alternatively, functions can be explicitly registered in the same style as [functools.singledispatch](https://docs.python.org/3/library/functools.html#functools.singledispatch). This syntax is also compatible with [mypy](https://mypy-lang.org), which by default checks that [each name is defined once](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-each-name-is-defined-once-no-redef). ```python @func.register def _(x: bool, y: bool): ... @func.register(object, bool) @func.register(bool, object) def _(x, y): # stackable without annotations ... ``` Multimethods are implemented as mappings from signatures to functions, and can be introspected as such. ```python method[type, ...] # get registered function method[type, ...] = func # register function by explicit types ``` Multimethods support any types that satisfy the `issubclass` relation, including abstract base classes in `collections.abc`. Note `typing` aliases do not support `issubclass` consistently, and are no longer needed for subscripts. Using ABCs instead is recommended. Subscripted generics are supported: * `Union[...]` or `... | ...` * `Mapping[...]` - the first key-value pair is checked * `tuple[...]` - all args are checked * `Iterable[...]` - the first arg is checked * `Type[...]` * `Literal[...]` * `Callable[[...], ...]` - parameter types are contravariant, return type is covariant Naturally checking subscripts is slower, but the implementation is optimized, cached, and bypassed if no subscripts are in use in the parameter. Empty iterables match any subscript, but don't special-case how the types are normally resolved. Dispatch resolution details: * If an exact match isn't registered, the next closest method is called (and cached). * If there are ambiguous methods - or none - a custom `TypeError` is raised. * Keyword-only parameters may be annotated, but won't affect dispatching. * A skipped annotation is equivalent to `: object`. * If no types are specified, it will inherently match all arguments. ### multidispatch `multidispatch` is a wrapper to provide compatibility with `functools.singledispatch`. It requires a base implementation and use of the `register` method instead of namespace lookup. It also supports dispatching on keyword arguments. ### instance checks `subtype` provisionally provides `isinstance` and `issubclass` checks for generic types. When called on a non-generic, it will return the origin type. ```python from multimethod import subtype cls = subtype(int | list[int]) for obj in (0, False, [0], [False], []): assert isinstance(obj, cls) for obj in (0.0, [0.0], (0,)): assert not isinstance(obj, cls) for subclass in (int, bool, list[int], list[bool]): assert issubclass(subclass, cls) for subclass in (float, list, list[float], tuple[int]): assert not issubclass(subclass, cls) ``` If a type implements a custom `__instancecheck__`, it can opt-in to dispatch (without caching) by registering its metaclass and bases with `subtype.origins`. `parametric` provides a convenient constructor, which will match the base class, predicate functions, and check attributes. ```python from multimethod import parametric Coroutine = parametric(Callable, inspect.iscoroutinefunction) IntArray = parametric(array, typecode='i') ``` ### classes `classmethod` and `staticmethod` may be used with a multimethod, but must be applied _last_, i.e., wrapping the final multimethod definition after all functions are registered. For class and instance methods, `cls` and `self` participate in the dispatch as usual. They may be left blank when using annotations, otherwise use `object` as a placeholder. ```python class Cls: # @classmethod: only works here if there are no more functions @multimethod def meth(cls, arg: str): ... # @classmethod: can not be used with `register` because `_` is not the multimethod @meth.register def _(cls, arg: int): ... meth = classmethod(meth) # done with registering ``` If a method spans multiple classes, then the namespace lookup can not work. The `register` method can be used instead. ```python class Base: @multimethod def meth(self, arg: str): ... class Subclass(Base): @Base.meth.register def _(self, arg: int): ... ``` If the base class can not be modified, the decorator - like any - can be called explicitly. ```python class Subclass(Base): meth = multimethod(Base.meth) ... ``` `multimeta` creates a class with a special namespace which converts callables to multimethods, and registers duplicate callables with the original. ```python class Cls(metaclass=multimeta): ... # all methods are multimethods ``` ## Installation ```console % pip install multimethod ``` ## Tests 100% branch coverage. ```console % pytest [--cov] ``` multimethod-2.0/docs/000077500000000000000000000000001473340572400146475ustar00rootroot00000000000000multimethod-2.0/docs/examples.ipynb000066400000000000000000000141521473340572400175330ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Examples\n", "## multimethod\n", "Multimethods are a mapping of signatures (tuple of types) to functions. They maintain an efficient dispatch tree, and cache the called signatures." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from multimethod import multimethod\n", "import operator\n", "\n", "classic_div = multimethod(operator.truediv)\n", "classic_div[int, int] = operator.floordiv\n", "classic_div" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "classic_div(3, 2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "classic_div(3.0, 2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "classic_div" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Multimethods introspect type annotations and use the name to find existing multimethods." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import itertools\n", "from collections.abc import Iterable, Sequence\n", "\n", "\n", "@multimethod\n", "def batched(values: Iterable, size):\n", " it = iter(values)\n", " return iter(lambda: list(itertools.islice(it, size)), [])\n", "\n", "\n", "@multimethod\n", "def batched(values: Sequence, size):\n", " for index in range(0, len(values), size):\n", " yield values[index : index + size]\n", "\n", "\n", "list(batched(iter('abcde'), 3))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "list(batched('abcde', 3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Multimethods also have an explicit `register` method similar to `functools.singledispatch`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@multimethod\n", "def window(values, size=2):\n", " its = itertools.tee(values, size)\n", " return zip(*(itertools.islice(it, index, None) for index, it in enumerate(its)))\n", "\n", "\n", "@window.register\n", "def _(values: Sequence, size=2):\n", " for index in range(len(values) - size + 1):\n", " yield values[index : index + size]\n", "\n", "\n", "list(window(iter('abcde')))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "list(window('abcde'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## parametric\n", "In addition to `issubclass`, multimethods can dispatch on `isinstance` with parametric checks." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import asyncio\n", "import inspect\n", "import time\n", "from collections.abc import Callable\n", "from concurrent import futures\n", "from multimethod import parametric\n", "\n", "Coroutine = parametric(Callable, inspect.iscoroutinefunction)\n", "\n", "\n", "@multimethod\n", "def wait(timeout, func, *args):\n", " return futures.ThreadPoolExecutor().submit(func, *args).result(timeout)\n", "\n", "\n", "@multimethod\n", "async def wait(timeout, func: Coroutine, *args):\n", " return await asyncio.wait_for(func(*args), timeout)\n", "\n", "\n", "wait(0.5, time.sleep, 0.01)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "wait(0.5, asyncio.sleep, 0.01)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from array import array\n", "\n", "IntArray = parametric(array, typecode='i')\n", "isinstance(array('i'), IntArray)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "isinstance(array('f'), IntArray)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## typing subscripts\n", "Support for type hints with subscripts." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import bisect\n", "import random\n", "\n", "\n", "@multimethod\n", "def samples(weights: dict):\n", " \"\"\"Generate weighted random samples using bisection.\"\"\"\n", " keys = list(weights)\n", " totals = list(itertools.accumulate(weights.values()))\n", " values = [total / totals[-1] for total in totals]\n", " while True:\n", " yield keys[bisect.bisect_right(values, random.random())]\n", "\n", "\n", "@multimethod\n", "def samples(weights: dict[object, int]):\n", " \"\"\"Generate weighted random samples more efficiently.\"\"\"\n", " keys = list(itertools.chain.from_iterable([key] * weights[key] for key in weights))\n", " while True:\n", " yield random.choice(keys)\n", "\n", "\n", "weights = {'a': 1, 'b': 2, 'c': 3}\n", "next(samples(weights))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "weights = {'a': 1.0, 'b': 2.0, 'c': 3.0}\n", "next(samples(weights))" ] } ], "metadata": { "celltoolbar": "Edit Metadata", "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.0" } }, "nbformat": 4, "nbformat_minor": 2 } multimethod-2.0/docs/index.md000077700000000000000000000000001473340572400177652../README.mdustar00rootroot00000000000000multimethod-2.0/docs/reference.md000066400000000000000000000002521473340572400171260ustar00rootroot00000000000000::: multimethod.multimethod ::: multimethod.multidispatch ::: multimethod.subtype ::: multimethod.parametric ::: multimethod.multimeta ::: multimethod.DispatchError multimethod-2.0/docs/requirements.in000066400000000000000000000000441473340572400177200ustar00rootroot00000000000000mkdocstrings[python] mkdocs-jupyter multimethod-2.0/mkdocs.yml000066400000000000000000000007731473340572400157310ustar00rootroot00000000000000site_name: multimethod site_url: https://coady.github.io/multimethod/ site_description: Multiple argument dispatching. theme: material repo_name: coady/multimethod repo_url: https://github.com/coady/multimethod edit_uri: "" nav: - Introduction: index.md - Reference: reference.md - Examples: examples.ipynb plugins: - search - mkdocstrings: handlers: python: options: show_root_heading: true - mkdocs-jupyter: execute: true allow_errors: false multimethod-2.0/multimethod/000077500000000000000000000000001473340572400162525ustar00rootroot00000000000000multimethod-2.0/multimethod/__init__.py000066400000000000000000000377731473340572400204040ustar00rootroot00000000000000import abc import collections import contextlib import functools import inspect import itertools import types import typing from collections.abc import Callable, Iterable, Iterator, Mapping from typing import Any, Literal, Optional, TypeVar, Union, get_type_hints, overload class DispatchError(TypeError): pass def get_origin(tp): return tp.__origin__ if isinstance(tp, subtype) else typing.get_origin(tp) def get_args(tp) -> tuple: if isinstance(tp, subtype) or typing.get_origin(tp) is Callable: return getattr(tp, '__args__', ()) return typing.get_args(tp) def get_mro(cls) -> tuple: # `inspect.getmro` doesn't handle all cases return tuple(type.mro(cls)) if isinstance(cls, type) else cls.mro() def common_bases(*bases): counts = collections.Counter() for base in bases: counts.update(cls for cls in get_mro(base) if issubclass(abc.ABCMeta, type(cls))) return tuple(cls for cls in counts if counts[cls] == len(bases)) class subtype(abc.ABCMeta): """A normalized generic type which checks subscripts. Transforms a generic alias into a concrete type which supports `issubclass` and `isinstance`. If the type ends up being equivalent to a builtin, the builtin is returned. """ __origin__: type __args__: tuple def __new__(cls, tp, *args): if tp is Any: return object if hasattr(tp, '__supertype__'): # isinstance(..., NewType) only supported >=3.10 return cls(tp.__supertype__, *args) if hasattr(typing, 'TypeAliasType') and isinstance(tp, typing.TypeAliasType): return cls(tp.__value__, *args) if isinstance(tp, TypeVar): return cls(Union[tp.__constraints__], *args) if tp.__constraints__ else object if isinstance(tp, typing._AnnotatedAlias): return cls(tp.__origin__, *args) origin = get_origin(tp) or tp if hasattr(types, 'UnionType') and isinstance(tp, types.UnionType): origin = Union # `|` syntax added in 3.10 args = tuple(map(cls, get_args(tp) or args)) if set(args) <= {object} and not (origin is tuple and args): return origin bases = (origin,) if type(origin) in (type, abc.ABCMeta) else () if origin is Literal: bases = (cls(Union[tuple(map(type, args))]),) if origin is Union: bases = common_bases(*args)[:1] if bases[0] in args: return bases[0] if origin is Callable and args[:1] == (...,): args = args[1:] namespace = {'__origin__': origin, '__args__': args} return type.__new__(cls, str(tp), bases, namespace) def __init__(self, tp, *args): ... def key(self) -> tuple: return self.__origin__, *self.__args__ def __eq__(self, other) -> bool: return hasattr(other, '__origin__') and self.key() == subtype.key(other) def __hash__(self) -> int: return hash(self.key()) def __subclasscheck__(self, subclass): origin = get_origin(subclass) or subclass args = get_args(subclass) if origin is Literal: return all(isinstance(arg, self) for arg in args) if origin is Union: return all(issubclass(cls, self) for cls in args) if self.__origin__ is Literal: return False if self.__origin__ is Union: return issubclass(subclass, self.__args__) if self.__origin__ is Callable: return ( origin is Callable and signature(self.__args__[-1:]) <= signature(args[-1:]) # covariant return and signature(args[:-1]) <= signature(self.__args__[:-1]) # contravariant args ) return ( # check args first to avoid recursion error: python/cpython#73407 len(args) == len(self.__args__) and issubclass(origin, self.__origin__) and all(pair[0] is pair[1] or issubclass(*pair) for pair in zip(args, self.__args__)) ) def __instancecheck__(self, instance): if self.__origin__ is Literal: return any(type(arg) is type(instance) and arg == instance for arg in self.__args__) if self.__origin__ is Union: return isinstance(instance, self.__args__) if hasattr(instance, '__orig_class__'): # user-defined generic type return issubclass(instance.__orig_class__, self) if self.__origin__ is type: # a class argument is expected return inspect.isclass(instance) and issubclass(instance, self.__args__) if not isinstance(instance, self.__origin__) or isinstance(instance, Iterator): return False if self.__origin__ is Callable: return issubclass(subtype(Callable, *get_type_hints(instance).values()), self) if self.__origin__ is tuple and self.__args__[-1:] != (...,): if len(instance) != len(self.__args__): return False elif issubclass(self, Mapping): instance = next(iter(instance.items()), ()) else: instance = itertools.islice(instance, 1) return all(map(isinstance, instance, self.__args__)) @functools.singledispatch def origins(self) -> Iterable[type]: """Return origin types which would require instance checks. Provisional custom usage: `subtype.origins.register(, lambda cls: ...) """ origin = get_origin(self) if origin is Literal: yield from set(map(type, self.__args__)) elif origin is Union: for arg in self.__args__: yield from subtype.origins(arg) elif origin is not None: yield origin class parametric(abc.ABCMeta): """A type which further customizes `issubclass` and `isinstance` beyond the base type. Args: base: base type funcs: all predicate functions are checked against the instance attrs: all attributes are checked for equality """ def __new__(cls, base: type, *funcs: Callable, **attrs): return super().__new__(cls, base.__name__, (base,), {'funcs': funcs, 'attrs': attrs}) def __init__(self, *_, **__): ... def __subclasscheck__(self, subclass): missing = object() attrs = getattr(subclass, 'attrs', {}) return ( set(subclass.__bases__).issuperset(self.__bases__) # python/cpython#73407 and set(getattr(subclass, 'funcs', ())).issuperset(self.funcs) and all(attrs.get(name, missing) == self.attrs[name] for name in self.attrs) ) def __instancecheck__(self, instance): missing = object() return ( isinstance(instance, self.__bases__) and all(func(instance) for func in self.funcs) and all(getattr(instance, name, missing) == self.attrs[name] for name in self.attrs) ) def __and__(self, other): (base,) = set(self.__bases__ + other.__bases__) return type(self)(base, *set(self.funcs + other.funcs), **(self.attrs | other.attrs)) subtype.origins.register(parametric, lambda cls: cls.__bases__) class signature(tuple): """A tuple of types that supports partial ordering.""" required: int parents: set sig: inspect.Signature def __new__(cls, types: Iterable, required: Optional[int] = None): return tuple.__new__(cls, map(subtype, types)) def __init__(self, types: Iterable, required: Optional[int] = None): self.required = len(self) if required is None else required @classmethod def from_hints(cls, func: Callable) -> 'signature': """Return evaluated type hints for positional parameters in order.""" if not hasattr(func, '__annotations__'): return cls(()) type_hints = get_type_hints(func) positionals = {inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD} params: Iterable = inspect.signature(func).parameters.values() params = [param for param in params if param.kind in positionals] # missing annotations are padded with `object`, but trailing objects are unnecessary indices = [index for index, param in enumerate(params) if param.name in type_hints] params = params[: max(indices, default=-1) + 1] hints = [type_hints.get(param.name, object) for param in params] required = sum(param.default is param.empty for param in params) return cls(hints, required) def __le__(self, other: tuple) -> bool: return self.required <= len(other) and all(map(issubclass, other, self)) def __lt__(self, other: tuple) -> bool: return self != other and self <= other def callable(self, *types) -> bool: """Check positional arity of associated function signature.""" try: return not hasattr(self, 'sig') or bool(self.sig.bind_partial(*types)) except TypeError: return False def instances(self, *args) -> bool: """Return whether all arguments are instances.""" return self.required <= len(args) and all(map(isinstance, args, self)) REGISTERED = TypeVar("REGISTERED", bound=Callable[..., Any]) class multimethod(dict): """A callable directed acyclic graph of methods.""" __name__: str pending: set generics: list[tuple] # positional bases which require instance checks def __new__(cls, func): homonym = inspect.currentframe().f_back.f_locals.get(func.__name__) if isinstance(homonym, multimethod): return homonym self = functools.update_wrapper(dict.__new__(cls), func) self.pending = set() self.generics = [] return self def __init__(self, func: Callable): try: self[signature.from_hints(func)] = func except (NameError, AttributeError): self.pending.add(func) @overload def register(self, __func: REGISTERED) -> REGISTERED: ... # pragma: no cover @overload def register(self, *args: type) -> Callable[[REGISTERED], REGISTERED]: ... # pragma: no cover def register(self, *args) -> Callable: """Decorator for registering a function. Optionally call with types to return a decorator for unannotated functions. """ if len(args) == 1 and hasattr(args[0], '__annotations__'): multimethod.__init__(self, *args) return self if self.__name__ == args[0].__name__ else args[0] return lambda func: self.__setitem__(args, func) or func def __get__(self, instance, owner): return self if instance is None else types.MethodType(self, instance) def parents(self, types: tuple) -> set: """Find immediate parents of potential key.""" parents = {key for key in list(self) if isinstance(key, signature) and key < types} return parents - {ancestor for parent in parents for ancestor in parent.parents} def clean(self): """Empty the cache.""" for key in list(self): if not isinstance(key, signature): super().__delitem__(key) def copy(self): """Return a new multimethod with the same methods.""" return dict.__new__(type(self)).__ior__(self) def __setitem__(self, types: tuple, func: Callable): self.clean() if not isinstance(types, signature): types = signature(types) parents = types.parents = self.parents(types) with contextlib.suppress(ValueError): types.sig = inspect.signature(func) self.pop(types, None) # ensure key is overwritten for key in self: if types < key and (not parents or parents & key.parents): key.parents -= parents key.parents.add(types) for index, cls in enumerate(types): if origins := set(subtype.origins(cls)): self.generics += [()] * (index + 1 - len(self.generics)) self.generics[index] = tuple(origins.union(self.generics[index])) super().__setitem__(types, func) self.__doc__ = self.docstring def __delitem__(self, types: tuple): self.clean() super().__delitem__(types) for key in self: if types in key.parents: key.parents = self.parents(key) self.__doc__ = self.docstring def select(self, types: tuple, keys: set[signature]) -> Callable: keys = {key for key in keys if key.callable(*types)} funcs = {self[key] for key in keys} if len(funcs) == 1: return funcs.pop() raise DispatchError(f"{self.__name__}: {len(keys)} methods found", types, keys) def __missing__(self, types: tuple) -> Callable: """Find and cache the next applicable method of given types.""" self.evaluate() types = tuple(map(subtype, types)) if types in self: return self[types] return self.setdefault(types, self.select(types, self.parents(types))) def dispatch(self, *args) -> Callable: types = tuple(map(type, args)) if not any(map(issubclass, types, self.generics)): return self[types] matches = {key for key in list(self) if isinstance(key, signature) and key.instances(*args)} matches -= {ancestor for match in matches for ancestor in match.parents} return self.select(types, matches) def __call__(self, *args, **kwargs): """Resolve and dispatch to best method.""" self.evaluate() func = self.dispatch(*args) try: return func(*args, **kwargs) except TypeError as ex: raise DispatchError(f"Function {func.__code__}") from ex def evaluate(self): """Evaluate any pending forward references.""" while self.pending: func = self.pending.pop() self[signature.from_hints(func)] = func @property def docstring(self): """a descriptive docstring of all registered functions""" docs = [] for key, func in self.items(): sig = getattr(key, 'sig', '') if func.__doc__: docs.append(f'{func.__name__}{sig}\n {func.__doc__}') return '\n\n'.join(docs) del overload # raise error on legacy import RETURN = TypeVar("RETURN") class multidispatch(multimethod, dict[tuple[type, ...], Callable[..., RETURN]]): """Wrapper for compatibility with `functools.singledispatch`. Only uses the [register][multimethod.multimethod.register] method instead of namespace lookup. Allows dispatching on keyword arguments based on the first function signature. """ signatures: dict[tuple, inspect.Signature] def __new__(cls, func: Callable[..., RETURN]) -> "multidispatch[RETURN]": return functools.update_wrapper(dict.__new__(cls), func) # type: ignore def __init__(self, func: Callable[..., RETURN]) -> None: self.pending = set() self.generics = [] self.signatures = {} self[()] = func def __get__(self, instance, owner) -> Callable[..., RETURN]: return self if instance is None else types.MethodType(self, instance) # type: ignore def __setitem__(self, types: tuple, func: Callable): super().__setitem__(types, func) with contextlib.suppress(ValueError): signature = inspect.signature(func) self.signatures.setdefault(tuple(signature.parameters), signature) def __call__(self, *args: Any, **kwargs: Any) -> RETURN: """Resolve and dispatch to best method.""" params = args if kwargs: for signature in self.signatures.values(): # pragma: no branch with contextlib.suppress(TypeError): params = signature.bind(*args, **kwargs).args break func = self.dispatch(*params) return func(*args, **kwargs) class multimeta(type): """Convert all callables in namespace to multimethods.""" class __prepare__(dict): def __init__(*args): pass def __setitem__(self, key, value): if callable(value): value = getattr(self.get(key), 'register', multimethod)(value) super().__setitem__(key, value) multimethod-2.0/multimethod/py.typed000066400000000000000000000000001473340572400177370ustar00rootroot00000000000000multimethod-2.0/pyproject.toml000066400000000000000000000026251473340572400166400ustar00rootroot00000000000000[project] name = "multimethod" version = "2.0" description = "Multiple argument dispatching." readme = "README.md" requires-python = ">=3.9" license = {file = "LICENSE.txt"} authors = [{name = "Aric Coady", email = "aric.coady@gmail.com"}] keywords = ["multiple", "dispatch", "multidispatch", "generic", "functions", "methods", "overload"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] [project.urls] Homepage = "https://github.com/coady/multimethod" Documentation = "https://coady.github.io/multimethod" Changelog = "https://github.com/coady/multimethod/blob/main/CHANGELOG.md" Issues = "https://github.com/coady/multimethod/issues" [tool.ruff] line-length = 100 [tool.ruff.lint] ignore = ["F811"] [tool.ruff.format] quote-style = "preserve" [tool.coverage.run] source = ["multimethod"] branch = true [tool.pytest.ini_options] markers = ["benchmark"] multimethod-2.0/tests/000077500000000000000000000000001473340572400150615ustar00rootroot00000000000000multimethod-2.0/tests/__init__.py000066400000000000000000000000001473340572400171600ustar00rootroot00000000000000multimethod-2.0/tests/requirements.in000066400000000000000000000000331473340572400201300ustar00rootroot00000000000000pytest-cov pytest-codspeed multimethod-2.0/tests/static.py000066400000000000000000000003041473340572400167170ustar00rootroot00000000000000"""Fixture for static analysis.""" from multimethod import multidispatch class cls: @multidispatch def method(self) -> int: return 0 reveal_type(cls().method()) # noqa: F821 multimethod-2.0/tests/test_dispatch.py000066400000000000000000000062461473340572400203010ustar00rootroot00000000000000from collections.abc import Iterable from concurrent import futures import pytest from multimethod import multidispatch, multimethod, signature, DispatchError def test_signature(): with pytest.raises(TypeError): signature([list]) <= signature([None]) # roshambo rock, paper, scissors = (type('', (), {}) for _ in range(3)) @multimethod def roshambo(left, right): return 'tie' @roshambo.register(scissors, rock) @roshambo.register(rock, scissors) def _(left, right): return 'rock smashes scissors' @roshambo.register(paper, scissors) @roshambo.register(scissors, paper) def _(left, right): return 'scissors cut paper' @roshambo.register(rock, paper) @roshambo.register(paper, rock) def _(left, right): return 'paper covers rock' def test_roshambo(): assert roshambo.__name__ == 'roshambo' r, p, s = rock(), paper(), scissors() assert len(roshambo) == 7 assert roshambo(r, p) == 'paper covers rock' assert roshambo(p, r) == 'paper covers rock' assert roshambo(r, s) == 'rock smashes scissors' assert roshambo(p, s) == 'scissors cut paper' assert roshambo(r, r) == 'tie' assert len(roshambo) == 8 del roshambo[()] del roshambo[rock, paper] assert len(roshambo) == 5 with pytest.raises(DispatchError, match="0 methods"): roshambo(r, r) r = roshambo.copy() assert isinstance(r, multimethod) assert r == roshambo # methods class cls: method = multidispatch(lambda self, other: None) @method.register(Iterable, object) def _(self, other): return 'left' @method.register(object, Iterable) def _(self, other): return 'right' def test_cls(): obj = cls() assert obj.method(None) is cls.method(None, None) is None assert obj.method('') == 'right' assert cls.method('', None) == 'left' with pytest.raises(DispatchError, match="2 methods"): cls.method('', '') cls.method[object, Iterable] = cls.method[Iterable, object] assert cls.method('', '') == 'left' def test_arguments(): def func(a, b: int, /, c: int = 0, d=None, *, f: int): ... assert signature.from_hints(func) == (object, int, int) @multidispatch def func(arg: ...): ... def test_defaults(): def func(a: int, b: float = 0.0): return b assert signature.from_hints(func) == (int, float) method = multimethod(func) assert method(1) == 0.0 assert method(0, 1.0) == method(0, b=1.0) == 1.0 with pytest.raises(DispatchError, match="0 methods"): method(0, 0) assert multidispatch(func)(0, b=1) assert multimethod(bool)(1) @pytest.mark.benchmark def test_keywords(): @multidispatch def func(arg): pass @func.register def _(arg: int): return int @func.register def _(arg: int, extra: float): return float assert func(0) is func(arg=0) is int assert func(0, 0.0) is func(arg=0, extra=0.0) is float def test_concurrency(): @multimethod def func(arg: int): ... submit = futures.ThreadPoolExecutor().submit args = [type('', (int,), {})() for _ in range(500)] fs = [submit(func, arg) for arg in args] assert all(future.result() is None for future in fs) multimethod-2.0/tests/test_docstring.py000066400000000000000000000007471473340572400204760ustar00rootroot00000000000000from multimethod import multimethod @multimethod def foo(bar: int): """ Argument is an integer """ pass @multimethod def foo(bar: str): """ Argument is a string """ pass @foo.register def _(bar: float): pass def test_docstring(): """ Test if multimethod collects its children's docstrings """ assert "Argument is an integer" in foo.__doc__ assert "Argument is a string" in foo.__doc__ assert "float" not in foo.__doc__ multimethod-2.0/tests/test_methods.py000066400000000000000000000155261473340572400201460ustar00rootroot00000000000000import enum import types import pytest from collections.abc import Collection, Iterable, Mapping, Set from typing import Annotated, Any, AnyStr, NewType, Protocol, Sized, TypeVar, Union from multimethod import DispatchError, multimeta, multimethod, signature, subtype # string join class tree(list): def walk(self): for value in self: if isinstance(value, type(self)): yield from value.walk() else: yield value class bracket(tuple): def __new__(cls, left, right): return tuple.__new__(cls, (left, right)) @multimethod def join(seq, sep): return sep.join(map(str, seq)) @multimethod def join(seq: object, sep: bracket): return sep[0] + join(seq, sep[1] + sep[0]) + sep[1] @multimethod def join(seq: tree, sep: object): return join(seq.walk(), sep) def test_join(): sep = '<>' seq = [0, tree([1]), 2] assert list(tree(seq).walk()) == list(range(3)) assert join(seq, sep) == '0<>[1]<>2' assert join(tree(seq), sep) == '0<>1<>2' assert join(seq, bracket(*sep)) == '<0><[1]><2>' with pytest.raises(DispatchError): assert join(tree(seq), bracket(*sep)) == '<0><1><2>' join[tree, bracket] = join[tree, object] assert join(tree(seq), bracket(*sep)) == '<0><1><2>' def subclass(*bases, **kwds): return types.new_class('', bases, kwds) @pytest.mark.benchmark def test_subtype(): assert len({subtype(list[int]), subtype(list[int])}) == 1 assert len({subtype(list[bool]), subtype(list[int])}) == 2 assert issubclass(int, subtype(Union[int, float])) assert issubclass(Union[float, int], subtype(Union[int, float])) assert issubclass(list[bool], subtype(list[int])) assert isinstance((0, 0.0), subtype(tuple[int, float])) assert not isinstance((0,), subtype(tuple[int, float])) assert isinstance((0,), subtype(tuple[int, ...])) assert not issubclass(tuple[int], subtype(tuple[int, ...])) assert not isinstance(iter('-'), subtype(Iterable[str])) assert not issubclass(tuple[int], subtype(tuple[int, float])) assert issubclass(Iterable[bool], subtype(Iterable[int])) assert issubclass(subtype(Iterable[int]), subtype(Iterable)) assert issubclass(subtype(list[int]), subtype(Iterable)) assert issubclass(list[bool], subtype(Union[list[int], list[float]])) assert issubclass(subtype(Union[bool, int]), int) assert issubclass(subtype(Union[Mapping, Set]), Collection) base = subclass(metaclass=subclass(type)) assert subtype(Union[base, subclass(base)]) assert not list(subtype.origins(subclass(subclass(Protocol)))) assert not list(subtype.origins(subclass(Sized))) assert not list(subtype.origins(subclass(Protocol[TypeVar('T')]))) assert subtype(Annotated[str, "test"]) is str @pytest.mark.benchmark def test_signature(): assert signature([Any, list, NewType('', int)]) == (object, list, int) assert signature([AnyStr]) == signature([Union[bytes, str]]) assert signature([TypeVar('T')]) == signature([object]) assert signature([list]) <= (list,) assert signature([list]) <= signature([list]) assert signature([list]) <= signature([list[int]]) class namespace: pass class cls: @multimethod def method(x, y: int, z=None) -> tuple: return object, int @multimethod def method(x: 'cls', y: 'list[float]'): return type(x), list @multimethod def dotted(x: 'namespace.cls'): return type(x), float def test_annotations(): obj = cls() assert obj.method([0.0]) == (cls, list) # run first to check exact match post-evaluation assert obj.method(0) == (object, int) assert cls.method(None, 0) == (object, int) with pytest.raises(DispatchError): cls.method(None, 0.0) key = cls, subtype(list[float]) cls.method.pending.add(cls.method.pop(key)) assert cls.method[key] # register out of order @multimethod def func(arg: bool): return bool @func.register def _(arg: object): return object @func.register def _(arg: int): return int @func.register def _(arg: Union[list[int], tuple[float], dict[str, int]]): return 'union' def test_register(): assert func(0.0) is object assert func(0) is int assert func(False) is bool assert func([0]) == func((0.0,)) == func({'': 0}) == func({}) == 'union' assert func([0.0]) is func((0.0, 1.0)) is object # multimeta def test_meta(): class meta(metaclass=multimeta): def method(self, x: str): return 'STR' def method(self, x: int): return 'INT' def normal(self, y): return 'OBJECT' def rebind(self, x: str): return 'INITIAL' rebind = 2 def rebind(self, x): return 'REBOUND' assert isinstance(meta.method, multimethod) assert isinstance(meta.normal, multimethod) assert isinstance(meta.rebind, multimethod) m = meta() assert m.method('') == 'STR' assert m.method(12) == 'INT' assert m.normal('') == 'OBJECT' assert m.rebind('') == 'REBOUND' def test_ellipsis(): @multimethod def func(arg: tuple[tuple[int, int], ...]): return arg tup = ((0, 1),) assert func(tup) == tup tup = ((0, 1), (2, 3)) assert func(tup) == tup assert func(()) == () with pytest.raises(DispatchError): func(((0, 1.0),)) def test_meta_types(): @multimethod def f(x): return "object" @f.register def f(x: type): return "type" @f.register def f(x: enum.EnumMeta): return "enum" @f.register def f(x: enum.Enum): return "member" dummy_enum = enum.Enum("DummyEnum", names="SPAM EGGS HAM") assert f(123) == "object" assert f(int) == "type" assert f(dummy_enum) == "enum" assert f(dummy_enum.EGGS) == "member" def test_name_shadowing(): # an object with the same name appearing previously in the same namespace temp = 123 # a multimethod shadowing that name @multimethod def temp(x: int): return "int" @multimethod def temp(x: float): return "float" assert isinstance(temp, multimethod) assert temp(0) == "int" assert temp(0.0) == "float" def test_dispatch_exception(): @multimethod def temp(x: int, y): return "int" @multimethod def temp(x: int, y: float): return "int, float" @multimethod def temp(x: bool): return "bool" @multimethod def temp(x: int, y: object): return "int, object" with pytest.raises(DispatchError, match="test_methods.py"): # invalid number of args, check source file is part of the exception args temp(1) assert temp(1, y=1.0) == "int" assert temp(True) == "bool" assert temp(True, 1.0) == "int, float" @multimethod def temp(x: bool, y: float = 0.0): return "optional" assert temp(True, 1.0) == "optional" multimethod-2.0/tests/test_subscripts.py000066400000000000000000000077201473340572400207010ustar00rootroot00000000000000import asyncio import inspect import sys import typing import pytest from array import array from collections.abc import Callable, Iterable, Mapping, Sequence from typing import Generic, Literal, Type, TypeVar, Union from multimethod import multimethod, parametric, subtype, DispatchError def test_literals(): assert issubclass(subtype(Literal['a', 'b']), str) assert not issubclass(subtype(Literal['a']), subtype(list[int])) assert issubclass(Literal[[0]], subtype(Iterable[int])) tp = subtype(Literal['a', 0]) assert isinstance('a', tp) assert isinstance(0, tp) assert not issubclass(Literal['a', 0.0], tp) assert not issubclass(tuple[str, int], tp) assert issubclass(tp, subtype(Union[str, int])) @multimethod def func(arg: Literal['a', 0]): return arg assert func(0) == 0 with pytest.raises(DispatchError): func(1) with pytest.raises(DispatchError): func(0.0) @pytest.mark.skipif(sys.version_info < (3, 10), reason="Union syntax added in 3.10") def test_union(): assert issubclass(int, subtype(int | float)) assert issubclass(subtype(int | float), subtype(int | float | None)) assert subtype(Iterable | Mapping | Sequence) is Iterable @pytest.mark.skipif(sys.version_info < (3, 12), reason="Type aliases added in 3.12") def test_type_alias(): Point = typing.TypeAliasType(name='Point', value=tuple[int, int]) assert isinstance((0, 0), subtype(Point)) def test_type(): @multimethod def func(arg: Type[int]): return arg assert isinstance(int, subtype(Type[int])) assert func(int) is int assert func(bool) is bool with pytest.raises(DispatchError): func(float) with pytest.raises(DispatchError): func(0) def test_generic(): class cls(Generic[TypeVar('T')]): pass @multimethod def func(x: cls[int]): pass obj = cls[int]() assert isinstance(obj, subtype(cls[int])) assert func(obj) is None def test_empty(): @multimethod def func(arg: list[int]): return int @func.register def _(arg: list[bool]): return bool assert func[list[int],] assert func([0]) is int assert func([False]) is func([]) is bool def test_callable(): def f(arg: bool) -> int: ... def g(arg: int) -> bool: ... def h(arg) -> bool: ... @multimethod def func(arg: Callable[[bool], bool]): return arg.__name__ @func.register def _(arg: Callable[..., bool]): return ... @func.register def _(arg: int): return 'int' @func.register def _(arg: Sequence[Callable[[bool], bool]]): return arg[0].__name__ + "0" with pytest.raises(DispatchError): func(f) assert func(g) == 'g' assert func([g]) == 'g0' assert func(h) is ... def test_final(): tp = subtype(Iterable[str]) d = {'': 0} assert isinstance(d, subtype(Mapping[str, int])) assert isinstance(d.keys(), tp) def test_args(): tp = type('', (), {'__args__': None}) assert subtype(tp) is tp assert not issubclass(tp, subtype(list[int])) assert subtype(typing.Callable) is Callable @pytest.mark.benchmark def test_parametric(): coro = parametric(Callable, inspect.iscoroutinefunction) assert issubclass(coro, Callable) assert not issubclass(Callable, coro) assert not issubclass(parametric(object, inspect.iscoroutinefunction), coro) assert isinstance(asyncio.sleep, coro) assert not isinstance(lambda: None, coro) assert list(subtype.origins(coro)) == [Callable] ints = parametric(array, typecode='i') assert issubclass(ints, array) assert not issubclass(array, ints) sized = parametric(array, itemsize=4) assert issubclass(sized & ints, ints) assert not issubclass(ints, sized & ints) assert not issubclass(parametric(object, typecode='i'), array) assert isinstance(array('i'), ints) assert not isinstance(array('l'), ints) assert list(subtype.origins(ints)) == [array]