-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
483ca31
commit 5e1ef5f
Showing
8 changed files
with
773 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/__pycache__/ | ||
/.pytest_cache/ | ||
|
||
/dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"restructuredtext.confPath": "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
Legacy API Wrapper | ||
================== | ||
|
||
This module defines a decorator to wrap legacy APIs. | ||
The primary use case is APIs defined before keyword-only parameters. | ||
|
||
>>> from legacy_api_wrap import legacy_api | ||
|
||
We have a function with many positional parameters lying around: | ||
|
||
>>> def fn(a, b=None, d=1, c=2): | ||
... return c, d, e | ||
|
||
We want to convert the positional parameters ``d`` and ``c`` to keyword-only, | ||
change their order and add a parameter. For this we only need to specify name | ||
and order of the old positional parameters in the decorator. | ||
|
||
>>> @legacy_api('d', 'c') | ||
... def fn(a, b=None, *, c=2, d=1, e=3): | ||
... return c, d, e | ||
|
||
After adding the decorator, users can keep calling the old API and get a | ||
``DeprecationWarning``: | ||
|
||
>>> fn(12, 13, 14) == (2, 14, 3) | ||
True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,78 @@ | ||
""" | ||
Legacy API wrapper. | ||
>>> from legacy_api_wrap import legacy_api | ||
>>> @legacy_api('d', 'c') | ||
... def fn(a, b=None, *, c=2, d=1, e=3): | ||
... return c, d, e | ||
>>> fn(12, 13, 14) == (2, 14, 3) | ||
True | ||
""" | ||
|
||
|
||
from functools import wraps | ||
from inspect import signature, Parameter | ||
from warnings import warn | ||
from typing import Sequence | ||
|
||
from get_version import get_version | ||
|
||
|
||
__version__ = get_version(__file__) | ||
|
||
def legacy_api(*positionals): | ||
INF = float('inf') | ||
|
||
|
||
def legacy_api(*old_positionals: Sequence[str]): | ||
""" | ||
Legacy API wrapper. | ||
You want to change the API of a function: | ||
>>> def fn(a, b=None, d=1, c=2, e=3): | ||
... return c, d, e | ||
Add a the decorator and modify the parameters after the ``*``: | ||
>>> @legacy_api('d', 'c') | ||
... def fn(a, b=None, *, c=2, d=1, e=3): | ||
... return c, d, e | ||
And the function can be called using one of both signatures. | ||
>>> fn(12, 13, 14) == (2, 14, 3) | ||
True | ||
Parameters | ||
---------- | ||
old_positionals | ||
The positional parameter names that the old function had after the new function’s ``*``. | ||
""" | ||
def wrapper(fn): | ||
sig = signature(fn) | ||
par_types = {p.kind for p in sig.parameters} | ||
n_positional = None if Parameter.s | ||
par_types = [p.kind for p in sig.parameters.values()] | ||
has_var = Parameter.VAR_POSITIONAL in par_types | ||
n_required = sum(1 for p in sig.parameters.values() if p.default is Parameter.empty) | ||
n_positional = INF if has_var else sum(1 for p in par_types if p in {Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD}) | ||
|
||
@wraps(fn) | ||
def fn_compatible(*args, **kw): | ||
if len(args) > len(fn) | ||
if len(args) > n_positional: | ||
args, args_rest = args[:n_positional], args[n_positional:] | ||
if args_rest: | ||
if len(args_rest) > len(old_positionals): | ||
raise TypeError( | ||
f'{fn.__name__}() takes from {n_required} to {n_positional+len(old_positionals)} parameters, ' | ||
f'but {len(args)+len(args_rest)} were given.' | ||
) | ||
warn( | ||
f'The specified parameters {old_positionals[:len(args_rest)]!r} are no longer positional. ' | ||
f'Please specify them like `{old_positionals[0]}={args_rest[0]!r}`', | ||
DeprecationWarning | ||
) | ||
kw = {**kw, **dict(zip(old_positionals, args_rest))} | ||
|
||
return fn_compatible(*args, **kw) | ||
return fn_new | ||
return fn(*args, **kw) | ||
return fn_compatible | ||
|
||
return wrapper | ||
|
||
|
||
def old(a, b=None, d=1, c=2): | ||
pass | ||
|
||
|
||
@legacy_api('d', 'c') | ||
def new(a, b=None, *, c=2, d=1, e=3): | ||
return dict(a=a, b=b, c=c, d=d, e=e) | ||
|
||
|
||
# Tests --------------------------- | ||
|
||
|
||
def test_inspection_correct(): | ||
assert str(signature(new)) == '(a, b=None, *, c=2, d=1, e=3)' | ||
|
||
|
||
def test_new_param_available(): | ||
new(12, e=13) | ||
|
||
|
||
def test_old_positional_order(): | ||
from pytest import warns | ||
with warns(DeprecationWarning): | ||
res = new(12, 13, 14) | ||
assert res['d'] == 14 | ||
|
||
|
||
if __name__ == '__main__': | ||
from pytest import main | ||
import sys | ||
main(sys.argv) # call on current file |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[build-system] | ||
requires = ['flit'] | ||
build-backend = 'flit.buildapi' | ||
|
||
[tool.flit.metadata] | ||
dist-name = 'legacy-api-wrap' | ||
module = 'legacy_api_wrap' | ||
description-file = 'README.rst' | ||
author = 'Philipp A.' | ||
author-email = 'flying-sheep@web.de' | ||
home-page = 'https://github.com/flying-sheep/legacy-api-wrap' | ||
requires = [ | ||
'get-version >=2.0.4', | ||
'setuptools', | ||
] | ||
classifiers = [# | ||
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[pytest] | ||
python_files = *.py | ||
testpaths = . | ||
addopts = --doctest-modules --doctest-glob='*.rst' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from inspect import signature | ||
|
||
from legacy_api_wrap import legacy_api | ||
|
||
|
||
# def old(a, b=None, d=1, c=2): | ||
# pass | ||
@legacy_api('d', 'c') | ||
def new(a, b=None, *, c=2, d=1, e=3): | ||
return dict(a=a, b=b, c=c, d=d, e=e) | ||
|
||
|
||
def test_inspection_correct(): | ||
assert str(signature(new)) == '(a, b=None, *, c=2, d=1, e=3)' | ||
|
||
|
||
def test_new_param_available(): | ||
new(12, e=13) | ||
|
||
|
||
def test_old_positional_order(): | ||
from pytest import warns | ||
with warns(DeprecationWarning): | ||
res = new(12, 13, 14) | ||
assert res['d'] == 14 | ||
|
||
|
||
def test_too_many_args(): | ||
from pytest import raises | ||
with raises(TypeError, match=r'new\(\) takes from 1 to 4 parameters, but 5 were given\.'): | ||
new(1, 2, 3, 4, 5) |