http://www.mediawiki.org/wiki/Special:Code/pywikipedia/10309
Revision: 10309 Author: drtrigon Date: 2012-06-07 23:01:11 +0000 (Thu, 07 Jun 2012) Log Message: ----------- Page.getSections() and corresponding unittest for wikipedia added
Modified Paths: -------------- trunk/pywikipedia/tests/test_query.py trunk/pywikipedia/wikipedia.py
Added Paths: ----------- trunk/pywikipedia/tests/test_wikipedia.py
Modified: trunk/pywikipedia/tests/test_query.py =================================================================== --- trunk/pywikipedia/tests/test_query.py 2012-06-07 18:03:40 UTC (rev 10308) +++ trunk/pywikipedia/tests/test_query.py 2012-06-07 23:01:11 UTC (rev 10309) @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*-
-"""Unit tests for userlib.py""" +"""Unit tests for query.py""" __version__ = '$Id$'
import unittest
Added: trunk/pywikipedia/tests/test_wikipedia.py =================================================================== --- trunk/pywikipedia/tests/test_wikipedia.py (rev 0) +++ trunk/pywikipedia/tests/test_wikipedia.py 2012-06-07 23:01:11 UTC (rev 10309) @@ -0,0 +1,204 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""Unit tests for wikipedia.py""" + +# This test script is intended to be used with mature unittest code. +# +# This script contains important unittests in order to ensure the function +# and stability of core code (for e.g. DrTrigonBot framework) and methods. +# You should not change code here except you want to add a new test case +# for a function, mechanism or else. + +__version__ = '$Id$' + +import unittest +import test_pywiki + +import wikipedia as pywikibot + + +# a set of hard pages for Page.getSections() +PAGE_SET_Page_getSections = [ +u'Benutzer Diskussion:Reiner Stoppok/Dachboden', +u'Wikipedia:Löschkandidaten/12. Dezember 2009', # https://bugzilla.wikimedia.org/show_bug.cgi?id=32753 +u'Wikipedia:Löschkandidaten/28. Juli 2006', +u'Wikipedia Diskussion:Persönliche Bekanntschaften/Archiv/2008', +u'Wikipedia:WikiProjekt München', # bugzilla:32753 +u'Wikipedia Diskussion:Hauptseite', +u'Diskussion:Selbstkühlendes Bierfass', +u'Benutzer Diskussion:P.Copp', +u'Benutzer Diskussion:David Ludwig', +u'Diskussion:Zufall', +u'Benutzer Diskussion:Dekator', +u'Benutzer Diskussion:Bautsch', +u'Benutzer Diskussion:Henbeu', +u'Benutzer Diskussion:Olaf Studt', +u'Diskussion:K.-o.-Tropfen', +u'Portal Diskussion:Fußball/Archiv6', +u'Benutzer Diskussion:Roland.M/Archiv2006-2007', +u'Benutzer Diskussion:Tigerente/Archiv2006', +u'Wikipedia:WikiProjekt Bremen/Beobachtungsliste', # bugzilla:32753 +u'Diskussion:Wirtschaft Chiles', +u'Benutzer Diskussion:Ausgangskontrolle', +u'Benutzer Diskussion:Amnesty.tina', +u'Diskussion:Chicagoer Schule', +#u'Wikipedia Diskussion:Hausaufgabenhilfe', # [ DELETED ] +u'Benutzer Diskussion:Niemot', +u'Benutzer Diskussion:Computer356', +u'Benutzer Diskussion:Bautsch', +u'Benutzer Diskussion:Infinite Monkey', +u'Benutzer Diskussion:Lsjm', +u'Benutzer Diskussion:Eduardo79', +u'Benutzer Diskussion:Rigidmc', +u'Benutzer Diskussion:Gilgamesch2010', +u'Benutzer Diskussion:Paulusschinew', +u'Benutzer Diskussion:Hollister71', +u'Benutzer Diskussion:Schott-PR', +u'Benutzer Diskussion:RoBoVsKi', +#u'Benutzer Diskussion:Tjaraaa', # [ REDIRECTED ] +u'Benutzer Diskussion:Jason Hits', +u'Benutzer Diskussion:Fit-Fabrik', +u'Benutzer Diskussion:SpaceRazor', +u'Benutzer Diskussion:Fachversicherer', +u'Benutzer Diskussion:Qniemiec', +u'Benutzer Diskussion:Ilikeriri', +u'Benutzer Diskussion:Casinoroyal', +u'Benutzer Diskussion:Havanabua', +u'Benutzer Diskussion:Euku/2010/II. Quartal', # bugzilla:32753 +u'Benutzer Diskussion:Mo4jolo/Archiv/2008', +u'Benutzer Diskussion:Eschweiler', +u'Benutzer Diskussion:Marilyn.hanson', +u'Benutzer Diskussion:A.Savin', +u'Benutzer Diskussion:W!B:/Knacknüsse', +u'Benutzer Diskussion:Euku/2009/II. Halbjahr', +u'Benutzer Diskussion:Gamma', +u'Hilfe Diskussion:Captcha', +u'Benutzer Diskussion:Zacke/Kokytos', +u'Benutzer Diskussion:Wolfgang1018', +u'Benutzer Diskussion:El bes', +u'Benutzer Diskussion:Janneman/Orkus', +u'Wikipedia Diskussion:Shortcuts', +u'Benutzer Diskussion:PDD', +u'Wikipedia:WikiProjekt Vorlagen/Werkstatt', +u'Wikipedia Diskussion:WikiProjekt Wuppertal/2008', +u'Benutzer Diskussion:SchirmerPower', +u'Benutzer Diskussion:Stefan Kühn/Check Wikipedia', +u'Benutzer Diskussion:Elian', +u'Wikipedia:Fragen zur Wikipedia', +u'Benutzer Diskussion:Michael Kühntopf', +u'Benutzer Diskussion:Drahreg01', +u'Wikipedia:Vandalismusmeldung', +u'Benutzer Diskussion:Jesusfreund', +u'Benutzer Diskussion:Velipp28', +u'Benutzer Diskussion:Jotge', +u'Benutzer Diskussion:DAJ', +u'Benutzer Diskussion:Karl-G. Walther', +u'Benutzer Diskussion:Pincerno', +u'Benutzer Diskussion:Polluks', +u'Portal:Serbien/Nachrichtenarchiv', +u'Benutzer Diskussion:Elly200253', +u'Benutzer Diskussion:Yak', +u'Wikipedia:Auskunft', +u'Benutzer Diskussion:Toolittle', +u'Benutzer Diskussion:He3nry', +u'Benutzer Diskussion:Euku/2009/I. Halbjahr', +u'Benutzer Diskussion:Elchbauer' , +u'Benutzer Diskussion:Matthiasb', +u'Benutzer Diskussion:Gripweed', +u'Wikipedia:Löschkandidaten/10. Februar 2011', +u'Benutzer Diskussion:Funkruf', +u'Benutzer Diskussion:Vux', +u'Benutzer Diskussion:Zollernalb/Archiv/2008' , +u'Benutzer Diskussion:Geiserich77/Archiv2009', +u'Benutzer Diskussion:Markus Mueller/Archiv' , +u'Benutzer Diskussion:Capaci34/Archiv/2009', +u'Wikipedia Diskussion:Persönliche Bekanntschaften/Archiv/2010', +u'Benutzer Diskussion:Leithian/Archiv/2009/Aug', +u'Benutzer Diskussion:Lady Whistler/Archiv/2010', +u'Benutzer Diskussion:Jens Liebenau/Archiv1', +u'Benutzer Diskussion:Tilla/Archiv/2009/Juli', +u'Benutzer Diskussion:Xqt', +u'Vorlage Diskussion:Benutzerdiskussionsseite', +u'Wikipedia Diskussion:Meinungsbilder/Gestaltung von Signaturen', +u'Benutzer Diskussion:JvB1953', +u'Benutzer Diskussion:J.-H. Janßen', +u'Benutzer Diskussion:Xqt/Archiv/2009-1', +u'Hilfe Diskussion:Weiterleitung/Archiv/001', +u'Benutzer Diskussion:Raymond/Archiv 2006-2', +u'Wikipedia Diskussion:Projektneuheiten/Archiv/2009', +u'Vorlage Diskussion:Erledigt', +u'Wikipedia:Bots/Anfragen/Archiv/2008-2', +u'Diskussion:Golfschläger/Archiv', +u'Wikipedia:Löschkandidaten/9. Januar 2006', +u'Benutzer Diskussion:Church of emacs/Archiv5', +u'Wikipedia:WikiProjekt Vorlagen/Werkstatt/Archiv 2006', +u'Wikipedia Diskussion:Löschkandidaten/Archiv7', +u'Benutzer Diskussion:Physikr', +u'Benutzer Diskussion:Haring/Archiv, Dez. 2005', +u'Benutzer Diskussion:Seewolf/Archiv 7', +u'Benutzer Diskussion:Mipago/Archiv', +u'Wikipedia Diskussion:WikiProjekt Syntaxkorrektur/Archiv/2009', +u'Benutzer Diskussion:PDD/monobook.js', +u'Wikipedia:Löschkandidaten/9. April 2010', +u'Benutzer Diskussion:Augiasstallputzer/Archiv', +u'Hilfe Diskussion:Variablen', +u'Benutzer Diskussion:Merlissimo/Archiv/2009', +u'Benutzer Diskussion:Elya/Archiv 2007-01', +u'Benutzer Diskussion:Merlissimo/Archiv/2010', +u'Benutzer Diskussion:Jonathan Groß/Archiv 2006', +u'Benutzer Diskussion:Erendissss', +u'Diskussion:Ilse Elsner', +u'Diskussion:Pedro Muñoz', +u'Diskussion:Stimmkreis Nürnberg-Süd', +u'Diskussion:Geschichte der Sozialversicherung in Deutschland', +u'Diskussion:Josef Kappius', +u'Diskussion:Bibra (Adelsgeschlecht)', +u'Diskussion:Stimmkreis Regensburg-Land-Ost', +u'Diskussion:Volkmar Kretkowski', +u'Diskussion:KS Cracovia', +u'Diskussion:Livingston (Izabal)', +u'Wikipedia Diskussion:WikiProjekt Gesprochene Wikipedia/Howto', +u'Benutzer Diskussion:Otfried Lieberknecht', +u'Benutzer Diskussion:Jahn Henne', +u'Wikipedia:WikiProjekt Begriffsklärungsseiten/Fließband', +u'Wikipedia:Löschprüfung', +u'Benutzer Diskussion:Hubertl', +u'Benutzer Diskussion:Diba', +u'Wikipedia:Qualitätssicherung/11. März 2012', +u'Benutzer Diskussion:Heubergen/Archiv/2012', +u'Benutzer Diskussion:DrTrigon/Archiv', +u'Wikipedia:Fotowerkstatt', +u'Wikipedia:Urheberrechtsfragen', +] + + +class PyWikiWikipediaTestCase(test_pywiki.PyWikiTestCase): + + def setUp(self): + result = test_pywiki.PyWikiTestCase.setUp(self) + self.site = pywikibot.getSite('de', 'wikipedia') + return result + + def test_Page_getSections(self): + self.assertEqual( len(PAGE_SET_Page_getSections), 148 ) + count = 0 + problems = [] + for i, TESTPAGE in enumerate(PAGE_SET_Page_getSections): + page = pywikibot.Page(self.site, TESTPAGE) + try: + sections = page.getSections(minLevel=1) + except pywikibot.exceptions.Error: + count += 1 + problems.append( (i, page) ) + print "Number of pages total:", len(PAGE_SET_Page_getSections) + print "Number of problematic pages:", count + #print "Problematic pages:", problems + print "Problematic pages:\n", "\n".join( map(str, problems) ) + #self.assertLessEqual( count, 4 ) + self.assertTrue( count <= 0 ) + + return + +if __name__ == "__main__": + unittest.main()
Property changes on: trunk/pywikipedia/tests/test_wikipedia.py ___________________________________________________________________ Added: svn:keywords + Id Added: svn:eol-style + native
Modified: trunk/pywikipedia/wikipedia.py =================================================================== --- trunk/pywikipedia/wikipedia.py 2012-06-07 18:03:40 UTC (rev 10308) +++ trunk/pywikipedia/wikipedia.py 2012-06-07 23:01:11 UTC (rev 10309) @@ -676,7 +676,8 @@ # * Old exceptions and contents do not apply any more # * Deleting _contents and _expandcontents to force reload for attr in ['_redirarg', '_getexception', - '_contents', '_expandcontents']: + '_contents', '_expandcontents', + '_sections']: if hasattr(self, attr): delattr(self, attr) else: @@ -999,6 +1000,193 @@ change_edit_time=change_edit_time )
+ ## @since r10309 (ADDED) + # @remarks needed by various bots + def getSections(self, minLevel=2, sectionsonly=False, force=False): + """Parses the page with API and return section information. + ADDED METHOD: needed by various bots + + @param minLevel: The minimal level of heading for section to be reported. + @type minLevel: int + @param sectionsonly: Report only the result from API call, do not assign + the headings to wiki text (for compression e.g.). + @type sectionsonly: bool + @param force: Use API for full section list resolution, works always but + is extremely slow, since each single section has to be retrieved. + @type force: bool + + Returns a list with entries: (byteoffset, level, wikiline, line, anchor) + This list may be empty and if sections are embedded by template, the according + byteoffset and wikiline entries are None. The wikiline is the wiki text, + line is the parsed text and anchor ist the (unique) link label. + """ + # replace 'byteoffset' ALWAYS by self calculated, since parsed does not match wiki text + # bug fix; JIRA: DRTRIGON-82 + + # was there already a call? already some info available? + if hasattr(self, '_sections'): + return self._sections + + # Old exceptions and contents do not apply any more. + for attr in ['_sections']: + if hasattr(self, attr): + delattr(self,attr) + + # call the wiki to get info + params = { + u'action' : u'parse', + u'page' : self.title(), + u'prop' : u'sections', + } + + pywikibot.get_throttle() + pywikibot.output(u"Reading section info from %s via API..." % self.title(asLink=True)) + + result = query.GetData(params, self.site()) + # JIRA: DRTRIGON-90; catch and convert error (convert it such that the whole page gets processed later) + try: + r = result[u'parse'][u'sections'] + except KeyError: # sequence of sometimes occuring "KeyError: u'parse'" + pywikibot.output(u'WARNING: Query result (gS): %r' % result) + raise pywikibot.Error('Problem occured during data retrieval for sections in %s!' % self.title(asLink=True)) + #debug_data = str(r) + '\n' + debug_data = str(result) + '\n' + + if not sectionsonly: + # assign sections with wiki text and section byteoffset + #pywikibot.output(u" Reading wiki page text (if not already done).") + + debug_data += str(len(self.__dict__.get('_contents',u''))) + '\n' + self.get() + debug_data += str(len(self._contents)) + '\n' + debug_data += self._contents + '\n' + + # code debugging + if verbose: + debugDump( 'Page.getSections', self.site, err, debug_data.encode(config.textfile_encoding) ) + + for setting in [(0.05,0.95), (0.4,0.8), (0.05,0.8), (0.0,0.8)]: # 0.6 is default upper border + try: + pos = 0 + for i, item in enumerate(r): + item[u'level'] = int(item[u'level']) + # byteoffset may be 0; 'None' means template + #if (item[u'byteoffset'] != None) and item[u'line']: + # (empty index means also template - workaround for bug: + # https://bugzilla.wikimedia.org/show_bug.cgi?id=32753) + if (item[u'byteoffset'] != None) and item[u'line'] and item[u'index']: + # section on this page and index in format u"%i" + self._getSectionByteOffset(item, pos, force, cutoff=setting) # raises 'Error' if not sucessfull ! + pos = item[u'wikiline_bo'] + len(item[u'wikiline']) + item[u'byteoffset'] = item[u'wikiline_bo'] + else: + # section embedded from template (index in format u"T-%i") or the + # parser was not able to recongnize section correct (e.g. html) at all + # (the byteoffset, index, ... may be correct or not) + item[u'wikiline'] = None + r[i] = item + break + except pywikibot.Error: + pos = None + if (pos == None): + raise # re-raise + + # check min. level + data = [] + for item in r: + if (item[u'level'] < minLevel): continue + data.append( item ) + r = data + + # prepare resulting data + self._sections = [ (item[u'byteoffset'], item[u'level'], item[u'wikiline'], item[u'line'], item[u'anchor']) for item in r ] + + return self._sections + + ## @since r10309 (ADDED) + # @remarks needed by wikipedia.Page.getSections() + def _getSectionByteOffset(self, section, pos, force=False, cutoff=(0.05, 0.95)): + """determine the byteoffset of the given section (can be slow due another API call). + ADDED METHOD: needed by 'getSections' + """ + wikitextlines = self._contents[pos:].splitlines() + possible_headers = [] + #print section[u'line'] + + if not force: + # how the heading should look like (re) + l = section[u'level'] + headers = [ u'^(\s*)%(spacer)s(.*?)%(spacer)s(\s*)((<!--(.*?)-->)?)(\s*)$' % {'line': section[u'line'], 'spacer': u'=' * l}, + u'^(\s*)<h%(level)i>(.*?)</h%(level)i>(.*?)$' % {'line': section[u'line'], 'level': l}, ] + + # try to give exact match for heading (remove HTML comments) + for h in headers: + #ph = re.search(h, pywikibot.removeDisabledParts(self._contents[pos:]), re.M) + ph = re.search(h, self._contents[pos:], re.M) + if ph: + ph = ph.group(0).strip() + possible_headers += [ (ph, section[u'line']) ] + + # how the heading could look like (difflib) + headers = [ u'%(spacer)s %(line)s %(spacer)s' % {'line': section[u'line'], 'spacer': u'=' * l}, + u'<h%(level)i>%(line)s</h%(level)i>' % {'line': section[u'line'], 'level': l}, ] + + # give possible match for heading + # http://stackoverflow.com/questions/2923420/fuzzy-string-matching-algorithm-i... + # http://docs.python.org/library/difflib.html + # (http://mwh.geek.nz/2009/04/26/python-damerau-levenshtein-distance/) + for h in headers: + ph = difflib.get_close_matches(h, wikitextlines, cutoff=cutoff[1]) # cutoff=0.6 (default) + possible_headers += [ (p, section[u'line']) for p in ph ] + #print h, possible_headers + + if not possible_headers and section[u'index']: # nothing found, try 'prop=revisions (rv)' + # call the wiki to get info + params = { + u'action' : u'query', + u'titles' : self.title(), + u'prop' : u'revisions', + u'rvprop' : u'content', + u'rvsection' : section[u'index'], + } + + pywikibot.get_throttle() + pywikibot.output(u" Reading section %s from %s via API..." % (section[u'index'], self.title(asLink=True))) + + result = query.GetData(params, self.site()) + # JIRA: DRTRIGON-90; catch and convert error (convert it such that the whole page gets processed later) + try: + r = result[u'query'][u'pages'].values()[0] + pl = r[u'revisions'][0][u'*'].splitlines() + except KeyError: # sequence of sometimes occuring "KeyError: u'parse'" + pywikibot.output(u'WARNING: Query result (gSBO): %r' % result) + raise pywikibot.Error('Problem occured during data retrieval for sections in %s!' % self.title(asLink=True)) + + if pl: + possible_headers = [ (pl[0], pl[0]) ] + + # find the most probable match for heading + #print possible_headers + best_match = (0.0, None) + for i, (ph, header) in enumerate(possible_headers): + #print u' ', i, difflib.SequenceMatcher(None, header, ph).ratio(), header, ph + mr = difflib.SequenceMatcher(None, header, ph).ratio() + if mr >= best_match[0]: best_match = (mr, ph) + if (i in [0, 1]) and (mr >= cutoff[0]): break # use first (exact; re) match directly (if good enough) + #print u' ', best_match + + # prepare resulting data + section[u'wikiline'] = best_match[1] + section[u'wikiline_mq'] = best_match[0] # match quality + section[u'wikiline_bo'] = -1 # byteoffset + if section[u'wikiline']: + section[u'wikiline_bo'] = self._contents.find(section[u'wikiline'], pos) + if section[u'wikiline_bo'] < 0: # nothing found, report/raise error ! + #page._getexception = ... + raise pywikibot.Error('Problem occured during attempt to retrieve and resolve sections in %s!' % self.title(asLink=True)) + #pywikibot.output(...) + # (or create a own error, e.g. look into interwiki.py) + def permalink(self): """Return the permalink URL for current revision of this page.""" return "%s://%s%s&oldid=%i" % (self.site().protocol(),
pywikipedia-svn@lists.wikimedia.org