Skip to content

Commit f47d82b

Browse files
committed
1 parent c13ab9d commit f47d82b

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed

src/sasctl/utils/decorators.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env python
2+
# encoding: utf-8
3+
#
4+
# Copyright © 2019, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
import functools
8+
import textwrap
9+
import warnings
10+
11+
import six
12+
13+
14+
class ExperimentalWarning(UserWarning):
15+
"""Warning raised by @experimental decorator."""
16+
pass
17+
18+
19+
def _insert_docstring_text(func, text):
20+
docstring = func.__doc__ or ''
21+
22+
# Dedent the existing docstring. Multi-line docstrings are only indented
23+
# after the first line, so split before dedenting.
24+
if '\n' in docstring:
25+
first_line, remainder = docstring.split('\n', 1)
26+
docstring = first_line + '\n' + textwrap.dedent(remainder)
27+
else:
28+
docstring = textwrap.dedent(docstring)
29+
30+
docstring = docstring.strip()
31+
32+
text = ('',
33+
text,
34+
'')
35+
36+
return docstring + '\n'.join(text)
37+
38+
39+
def deprecated(reason=None, version=None, removed_in=None):
40+
"""Decorate a function or class to designated it as deprecated.
41+
42+
Will raise a `DeprecationWarning` when used and automatically adds a
43+
Sphinx '.. deprecated::' directive to the docstring.
44+
45+
Parameters
46+
----------
47+
reason : str, optional
48+
User-friendly reason for deprecating.
49+
version : str
50+
Version in which initially marked as deprecated.
51+
removed_in : str, optional
52+
Version in which the class or function will be removed.
53+
54+
Returns
55+
-------
56+
decorator
57+
58+
"""
59+
if version is None:
60+
raise ValueError('version must be specified.')
61+
62+
def decorator(func):
63+
64+
@functools.wraps(func)
65+
def _wrapper(*args, **kwargs):
66+
warning = '%s is deprecated since version %s' % (func.__name__, version)
67+
68+
if removed_in is not None:
69+
warning += ' and will be removed in version %s.' % removed_in
70+
else:
71+
warning += ' and may be removed in a future version.'
72+
73+
if reason is not None:
74+
warning = warning + ' ' + reason
75+
76+
warnings.warn(warning,
77+
category=DeprecationWarning, stacklevel=2)
78+
return func(*args, **kwargs)
79+
80+
# Generate Sphinx deprecated directive
81+
directive = '.. deprecated:: %s' % version
82+
83+
if reason is not None:
84+
directive += '\n %s' % reason
85+
86+
# Insert directive into original docstring
87+
_wrapper.__doc__ = _insert_docstring_text(func, directive)
88+
89+
return _wrapper
90+
91+
return decorator
92+
93+
94+
def experimental(func):
95+
"""Decorate a function or class to designated it as experimental.
96+
97+
Will raise an `ExperimentalWarning` when used and automatically adds a
98+
Sphinx '.. warning::' directive to the docstring.
99+
100+
Parameters
101+
----------
102+
func
103+
104+
Returns
105+
-------
106+
func
107+
108+
"""
109+
@functools.wraps(func)
110+
def _wrapper(*args, **kwargs):
111+
warning = '%s is experimental and may be modified or removed without warning.' % func.__name__
112+
warnings.warn(warning,
113+
category=ExperimentalWarning, stacklevel=2)
114+
return func(*args, **kwargs)
115+
116+
type_ = 'class' if isinstance(func, six.class_types) else 'method'
117+
directive = '.. warning:: This %s is experimental and may be modified or removed without warning.' % type_
118+
119+
# Insert directive into original docstring
120+
_wrapper.__doc__ = _insert_docstring_text(func, directive)
121+
122+
return _wrapper
123+

tests/unit/test_decorators.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env python
2+
# encoding: utf-8
3+
#
4+
# Copyright © 2019, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
import pytest
8+
9+
from sasctl.utils.decorators import deprecated, experimental, ExperimentalWarning
10+
11+
12+
def test_deprecated():
13+
"""Function can be deprecated with @deprecated(version=XX)."""
14+
@deprecated(version=1.2)
15+
def old_function(a):
16+
"""Do old stuff.
17+
18+
Parameters
19+
----------
20+
a : any
21+
22+
Returns
23+
-------
24+
stuff
25+
26+
"""
27+
return a
28+
29+
# Calling the function should raise a warning
30+
with pytest.warns(DeprecationWarning) as warnings:
31+
result = old_function('spam')
32+
33+
msg = warnings[0].message
34+
assert 'old_function is deprecated since version 1.2 and may be removed in a future version.' == str(msg)
35+
assert '.. deprecated:: 1.2' in old_function.__doc__
36+
37+
# Return value from function should be unchanged.
38+
assert result == 'spam'
39+
40+
41+
def test_deprecated_with_reason():
42+
"""Function can be deprecated with @deprecated(reason, version=XX)."""
43+
@deprecated('Use new_function instead.', version=1.3)
44+
def old_function(a):
45+
"""Do old stuff.
46+
47+
Parameters
48+
----------
49+
a : any
50+
51+
Returns
52+
-------
53+
stuff
54+
55+
"""
56+
return a
57+
58+
# Calling the function should raise a warning
59+
with pytest.warns(DeprecationWarning) as warnings:
60+
result = old_function('spam')
61+
62+
msg = warnings[0].message
63+
assert 'old_function is deprecated since version 1.3 and may be removed in a future version. Use new_function instead.' == str(msg)
64+
assert '.. deprecated:: 1.3\n Use new_function instead.' in old_function.__doc__
65+
66+
# Return value from function should be unchanged.
67+
assert result == 'spam'
68+
69+
70+
def test_experimental_function():
71+
@experimental
72+
def new_function(p):
73+
"""Revive a dead parrot.
74+
75+
Parameters
76+
----------
77+
p : parrot
78+
79+
Returns
80+
-------
81+
parrot
82+
83+
"""
84+
return p
85+
86+
# Calling the function should raise a warning
87+
with pytest.warns(ExperimentalWarning) as warnings:
88+
result = new_function('norwegian blue')
89+
90+
assert '.. warning:: ' in new_function.__doc__
91+
92+
# Return value from function should be unchanged.
93+
assert result == 'norwegian blue'
94+
95+
96+
def test_experimental_class():
97+
@experimental
98+
class NewClass:
99+
pass
100+
101+
# Calling the function should raise a warning
102+
with pytest.warns(ExperimentalWarning) as warnings:
103+
result = NewClass()
104+
105+
assert '.. warning:: ' in NewClass.__doc__

0 commit comments

Comments
 (0)