Add annotation to mark deprecated or experimental APIs via decorators.
Project: http://git-wip-us.apache.org/repos/asf/incubator-beam/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-beam/commit/5e87980b Tree: http://git-wip-us.apache.org/repos/asf/incubator-beam/tree/5e87980b Diff: http://git-wip-us.apache.org/repos/asf/incubator-beam/diff/5e87980b Branch: refs/heads/python-sdk Commit: 5e87980b3cd19bd62aec585808f3f7a349486a79 Parents: dc92438 Author: Maria Garcia Herrero <[email protected]> Authored: Sat Sep 24 02:05:46 2016 -0700 Committer: Robert Bradshaw <[email protected]> Committed: Fri Sep 30 16:48:05 2016 -0700 ---------------------------------------------------------------------- sdks/python/apache_beam/utils/annotations.py | 99 ++++++++++++++++ .../apache_beam/utils/annotations_test.py | 113 +++++++++++++++++++ 2 files changed, 212 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-beam/blob/5e87980b/sdks/python/apache_beam/utils/annotations.py ---------------------------------------------------------------------- diff --git a/sdks/python/apache_beam/utils/annotations.py b/sdks/python/apache_beam/utils/annotations.py new file mode 100644 index 0000000..aa53554 --- /dev/null +++ b/sdks/python/apache_beam/utils/annotations.py @@ -0,0 +1,99 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" Deprecated and experimental annotations. + +Annotations come in two flavors: deprecated and experimental + +The 'deprecated' annotation requires a 'since" parameter to specify +what version deprecated it. +Both 'deprecated' and 'experimental' annotations can specify the +current recommended version to use by means of a 'current' parameter. + +The following example illustrates how to annotate coexisting versions of the +same function 'multiply'. +def multiply(arg1, arg2): + print arg1, '*', arg2, '=', + return arg1*arg2 + +# This annotation marks 'old_multiply' as deprecated since 'v.1' and suggests +# using 'multiply' instead. +@deprecated(since='v.1', current='multiply') +def old_multiply(arg1, arg2): + result = 0 + for i in xrange(arg1): + result += arg2 + print arg1, '*', arg2, '(the old way)=', + return result + +# This annotation marks 'exp_multiply' as experimental and suggests +# using 'multiply' instead. +@experimental(since='v.1', current='multiply') +def exp_multiply(arg1, arg2): + print arg1, '*', arg2, '(the experimental way)=', + return (arg1*arg2)*(arg1/arg2)*(arg2/arg1) + +# Set a warning filter to control how often warnings are produced +warnings.simplefilter("always") +print multiply(5, 6) +print old_multiply(5,6) +print exp_multiply(5,6) +""" + +import warnings +from functools import partial +from functools import wraps + +# Produce only the first occurrence of matching warnings regardless of +# location per line of execution. Since the number of lines of execution +# depends on the concrete runner, the number of warnings produced will +# vary depending on the runner. +warnings.simplefilter("once") + + +def annotate(label, since, current): + """Decorates a function with a deprecated or experimental annotation. + + Args: + label: the kind of annotation ('deprecated' or 'experimental'). + since: the version that causes the annotation. + current: the suggested replacement function. + + Returns: + The decorator for the function. + """ + def _annotate(fnc): + @wraps(fnc) + def inner(*args, **kwargs): + if label == 'deprecated': + warning_type = DeprecationWarning + else: + warning_type = FutureWarning + message = '%s is %s' % (fnc.__name__, label) + if label == 'deprecated': + message += ' since %s' % since + message += '. Use %s instead.'% current if current else '.' + warnings.warn(message, warning_type) + return fnc(*args, **kwargs) + return inner + return _annotate + +# Use partial application to customize each annotation. +# 'current' will be optional in both deprecated and experimental +# while 'since' will be mandatory for deprecated. +deprecated = partial(annotate, label='deprecated', current=None) +experimental = partial(annotate, label='experimental', current=None, since=None) http://git-wip-us.apache.org/repos/asf/incubator-beam/blob/5e87980b/sdks/python/apache_beam/utils/annotations_test.py ---------------------------------------------------------------------- diff --git a/sdks/python/apache_beam/utils/annotations_test.py b/sdks/python/apache_beam/utils/annotations_test.py new file mode 100644 index 0000000..af89590 --- /dev/null +++ b/sdks/python/apache_beam/utils/annotations_test.py @@ -0,0 +1,113 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest +import warnings +from annotations import deprecated +from annotations import experimental + + +class AnnotationTests(unittest.TestCase): + # Note: use different names for each of the the functions decorated + # so that a warning is produced for each of them. + def test_deprecated_with_since_current(self): + with warnings.catch_warnings(record=True) as w: + @deprecated(since='v.1', current='multiply') + def fnc_test_deprecated_with_since_current(): + return 'lol' + fnc_test_deprecated_with_since_current() + self.check_annotation(w, 1, DeprecationWarning, + 'fnc_test_deprecated_with_since_current', + 'deprecated', + [('since', True), ('instead', True)]) + + def test_deprecated_without_current(self): + with warnings.catch_warnings(record=True) as w: + @deprecated(since='v.1') + def fnc_test_deprecated_without_current(): + return 'lol' + fnc_test_deprecated_without_current() + self.check_annotation(w, 1, DeprecationWarning, + 'fnc_test_deprecated_without_current', 'deprecated', + [('since', True), ('instead', False)]) + + def test_deprecated_without_since_should_fail(self): + with warnings.catch_warnings(record=True) as w: + with self.assertRaises(TypeError): + + @deprecated() + def fnc_test_deprecated_without_since_should_fail(): + return 'lol' + fnc_test_deprecated_without_since_should_fail() + assert len(w) == 0 + + def test_experimental_with_current(self): + with warnings.catch_warnings(record=True) as w: + @experimental(current='multiply') + def fnc_test_experimental_with_current(): + return 'lol' + fnc_test_experimental_with_current() + self.check_annotation(w, 1, FutureWarning, + 'fnc_test_experimental_with_current', + 'experimental', [('instead', True)]) + + def test_experimental_without_current(self): + with warnings.catch_warnings(record=True) as w: + @experimental() + def fnc_test_experimental_without_current(): + return 'lol' + fnc_test_experimental_without_current() + self.check_annotation(w, 1, FutureWarning, + 'fnc_test_experimental_without_current', + 'experimental', [('instead', False)]) + + def test_frequency(self): + """Tests that the filter 'once' is sufficient to print once per + warning independently of location.""" + with warnings.catch_warnings(record=True) as w: + @experimental() + def fnc_test_annotate_frequency(): + return 'lol' + + @experimental() + def fnc2_test_annotate_frequency(): + return 'lol' + fnc_test_annotate_frequency() + fnc_test_annotate_frequency() + fnc2_test_annotate_frequency() + self.check_annotation([w[0]], 1, FutureWarning, + 'fnc_test_annotate_frequency', 'experimental', + []) + self.check_annotation([w[1]], 1, FutureWarning, + 'fnc2_test_annotate_frequency', 'experimental', + []) + + # helper function + def check_annotation(self, warning, warning_size, warning_type, fnc_name, + annotation_type, label_check_list): + self.assertEqual(1, warning_size) + self.assertTrue(issubclass(warning[-1].category, warning_type)) + self.assertIn(fnc_name + ' is ' + annotation_type, str(warning[-1].message)) + for label in label_check_list: + if label[1] is True: + self.assertIn(label[0], str(warning[-1].message)) + else: + self.assertNotIn(label[0], str(warning[-1].message)) + + +if __name__ == '__main__': # It doesn't like these 2 lines + unittest.main()
