jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/640399 )
Change subject: [IMPR] move TokenWallet to site/_tokenwallet.py file ......................................................................
[IMPR] move TokenWallet to site/_tokenwallet.py file
Change-Id: I1c13ecad768ca8ca72493f3fcf906075bfce31d0 --- M docs/api_ref/pywikibot.site.rst M pywikibot/CONTENT.rst M pywikibot/site/__init__.py A pywikibot/site/_tokenwallet.py M tests/__init__.py M tests/site_tests.py A tests/token_tests.py 7 files changed, 328 insertions(+), 286 deletions(-)
Approvals: Xqt: Looks good to me, approved jenkins-bot: Verified
diff --git a/docs/api_ref/pywikibot.site.rst b/docs/api_ref/pywikibot.site.rst index 6e5796e..9545088 100644 --- a/docs/api_ref/pywikibot.site.rst +++ b/docs/api_ref/pywikibot.site.rst @@ -20,3 +20,8 @@ --------------------------------
.. automodule:: pywikibot.site._siteinfo + +pywikibot.site._tokenwallet module +----------------------------------- + +.. automodule:: pywikibot.site._tokenwallet diff --git a/pywikibot/CONTENT.rst b/pywikibot/CONTENT.rst index f34e7fe..fc9fa1e 100644 --- a/pywikibot/CONTENT.rst +++ b/pywikibot/CONTENT.rst @@ -119,6 +119,8 @@ +----------------------------+------------------------------------------------------+ | _siteinfo.py | Objects representing site info data contents. | +----------------------------+------------------------------------------------------+ + | _tokenwallet.py | Objects representing api tokens. | + +----------------------------+------------------------------------------------------+
+----------------------------+------------------------------------------------------+ diff --git a/pywikibot/site/__init__.py b/pywikibot/site/__init__.py index 9526291..3b47505 100644 --- a/pywikibot/site/__init__.py +++ b/pywikibot/site/__init__.py @@ -69,6 +69,7 @@ from pywikibot.site._decorators import need_extension, need_right, need_version from pywikibot.site._interwikimap import _InterwikiMap from pywikibot.site._siteinfo import Siteinfo +from pywikibot.site._tokenwallet import TokenWallet from pywikibot.throttle import Throttle from pywikibot.tools import ( ComparableMixin, @@ -1108,87 +1109,6 @@ return api.encode_url(query)
-class TokenWallet: - - """Container for tokens.""" - - def __init__(self, site): - """Initializer. - - @type site: pywikibot.site.APISite - """ - self.site = site - self._tokens = {} - self.failed_cache = set() # cache unavailable tokens. - - def load_tokens(self, types, all=False): - """ - Preload one or multiple tokens. - - @param types: the types of token. - @type types: iterable - @param all: load all available tokens, if None only if it can be done - in one request. - @type all: bool - """ - if self.site.user() is None: - self.site.login() - - self._tokens.setdefault(self.site.user(), {}).update( - self.site.get_tokens(types, all=all)) - - # Preload all only the first time. - # When all=True types is extended in site.get_tokens(). - # Keys not recognised as tokens, are cached so they are not requested - # any longer. - if all is not False: - for key in types: - if key not in self._tokens[self.site.user()]: - self.failed_cache.add((self.site.user(), key)) - - def __getitem__(self, key): - """Get token value for the given key.""" - if self.site.user() is None: - self.site.login() - - user_tokens = self._tokens.setdefault(self.site.user(), {}) - # always preload all for users without tokens - failed_cache_key = (self.site.user(), key) - - try: - key = self.site.validate_tokens([key])[0] - except IndexError: - raise Error( - "Requested token '{0}' is invalid on {1} wiki." - .format(key, self.site)) - - if (key not in user_tokens - and failed_cache_key not in self.failed_cache): - self.load_tokens([key], all=False if user_tokens else None) - - if key in user_tokens: - return user_tokens[key] - else: - # token not allowed for self.site.user() on self.site - self.failed_cache.add(failed_cache_key) - # to be changed back to a plain KeyError? - raise Error( - "Action '{0}' is not allowed for user {1} on {2} wiki." - .format(key, self.site.user(), self.site)) - - def __contains__(self, key): - """Return True if the given token name is cached.""" - return key in self._tokens.setdefault(self.site.user(), {}) - - def __str__(self): - """Return a str representation of the internal tokens dictionary.""" - return self._tokens.__str__() - - def __repr__(self): - """Return a representation of the internal tokens dictionary.""" - return self._tokens.__repr__() - - class RemovedSite(BaseSite):
"""Site removed from a family.""" diff --git a/pywikibot/site/_tokenwallet.py b/pywikibot/site/_tokenwallet.py new file mode 100644 index 0000000..b161b29 --- /dev/null +++ b/pywikibot/site/_tokenwallet.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +"""Objects representing api tokens.""" +# +# (C) Pywikibot team, 2008-2020 +# +# Distributed under the terms of the MIT license. +# +from pywikibot.exceptions import Error + + +class TokenWallet: + + """Container for tokens.""" + + def __init__(self, site): + """Initializer. + + @type site: pywikibot.site.APISite + """ + self.site = site + self._tokens = {} + self.failed_cache = set() # cache unavailable tokens. + + def load_tokens(self, types, all=False): + """ + Preload one or multiple tokens. + + @param types: the types of token. + @type types: iterable + @param all: load all available tokens, if None only if it can be done + in one request. + @type all: bool + """ + if self.site.user() is None: + self.site.login() + + self._tokens.setdefault(self.site.user(), {}).update( + self.site.get_tokens(types, all=all)) + + # Preload all only the first time. + # When all=True types is extended in site.get_tokens(). + # Keys not recognised as tokens, are cached so they are not requested + # any longer. + if all is not False: + for key in types: + if key not in self._tokens[self.site.user()]: + self.failed_cache.add((self.site.user(), key)) + + def __getitem__(self, key): + """Get token value for the given key.""" + if self.site.user() is None: + self.site.login() + + user_tokens = self._tokens.setdefault(self.site.user(), {}) + # always preload all for users without tokens + failed_cache_key = (self.site.user(), key) + + try: + key = self.site.validate_tokens([key])[0] + except IndexError: + raise Error( + "Requested token '{0}' is invalid on {1} wiki." + .format(key, self.site)) + + if (key not in user_tokens + and failed_cache_key not in self.failed_cache): + self.load_tokens([key], all=False if user_tokens else None) + + if key in user_tokens: + return user_tokens[key] + else: + # token not allowed for self.site.user() on self.site + self.failed_cache.add(failed_cache_key) + # to be changed back to a plain KeyError? + raise Error( + "Action '{0}' is not allowed for user {1} on {2} wiki." + .format(key, self.site.user(), self.site)) + + def __contains__(self, key): + """Return True if the given token name is cached.""" + return key in self._tokens.setdefault(self.site.user(), {}) + + def __str__(self): + """Return a str representation of the internal tokens dictionary.""" + return self._tokens.__str__() + + def __repr__(self): + """Return a representation of the internal tokens dictionary.""" + return self._tokens.__repr__() diff --git a/tests/__init__.py b/tests/__init__.py index f313e60..e03eba8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -114,6 +114,7 @@ 'timestamp', 'timestripper', 'tk', + 'token', 'tools', 'tools_chars', 'tools_formatter', diff --git a/tests/site_tests.py b/tests/site_tests.py index 0806f34..1d37ee8 100644 --- a/tests/site_tests.py +++ b/tests/site_tests.py @@ -29,7 +29,6 @@ DefaultWikidataClientTestCase, DeprecationTestCase, TestCase, - TestCaseBase, unittest, WikimediaDefaultSiteTestCase, WikidataTestCase, @@ -37,34 +36,6 @@ from tests.basepage import BasePageLoadRevisionsCachingTestBase
-class TokenTestBase(TestCaseBase): - - """Verify token exists before running tests.""" - - def setUp(self): - """Skip test if user does not have token and clear site wallet.""" - super().setUp() - mysite = self.get_site() - ttype = self.token_type - try: - token = mysite.tokens[ttype] - except pywikibot.Error as error_msg: - self.assertRegex( - str(error_msg), - "Action '[a-z]+' is not allowed for user .* on .* wiki.") - self.assertNotIn(self.token_type, self.site.tokens) - self.skipTest(error_msg) - - self.token = token - self._orig_wallet = self.site.tokens - self.site.tokens = pywikibot.site.TokenWallet(self.site) - - def tearDown(self): - """Restore site tokens.""" - self.site.tokens = self._orig_wallet - super().tearDown() - - class TestSiteObjectDeprecatedFunctions(DefaultSiteTestCase, DeprecationTestCase):
@@ -2099,59 +2070,6 @@ self.assertEqual(cnt, len(all_users), 'Some test usernames not found')
-class PatrolTestCase(TokenTestBase, TestCase): - - """Test patrol method.""" - - family = 'wikipedia' - code = 'test' - - user = True - token_type = 'patrol' - write = True - - def test_patrol(self): - """Test the site.patrol() method.""" - mysite = self.get_site() - - rc = list(mysite.recentchanges(total=1)) - if not rc: - self.skipTest('no recent changes to patrol') - - rc = rc[0] - - # site.patrol() needs params - self.assertRaises(pywikibot.Error, lambda x: list(x), mysite.patrol()) - try: - result = list(mysite.patrol(rcid=rc['rcid'])) - except api.APIError as error: - if error.code == 'permissiondenied': - self.skipTest(error) - raise - - if hasattr(mysite, '_patroldisabled') and mysite._patroldisabled: - self.skipTest('Patrolling is disabled on {} wiki.'.format(mysite)) - - result = result[0] - self.assertIsInstance(result, dict) - - params = {'rcid': 0} - if mysite.mw_version >= '1.22': - params['revid'] = [0, 1] - - raised = False - try: - # no such rcid, revid or too old revid - list(mysite.patrol(**params)) - except api.APIError as error: - if error.code == 'badtoken': - self.skipTest(error) - except pywikibot.Error: - # expected result - raised = True - self.assertTrue(raised, msg='pywikibot.Error not raised') - - class SiteRandomTestCase(DefaultSiteTestCase):
"""Test random methods of a site.""" @@ -2203,129 +2121,6 @@ self.assertIn(rndpage.namespace(), [6, 7])
-class TestSiteTokens(DefaultSiteTestCase): - - """Test cases for tokens in Site methods. - - Versions of sites are simulated if actual versions are higher than - needed by the test case. - - Test is skipped if site version is not compatible. - - """ - - user = True - - def setUp(self): - """Store version.""" - super().setUp() - self.mysite = self.get_site() - self._version = self.mysite.mw_version - self.orig_version = self.mysite.version - - def tearDown(self): - """Restore version.""" - super().tearDown() - self.mysite.version = self.orig_version - - def _test_tokens(self, version, test_version, additional_token): - """Test tokens.""" - if version and (self._version < version - or self._version < test_version): - raise unittest.SkipTest( - 'Site {} version {} is too low for this tests.' - .format(self.mysite, self._version)) - - self.mysite.version = lambda: test_version - - for ttype in ('edit', 'move', additional_token): - tokentype = self.mysite.validate_tokens([ttype]) - try: - token = self.mysite.tokens[ttype] - except pywikibot.Error as error_msg: - if tokentype: - self.assertRegex( - str(error_msg), - "Action '[a-z]+' is not allowed " - 'for user .* on .* wiki.') - # test __contains__ - self.assertNotIn(tokentype[0], self.mysite.tokens) - else: - self.assertRegex( - str(error_msg), - "Requested token '[a-z]+' is invalid on .* wiki.") - else: - self.assertIsInstance(token, str) - self.assertEqual(token, self.mysite.tokens[ttype]) - # test __contains__ - self.assertIn(tokentype[0], self.mysite.tokens) - - def test_tokens_in_mw_119(self): - """Test ability to get page tokens.""" - self._test_tokens(None, '1.19', 'delete') - - def test_patrol_tokens_in_mw_119(self): - """Test ability to get patrol token on MW 1.19 wiki.""" - self._test_tokens('1.19', '1.19', 'patrol') - - def test_tokens_in_mw_120_124wmf18(self): - """Test ability to get page tokens.""" - self._test_tokens('1.20', '1.21', 'deleteglobalaccount') - - def test_patrol_tokens_in_mw_120(self): - """Test ability to get patrol token.""" - self._test_tokens('1.19', '1.20', 'patrol') - - def test_tokens_in_mw_124wmf19(self): - """Test ability to get page tokens.""" - self._test_tokens('1.24wmf19', '1.24wmf20', 'deleteglobalaccount') - - def testInvalidToken(self): - """Test invalid token.""" - self.assertRaises(pywikibot.Error, lambda t: self.mysite.tokens[t], - 'invalidtype') - - -class TestDeprecatedEditTokenFunctions(TokenTestBase, - DefaultSiteTestCase, - DeprecationTestCase): - - """Test cases for Site edit token deprecated methods.""" - - cached = True - user = True - token_type = 'edit' - - def test_getToken(self): - """Test ability to get page tokens using site.getToken.""" - self.mysite = self.site - self.assertEqual(self.mysite.getToken(), self.mysite.tokens['edit']) - self.assertOneDeprecationParts('pywikibot.site.APISite.getToken', - "the 'tokens' property") - - -class TestDeprecatedPatrolToken(DefaultSiteTestCase, DeprecationTestCase): - - """Test cases for Site patrol token deprecated methods.""" - - cached = True - user = True - - def test_getPatrolToken(self): - """Test site.getPatrolToken.""" - self.mysite = self.site - try: - self.assertEqual(self.mysite.getPatrolToken(), - self.mysite.tokens['patrol']) - self.assertOneDeprecation() - except pywikibot.Error as error_msg: - self.assertRegex( - str(error_msg), - "Action '[a-z]+' is not allowed for user .* on .* wiki.") - # test __contains__ - self.assertNotIn('patrol', self.mysite.tokens) - - class TestSiteExtensions(WikimediaDefaultSiteTestCase):
"""Test cases for Site extensions.""" diff --git a/tests/token_tests.py b/tests/token_tests.py new file mode 100644 index 0000000..6e5ae57 --- /dev/null +++ b/tests/token_tests.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +"""Tests for tokens.""" +# +# (C) Pywikibot team, 2015-2020 +# +# Distributed under the terms of the MIT license. +# +from contextlib import suppress + +import pywikibot + +from pywikibot.data import api +from pywikibot.site import TokenWallet + +from tests.aspects import ( + DefaultSiteTestCase, + DeprecationTestCase, + TestCase, + TestCaseBase, + unittest, +) + + +class TestSiteTokens(DefaultSiteTestCase): + + """Test cases for tokens in Site methods. + + Versions of sites are simulated if actual versions are higher than + needed by the test case. + + Test is skipped if site version is not compatible. + + """ + + user = True + + def setUp(self): + """Store version.""" + super().setUp() + self.mysite = self.get_site() + self._version = self.mysite.mw_version + self.orig_version = self.mysite.version + + def tearDown(self): + """Restore version.""" + super().tearDown() + self.mysite.version = self.orig_version + + def _test_tokens(self, version, test_version, additional_token): + """Test tokens.""" + if version and (self._version < version + or self._version < test_version): + raise unittest.SkipTest( + 'Site {} version {} is too low for this tests.' + .format(self.mysite, self._version)) + + self.mysite.version = lambda: test_version + + for ttype in ('edit', 'move', additional_token): + tokentype = self.mysite.validate_tokens([ttype]) + try: + token = self.mysite.tokens[ttype] + except pywikibot.Error as error_msg: + if tokentype: + self.assertRegex( + str(error_msg), + "Action '[a-z]+' is not allowed " + 'for user .* on .* wiki.') + # test __contains__ + self.assertNotIn(tokentype[0], self.mysite.tokens) + else: + self.assertRegex( + str(error_msg), + "Requested token '[a-z]+' is invalid on .* wiki.") + else: + self.assertIsInstance(token, str) + self.assertEqual(token, self.mysite.tokens[ttype]) + # test __contains__ + self.assertIn(tokentype[0], self.mysite.tokens) + + def test_tokens_in_mw_119(self): + """Test ability to get page tokens.""" + self._test_tokens(None, '1.19', 'delete') + + def test_patrol_tokens_in_mw_119(self): + """Test ability to get patrol token on MW 1.19 wiki.""" + self._test_tokens('1.19', '1.19', 'patrol') + + def test_tokens_in_mw_120_124wmf18(self): + """Test ability to get page tokens.""" + self._test_tokens('1.20', '1.21', 'deleteglobalaccount') + + def test_patrol_tokens_in_mw_120(self): + """Test ability to get patrol token.""" + self._test_tokens('1.19', '1.20', 'patrol') + + def test_tokens_in_mw_124wmf19(self): + """Test ability to get page tokens.""" + self._test_tokens('1.24wmf19', '1.24wmf20', 'deleteglobalaccount') + + def test_invalid_token(self): + """Test invalid token.""" + self.assertRaises(pywikibot.Error, lambda t: self.mysite.tokens[t], + 'invalidtype') + + +class TokenTestBase(TestCaseBase): + + """Verify token exists before running tests.""" + + def setUp(self): + """Skip test if user does not have token and clear site wallet.""" + super().setUp() + mysite = self.get_site() + ttype = self.token_type + try: + token = mysite.tokens[ttype] + except pywikibot.Error as error_msg: + self.assertRegex( + str(error_msg), + "Action '[a-z]+' is not allowed for user .* on .* wiki.") + self.assertNotIn(self.token_type, self.site.tokens) + self.skipTest(error_msg) + + self.token = token + self._orig_wallet = self.site.tokens + self.site.tokens = TokenWallet(self.site) + + def tearDown(self): + """Restore site tokens.""" + self.site.tokens = self._orig_wallet + super().tearDown() + + +class PatrolTestCase(TokenTestBase, TestCase): + + """Test patrol method.""" + + family = 'wikipedia' + code = 'test' + + user = True + token_type = 'patrol' + write = True + + def test_patrol(self): + """Test the site.patrol() method.""" + mysite = self.get_site() + + rc = list(mysite.recentchanges(total=1)) + if not rc: + self.skipTest('no recent changes to patrol') + + rc = rc[0] + + # site.patrol() needs params + self.assertRaises(pywikibot.Error, lambda x: list(x), mysite.patrol()) + try: + result = list(mysite.patrol(rcid=rc['rcid'])) + except api.APIError as error: + if error.code == 'permissiondenied': + self.skipTest(error) + raise + + if hasattr(mysite, '_patroldisabled') and mysite._patroldisabled: + self.skipTest('Patrolling is disabled on {} wiki.'.format(mysite)) + + result = result[0] + self.assertIsInstance(result, dict) + + params = {'rcid': 0} + if mysite.mw_version >= '1.22': + params['revid'] = [0, 1] + + raised = False + try: + # no such rcid, revid or too old revid + list(mysite.patrol(**params)) + except api.APIError as error: + if error.code == 'badtoken': + self.skipTest(error) + except pywikibot.Error: + # expected result + raised = True + self.assertTrue(raised, msg='pywikibot.Error not raised') + + +class TestDeprecatedEditTokenFunctions(TokenTestBase, + DefaultSiteTestCase, + DeprecationTestCase): + + """Test cases for Site edit token deprecated methods.""" + + cached = True + user = True + token_type = 'edit' + + def test_get_token(self): + """Test ability to get page tokens using site.getToken.""" + self.mysite = self.site + self.assertEqual(self.mysite.getToken(), self.mysite.tokens['edit']) + self.assertOneDeprecationParts('pywikibot.site.APISite.getToken', + "the 'tokens' property") + + +class TestDeprecatedPatrolToken(DefaultSiteTestCase, DeprecationTestCase): + + """Test cases for Site patrol token deprecated methods.""" + + cached = True + user = True + + def test_get_patrol_token(self): + """Test site.getPatrolToken.""" + self.mysite = self.site + try: + self.assertEqual(self.mysite.getPatrolToken(), + self.mysite.tokens['patrol']) + self.assertOneDeprecation() + except pywikibot.Error as error_msg: + self.assertRegex( + str(error_msg), + "Action '[a-z]+' is not allowed for user .* on .* wiki.") + # test __contains__ + self.assertNotIn('patrol', self.mysite.tokens) + + +if __name__ == '__main__': # pragma: no cover + with suppress(SystemExit): + unittest.main()