diff --git a/.travis.yml b/.travis.yml index 2aeb4b579d4a2d707a105ae59eb7f0daf27c0941..d73d91226656d5b30fdf88eea1f765751f7e43c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ sudo: false language: python python: - - "2.6" - "2.7" - "3.3" - "3.4" diff --git a/README.rst b/README.rst index 73cf948ff500f85c54779adde7ccf3bd7c85a4ff..d99a91256bfba49a4149ace3600a09f0274deaf1 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ mwclient mwclient is a lightweight Python client library to the `MediaWiki API <https://mediawiki.org/wiki/API>`_ which provides access to most API functionality. -It works with Python 2.6, 2.7, 3.3 and above, and supports MediaWiki 1.16 and above. +It works with Python 2.7, 3.3 and above, and supports MediaWiki 1.16 and above. For functions not available in the current MediaWiki, a ``MediaWikiVersionError`` is raised. The current stable `version 0.8.1 <https://github.com/mwclient/mwclient/archive/v0.8.1.zip>`_ @@ -53,7 +53,7 @@ can be installed from GitHub: $ pip install git+git://github.com/mwclient/mwclient.git -Please see the +Please see the `release notes <https://github.com/mwclient/mwclient/blob/master/RELEASE-NOTES.md>`_ for a list of changes. @@ -111,7 +111,7 @@ without their two-letter prefix. Exceptions to this rule: * ``categorymembers`` is implemented as ``Category.members`` * ``deletedrevs`` is ``deletedrevisions`` * ``usercontribs`` is ``usercontributions`` -* First parameters of ``search`` and ``usercontributions`` are ``search`` and ``user`` +* First parameters of ``search`` and ``usercontributions`` are ``search`` and ``user`` respectively Properties and generators are implemented as Python generators. diff --git a/docs/source/index.rst b/docs/source/index.rst index ec549df7331d3c6b864ed38a74596c6ee8f1c6d4..aa2eb2f0f6e56c278fa8393938f10b919b0a8b5e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,8 +8,7 @@ mwclient: lightweight MediaWiki client Mwclient is a :ref:`MIT licensed <license>` client library to the `MediaWiki API`_ that should work well with both Wikimedia wikis and other wikis running -MediaWiki 1.16 or above. It supports Python 2.6 and above -(tested with Python 2.6, 2.7, 3.3 and 3.4). +MediaWiki 1.16 or above. It works with Python 2.7 and 3.3+. .. _install: diff --git a/docs/source/user/connecting.rst b/docs/source/user/connecting.rst index 1cd5dce309d2dffb1064cf5959faba93d132fb35..d1a85eb9f7600e4d29accb063f446cb941c68c93 100644 --- a/docs/source/user/connecting.rst +++ b/docs/source/user/connecting.rst @@ -38,9 +38,11 @@ If you are connecting to a Wikimedia site, you should follow the `Wikimedia User-Agent policy <https://meta.wikimedia.org/wiki/User-Agent_policy>`_ and identify your tool like so: - >>> ua = 'MyCoolTool. Run by User:Xyz. Using mwclient/' + mwclient.__ver__ + >>> ua = 'MyCoolTool/0.2 run by User:Xyz' >>> site = mwclient.Site('test.wikipedia.org', clients_useragent=ua) +Mwclient will append `' - MwClient/{version} ({url})'` to the User-Agent string. + .. _auth: Authenticating diff --git a/mwclient/client.py b/mwclient/client.py index ebbb1d05f17a26360e889f8632210c4e856c8c64..4c868098912debd8f53d1f685739340a9ac4b41e 100644 --- a/mwclient/client.py +++ b/mwclient/client.py @@ -4,12 +4,7 @@ import logging from six import text_type import six -try: - # Python 2.7+ - from collections import OrderedDict -except ImportError: - # Python 2.6 - from ordereddict import OrderedDict +from collections import OrderedDict try: import json @@ -94,9 +89,15 @@ class Site(object): if pool is None: self.connection = requests.Session() self.connection.auth = auth - self.connection.headers['User-Agent'] = 'MwClient/' + __ver__ + ' (https://github.com/mwclient/mwclient)' - if clients_useragent: - self.connection.headers['User-Agent'] = clients_useragent + ' - ' + self.connection.headers['User-Agent'] + + prefix = '{} - '.format(clients_useragent) if clients_useragent else '' + self.connection.headers['User-Agent'] = ( + '{prefix}MwClient/{ver} ({url})'.format( + prefix=prefix, + ver=__ver__, + url='https://github.com/mwclient/mwclient' + ) + ) else: self.connection = pool @@ -121,7 +122,7 @@ class Site(object): raise errors.OAuthAuthorizationError(e.code, e.info) # Private wiki, do init after login - if e.args[0] not in (u'unknown_action', u'readapidenied'): + if e.args[0] not in {u'unknown_action', u'readapidenied'}: raise def site_init(self): @@ -136,33 +137,18 @@ class Site(object): return meta = self.api('query', meta='siteinfo|userinfo', - siprop='general|namespaces', uiprop='groups|rights', retry_on_error=False) + siprop='general|namespaces', uiprop='groups|rights', + retry_on_error=False) # Extract site info self.site = meta['query']['general'] - self.namespaces = dict(((i['id'], i.get('*', '')) for i in six.itervalues(meta['query']['namespaces']))) + self.namespaces = { + namespace['id']: namespace.get('*', '') + for namespace in six.itervalues(meta['query']['namespaces']) + } self.writeapi = 'writeapi' in self.site - # Determine version - if self.site['generator'].startswith('MediaWiki '): - version = self.site['generator'][10:].split('.') - - def split_num(s): - i = 0 - while i < len(s): - if s[i] < '0' or s[i] > '9': - break - i += 1 - if s[i:]: - return (int(s[:i]), s[i:], ) - else: - return (int(s[:i]), ) - self.version = sum((split_num(s) for s in version), ()) - - if len(self.version) < 2: - raise errors.MediaWikiVersionError('Unknown MediaWiki %s' % '.'.join(version)) - else: - raise errors.MediaWikiVersionError('Unknown generator %s' % self.site['generator']) + self.version = self.version_tuple_from_generator(self.site['generator']) # Require MediaWiki version >= 1.16 self.require(1, 16) @@ -174,9 +160,50 @@ class Site(object): self.rights = userinfo.get('rights', []) self.initialized = True - default_namespaces = {0: u'', 1: u'Talk', 2: u'User', 3: u'User talk', 4: u'Project', 5: u'Project talk', - 6: u'Image', 7: u'Image talk', 8: u'MediaWiki', 9: u'MediaWiki talk', 10: u'Template', 11: u'Template talk', - 12: u'Help', 13: u'Help talk', 14: u'Category', 15: u'Category talk', -1: u'Special', -2: u'Media'} + @staticmethod + def version_tuple_from_generator(string, prefix='MediaWiki '): + """Return a version tuple from a MediaWiki Generator string + + Example: "MediaWiki 1.5.1" → (1, 5, 1) + + :param prefix: the expected prefix of the string + """ + if not string.startswith(prefix): + raise errors.MediaWikiVersionError('Unknown generator {}'.format(string)) + + version = string[len(prefix):].split('.') + + def split_num(s): + """Split the string on the first non-digit character. + + :return: a tuple of the digit part as int and, if + available, the rest of the string. + """ + i = 0 + while i < len(s): + if s[i] < '0' or s[i] > '9': + break + i += 1 + if s[i:]: + return (int(s[:i]), s[i:], ) + else: + return (int(s[:i]), ) + + version_tuple = sum((split_num(s) for s in version), ()) + + if len(version_tuple) < 2: + raise errors.MediaWikiVersionError('Unknown MediaWiki {}' + .format('.'.join(version))) + + return version_tuple + + default_namespaces = { + 0: u'', 1: u'Talk', 2: u'User', 3: u'User talk', 4: u'Project', + 5: u'Project talk', 6: u'Image', 7: u'Image talk', 8: u'MediaWiki', + 9: u'MediaWiki talk', 10: u'Template', 11: u'Template talk', 12: u'Help', + 13: u'Help talk', 14: u'Category', 15: u'Category talk', + -1: u'Special', -2: u'Media' + } def __repr__(self): return "<Site object '%s%s'>" % (self.host, self.path) @@ -239,7 +266,8 @@ class Site(object): self.hasmsg = 'messages' in userinfo self.logged_in = 'anon' not in userinfo if 'error' in info: - if info['error']['code'] in (u'internal_api_error_DBConnectionError', u'internal_api_error_DBQueryError'): + if info['error']['code'] in {u'internal_api_error_DBConnectionError', + u'internal_api_error_DBQueryError'}: sleeper.sleep() return False if '*' in info['error']: @@ -252,8 +280,8 @@ class Site(object): @staticmethod def _query_string(*args, **kwargs): kwargs.update(args) - qs1 = [(k, v) for k, v in six.iteritems(kwargs) if k not in ('wpEditToken', 'token')] - qs2 = [(k, v) for k, v in six.iteritems(kwargs) if k in ('wpEditToken', 'token')] + qs1 = [(k, v) for k, v in six.iteritems(kwargs) if k not in {'wpEditToken', 'token'}] + 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): @@ -291,10 +319,12 @@ class Site(object): fullurl = '{scheme}://{host}{url}'.format(scheme=scheme, host=host, url=url) try: - stream = self.connection.post(fullurl, data=data, files=files, headers=headers, **self.requests) + stream = self.connection.post(fullurl, 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. Waiting for %d seconds', wait_time) + log.warning('Database lag exceeds max lag. ' + 'Waiting for {} seconds'.format(wait_time)) sleeper.sleep(wait_time) elif stream.status_code == 200: return stream.text @@ -303,7 +333,10 @@ class Site(object): else: if not retry_on_error: stream.raise_for_status() - log.warning('Received %s response: %s. Retrying in a moment.', stream.status_code, stream.text) + log.warning('Received {status} response: {text}. ' + 'Retrying in a moment.' + .format(status=stream.status_code, + text=stream.text)) sleeper.sleep() except requests.exceptions.ConnectionError: @@ -349,8 +382,12 @@ class Site(object): if self.version[:2] >= (major, minor): return True elif raise_error: - raise errors.MediaWikiVersionError('Requires version %s.%s, current version is %s.%s' - % ((major, minor) + self.version[:2])) + raise errors.MediaWikiVersionError( + 'Requires version {required[0]}.{required[1]}, ' + 'current version is {current[0]}.{current[1]}' + .format(required=(major, minor), + current=(self.version[:2])) + ) else: return False else: @@ -426,7 +463,7 @@ class Site(object): if self.version[:2] >= (1, 24): # The 'csrf' (cross-site request forgery) token introduced in 1.24 replaces # the majority of older tokens, like edittoken and movetoken. - if type not in ['watch', 'patrol', 'rollback', 'userrights']: + if type not in {'watch', 'patrol', 'rollback', 'userrights'}: type = 'csrf' if type not in self.tokens: @@ -543,7 +580,8 @@ class Site(object): if self.handle_api_result(info, kwargs=predata, sleeper=sleeper): return info.get('upload', {}) - def parse(self, text=None, title=None, page=None, prop=None, redirects=False, mobileformat=False): + def parse(self, text=None, title=None, page=None, prop=None, + redirects=False, mobileformat=False): kwargs = {} if text is not None: kwargs['text'] = text @@ -573,46 +611,59 @@ class Site(object): """Retrieve all pages on the wiki as a generator.""" pfx = listing.List.get_prefix('ap', generator) - kwargs = dict(listing.List.generate_kwargs(pfx, ('from', start), ('to', end), prefix=prefix, - minsize=minsize, maxsize=maxsize, prtype=prtype, prlevel=prlevel, - namespace=namespace, filterredir=filterredir, dir=dir, - filterlanglinks=filterlanglinks)) - return listing.List.get_list(generator)(self, 'allpages', 'ap', limit=limit, return_values='title', **kwargs) + kwargs = dict(listing.List.generate_kwargs( + pfx, ('from', start), ('to', end), prefix=prefix, + minsize=minsize, maxsize=maxsize, prtype=prtype, prlevel=prlevel, + namespace=namespace, filterredir=filterredir, dir=dir, + filterlanglinks=filterlanglinks, + )) + return listing.List.get_list(generator)(self, 'allpages', 'ap', + limit=limit, return_values='title', + **kwargs) def allimages(self, start=None, prefix=None, minsize=None, maxsize=None, limit=None, dir='ascending', sha1=None, sha1base36=None, generator=True, end=None): """Retrieve all images on the wiki as a generator.""" pfx = listing.List.get_prefix('ai', generator) - kwargs = dict(listing.List.generate_kwargs(pfx, ('from', start), ('to', end), prefix=prefix, - minsize=minsize, maxsize=maxsize, - dir=dir, sha1=sha1, sha1base36=sha1base36)) - return listing.List.get_list(generator)(self, 'allimages', 'ai', limit=limit, return_values='timestamp|url', **kwargs) + kwargs = dict(listing.List.generate_kwargs( + pfx, ('from', start), ('to', end), prefix=prefix, + minsize=minsize, maxsize=maxsize, + dir=dir, sha1=sha1, sha1base36=sha1base36, + )) + return listing.List.get_list(generator)(self, 'allimages', 'ai', limit=limit, + return_values='timestamp|url', + **kwargs) def alllinks(self, start=None, prefix=None, unique=False, prop='title', namespace='0', limit=None, generator=True, end=None): """Retrieve a list of all links on the wiki as a generator.""" pfx = listing.List.get_prefix('al', generator) - kwargs = dict(listing.List.generate_kwargs(pfx, ('from', start), ('to', end), prefix=prefix, + kwargs = dict(listing.List.generate_kwargs(pfx, ('from', start), ('to', end), + prefix=prefix, prop=prop, namespace=namespace)) if unique: kwargs[pfx + 'unique'] = '1' - return listing.List.get_list(generator)(self, 'alllinks', 'al', limit=limit, return_values='title', **kwargs) + return listing.List.get_list(generator)(self, 'alllinks', 'al', limit=limit, + return_values='title', **kwargs) - def allcategories(self, start=None, prefix=None, dir='ascending', limit=None, generator=True, - end=None): + def allcategories(self, start=None, prefix=None, dir='ascending', limit=None, + generator=True, end=None): """Retrieve all categories on the wiki as a generator.""" pfx = listing.List.get_prefix('ac', generator) - kwargs = dict(listing.List.generate_kwargs(pfx, ('from', start), ('to', end), prefix=prefix, dir=dir)) - return listing.List.get_list(generator)(self, 'allcategories', 'ac', limit=limit, **kwargs) + kwargs = dict(listing.List.generate_kwargs(pfx, ('from', start), ('to', end), + prefix=prefix, dir=dir)) + return listing.List.get_list(generator)(self, 'allcategories', 'ac', limit=limit, + **kwargs) def allusers(self, start=None, prefix=None, group=None, prop=None, limit=None, witheditsonly=False, activeusers=False, rights=None, end=None): """Retrieve all users on the wiki as a generator.""" - kwargs = dict(listing.List.generate_kwargs('au', ('from', start), ('to', end), prefix=prefix, + kwargs = dict(listing.List.generate_kwargs('au', ('from', start), ('to', end), + prefix=prefix, group=group, prop=prop, rights=rights, witheditsonly=witheditsonly, @@ -675,15 +726,18 @@ class Site(object): dir='older', user=None, title=None, limit=None, action=None): """Retrieve logevents as a generator.""" kwargs = dict(listing.List.generate_kwargs('le', prop=prop, type=type, start=start, - end=end, dir=dir, user=user, title=title, action=action)) + end=end, dir=dir, user=user, + title=title, action=action)) return listing.List(self, 'logevents', 'le', limit=limit, **kwargs) - def checkuserlog(self, user=None, target=None, limit=10, dir='older', start=None, end=None): + def checkuserlog(self, user=None, target=None, limit=10, dir='older', + start=None, end=None): """Retrieve checkuserlog items as a generator.""" kwargs = dict(listing.List.generate_kwargs('cul', target=target, start=start, end=end, dir=dir, user=user)) - return listing.NestedList('entries', self, 'checkuserlog', 'cul', limit=limit, **kwargs) + return listing.NestedList('entries', self, 'checkuserlog', 'cul', + limit=limit, **kwargs) # def protectedtitles requires 1.15 def random(self, namespace, limit=20): @@ -705,7 +759,8 @@ class Site(object): List recent changes to the wiki, à la Special:Recentchanges. """ kwargs = dict(listing.List.generate_kwargs('rc', start=start, end=end, dir=dir, - namespace=namespace, prop=prop, show=show, type=type, + namespace=namespace, prop=prop, + show=show, type=type, toponly='1' if toponly else None)) return listing.List(self, 'recentchanges', 'rc', limit=limit, **kwargs) @@ -774,7 +829,8 @@ class Site(object): Returns: mwclient.listings.List: Search results iterator """ - kwargs = dict(listing.List.generate_kwargs('sr', search=search, namespace=namespace, what=what)) + kwargs = dict(listing.List.generate_kwargs('sr', search=search, + namespace=namespace, what=what)) if redirects: kwargs['srredirects'] = '1' return listing.List(self, 'search', 'sr', limit=limit, **kwargs) @@ -787,7 +843,8 @@ class Site(object): API doc: https://www.mediawiki.org/wiki/API:Usercontribs """ kwargs = dict(listing.List.generate_kwargs('uc', user=user, start=start, end=end, - dir=dir, namespace=namespace, prop=prop, show=show)) + dir=dir, namespace=namespace, + prop=prop, show=show)) return listing.List(self, 'usercontribs', 'uc', limit=limit, **kwargs) def users(self, users, prop='blockinfo|groups|editcount'): @@ -808,7 +865,8 @@ class Site(object): """ kwargs = dict(listing.List.generate_kwargs('wl', start=start, end=end, - namespace=namespace, dir=dir, prop=prop, show=show)) + namespace=namespace, dir=dir, + prop=prop, show=show)) if allrev: kwargs['wlallrev'] = '1' return listing.List(self, 'watchlist', 'wl', limit=limit, **kwargs) diff --git a/mwclient/errors.py b/mwclient/errors.py index bf5d2cb00127a56e89eda08bb6a6a75f733031ec..3dcc83a6044934789b96691da4f8006dd071e891 100644 --- a/mwclient/errors.py +++ b/mwclient/errors.py @@ -19,7 +19,7 @@ class APIError(MwClientError): def __init__(self, code, info, kwargs): self.code = code self.info = info - MwClientError.__init__(self, code, info, kwargs) + super(APIError, self).__init__(code, info, kwargs) class InsufficientPermission(MwClientError): @@ -75,7 +75,7 @@ class InvalidResponse(MwClientError): 'you used the correct hostname. If you did, the server might ' + \ 'be wrongly configured or experiencing temporary problems.' self.response_text = response_text - MwClientError.__init__(self, self.message, response_text) + super(InvalidResponse, self).__init__(self.message, response_text) def __str__(self): return self.message diff --git a/mwclient/ex.py b/mwclient/ex.py index c0b1eaebcef32f453f06732a584ac08e4805660f..959599c30ffc91a4ff80c206a482cf65362e5a64 100644 --- a/mwclient/ex.py +++ b/mwclient/ex.py @@ -47,11 +47,14 @@ class ConfiguredSite(client.Site): do_login = 'username' in self.config and 'password' in self.config - client.Site.__init__(self, host=self.config['host'], - path=self.config['path'], ext=self.config.get('ext', '.php'), - do_init=not do_login, - retry_timeout=self.config.get('retry_timeout', 30), - max_retries=self.config.get('max_retries', -1)) + super(ConfiguredSite, self).__init__( + host=self.config['host'], + path=self.config['path'], + ext=self.config.get('ext', '.php'), + do_init=not do_login, + retry_timeout=self.config.get('retry_timeout', 30), + max_retries=self.config.get('max_retries', -1), + ) if do_login: self.login(self.config['username'], diff --git a/mwclient/image.py b/mwclient/image.py index 83dbfd67ac20d6d4c33118466d30dcb09e1b4ed6..828ff4c71211d4fddb9254729ffd3bf607198e77 100644 --- a/mwclient/image.py +++ b/mwclient/image.py @@ -5,7 +5,7 @@ import mwclient.page class Image(mwclient.page.Page): def __init__(self, site, name, info=None): - mwclient.page.Page.__init__(self, site, name, info, + super(Image, self).__init__(site, name, info, extra_properties={'imageinfo': (('iiprop', 'timestamp|user|comment|url|size|sha1|metadata|archivename'), )}) self.imagerepository = self._info.get('imagerepository', '') self.imageinfo = self._info.get('imageinfo', ({}, ))[0] diff --git a/mwclient/listing.py b/mwclient/listing.py index b47e03f9db3ba4c806c154d6681255504894c34f..2e626e710b90384966d49d0bc09e58087bb72604 100644 --- a/mwclient/listing.py +++ b/mwclient/listing.py @@ -7,8 +7,16 @@ import mwclient.image class List(object): + """Base class for lazy object iteration - def __init__(self, site, list_name, prefix, limit=None, return_values=None, max_items=None, *args, **kwargs): + This is a class providing lazy iteration. This means that the + content is loaded in chunks as long as the response hints at + continuing content. + """ + + def __init__(self, site, list_name, prefix, + limit=None, return_values=None, max_items=None, + *args, **kwargs): # NOTE: Fix limit self.site = site self.list_name = list_name @@ -57,14 +65,32 @@ class List(object): if self.last: raise StopIteration self.load_chunk() - return List.__next__(self, full=full) + return self.__next__(full=full) - def next(self, full=False): + def next(self, *args, **kwargs): """ For Python 2.x support """ - return self.__next__(full) + return self.__next__(*args, **kwargs) def load_chunk(self): - data = self.site.api('query', (self.generator, self.list_name), *[(text_type(k), v) for k, v in six.iteritems(self.args)]) + """Query a new chunk of data + + If the query is empty, `raise StopIteration`. + + Else, update the iterator accordingly. + + If 'continue' is in the response, it is added to `self.args` + (new style continuation, added in MediaWiki 1.21). + + If not, but 'query-continue' is in the response, query its + item called `self.list_name` and add this to `self.args` (old + style continuation). + + Else, set `self.last` to True. + """ + data = self.site.api( + 'query', (self.generator, self.list_name), + *[(text_type(k), v) for k, v in six.iteritems(self.args)] + ) if not data: # Non existent page raise StopIteration @@ -82,6 +108,7 @@ class List(object): self.last = True def set_iter(self, data): + """Set `self._iter` to the API response `data`.""" if self.result_member not in data['query']: self._iter = iter(six.moves.range(0)) elif type(data['query'][self.result_member]) is list: @@ -101,22 +128,16 @@ class List(object): @staticmethod def get_prefix(prefix, generator=False): - if generator: - return 'g' + prefix - else: - return prefix + return ('g' if generator else '') + prefix @staticmethod def get_list(generator=False): - if generator: - return GeneratorList - else: - return List + return GeneratorList if generator else List class NestedList(List): def __init__(self, nested_param, *args, **kwargs): - List.__init__(self, *args, **kwargs) + super(NestedList, self).__init__(*args, **kwargs) self.nested_param = nested_param def set_iter(self, data): @@ -126,7 +147,8 @@ class NestedList(List): class GeneratorList(List): def __init__(self, site, list_name, prefix, *args, **kwargs): - List.__init__(self, site, list_name, prefix, *args, **kwargs) + super(GeneratorList, self).__init__(site, list_name, prefix, + *args, **kwargs) self.args['g' + self.prefix + 'limit'] = self.args[self.prefix + 'limit'] del self.args[self.prefix + 'limit'] @@ -140,22 +162,18 @@ class GeneratorList(List): self.page_class = mwclient.page.Page def __next__(self): - info = List.__next__(self, full=True) + info = super(GeneratorList, self).__next__(full=True) if info['ns'] == 14: return Category(self.site, u'', info) if info['ns'] == 6: return mwclient.image.Image(self.site, u'', info) return mwclient.page.Page(self.site, u'', info) - def next(self): - """ For Python 2.x support """ - return self.__next__() - def load_chunk(self): # Put this here so that the constructor does not fail # on uninitialized sites self.args['iiprop'] = 'timestamp|user|comment|url|size|sha1|metadata|archivename' - return List.load_chunk(self) + return super(GeneratorList, self).load_chunk() class Category(mwclient.page.Page, GeneratorList): @@ -192,30 +210,54 @@ class PageList(GeneratorList): if end: kwargs['gapto'] = end - GeneratorList.__init__(self, site, 'allpages', 'ap', - gapnamespace=text_type(namespace), gapfilterredir=redirects, **kwargs) + super(PageList, self).__init__(site, 'allpages', 'ap', + gapnamespace=text_type(namespace), + gapfilterredir=redirects, + **kwargs) def __getitem__(self, name): return self.get(name, None) def get(self, name, info=()): - if self.namespace == 14: - return Category(self.site, self.site.namespaces[14] + ':' + name, info) - elif self.namespace == 6: - return mwclient.image.Image(self.site, self.site.namespaces[6] + ':' + name, info) - elif self.namespace != 0: - return mwclient.page.Page(self.site, self.site.namespaces[self.namespace] + ':' + name, info) + """Return the page of name `name` as an object. + + If self.namespace is not zero, use {namespace}:{name} as the + page name, otherwise guess the namespace from the name using + `self.guess_namespace`. + + :rtype: One of Category, Image, or Page (default), according + to the namespace. + """ + if self.namespace != 0: + full_page_name = u"{namespace}:{name}".format( + namespace=self.site.namespaces[self.namespace], + name=name, + ) + namespace = self.namespace else: - # Guessing page class - if type(name) is not int: + full_page_name = name + try: namespace = self.guess_namespace(name) - if namespace == 14: - return Category(self.site, name, info) - elif namespace == 6: - return mwclient.image.Image(self.site, name, info) - return mwclient.page.Page(self.site, name, info) + except AttributeError: + # raised when `namespace` doesn't have a `startswith` attribute + namespace = 0 + + cls = { + 14: Category, + 6: mwclient.image.Image, + }.get(namespace, mwclient.page.Page) + + return cls(self.site, full_page_name, info) def guess_namespace(self, name): + """Guess the namespace from name + + If name starts with any of the site's namespaces' names or + default_namespaces, use that. Else, return zero. + + :param name: The pagename as a string (having `.startswith`) + :return: the id of the guessed namespace or zero. + """ for ns in self.site.namespaces: if ns == 0: continue @@ -230,7 +272,9 @@ class PageList(GeneratorList): class PageProperty(List): def __init__(self, page, prop, prefix, *args, **kwargs): - List.__init__(self, page.site, prop, prefix, titles=page.name, *args, **kwargs) + super(PageProperty, self).__init__(page.site, prop, prefix, + titles=page.name, + *args, **kwargs) self.page = page self.generator = 'prop' @@ -245,7 +289,9 @@ class PageProperty(List): class PagePropertyGenerator(GeneratorList): def __init__(self, page, prop, prefix, *args, **kwargs): - GeneratorList.__init__(self, page.site, prop, prefix, titles=page.name, *args, **kwargs) + super(PagePropertyGenerator, self).__init__(page.site, prop, prefix, + titles=page.name, + *args, **kwargs) self.page = page @@ -254,4 +300,4 @@ class RevisionsIterator(PageProperty): def load_chunk(self): if 'rvstartid' in self.args and 'rvstart' in self.args: del self.args['rvstart'] - return PageProperty.load_chunk(self) + return super(RevisionsIterator, self).load_chunk() diff --git a/mwclient/page.py b/mwclient/page.py index 7c0e601b452696eb54cca74698488ab462fca19b..dba423b7290385d3349011ae70762dd68ea4e50a 100644 --- a/mwclient/page.py +++ b/mwclient/page.py @@ -47,7 +47,11 @@ class Page(object): self.revision = info.get('lastrevid', 0) self.exists = 'missing' not in info self.length = info.get('length') - self.protection = dict([(i['type'], (i['level'], i['expiry'])) for i in info.get('protection', ()) if i]) + self.protection = { + i['type']: (i['level'], i['expiry']) + for i in info.get('protection', ()) + if i + } self.redirect = 'redirect' in info self.pageid = info.get('pageid', None) self.contentmodel = info.get('contentmodel', None) @@ -117,14 +121,17 @@ class Page(object): def get_expanded(self): """Deprecated. Use page.text(expandtemplates=True) instead""" - warnings.warn("page.get_expanded() was deprecated in mwclient 0.7.0 and will be removed in 0.8.0, use page.text(expandtemplates=True) instead.", + warnings.warn("page.get_expanded() was deprecated in mwclient 0.7.0 " + "and will be removed in 0.8.0, " + "use page.text(expandtemplates=True) instead.", category=DeprecationWarning, stacklevel=2) return self.text(expandtemplates=True) def edit(self, *args, **kwargs): """Deprecated. Use page.text() instead""" - warnings.warn("page.edit() was deprecated in mwclient 0.7.0 and will be removed in 0.8.0, please use page.text() instead.", + warnings.warn("page.edit() was deprecated in mwclient 0.7.0 " + "and will be removed in 0.8.0, please use page.text() instead.", category=DeprecationWarning, stacklevel=2) return self.text(*args, **kwargs) @@ -154,7 +161,8 @@ class Page(object): if cache and key in self._textcache: return self._textcache[key] - revs = self.revisions(prop='content|timestamp', limit=1, section=section, expandtemplates=expandtemplates) + revs = self.revisions(prop='content|timestamp', limit=1, section=section, + expandtemplates=expandtemplates) try: rev = next(revs) text = rev['*'] @@ -175,10 +183,13 @@ class Page(object): """ if not self.site.logged_in and self.site.force_login: # Should we really check for this? - raise mwclient.errors.LoginError(self.site, 'By default, mwclient protects you from ' + - 'accidentally editing without being logged in. If you ' + - 'actually want to edit without logging in, you can set ' + - 'force_login on the Site object to False.') + raise mwclient.errors.LoginError( + self.site, + 'By default, mwclient protects you from accidentally editing ' + 'without being logged in. ' + 'If you actually want to edit without logging in, ' + 'you can set force_login on the Site object to False.' + ) if self.site.blocked: raise mwclient.errors.UserBlocked(self.site.blocked) if not self.can('edit'): @@ -234,8 +245,9 @@ class Page(object): def handle_edit_error(self, e, summary): if e.code == 'editconflict': raise mwclient.errors.EditError(self, summary, e.info) - elif e.code in ('protectedtitle', 'cantcreate', 'cantcreate-anon', 'noimageredirect-anon', - 'noimageredirect', 'noedit-anon', 'noedit'): + elif e.code in {'protectedtitle', 'cantcreate', 'cantcreate-anon', + 'noimageredirect-anon', 'noimageredirect', 'noedit-anon', + 'noedit'}: raise mwclient.errors.ProtectedPageError(self, e.code, e.info) else: raise @@ -300,7 +312,8 @@ class Page(object): # def watch: requires 1.14 # Properties - def backlinks(self, namespace=None, filterredir='all', redirect=False, limit=None, generator=True): + def backlinks(self, namespace=None, filterredir='all', redirect=False, + limit=None, generator=True): """ List pages that link to the current page, similar to Special:Whatlinkshere. @@ -308,12 +321,17 @@ class Page(object): """ prefix = mwclient.listing.List.get_prefix('bl', generator) - kwargs = dict(mwclient.listing.List.generate_kwargs(prefix, namespace=namespace, filterredir=filterredir)) + kwargs = dict(mwclient.listing.List.generate_kwargs( + prefix, namespace=namespace, filterredir=filterredir, + )) if redirect: kwargs['%sredirect' % prefix] = '1' kwargs[prefix + 'title'] = self.name - return mwclient.listing.List.get_list(generator)(self.site, 'backlinks', 'bl', limit=limit, return_values='title', **kwargs) + return mwclient.listing.List.get_list(generator)( + self.site, 'backlinks', 'bl', limit=limit, return_values='title', + **kwargs + ) def categories(self, generator=True): """ @@ -326,7 +344,8 @@ class Page(object): return mwclient.listing.PagePropertyGenerator(self, 'categories', 'cl') else: # TODO: return sortkey if wanted - return mwclient.listing.PageProperty(self, 'categories', 'cl', return_values='title') + return mwclient.listing.PageProperty(self, 'categories', 'cl', + return_values='title') def embeddedin(self, namespace=None, filterredir='all', limit=None, generator=True): """ @@ -345,10 +364,14 @@ class Page(object): mwclient.listings.List: Page iterator """ prefix = mwclient.listing.List.get_prefix('ei', generator) - kwargs = dict(mwclient.listing.List.generate_kwargs(prefix, namespace=namespace, filterredir=filterredir)) + kwargs = dict(mwclient.listing.List.generate_kwargs(prefix, namespace=namespace, + filterredir=filterredir)) kwargs[prefix + 'title'] = self.name - return mwclient.listing.List.get_list(generator)(self.site, 'embeddedin', 'ei', limit=limit, return_values='title', **kwargs) + return mwclient.listing.List.get_list(generator)( + self.site, 'embeddedin', 'ei', limit=limit, return_values='title', + **kwargs + ) def extlinks(self): """ @@ -369,7 +392,8 @@ class Page(object): if generator: return mwclient.listing.PagePropertyGenerator(self, 'images', '') else: - return mwclient.listing.PageProperty(self, 'images', '', return_values='title') + return mwclient.listing.PageProperty(self, 'images', '', + return_values='title') def iwlinks(self): """ @@ -378,7 +402,8 @@ class Page(object): API doc: https://www.mediawiki.org/wiki/API:Iwlinks """ - return mwclient.listing.PageProperty(self, 'iwlinks', 'iw', return_values=('prefix', '*')) + return mwclient.listing.PageProperty(self, 'iwlinks', 'iw', + return_values=('prefix', '*')) def langlinks(self, **kwargs): """ @@ -387,7 +412,9 @@ class Page(object): API doc: https://www.mediawiki.org/wiki/API:Langlinks """ - return mwclient.listing.PageProperty(self, 'langlinks', 'll', return_values=('lang', '*'), **kwargs) + return mwclient.listing.PageProperty(self, 'langlinks', 'll', + return_values=('lang', '*'), + **kwargs) def links(self, namespace=None, generator=True, redirects=False): """ @@ -404,11 +431,13 @@ class Page(object): if generator: return mwclient.listing.PagePropertyGenerator(self, 'links', 'pl', **kwargs) else: - return mwclient.listing.PageProperty(self, 'links', 'pl', return_values='title', **kwargs) + return mwclient.listing.PageProperty(self, 'links', 'pl', return_values='title', + **kwargs) def revisions(self, startid=None, endid=None, start=None, end=None, dir='older', user=None, excludeuser=None, limit=50, - prop='ids|timestamp|flags|comment|user', expandtemplates=False, section=None, + prop='ids|timestamp|flags|comment|user', + expandtemplates=False, section=None, diffto=None): """ List revisions of the current page. @@ -445,7 +474,8 @@ class Page(object): if section is not None: kwargs['rvsection'] = section - return mwclient.listing.RevisionsIterator(self, 'revisions', 'rv', limit=limit, **kwargs) + return mwclient.listing.RevisionsIterator(self, 'revisions', 'rv', limit=limit, + **kwargs) def templates(self, namespace=None, generator=True): """ @@ -457,6 +487,8 @@ class Page(object): prefix = mwclient.listing.List.get_prefix('tl', generator) kwargs = dict(mwclient.listing.List.generate_kwargs(prefix, namespace=namespace)) if generator: - return mwclient.listing.PagePropertyGenerator(self, 'templates', prefix, **kwargs) + return mwclient.listing.PagePropertyGenerator(self, 'templates', prefix, + **kwargs) else: - return mwclient.listing.PageProperty(self, 'templates', prefix, return_values='title', **kwargs) + return mwclient.listing.PageProperty(self, 'templates', prefix, + return_values='title', **kwargs) diff --git a/setup.py b/setup.py index 4f5da5b77bdd7e13b207fdcf4b7bab2499912880..66fd3958c1a4ed4a948a2499021b3b1f1696b796 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,6 @@ setup(name='mwclient', long_description=README, classifiers=[ 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', diff --git a/tox.ini b/tox.ini index d1ce86c6a687d8a2fc3762b3f1786249ebbdd29a..ba8f673edc6f21641ddf50d309b166783f5b438c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,13 @@ [tox] -envlist = py26,py27,py34 +envlist = py27,py34,py35 [testenv] deps=pytest pytest-pep8 responses mock commands=py.test -v --pep8 mwclient tests + +[flake8] +max-line-length=90 +[pep8] +max-line-length=90