Skip to content

Commit

Permalink
v0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep committed Nov 26, 2018
1 parent 483ca31 commit 5e1ef5f
Show file tree
Hide file tree
Showing 8 changed files with 773 additions and 39 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/__pycache__/
/.pytest_cache/

/dist/
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"restructuredtext.confPath": ""
}
619 changes: 619 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions README.rst
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
107 changes: 68 additions & 39 deletions legacy_api_wrap.py
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
18 changes: 18 additions & 0 deletions pyproject.toml
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+)',
]
4 changes: 4 additions & 0 deletions pytest.ini
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'
31 changes: 31 additions & 0 deletions tests.py
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)

0 comments on commit 5e1ef5f

Please sign in to comment.