ovld 0.3.2__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ovld-0.3.2 → ovld-0.3.4}/PKG-INFO +5 -4
- {ovld-0.3.2 → ovld-0.3.4}/ovld/core.py +51 -6
- {ovld-0.3.2 → ovld-0.3.4}/ovld/utils.py +13 -0
- ovld-0.3.4/ovld/version.py +1 -0
- {ovld-0.3.2 → ovld-0.3.4}/pyproject.toml +17 -8
- ovld-0.3.2/ovld/version.py +0 -1
- ovld-0.3.2/setup.py +0 -26
- {ovld-0.3.2 → ovld-0.3.4}/LICENSE +0 -0
- {ovld-0.3.2 → ovld-0.3.4}/README.md +0 -0
- {ovld-0.3.2 → ovld-0.3.4}/ovld/__init__.py +0 -0
- {ovld-0.3.2 → ovld-0.3.4}/ovld/mro.py +0 -0
@@ -1,18 +1,19 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ovld
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.4
|
4
4
|
Summary: Overloading Python functions
|
5
5
|
Home-page: https://github.com/breuleux/ovld
|
6
6
|
License: MIT
|
7
7
|
Author: Olivier Breuleux
|
8
8
|
Author-email: breuleux@gmail.com
|
9
|
-
Requires-Python: >=3.
|
9
|
+
Requires-Python: >=3.8,<4.0
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
12
|
-
Classifier: Programming Language :: Python :: 3.6
|
13
|
-
Classifier: Programming Language :: Python :: 3.7
|
14
12
|
Classifier: Programming Language :: Python :: 3.8
|
15
13
|
Classifier: Programming Language :: Python :: 3.9
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
16
17
|
Project-URL: Repository, https://github.com/breuleux/ovld
|
17
18
|
Description-Content-Type: text/markdown
|
18
19
|
|
@@ -7,10 +7,27 @@ import textwrap
|
|
7
7
|
import typing
|
8
8
|
from types import FunctionType
|
9
9
|
|
10
|
+
try:
|
11
|
+
from types import GenericAlias, UnionType
|
12
|
+
except ImportError: # pragma: no cover
|
13
|
+
UnionType = None
|
14
|
+
|
15
|
+
class GenericAliasMC(type):
|
16
|
+
def __instancecheck__(cls, obj):
|
17
|
+
return hasattr(obj, "__origin__")
|
18
|
+
|
19
|
+
class GenericAlias(metaclass=GenericAliasMC):
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
10
23
|
from .mro import compose_mro
|
11
24
|
from .utils import BOOTSTRAP, MISSING, keyword_decorator
|
12
25
|
|
13
26
|
|
27
|
+
def is_type_of_type(t):
|
28
|
+
return getattr(t, "__origin__", None) is type
|
29
|
+
|
30
|
+
|
14
31
|
class TypeMap(dict):
|
15
32
|
"""Represents a mapping from types to handlers.
|
16
33
|
|
@@ -30,8 +47,14 @@ class TypeMap(dict):
|
|
30
47
|
|
31
48
|
def register(self, obj_t, handler):
|
32
49
|
"""Register a handler for the given object type."""
|
50
|
+
if isinstance(obj_t, str):
|
51
|
+
obj_t = eval(obj_t, getattr(handler[0], "__globals__", {}))
|
52
|
+
|
33
53
|
self.clear()
|
34
|
-
|
54
|
+
if is_type_of_type(obj_t):
|
55
|
+
self.types.add(obj_t.__args__[0])
|
56
|
+
else:
|
57
|
+
self.types.add(obj_t)
|
35
58
|
s = self.entries.setdefault(obj_t, set())
|
36
59
|
s.add(handler)
|
37
60
|
|
@@ -42,7 +65,12 @@ class TypeMap(dict):
|
|
42
65
|
the next time getitem is called.
|
43
66
|
"""
|
44
67
|
results = {}
|
45
|
-
|
68
|
+
if is_type_of_type(obj_t):
|
69
|
+
mro = [type[t] for t in compose_mro(obj_t.__args__[0], self.types)]
|
70
|
+
mro.append(type)
|
71
|
+
mro.append(object)
|
72
|
+
else:
|
73
|
+
mro = compose_mro(obj_t, self.types)
|
46
74
|
for lvl, cls in enumerate(reversed(mro)):
|
47
75
|
handlers = self.entries.get(cls, None)
|
48
76
|
if handlers:
|
@@ -77,9 +105,16 @@ class MultiTypeMap(dict):
|
|
77
105
|
def __init__(self, key_error=KeyError):
|
78
106
|
self.maps = {}
|
79
107
|
self.empty = MISSING
|
80
|
-
self.transform = type
|
81
108
|
self.key_error = key_error
|
82
109
|
|
110
|
+
def transform(self, obj):
|
111
|
+
if isinstance(obj, GenericAlias):
|
112
|
+
return type[obj.__origin__]
|
113
|
+
elif isinstance(obj, type):
|
114
|
+
return type[obj]
|
115
|
+
else:
|
116
|
+
return type(obj)
|
117
|
+
|
83
118
|
def register(self, obj_t_tup, nargs, handler):
|
84
119
|
"""Register a handler for a tuple of argument types.
|
85
120
|
|
@@ -326,6 +361,9 @@ class _Ovld:
|
|
326
361
|
def clsname(cls):
|
327
362
|
if cls is object:
|
328
363
|
return "*"
|
364
|
+
elif is_type_of_type(cls):
|
365
|
+
arg = clsname(cls.__args__[0])
|
366
|
+
return f"type[{arg}]"
|
329
367
|
elif hasattr(cls, "__name__"):
|
330
368
|
return cls.__name__
|
331
369
|
else:
|
@@ -489,11 +527,15 @@ class _Ovld:
|
|
489
527
|
|
490
528
|
def _normalize_type(t, force_tuple=False):
|
491
529
|
origin = getattr(t, "__origin__", None)
|
492
|
-
if
|
530
|
+
if UnionType and isinstance(t, UnionType):
|
531
|
+
return _normalize_type(t.__args__)
|
532
|
+
elif origin is type:
|
533
|
+
return (t,) if force_tuple else t
|
534
|
+
elif origin is typing.Union:
|
493
535
|
return _normalize_type(t.__args__)
|
494
536
|
elif origin is not None:
|
495
537
|
raise TypeError(
|
496
|
-
"ovld does not accept generic types except Union or Optional"
|
538
|
+
"ovld does not accept generic types except type, Union or Optional"
|
497
539
|
)
|
498
540
|
elif isinstance(t, tuple):
|
499
541
|
return tuple(_normalize_type(t2) for t2 in t)
|
@@ -923,7 +965,10 @@ class Conformer:
|
|
923
965
|
|
924
966
|
def rename_code(co, newname): # pragma: no cover
|
925
967
|
if hasattr(co, "replace"):
|
926
|
-
|
968
|
+
if hasattr(co, "co_qualname"):
|
969
|
+
return co.replace(co_name=newname, co_qualname=newname)
|
970
|
+
else:
|
971
|
+
return co.replace(co_name=newname)
|
927
972
|
else:
|
928
973
|
return type(co)(
|
929
974
|
co.co_argcount,
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
import functools
|
4
4
|
import sys
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import Protocol, runtime_checkable
|
5
7
|
|
6
8
|
|
7
9
|
class Named:
|
@@ -143,9 +145,20 @@ def has_attribute(*attrs):
|
|
143
145
|
return check
|
144
146
|
|
145
147
|
|
148
|
+
@runtime_checkable
|
149
|
+
@dataclass
|
150
|
+
class Dataclass(Protocol):
|
151
|
+
@classmethod
|
152
|
+
def __subclasshook__(cls, subclass):
|
153
|
+
return hasattr(subclass, "__dataclass_fields__") and hasattr(
|
154
|
+
subclass, "__dataclass_params__"
|
155
|
+
)
|
156
|
+
|
157
|
+
|
146
158
|
__all__ = [
|
147
159
|
"BOOTSTRAP",
|
148
160
|
"MISSING",
|
161
|
+
"Dataclass",
|
149
162
|
"Named",
|
150
163
|
"deferred",
|
151
164
|
"exactly",
|
@@ -0,0 +1 @@
|
|
1
|
+
version = "0.3.4"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "ovld"
|
3
|
-
version = "0.3.
|
3
|
+
version = "0.3.4"
|
4
4
|
description = "Overloading Python functions"
|
5
5
|
authors = ["Olivier Breuleux <breuleux@gmail.com>"]
|
6
6
|
license = "MIT"
|
@@ -9,15 +9,14 @@ homepage = "https://github.com/breuleux/ovld"
|
|
9
9
|
repository = "https://github.com/breuleux/ovld"
|
10
10
|
|
11
11
|
[tool.poetry.dependencies]
|
12
|
-
python = "^3.
|
12
|
+
python = "^3.8"
|
13
13
|
|
14
14
|
[tool.poetry.dev-dependencies]
|
15
|
-
black = "^
|
16
|
-
|
15
|
+
black = "^24.3.0"
|
16
|
+
ruff = "^0.3.5"
|
17
|
+
codefind = "^0.1.0"
|
17
18
|
pytest = "^6.0.1"
|
18
|
-
flake8 = "^3.8.3"
|
19
19
|
pytest-cov = "^2.10.0"
|
20
|
-
isort = "^5.4.2"
|
21
20
|
|
22
21
|
[tool.black]
|
23
22
|
line-length = 80
|
@@ -30,5 +29,15 @@ include_trailing_comma = true
|
|
30
29
|
combine_as_imports = true
|
31
30
|
|
32
31
|
[build-system]
|
33
|
-
requires = ["poetry>=0.
|
34
|
-
build-backend = "poetry.masonry.api"
|
32
|
+
requires = ["poetry-core>=1.0.8"]
|
33
|
+
build-backend = "poetry.core.masonry.api"
|
34
|
+
|
35
|
+
[tool.ruff]
|
36
|
+
line-length = 99
|
37
|
+
|
38
|
+
[tool.ruff.lint]
|
39
|
+
extend-select = ["I"]
|
40
|
+
ignore = ["E241", "F722", "E501", "E203", "F811", "F821"]
|
41
|
+
|
42
|
+
[tool.ruff.lint.per-file-ignores]
|
43
|
+
"__init__.py" = ["F401", "F403"]
|
ovld-0.3.2/ovld/version.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
version = "0.3.2"
|
ovld-0.3.2/setup.py
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
from setuptools import setup
|
3
|
-
|
4
|
-
packages = \
|
5
|
-
['ovld']
|
6
|
-
|
7
|
-
package_data = \
|
8
|
-
{'': ['*']}
|
9
|
-
|
10
|
-
setup_kwargs = {
|
11
|
-
'name': 'ovld',
|
12
|
-
'version': '0.3.2',
|
13
|
-
'description': 'Overloading Python functions',
|
14
|
-
'long_description': '\n# Ovld\n\nMultiple dispatch in Python, with some extra features.\n\nWith ovld, you can write a version of the same function for every type signature using annotations instead of writing an awkward sequence of `isinstance` statements. Unlike Python `singledispatch`, it works for multiple arguments.\n\nOther features of `ovld`:\n\n* Multiple dispatch for methods (with `metaclass=ovld.OvldMC`)\n* Create variants of functions\n* Built-in support for extensible, stateful recursion\n* Function wrappers\n* Function postprocessors\n* Nice stack traces\n\n## Example\n\nHere\'s a function that adds lists, tuples and dictionaries:\n\n```python\nfrom ovld import ovld\n\n@ovld\ndef add(x: list, y: list):\n return [add(a, b) for a, b in zip(x, y)]\n\n@ovld\ndef add(x: tuple, y: tuple):\n return tuple(add(a, b) for a, b in zip(x, y))\n\n@ovld\ndef add(x: dict, y: dict):\n return {k: add(v, y[k]) for k, v in x.items()}\n\n@ovld\ndef add(x: object, y: object):\n return x + y\n```\n\n## Bootstrapping and variants\n\nNow, there is another way to do this using ovld\'s *auto-bootstrapping*. Simply list `self` as the first argument to the function, and `self` will be bound to the function itself, so you can call `self(x, y)` for the recursion instead of `add(x, y)`:\n\n\n```python\n@ovld\ndef add(self, x: list, y: list):\n return [self(a, b) for a, b in zip(x, y)]\n\n@ovld\ndef add(self, x: tuple, y: tuple):\n return tuple(self(a, b) for a, b in zip(x, y))\n\n@ovld\ndef add(self, x: dict, y: dict):\n return {k: self(v, y[k]) for k, v in x.items()}\n\n@ovld\ndef add(self, x: object, y: object):\n return x + y\n```\n\nWhy is this useful, though? Observe:\n\n```python\n@add.variant\ndef mul(self, x: object, y: object):\n return x * y\n\nassert add([1, 2], [3, 4]) == [4, 6]\nassert mul([1, 2], [3, 4]) == [3, 8]\n```\n\nA `variant` of a function is a copy which inherits all of the original\'s implementations but may define new ones. And because `self` is bound to the function that\'s called at the top level, the implementations for `list`, `tuple` and `dict` will bind `self` to `add` or `mul` depending on which one was called. You may also call `self.super(*args)` to invoke the parent implementation for that type.\n\n## State\n\nYou can pass `initial_state` to `@ovld` or `variant`. The initial state must be a function that takes no arguments. Its return value will be available in `self.state`. The state is initialized at the top level call, but recursive calls to `self` will preserve it.\n\nIn other words, you can do something like this:\n\n```python\n@add.variant(initial_state=lambda: 0)\ndef count(self, x, y):\n self.state += 1\n return (f"#{self.state}", x + y)\n\nassert count([1, 2, 3], [4, 5, 6]) == [("#1", 5), ("#2", 7), ("#3", 9)]\n```\n\nThe initial_state function can return any object and you can use the state to any purpose (e.g. cache or memoization).\n\n## Custom dispatch\n\nYou can define your own dispatching function. The dispatcher\'s first argument is always `self`.\n\n* `self.resolve(x, y)` to get the right function for the types of x and y\n* `self[type(x), type(y)]` will also return the right function for these types, but it works directly with the types.\n\nFor example, here is how you might define a function such that f(x) <=> f(x, x):\n\n```python\n@ovld.dispatch\ndef add_default(self, x, y=None):\n if y is None:\n y = x\n return self.resolve(x, y)(x, y)\n\n@ovld\ndef add_default(x: int, y: int):\n return x + y\n\n@ovld\ndef add_default(x: str, y: str):\n return x + y\n\n@ovld\ndef add_default(xs: list, ys: list):\n return [add_default(x, y) for x, y in zip(xs, ys)]\n\nassert add_default([1, 2, "alouette"]) == [2, 4, "alouettealouette"]\n```\n\nThere are other uses for this feature, e.g. memoization.\n\nThe normal functions may also have a `self`, which works the same as bootstrapping, and you can give an `initial_state` to `@ovld.dispatch` as well.\n\n## Postprocess\n\n`@ovld`, `@ovld.dispatch`, etc. take a `postprocess` argument which should be a function of one argument. That function will be called with the result of the call and must return the final result of the call.\n\nNote that intermediate, bootstrapped recursive calls (recursive calls using `self()`) will **not** be postprocessed (if you want to wrap these calls, you can do so otherwise, like defining a custom dispatch). Only the result of the top level call is postprocessed.\n\n## Methods\n\nUse the `OvldMC` metaclass to use multiple dispatch on methods. In this case there is no bootstrapping as described above and `self` is simply bound to the class instance.\n\n```python\nfrom ovld import OvldMC\n\nclass Cat(metaclass=OvldMC):\n def interact(self, x: Mouse):\n return "catch"\n\n def interact(self, x: Food):\n return "devour"\n\n def interact(self, x: PricelessVase):\n return "destroy"\n```\n\nSubclasses of `Cat` will inherit the overloaded `interact` and it may define additional overloaded methods which will only be valid for the subclass.\n\n**Note:** It is possible to use `ovld.dispatch` on methods, but in this case be aware that the first argument for the dispatch method will not be the usual `self` but an `OvldCall` object. The `self` can be retrived as `ovldcall.obj`. Here\'s an example to make it all clear:\n\n```python\nclass Stuff(metaclass=OvldMC):\n def __init__(self, mul):\n self.mul = mul\n\n @ovld.dispatch\n def calc(ovldcall, x):\n # Wraps every call to self.calc, but we receive ovldcall instead of self\n # ovldcall[type(x)] returns the right method to call\n # ovldcall.obj is the self (the actual instance of Stuff)\n return ovldcall[type(x)](x) * ovldcall.obj.mul\n\n def calc(self, x: int):\n return x + 1\n\n def calc(self, xs: list):\n return [self.calc(x) for x in xs]\n\nprint(Stuff(2).calc([1, 2, 3])) # [4, 6, 8, 4, 6, 8]\n```\n\n### Mixins in subclasses\n\nThe `@extend_super` decorator on a method will combine the method with the definition on the superclass:\n\n```python\nfrom ovld import OvldMC, extend_super\n\nclass One(metaclass=OvldMC):\n def f(self, x: int):\n return "an integer"\n\nclass Two(One):\n @extend_super\n def f(self, x: str):\n return "a string"\n\nassert Two().f(1) == "an integer"\nassert Two().f("s") == "a string"\n```\n\n## Ambiguous calls\n\nThe following definitions will cause a TypeError at runtime when called with two ints, because it is unclear which function is the right match:\n\n```python\n@ovld\ndef ambig(x: int, y: object):\n print("io")\n\n@ovld\ndef ambig(x: object, y: int):\n print("oi")\n\nambig(8, 8) # ???\n```\n\nYou may define an additional function with signature (int, int) to disambiguate:\n\n```python\n@ovld\ndef ambig(x: int, y: int):\n print("ii")\n```\n\n## Other features\n\n### meta\n\nTo test arbitrary conditions, you can use `meta`:\n\n```python\nfrom ovld import ovld, meta\n\n@meta\ndef StartsWithT(cls):\n return cls.__name__.startswith("T")\n\n@ovld\ndef f(x: StartsWithT):\n return "T"\n\nassert f(TypeError("xyz")) == "T"\n\n\n# Or: a useful example, since dataclasses have no common superclass:\n\nfrom dataclasses import dataclass, is_dataclass\n\n@dataclass\nclass Point:\n x: int\n y: int\n\n@ovld\ndef f(x: meta(is_dataclass)):\n return "dataclass"\n\nassert f(Point(1, 2)) == "dataclass"\n```\n\n\n### deferred\n\nYou may define overloads for certain classes from external packages without\nhaving to import them:\n\n\n```python\nfrom ovld import ovld, deferred\n\n@ovld\ndef f(x: deferred("numpy.ndarray")):\n return "ndarray"\n\n# numpy is not imported\nassert "numpy" not in sys.modules\n\n# But once we import it, the ovld works:\nimport numpy\nassert f(numpy.arange(10)) == "ndarray"\n```\n\n\n### Tracebacks\n\n`ovld` automagically renames functions so that the stack trace is more informative:\n\n```python\n@add.variant\ndef bad(self, x: object, y: object):\n raise Exception("Bad.")\n\nbad([1], [2])\n\n"""\n File "/Users/breuleuo/code/ovld/ovld/core.py", line 148, in bad.entry\n res = ovc(*args, **kwargs)\n File "/Users/breuleuo/code/ovld/ovld/core.py", line 182, in bad.dispatch\n return method(self.bind_to, *args, **kwargs)\n File "example.py", line 6, in bad[list, list]\n return [self(a, b) for a, b in zip(x, y)]\n File "example.py", line 6, in <listcomp>\n return [self(a, b) for a, b in zip(x, y)]\n File "/Users/breuleuo/code/ovld/ovld/core.py", line 182, in bad.dispatch\n return method(self.bind_to, *args, **kwargs)\n File "example.py", line 26, in bad[*, *]\n raise Exception("Bad.")\n Exception: Bad.\n"""\n```\n\nThe functions on the stack have names like `bad.entry`, `bad.dispatch`, `bad[list, list]` and `bad[*, *]` (`*` stands for `object`), which lets you better understand what happened just from the stack trace.\n\nThis also means profilers will be able to differentiate between these paths and between variants, even if they share code paths.\n',
|
15
|
-
'author': 'Olivier Breuleux',
|
16
|
-
'author_email': 'breuleux@gmail.com',
|
17
|
-
'maintainer': None,
|
18
|
-
'maintainer_email': None,
|
19
|
-
'url': 'https://github.com/breuleux/ovld',
|
20
|
-
'packages': packages,
|
21
|
-
'package_data': package_data,
|
22
|
-
'python_requires': '>=3.6,<4.0',
|
23
|
-
}
|
24
|
-
|
25
|
-
|
26
|
-
setup(**setup_kwargs)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|