jenkins-bot has submitted this change and it was merged.
Change subject: [FIX] Allow deprecation of trailing parameters ......................................................................
[FIX] Allow deprecation of trailing parameters
With 7aa43ba4a0c8e12bda4a62e32e062672ec8851fa the parameters of several methods have been deprecated leaving just 'self'. Because only one or two parameters were used before, the possibility is high that they were used positionally (without the parameter name) and the @deprecated_args syntax can't support that (reliably).
This adds a decorator to deprecate parameters which appeared at the end of a method/function. The names of those parameters must be specified and if that method/function is called with too many parameters those are removed and a warning is issued.
Bug: T75489 Change-Id: I9035f3b6be86fe7b34e515e19affe8ef1a9a558e --- M pywikibot/page.py M pywikibot/site.py M pywikibot/tools.py M tests/deprecation_tests.py 4 files changed, 227 insertions(+), 11 deletions(-)
Approvals: John Vandenberg: Looks good to me, approved jenkins-bot: Verified
diff --git a/pywikibot/page.py b/pywikibot/page.py index 2d71d8a..fb80790 100644 --- a/pywikibot/page.py +++ b/pywikibot/page.py @@ -49,7 +49,8 @@ SiteDefinitionError ) from pywikibot.tools import ( - ComparableMixin, deprecated, deprecate_arg, deprecated_args + ComparableMixin, deprecated, deprecate_arg, deprecated_args, + remove_last_args ) from pywikibot import textlib
@@ -241,7 +242,7 @@ title = title.replace(forbidden, '_') return title
- @deprecated_args(decode=None, underscore=None) + @remove_last_args(('decode', 'underscore')) def section(self): """Return the name of the section this Page refers to.
@@ -540,7 +541,7 @@
return self._lastNonBotUser
- @deprecated_args(datetime=None) + @remove_last_args(('datetime', )) def editTime(self): """Return timestamp of last revision to page.
diff --git a/pywikibot/site.py b/pywikibot/site.py index 2a55df6..8f984ca 100644 --- a/pywikibot/site.py +++ b/pywikibot/site.py @@ -29,7 +29,7 @@ import pywikibot.family from pywikibot.tools import ( itergroup, deprecated, deprecate_arg, UnicodeMixin, ComparableMixin, - redirect_func, add_decorated_full_name, deprecated_args, + redirect_func, add_decorated_full_name, deprecated_args, remove_last_args, SelfCallDict, SelfCallString, signature, ) from pywikibot.tools import MediaWikiVersion as LV @@ -707,17 +707,17 @@ old_name='normalizeNamespace', class_name='BaseSite')
- @deprecated_args(default=None) + @remove_last_args(('default', )) def redirect(self): """Return list of localized redirect tags for the site.""" return [u"REDIRECT"]
- @deprecated_args(default=None) + @remove_last_args(('default', )) def pagenamecodes(self): """Return list of localized PAGENAME tags for the site.""" return [u"PAGENAME"]
- @deprecated_args(default=None) + @remove_last_args(('default', )) def pagename2codes(self): """Return list of localized PAGENAMEE tags for the site.""" return [u"PAGENAMEE"] @@ -2015,7 +2015,7 @@ else: return [word]
- @deprecated_args(default=None) + @remove_last_args(('default', )) def redirect(self): """Return the localized #REDIRECT keyword.""" # return the magic word without the preceding '#' character @@ -2038,12 +2038,12 @@ pattern = None return BaseSite.redirectRegex(self, pattern)
- @deprecated_args(default=None) + @remove_last_args(('default', )) def pagenamecodes(self): """Return list of localized PAGENAME tags for the site.""" return self.getmagicwords("pagename")
- @deprecated_args(default=None) + @remove_last_args(('default', )) def pagename2codes(self): """Return list of localized PAGENAMEE tags for the site.""" return self.getmagicwords("pagenamee") diff --git a/pywikibot/tools.py b/pywikibot/tools.py index d34291d..7ff4008 100644 --- a/pywikibot/tools.py +++ b/pywikibot/tools.py @@ -705,6 +705,74 @@ return decorator
+def remove_last_args(arg_names): + """ + Decorator to declare all args additionally provided deprecated. + + All positional arguments appearing after the normal arguments are marked + deprecated. It marks also all keyword arguments present in arg_names as + deprecated. Any arguments (positional or keyword) which are not present in + arg_names are forwarded. For example a call with 3 parameters and the + original function requests one and arg_names contain one name will result + in an error, because the function got called with 2 parameters. + + The decorated function may not use *args or **kwargs. + + @param arg_names: The names of all arguments. + @type arg_names: iterable; for the most explanatory message it should + retain the given order (so not a set for example). + """ + def decorator(obj): + """Outer wrapper. + + The outer wrapper is used to create the decorating wrapper. + + @param obj: function being wrapped + @type obj: object + """ + def wrapper(*__args, **__kw): + """Replacement function. + + @param __args: args passed to the decorated function + @type __args: list + @param __kwargs: kwargs passed to the decorated function + @type __kwargs: dict + @return: the value returned by the decorated function + @rtype: any + """ + name = obj.__full_name__ + args, varargs, kwargs, _ = inspect.getargspec(wrapper.__wrapped__) + if varargs is not None and kwargs is not None: + raise ValueError(u'{1} may not have * or ** args.'.format( + name)) + deprecated = set(__kw) & set(arg_names) + if len(__args) > len(args): + deprecated.update(arg_names[:len(__args) - len(args)]) + # remove at most |arg_names| entries from the back + new_args = tuple(__args[:max(len(args), len(__args) - len(arg_names))]) + new_kwargs = dict((arg, val) for arg, val in __kw.items() + if arg not in arg_names) + + if deprecated: + # sort them according to arg_names + deprecated = [arg for arg in arg_names if arg in deprecated] + warning(u"The trailing arguments ('{0}') of {1} are " + "deprecated. The value(s) provided for '{2}' have " + "been dropped.".format("', '".join(arg_names), name, + "', '".join(deprecated))) + return obj(*new_args, **new_kwargs) + + wrapper.__doc__ = obj.__doc__ + wrapper.__name__ = obj.__name__ + wrapper.__module__ = obj.__module__ + if not hasattr(obj, '__full_name__'): + add_decorated_full_name(obj) + wrapper.__full_name__ = obj.__full_name__ + wrapper.__wrapped__ = getattr(obj, '__wrapped__', obj) + return wrapper + return decorator + + def redirect_func(target, source_module=None, target_module=None, old_name=None, class_name=None): """ diff --git a/tests/deprecation_tests.py b/tests/deprecation_tests.py index 98f3ccc..a7a27bc 100644 --- a/tests/deprecation_tests.py +++ b/tests/deprecation_tests.py @@ -8,7 +8,7 @@ __version__ = '$Id$'
from pywikibot.tools import ( - deprecated, deprecate_arg, deprecated_args, add_full_name + deprecated, deprecate_arg, deprecated_args, add_full_name, remove_last_args ) from tests.aspects import unittest, DeprecationTestCase
@@ -116,6 +116,16 @@ return foo
+@remove_last_args(['foo', 'bar']) +def deprecated_all(): + return None + + +@remove_last_args(['bar']) +def deprecated_all2(foo): + return foo + + class DeprecatedMethodClass(object):
"""Class with methods deprecated.""" @@ -165,6 +175,14 @@ @deprecated() def deprecated_instance_method_and_arg2(self, foo): self.foo = foo + return foo + + @remove_last_args(['foo', 'bar']) + def deprecated_all(self): + return None + + @remove_last_args(['bar']) + def deprecated_all2(self, foo): return foo
@@ -408,6 +426,135 @@
DeprecatorTestCase._reset_messages()
+ def test_function_remove_last_args(self): + """Test @remove_last_args on functions.""" + rv = deprecated_all() + self.assertEqual(rv, None) + self.assertNoDeprecation() + + rv = deprecated_all(foo=42) + self.assertEqual(rv, None) + self.assertDeprecation("The trailing arguments ('foo', 'bar') of " + __name__ + ".deprecated_all are deprecated. The value(s) provided for 'foo' have been dropped.") + + self._reset_messages() + + rv = deprecated_all(42) + self.assertEqual(rv, None) + self.assertDeprecation("The trailing arguments ('foo', 'bar') of " + __name__ + ".deprecated_all are deprecated. The value(s) provided for 'foo' have been dropped.") + + self._reset_messages() + + rv = deprecated_all(foo=42, bar=47) + self.assertEqual(rv, None) + self.assertDeprecation("The trailing arguments ('foo', 'bar') of " + __name__ + ".deprecated_all are deprecated. The value(s) provided for 'foo', 'bar' have been dropped.") + + self._reset_messages() + + rv = deprecated_all(42, 47) + self.assertEqual(rv, None) + self.assertDeprecation("The trailing arguments ('foo', 'bar') of " + __name__ + ".deprecated_all are deprecated. The value(s) provided for 'foo', 'bar' have been dropped.") + + self._reset_messages() + + rv = deprecated_all2(foo=42) + self.assertEqual(rv, 42) + self.assertNoDeprecation() + + rv = deprecated_all2(42) + self.assertEqual(rv, 42) + self.assertNoDeprecation() + + rv = deprecated_all2(42, bar=47) + self.assertEqual(rv, 42) + self.assertDeprecation("The trailing arguments ('bar') of " + __name__ + ".deprecated_all2 are deprecated. The value(s) provided for 'bar' have been dropped.") + + self._reset_messages() + + def test_method_remove_last_args(self): + """Test @remove_last_args on functions.""" + f = DeprecatedMethodClass() + + rv = f.deprecated_all() + self.assertEqual(rv, None) + self.assertNoDeprecation() + + rv = f.deprecated_all(foo=42) + self.assertEqual(rv, None) + self.assertDeprecation("The trailing arguments ('foo', 'bar') of " + __name__ + ".DeprecatedMethodClass.deprecated_all are deprecated. The value(s) provided for 'foo' have been dropped.") + + self._reset_messages() + + rv = f.deprecated_all(42) + self.assertEqual(rv, None) + self.assertDeprecation("The trailing arguments ('foo', 'bar') of " + __name__ + ".DeprecatedMethodClass.deprecated_all are deprecated. The value(s) provided for 'foo' have been dropped.") + + self._reset_messages() + + rv = f.deprecated_all(foo=42, bar=47) + self.assertEqual(rv, None) + self.assertDeprecation("The trailing arguments ('foo', 'bar') of " + __name__ + ".DeprecatedMethodClass.deprecated_all are deprecated. The value(s) provided for 'foo', 'bar' have been dropped.") + + self._reset_messages() + + rv = f.deprecated_all(42, 47) + self.assertEqual(rv, None) + self.assertDeprecation("The trailing arguments ('foo', 'bar') of " + __name__ + ".DeprecatedMethodClass.deprecated_all are deprecated. The value(s) provided for 'foo', 'bar' have been dropped.") + + self._reset_messages() + + rv = f.deprecated_all2(foo=42) + self.assertEqual(rv, 42) + self.assertNoDeprecation() + + rv = f.deprecated_all2(42) + self.assertEqual(rv, 42) + self.assertNoDeprecation() + + rv = f.deprecated_all2(42, bar=47) + self.assertEqual(rv, 42) + self.assertDeprecation("The trailing arguments ('bar') of " + __name__ + ".DeprecatedMethodClass.deprecated_all2 are deprecated. The value(s) provided for 'bar' have been dropped.") + + def test_remove_last_args_invalid(self): + self.assertRaisesRegex( + TypeError, + r"(deprecated_all2() missing 1 required positional argument: 'foo'|" # Python 3 + "deprecated_all2() takes exactly 1 argument (0 given))", # Python 2 + deprecated_all2) + + self.assertRaisesRegex( + TypeError, + r"deprecated_all2() got an unexpected keyword argument 'hello'", + deprecated_all2, + hello='world') + + self.assertRaisesRegex( + TypeError, + r'deprecated_all2() takes (exactly )?1 (positional )?argument' + ' (but 2 were given|(2 given))', + deprecated_all2, + 1, 2, 3) + + f = DeprecatedMethodClass() + + self.assertRaisesRegex( + TypeError, + r"(deprecated_all2() missing 1 required positional argument: 'foo'|" # Python 3 + "deprecated_all2() takes exactly 2 arguments (1 given))", # Python 2 + f.deprecated_all2) + + self.assertRaisesRegex( + TypeError, + r"deprecated_all2() got an unexpected keyword argument 'hello'", + f.deprecated_all2, + hello='world') + + self.assertRaisesRegex( + TypeError, + r'deprecated_all2() takes (exactly )?2 (positional )?arguments ' + '(but 3 were given|(3 given))', + f.deprecated_all2, + 1, 2, 3) + def test_deprecated_instance_method_zero_arg(self): """Test @deprecate_arg with classes, without arguments.""" f = DeprecatedMethodClass()
pywikibot-commits@lists.wikimedia.org