diff --git a/mwclient/client.py b/mwclient/client.py index dff61e9e194e10eabbb9011999321ffb6acdcb28..98e060c3071463adff210c94e1aecc2ff9f0292c 100644 --- a/mwclient/client.py +++ b/mwclient/client.py @@ -131,7 +131,7 @@ class Site(object): def site_init(self): if self.initialized: - info = self.api('query', meta='userinfo', uiprop='groups|rights') + info = self.get('query', meta='userinfo', uiprop='groups|rights') userinfo = info['query']['userinfo'] self.username = userinfo['name'] self.groups = userinfo.get('groups', []) @@ -139,7 +139,7 @@ class Site(object): self.tokens = {} return - meta = self.api('query', meta='siteinfo|userinfo', + meta = self.get('query', meta='siteinfo|userinfo', siprop='general|namespaces', uiprop='groups|rights', retry_on_error=False) @@ -214,7 +214,29 @@ class Site(object): def __repr__(self): return "<Site object '%s%s'>" % (self.host, self.path) - def api(self, action, *args, **kwargs): + def get(self, action, *args, **kwargs): + """Perform a generic API call using GET. + + This is just a shorthand for calling api() with http_method='GET'. + All arguments will be passed on. + + Returns: + The raw response from the API call, as a dictionary. + """ + return self.api(action, 'GET', *args, **kwargs) + + def post(self, action, *args, **kwargs): + """Perform a generic API call using POST. + + This is just a shorthand for calling api() with http_method='POST'. + All arguments will be passed on. + + Returns: + The raw response from the API call, as a dictionary. + """ + return self.api(action, 'POST', *args, **kwargs) + + def api(self, action, http_method='POST', *args, **kwargs): """Perform a generic API call and handle errors. All arguments will be passed on. @@ -252,7 +274,7 @@ class Site(object): sleeper = self.sleepers.make() while True: - info = self.raw_api(action, **kwargs) + info = self.raw_api(action, http_method, **kwargs) if not info: info = {} if self.handle_api_result(info, sleeper=sleeper): @@ -291,7 +313,7 @@ class Site(object): qs2 = [(k, v) for k, v in six.iteritems(kwargs) if k in {'wpEditToken', 'token'}] return OrderedDict(qs1 + qs2) - def raw_call(self, script, data, files=None, retry_on_error=True): + def raw_call(self, script, data, files=None, retry_on_error=True, http_method='POST'): """ Perform a generic request and return the raw text. @@ -312,22 +334,28 @@ class Site(object): Returns: The raw text response. """ - url = self.path + script + self.ext headers = {} if self.compress and gzip: headers['Accept-Encoding'] = 'gzip' sleeper = self.sleepers.make((script, data)) - while True: - scheme = 'https' - host = self.host - if isinstance(host, (list, tuple)): - scheme, host = host - fullurl = '{scheme}://{host}{url}'.format(scheme=scheme, host=host, url=url) + scheme = 'https' + host = self.host + if isinstance(host, (list, tuple)): + scheme, host = host + + url = '{scheme}://{host}{path}{script}{ext}'.format(scheme=scheme, host=host, + path=self.path, script=script, + ext=self.ext) + while True: try: - stream = self.connection.post(fullurl, data=data, files=files, - headers=headers, **self.requests) + if http_method == 'GET': + stream = self.connection.get(url, params=data, files=files, + headers=headers, **self.requests) + else: + stream = self.connection.post(url, data=data, files=files, + headers=headers, **self.requests) if stream.headers.get('x-database-lag'): wait_time = int(stream.headers.get('retry-after')) log.warning('Database lag exceeds max lag. ' @@ -355,7 +383,7 @@ class Site(object): log.warning('Connection error. Retrying in a moment.') sleeper.sleep() - def raw_api(self, action, *args, **kwargs): + def raw_api(self, action, http_method='POST', *args, **kwargs): """Send a call to the API.""" try: retry_on_error = kwargs.pop('retry_on_error') @@ -364,7 +392,8 @@ class Site(object): kwargs['action'] = action kwargs['format'] = 'json' data = self._query_string(*args, **kwargs) - res = self.raw_call('api', data, retry_on_error=retry_on_error) + res = self.raw_call('api', data, retry_on_error=retry_on_error, + http_method=http_method) try: return json.loads(res) @@ -373,12 +402,12 @@ class Site(object): raise errors.APIDisabledError raise errors.InvalidResponse(res) - def raw_index(self, action, *args, **kwargs): - """Send a call to index.php rather than the API.""" + def raw_index(self, action, http_method='POST', *args, **kwargs): + """Sends a call to index.php rather than the API.""" kwargs['action'] = action kwargs['maxlag'] = self.max_lag data = self._query_string(*args, **kwargs) - return self.raw_call('index', data) + return self.raw_call('index', data, http_method=http_method) def require(self, major, minor, revision=None, raise_error=True): if self.version is None: @@ -428,8 +457,8 @@ class Site(object): token = self.get_token('email') try: - info = self.api('emailuser', target=user, subject=subject, - text=text, ccme=cc, token=token) + info = self.post('emailuser', target=user, subject=subject, + text=text, ccme=cc, token=token) except errors.APIError as e: if e.args[0] == u'noemail': raise errors.NoSpecifiedEmail(user, e.args[1]) @@ -454,7 +483,7 @@ class Site(object): if self.credentials[2]: kwargs['lgdomain'] = self.credentials[2] while True: - login = self.api('login', **kwargs) + login = self.post('login', **kwargs) if login['login']['result'] == 'Success': break elif login['login']['result'] == 'NeedToken': @@ -480,15 +509,15 @@ class Site(object): if self.tokens.get(type, '0') == '0' or force: if self.version[:2] >= (1, 24): - info = self.api('query', meta='tokens', type=type) + info = self.post('query', meta='tokens', type=type) self.tokens[type] = info['query']['tokens']['%stoken' % type] else: if title is None: # Some dummy title was needed to get a token prior to 1.24 title = 'Test' - info = self.api('query', titles=title, - prop='info', intoken=type) + info = self.post('query', titles=title, + prop='info', intoken=type) for i in six.itervalues(info['query']['pages']): if i['title'] == title: self.tokens[type] = i['%stoken' % type] @@ -608,7 +637,7 @@ class Site(object): kwargs['redirects'] = '1' if mobileformat: kwargs['mobileformat'] = '1' - result = self.api('parse', **kwargs) + result = self.get('parse', **kwargs) return result['parse'] # def block(self): TODO? @@ -813,7 +842,7 @@ class Site(object): kwargs['rvdiffto'] = diffto revisions = [] - pages = self.api('query', **kwargs).get('query', {}).get('pages', {}).values() + pages = self.get('query', **kwargs).get('query', {}).get('pages', {}).values() for page in pages: for revision in page.get('revisions', ()): revision['pageid'] = page.get('pageid') @@ -902,7 +931,7 @@ class Site(object): if generatexml: kwargs['generatexml'] = '1' - result = self.api('expandtemplates', text=text, **kwargs) + result = self.get('expandtemplates', text=text, **kwargs) if generatexml: return result['expandtemplates']['*'], result['parsetree']['*'] diff --git a/mwclient/listing.py b/mwclient/listing.py index b612c2c8c3db711c1c1598b8c5ca7cb897a403f0..34d4d72fd6d8a72fb9f05beb189360cea05edfd2 100644 --- a/mwclient/listing.py +++ b/mwclient/listing.py @@ -86,7 +86,7 @@ class List(object): Else, set `self.last` to True. """ - data = self.site.api( + data = self.site.get( 'query', (self.generator, self.list_name), *[(text_type(k), v) for k, v in six.iteritems(self.args)] ) diff --git a/mwclient/page.py b/mwclient/page.py index 7d6d071e3881e753fc06ea5191cd1c79eacc86cf..54c763ce07ea10b5144af67e14d5c05244755306 100644 --- a/mwclient/page.py +++ b/mwclient/page.py @@ -28,10 +28,10 @@ class Page(object): extra_props = () if type(name) is int: - info = self.site.api('query', prop=prop, pageids=name, + info = self.site.get('query', prop=prop, pageids=name, inprop='protection', *extra_props) else: - info = self.site.api('query', prop=prop, titles=name, + info = self.site.get('query', prop=prop, titles=name, inprop='protection', *extra_props) info = six.next(six.itervalues(info['query']['pages'])) self._info = info @@ -63,7 +63,7 @@ class Page(object): def redirects_to(self): """ Returns the redirect target page, or None if the page is not a redirect page.""" - info = self.site.api('query', prop='pageprops', titles=self.name, redirects='')['query'] + info = self.site.get('query', prop='pageprops', titles=self.name, redirects='')['query'] if 'redirects' in info: for page in info['redirects']: if page['from'] == self.name: @@ -206,9 +206,9 @@ class Page(object): data.update(kwargs) def do_edit(): - result = self.site.api('edit', title=self.name, text=text, - summary=summary, token=self.get_token('edit'), - **data) + result = self.site.post('edit', title=self.name, text=text, + summary=summary, token=self.get_token('edit'), + **data) if result['edit'].get('result').lower() == 'failure': raise mwclient.errors.EditError(self, result['edit']) return result @@ -264,8 +264,8 @@ class Page(object): data['movetalk'] = '1' if no_redirect: data['noredirect'] = '1' - result = self.site.api('move', ('from', self.name), to=new_title, - token=self.get_token('move'), reason=reason, **data) + result = self.site.post('move', ('from', self.name), to=new_title, + token=self.get_token('move'), reason=reason, **data) return result['move'] def delete(self, reason='', watch=False, unwatch=False, oldimage=False): @@ -288,9 +288,9 @@ class Page(object): data['unwatch'] = '1' if oldimage: data['oldimage'] = oldimage - result = self.site.api('delete', title=self.name, - token=self.get_token('delete'), - reason=reason, **data) + result = self.site.post('delete', title=self.name, + token=self.get_token('delete'), + reason=reason, **data) return result['delete'] def purge(self): diff --git a/tests/test_client.py b/tests/test_client.py index 2bf3847920274d0e8b100e1f3d8a7d0608499d19..bcf7e0310578466a92454b1cb2323465843fca7c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -45,12 +45,13 @@ class TestCase(unittest.TestCase): return json.dumps(self.metaResponse(**kwargs)) def httpShouldReturn(self, body=None, callback=None, scheme='https', host='test.wikipedia.org', path='/w/', - script='api', headers=None, status=200): + script='api', headers=None, status=200, method='GET'): url = '{scheme}://{host}{path}{script}.php'.format(scheme=scheme, host=host, path=path, script=script) + mock = responses.GET if method == 'GET' else responses.POST if body is None: - responses.add_callback(responses.POST, url, callback=callback) + responses.add_callback(mock, url, callback=callback) else: - responses.add(responses.POST, url, body=body, content_type='application/json', + responses.add(mock, url, body=body, content_type='application/json', adding_headers=headers, status=status) def stdSetup(self): @@ -105,7 +106,7 @@ class TestClient(TestCase): site = mwclient.Site('test.wikipedia.org') assert len(responses.calls) == 1 - assert responses.calls[0].request.method == 'POST' + assert responses.calls[0].request.method == 'GET' @responses.activate def test_max_lag(self): @@ -134,18 +135,6 @@ class TestClient(TestCase): with pytest.raises(requests.exceptions.HTTPError): site = mwclient.Site('test.wikipedia.org') - @responses.activate - def test_headers(self): - # Content-type should be 'application/x-www-form-urlencoded' - - self.httpShouldReturn(self.metaResponseAsJson(), scheme='https') - - site = mwclient.Site('test.wikipedia.org') - - assert len(responses.calls) == 1 - assert 'content-type' in responses.calls[0].request.headers - assert responses.calls[0].request.headers['content-type'] == 'application/x-www-form-urlencoded' - @responses.activate def test_force_http(self): # Setting http should work @@ -173,8 +162,8 @@ class TestClient(TestCase): site = mwclient.Site('test.wikipedia.org') - assert 'action=query' in responses.calls[0].request.body - assert 'meta=siteinfo%7Cuserinfo' in responses.calls[0].request.body + assert 'action=query' in responses.calls[0].request.url + assert 'meta=siteinfo%7Cuserinfo' in responses.calls[0].request.url @responses.activate def test_httpauth_defaults_to_basic_auth(self): @@ -249,6 +238,19 @@ class TestClient(TestCase): # ----- Use standard setup for rest + @responses.activate + def test_headers(self): + # Content-type should be 'application/x-www-form-urlencoded' for POST requests + + site = self.stdSetup() + + self.httpShouldReturn('{}', method='POST') + site.post('purge', title='Main Page') + + assert len(responses.calls) == 1 + assert 'content-type' in responses.calls[0].request.headers + assert responses.calls[0].request.headers['content-type'] == 'application/x-www-form-urlencoded' + @responses.activate def test_raw_index(self): # Initializing the client should result in one request @@ -256,7 +258,7 @@ class TestClient(TestCase): site = self.stdSetup() self.httpShouldReturn('Some data', script='index') - site.raw_index(action='purge', title='Main Page') + site.raw_index(action='purge', title='Main Page', http_method='GET') assert len(responses.calls) == 1 @@ -272,7 +274,7 @@ class TestClient(TestCase): 'info': 'Assertion that the user is logged in failed', '*': 'See https://en.wikipedia.org/w/api.php for API usage' } - })) + }), method='POST') with pytest.raises(mwclient.errors.APIError) as excinfo: site.api(action='edit', title='Wikipedia:Sandbox') @@ -351,8 +353,8 @@ class TestClientApiMethods(TestCase): call_args = self.api.call_args_list assert len(call_args) == 3 - assert call_args[1] == mock.call('login', lgname='myusername', lgpassword='mypassword') - assert call_args[2] == mock.call('login', lgname='myusername', lgpassword='mypassword', lgtoken=login_token) + assert call_args[1] == mock.call('login', 'POST', lgname='myusername', lgpassword='mypassword') + assert call_args[2] == mock.call('login', 'POST', lgname='myusername', lgpassword='mypassword', lgtoken=login_token) class TestClientUploadArgs(TestCase): diff --git a/tests/test_listing.py b/tests/test_listing.py index 749649ed390c5d1b31cf07bbfb72fde9d3cc46a4..f1d5e84b04de87b410351bdf0402fbca5a707059 100644 --- a/tests/test_listing.py +++ b/tests/test_listing.py @@ -30,7 +30,7 @@ class TestList(unittest.TestCase): def setupDummyResponses(self, mock_site, result_member, ns=None): if ns is None: ns = [0, 0, 0] - mock_site.api.side_effect = [ + mock_site.get.side_effect = [ { 'continue': { 'apcontinue': 'Kre_Mbaye', diff --git a/tests/test_page.py b/tests/test_page.py index c2228eacbe4c6f1ace5771d821bca21d7f18ce3b..d7babe51b678b5e2ba3e8f2848721c4ddce34e28 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -9,6 +9,7 @@ import responses import mock import mwclient from mwclient.page import Page +from mwclient.client import Site try: import json @@ -29,23 +30,23 @@ class TestPage(unittest.TestCase): @mock.patch('mwclient.client.Site') def test_api_call_on_page_init(self, mock_site): - # Check that site.api() is called once on Page init + # Check that site.get() is called once on Page init title = 'Some page' - mock_site.api.return_value = { + mock_site.get.return_value = { 'query': {'pages': {'1': {}}} } page = Page(mock_site, title) - # test that Page called site.api with the right parameters - mock_site.api.assert_called_once_with('query', inprop='protection', titles=title, prop='info') + # test that Page called site.get with the right parameters + mock_site.get.assert_called_once_with('query', inprop='protection', titles=title, prop='info') @mock.patch('mwclient.client.Site') def test_nonexisting_page(self, mock_site): # Check that API response results in page.exists being set to False title = 'Some nonexisting page' - mock_site.api.return_value = { + mock_site.get.return_value = { 'query': {'pages': {'-1': {'missing': ''}}} } page = Page(mock_site, title) @@ -57,7 +58,7 @@ class TestPage(unittest.TestCase): # Check that API response results in page.exists being set to True title = 'Norge' - mock_site.api.return_value = { + mock_site.get.return_value = { 'query': {'pages': {'728': {}}} } page = Page(mock_site, title) @@ -69,7 +70,7 @@ class TestPage(unittest.TestCase): # Check that variouse page props are read correctly from API response title = 'Some page' - mock_site.api.return_value = { + mock_site.get.return_value = { 'query': { 'pages': { '728': { @@ -102,7 +103,7 @@ class TestPage(unittest.TestCase): # If page is protected, check that protection is parsed correctly title = 'Some page' - mock_site.api.return_value = { + mock_site.get.return_value = { 'query': { 'pages': { '728': { @@ -146,7 +147,7 @@ class TestPage(unittest.TestCase): # Check that page.redirect is set correctly title = 'Some redirect page' - mock_site.api.return_value = { + mock_site.get.return_value = { "query": { "pages": { "796917": { @@ -177,11 +178,11 @@ class TestPage(unittest.TestCase): mock_site.rights = ['read', 'edit'] title = 'Norge' - mock_site.api.return_value = { + mock_site.get.return_value = { 'query': {'pages': {'728': {'protection': []}}} } page = Page(mock_site, title) - mock_site.api.return_value = { + mock_site.post.return_value = { 'edit': {'result': 'Failure', 'captcha': { 'type': 'math', 'mime': 'text/tex', @@ -205,17 +206,17 @@ class TestPageApiArgs(unittest.TestCase): MockSite = mock.patch('mwclient.client.Site').start() self.site = MockSite() - self.site.api.return_value = {'query': {'pages': {'1': {'title': title}}}} + self.site.get.return_value = {'query': {'pages': {'1': {'title': title}}}} self.site.rights = ['read'] self.page = Page(self.site, title) - self.site.api.return_value = {'query': {'pages': {'2': { + self.site.get.return_value = {'query': {'pages': {'2': { 'ns': 0, 'pageid': 2, 'revisions': [{'*': 'Hello world', 'timestamp': '2014-08-29T22:25:15Z'}], 'title': title }}}} def get_last_api_call_args(self): - args, kwargs = self.site.api.call_args + args, kwargs = self.site.get.call_args action = args[0] args = args[1:] kwargs.update(args)