├── .coveragerc ├── .flake8 ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── polypie.py ├── pytest.ini ├── requirements_dev.txt ├── setup.py ├── tests ├── samples │ ├── clash1.py │ ├── clash2.py │ └── specialattrs.py └── test_polypie.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = polypie 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = F811 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | .tox/ 4 | .cache/ 5 | .coverage 6 | .pytest_cache/ 7 | 8 | dist/ 9 | build/ 10 | polypie.egg-info/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - 3.5 5 | - 3.6 6 | - 3.7 7 | install: pip install tox-travis coveralls 8 | script: tox -- --cov 9 | after_success: coveralls 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, 2017, 2018, 2019 un.def 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY 14 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 17 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 23 | DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | polypie 2 | ======= 3 | 4 | |Build Status| |Coverage Status| |PyPI version| |PyPI license| 5 | 6 | Python polymorphic function declaration with obvious syntax. Just use 7 | the same function name and mark each function definition with 8 | ``@polymorphic`` decorator. 9 | 10 | Installation 11 | ~~~~~~~~~~~~ 12 | 13 | ``pip install polypie`` 14 | 15 | Requirements 16 | ~~~~~~~~~~~~ 17 | 18 | - Python 3.5+ 19 | - `typeguard `__ (will be installed automatically) 20 | 21 | Example 22 | ~~~~~~~ 23 | 24 | .. code:: python 25 | 26 | from typing import Any, Sequence 27 | 28 | from polypie import polymorphic, PolypieException 29 | 30 | 31 | @polymorphic 32 | def example(a: int, b): 33 | print('(1)') 34 | 35 | 36 | @polymorphic 37 | def example(a: str, b: Any): 38 | print('(2)') 39 | 40 | 41 | @polymorphic 42 | def example(a: Sequence[str]): 43 | print('(3)') 44 | 45 | 46 | example(100, 200) # (1) 47 | example('foo', 200) # (2) 48 | example(['foo']) # (3) 49 | example(('bar', 'baz')) # (3) 50 | try: 51 | example({'foo': 'bar'}) 52 | except PolypieException as exc: 53 | print(exc) # Matching signature <...> not found 54 | 55 | 56 | class Example: 57 | 58 | def __init__(self): 59 | self.values = {} 60 | 61 | @polymorphic 62 | def value(self, name): 63 | return self.values[name] 64 | 65 | @polymorphic 66 | def value(self, name, value): 67 | self.values[name] = value 68 | 69 | 70 | instance = Example() 71 | instance.value('foo', 100) 72 | instance.value('bar', 'baz') 73 | print(instance.value('foo')) # 100 74 | print(instance.value('bar')) # baz 75 | 76 | Tests 77 | ~~~~~ 78 | 79 | ``tox [-e ENV] [-- --cov]`` 80 | 81 | .. |Build Status| image:: https://travis-ci.org/un-def/polypie.svg?branch=master 82 | :target: https://travis-ci.org/un-def/polypie 83 | .. |Coverage Status| image:: https://coveralls.io/repos/github/un-def/polypie/badge.svg?branch=master 84 | :target: https://coveralls.io/github/un-def/polypie?branch=master 85 | .. |PyPI version| image:: https://badge.fury.io/py/polypie.svg 86 | :target: https://pypi.python.org/pypi/polypie/ 87 | .. |PyPI license| image:: https://img.shields.io/pypi/l/polypie.svg?maxAge=3600 88 | :target: https://raw.githubusercontent.com/un-def/polypie/master/LICENSE 89 | -------------------------------------------------------------------------------- /polypie.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from functools import update_wrapper 3 | from inspect import signature 4 | from typing import get_type_hints 5 | 6 | from typeguard import check_argument_types, _CallMemo 7 | 8 | 9 | __author__ = 'un.def ' 10 | __version__ = '0.2.0' 11 | 12 | 13 | _registry = {} 14 | 15 | 16 | class PolypieException(Exception): 17 | 18 | pass 19 | 20 | 21 | def _call_func(func_key, args=None, kwargs=None): 22 | args = args or () 23 | kwargs = kwargs or {} 24 | for func in _registry[func_key].values(): 25 | try: 26 | check_argument_types(_CallMemo(func, args=args, kwargs=kwargs)) 27 | except TypeError: 28 | continue 29 | return func(*args, **kwargs) 30 | raise PolypieException( 31 | "Matсhing signature for function '{func}' with " 32 | "args={args} and kwargs={kwargs} not found".format( 33 | func=func_key, args=args, kwargs=kwargs) 34 | ) 35 | 36 | 37 | def update_dispatcher(dispatcher, func, assign=True): 38 | assigned = ('__name__', '__qualname__', '__module__') if assign else () 39 | dispatcher = update_wrapper(dispatcher, func, assigned) 40 | del dispatcher.__wrapped__ 41 | return dispatcher 42 | 43 | 44 | def polymorphic(func): 45 | global _registry 46 | func_key = func.__module__ + '.' + func.__qualname__ 47 | parameters = signature(func).parameters 48 | parameters_tuple = tuple(parameters.values()) 49 | if func_key not in _registry: 50 | def dispatcher(*args, **kwargs): 51 | return _call_func(func_key, args, kwargs) 52 | dispatcher = update_dispatcher(dispatcher, func, assign=True) 53 | signature_mapping = OrderedDict() 54 | signature_mapping[parameters_tuple] = func 55 | signature_mapping.dispatcher = dispatcher 56 | _registry[func_key] = signature_mapping 57 | return dispatcher 58 | elif parameters_tuple not in _registry[func_key]: 59 | _registry[func_key][parameters_tuple] = func 60 | dispatcher = _registry[func_key].dispatcher 61 | return update_dispatcher(dispatcher, func, assign=False) 62 | else: 63 | hints = get_type_hints(func) 64 | sig_gen = ( 65 | '{}:{}'.format(p, hints[p]) if p in hints else p 66 | for p in parameters 67 | ) 68 | raise PolypieException( 69 | "Function '{func}' with signature ({sig}) " 70 | "already exists".format(func=func_key, sig=', '.join(sig_gen)) 71 | ) 72 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --verbose 3 | python_files = test_polypie.py 4 | filterwarnings = 5 | default 6 | ignore:Using or importing the ABCs from 'collections':DeprecationWarning:typeguard 7 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | typeguard 2 | 3 | pytest-cov 4 | flake8 5 | tox 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from setuptools import setup 5 | 6 | 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | 10 | with open('README.rst') as f: 11 | long_description = f.read() 12 | 13 | with open('polypie.py') as f: 14 | author, author_email, version = re.search( 15 | "__author__ = '(.+) <(.+)>'.+__version__ = '([.0-9a-z]+)'", 16 | f.read(), 17 | flags=re.DOTALL 18 | ).groups() 19 | 20 | 21 | setup( 22 | name='polypie', 23 | version=version, 24 | py_modules=['polypie'], 25 | install_requires=['typeguard'], 26 | python_requires='>=3.5', 27 | license='BSD License', 28 | description='Python polymorphic function declaration with obvious syntax', 29 | long_description=long_description, 30 | url='https://github.com/un-def/polypie', 31 | author=author, 32 | author_email=author_email, 33 | classifiers=[ 34 | 'Intended Audience :: Developers', 35 | 'License :: OSI Approved :: BSD License', 36 | 'Operating System :: OS Independent', 37 | 'Programming Language :: Python :: 3', 38 | 'Programming Language :: Python :: 3.5', 39 | 'Programming Language :: Python :: 3.6', 40 | 'Programming Language :: Python :: 3.7', 41 | 'Topic :: Software Development :: Libraries', 42 | 'Topic :: Utilities', 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /tests/samples/clash1.py: -------------------------------------------------------------------------------- 1 | from polypie import polymorphic 2 | 3 | 4 | RESULT = 'clash1' 5 | 6 | 7 | @polymorphic 8 | def check_clash(a: int): 9 | return RESULT 10 | -------------------------------------------------------------------------------- /tests/samples/clash2.py: -------------------------------------------------------------------------------- 1 | from polypie import polymorphic 2 | 3 | 4 | RESULT = 'clash2' 5 | 6 | 7 | @polymorphic 8 | def check_clash(a: int): 9 | return RESULT 10 | -------------------------------------------------------------------------------- /tests/samples/specialattrs.py: -------------------------------------------------------------------------------- 1 | from polypie import polymorphic 2 | 3 | 4 | class Wrapper: 5 | 6 | ATTR1 = 'attr1' 7 | ATTR2 = 'attr2' 8 | 9 | def check_special_attrs(a: int): 10 | return True 11 | 12 | check_special_attrs.attr1 = ATTR1 13 | 14 | NAME = check_special_attrs.__name__ 15 | QUALNAME = check_special_attrs.__qualname__ 16 | MODULE = check_special_attrs.__module__ 17 | 18 | check_special_attrs = polymorphic(check_special_attrs) 19 | 20 | def check_special_attrs(a: str): 21 | return False 22 | 23 | check_special_attrs.attr2 = ATTR2 24 | 25 | check_special_attrs = polymorphic(check_special_attrs) 26 | -------------------------------------------------------------------------------- /tests/test_polypie.py: -------------------------------------------------------------------------------- 1 | from imp import reload 2 | from typing import Any, Sequence, Tuple, Union 3 | 4 | import pytest 5 | 6 | import polypie 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def reload_polypie(): 11 | reload(polypie) 12 | 13 | 14 | def test_without_annotations(): 15 | 16 | @polypie.polymorphic 17 | def f(): 18 | return 0 19 | 20 | @polypie.polymorphic 21 | def f(a): 22 | return 1 23 | 24 | @polypie.polymorphic 25 | def f(a, b): 26 | return 2 27 | 28 | assert f() == 0 29 | assert f(120) == 1 30 | assert f(120, 'foo') == 2 31 | with pytest.raises(polypie.PolypieException, match='not found'): 32 | f(120, 240, 'foo') 33 | 34 | 35 | def test_same_types_annotations(): 36 | 37 | @polypie.polymorphic 38 | def f(a: int, b: int): 39 | return 'int, int first' 40 | 41 | @polypie.polymorphic 42 | def f(x: int, y: int): 43 | return 'int, int second' 44 | 45 | @polypie.polymorphic 46 | def f(a: int, b: str): 47 | return 'int, str' 48 | 49 | assert f(120, 240) == 'int, int first' 50 | assert f(a=120, b=240) == 'int, int first' 51 | assert f(x=120, y=240) == 'int, int second' 52 | assert f(120, 'foo') == 'int, str' 53 | assert f(a=120, b='foo') == 'int, str' 54 | 55 | 56 | def test_builtin_types_annotations(): 57 | 58 | @polypie.polymorphic 59 | def f(a: int, b: int): 60 | return 'int, int' 61 | 62 | @polypie.polymorphic 63 | def f(a: int, b: str): 64 | return 'int, str' 65 | 66 | @polypie.polymorphic 67 | def f(a, b: dict): 68 | return 'any, dict' 69 | 70 | assert f(120, 240) == 'int, int' 71 | assert f(120, 'foo') == 'int, str' 72 | assert f(120, {}) == 'any, dict' 73 | assert f('foo', {}) == 'any, dict' 74 | 75 | 76 | def test_own_types_annotations(): 77 | 78 | class Foo: 79 | pass 80 | 81 | class Bar: 82 | pass 83 | 84 | @polypie.polymorphic 85 | def f(a: Foo, b: Bar): 86 | return 'Foo, Bar' 87 | 88 | @polypie.polymorphic 89 | def f(a: Bar, b: Foo): 90 | return 'Bar, Foo' 91 | 92 | foo = Foo() 93 | bar = Bar() 94 | assert f(foo, bar) == 'Foo, Bar' 95 | assert f(bar, foo) == 'Bar, Foo' 96 | with pytest.raises(polypie.PolypieException, match='not found'): 97 | f(foo, foo) 98 | 99 | 100 | def test_typing_annotations(): 101 | 102 | @polypie.polymorphic 103 | def f(a: Any, b: Sequence): 104 | return 'Any, Sequence' 105 | 106 | @polypie.polymorphic 107 | def f(a: Tuple[int, str], b: Union[int, bool]): 108 | return 'Tuple, Union' 109 | 110 | assert f(120, [1, 2, 3]) == 'Any, Sequence' 111 | assert f((120, 'foo'), 120) == 'Tuple, Union' 112 | assert f((120, 'foo'), True) == 'Tuple, Union' 113 | with pytest.raises(polypie.PolypieException, match='not found'): 114 | f(('foo', 120), 100) 115 | with pytest.raises(polypie.PolypieException, match='not found'): 116 | f((120, 'foo'), None) 117 | 118 | 119 | def test_name_clashing(): 120 | 121 | @polypie.polymorphic 122 | def check_clash(a: int): 123 | return 'top' 124 | 125 | class Wrapper: 126 | @polypie.polymorphic 127 | def check_clash(a: int): 128 | return 'wrapped' 129 | 130 | from samples import clash1, clash2 131 | assert check_clash(1) == 'top' 132 | assert Wrapper.check_clash(1), 'wrapped' 133 | assert clash1.check_clash(1) == clash1.RESULT 134 | assert clash2.check_clash(1) == clash2.RESULT 135 | 136 | 137 | def test_methods(): 138 | 139 | class TestClass: 140 | 141 | value = 'cls' 142 | 143 | def __init__(self): 144 | self.value = None 145 | 146 | def getter(self): 147 | return self.value 148 | 149 | @polypie.polymorphic 150 | def setter(self, value: str): 151 | self.value = value 152 | 153 | @polypie.polymorphic 154 | def setter(self, value: int): 155 | self.value = str(value) 156 | 157 | @classmethod 158 | def cls_getter(cls): 159 | return cls.value 160 | 161 | @classmethod 162 | @polypie.polymorphic 163 | def cls_setter(cls, value: str): 164 | cls.value = value 165 | 166 | @classmethod 167 | @polypie.polymorphic 168 | def cls_setter(cls, value: int): 169 | cls.value = str(value) 170 | 171 | @staticmethod 172 | def static_getter(obj): 173 | return obj.value 174 | 175 | @staticmethod 176 | @polypie.polymorphic 177 | def static_setter(obj, value: str): 178 | obj.value = value 179 | 180 | @staticmethod 181 | @polypie.polymorphic 182 | def static_setter(obj, value: int): 183 | obj.value = str(value) 184 | 185 | instance = TestClass() 186 | # instance methods 187 | instance.setter('foo') 188 | assert instance.getter() == 'foo' 189 | instance.setter(1) 190 | assert instance.getter() == '1' 191 | # cls methods 192 | assert instance.cls_getter() == 'cls' 193 | instance.cls_setter('bar') 194 | assert instance.cls_getter() == 'bar' 195 | instance.cls_setter(2) 196 | assert instance.cls_getter() == '2' 197 | assert instance.getter() == '1' 198 | # static methods 199 | instance.static_setter(instance, 'baz') 200 | instance.static_setter(TestClass, 'xyzzy') 201 | assert instance.static_getter(instance) == 'baz' 202 | assert instance.static_getter(TestClass) == 'xyzzy' 203 | instance.static_setter(instance, 100) 204 | instance.static_setter(TestClass, 200) 205 | assert instance.static_getter(instance) == '100' 206 | assert instance.static_getter(TestClass) == '200' 207 | 208 | 209 | def test_exception_due_to_existent_signature(): 210 | 211 | @polypie.polymorphic 212 | def f(a): 213 | pass 214 | 215 | @polypie.polymorphic 216 | def f(a, b): 217 | pass 218 | 219 | @polypie.polymorphic 220 | def f(a, b: str): 221 | pass 222 | 223 | @polypie.polymorphic 224 | def f(a, b: int): 225 | pass 226 | 227 | with pytest.raises(polypie.PolypieException, match='already exists'): 228 | @polypie.polymorphic 229 | def f(a, b: str): 230 | pass 231 | 232 | 233 | def test_function_special_attrs(): 234 | from samples.specialattrs import Wrapper 235 | assert Wrapper.check_special_attrs.__name__ == Wrapper.NAME 236 | assert Wrapper.check_special_attrs.__qualname__ == Wrapper.QUALNAME 237 | assert Wrapper.check_special_attrs.__module__ == Wrapper.MODULE 238 | assert Wrapper.check_special_attrs.attr1 == Wrapper.ATTR1 239 | assert Wrapper.check_special_attrs.attr2 == Wrapper.ATTR2 240 | assert not Wrapper.check_special_attrs.__annotations__ 241 | assert not hasattr(Wrapper.check_special_attrs, '__wrapped__') 242 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35,py36,py37,flake8 3 | 4 | [testenv] 5 | deps = pytest-cov 6 | commands = pytest {posargs} 7 | 8 | [testenv:flake8] 9 | skip_install = true 10 | deps = flake8 11 | commands = flake8 polypie.py setup.py tests 12 | --------------------------------------------------------------------------------