jenkins-bot has submitted this change and it was merged.
Change subject: delete.py: warn if pages link to "page to be deleted"
......................................................................
delete.py: warn if pages link to "page to be deleted"
Add parameter:
-isorphan to check 'What links here' for pages to be deleted.
-orphansonly to delete only pages that are orphans in namespaces
specified by this parameter.
Set also automatic_quit=True in page.delete(), to quit script.
Change-Id: I365a5f9c067bae5637a6c628e308b35f8c4eb1b7
---
M pywikibot/page.py
M pywikibot/tools/__init__.py
M scripts/delete.py
M tests/tools_tests.py
4 files changed, 268 insertions(+), 8 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/page.py b/pywikibot/page.py
index dc83ae4..ad06c2d 100644
--- a/pywikibot/page.py
+++ b/pywikibot/page.py
@@ -1733,7 +1733,7 @@
noredirect=deleteAndMove)
@deprecate_arg("throttle", None)
- def delete(self, reason=None, prompt=True, mark=False):
+ def delete(self, reason=None, prompt=True, mark=False, quit=False):
"""
Delete the page from the wiki. Requires administrator status.
@@ -1743,6 +1743,7 @@
@param mark: If true, and user does not have sysop rights, place a
speedy-deletion request on the page instead. If false, non-sysops
will be asked before marking pages for deletion.
+ @param quit: show also the quit option, when asking for confirmation.
"""
if reason is None:
pywikibot.output(u'Deleting %s.' % (self.title(asLink=True)))
@@ -1756,7 +1757,7 @@
u'Do you want to delete %s?' % self.title(
asLink=True, forceInterwiki=True),
[('Yes', 'y'), ('No', 'n'),
('All', 'a')],
- 'n', automatic_quit=False)
+ 'n', automatic_quit=quit)
if answer == 'a':
answer = 'y'
self.site._noDeletePrompt = True
diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py
index 714f047..60070eb 100644
--- a/pywikibot/tools/__init__.py
+++ b/pywikibot/tools/__init__.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Miscellaneous helper functions (not wiki-dependent)."""
#
-# (C) Pywikibot team, 2008-2015
+# (C) Pywikibot team, 2008-2016
#
# Distributed under the terms of the MIT license.
#
@@ -11,6 +11,7 @@
import collections
import gzip
import inspect
+import itertools
import re
import subprocess
import sys
@@ -609,6 +610,49 @@
yield group
+def islice_with_ellipsis(iterable, *args, **kwargs):
+ u"""
+ Generator which yields the first n elements of the iterable.
+
+ If more elements are available and marker is True, it returns an extra
+ string marker as continuation mark.
+
+ Function takes the
+ and the additional keyword marker.
+
+ @param iterable: the iterable to work on
+ @type iterable: iterable
+ @param args: same args as:
+ - C{itertools.islice(iterable, stop)}
+ - C{itertools.islice(iterable, start, stop[, step])}
+ @keyword marker: element to yield if iterable still contains elements
+ after showing the required number.
+ Default value: '…'
+ No other kwargs are considered.
+ @type marker: str
+ """
+ s = slice(*args)
+ marker = kwargs.pop('marker', '…')
+ try:
+ k, v = kwargs.popitem()
+ raise TypeError(
+ "islice_with_ellipsis() take only 'marker' as keyword arg, not
%s"
+ % k)
+ except KeyError:
+ pass
+
+ _iterable = iter(iterable)
+ for el in itertools.islice(_iterable, *args):
+ yield el
+ if marker and s.stop is not None:
+ try:
+ next(_iterable)
+ except StopIteration:
+ pass
+ else:
+ yield marker
+
+
class ThreadList(list):
"""A simple threadpool class to limit the number of simultaneous
threads.
diff --git a/scripts/delete.py b/scripts/delete.py
index cd52c6d..52a3bc7 100755
--- a/scripts/delete.py
+++ b/scripts/delete.py
@@ -11,12 +11,32 @@
Furthermore, the following command line parameters are supported:
--always: Don't prompt to delete pages, just do it.
+-always Don't prompt to delete pages, just do it.
--summary: Supply a custom edit summary.
+-summary:XYZ Set the summary message text for the edit to XYZ.
--undelete: Actually undelete pages instead of deleting.
+-undelete Actually undelete pages instead of deleting.
Obviously makes sense only with -page and -file.
+
+-isorphan Alert if there are pages that link to page to be
+ deleted (check 'What links here').
+ By default it is active and only the summary per namespace
+ is be given.
+ If given as -isorphan:n, n pages per namespace will be shown,
+ If given as -isorphan:0, only the summary per namespace will
+ be shown,
+ If given as -isorphan:n, with n < 0, the option is disabled.
+ This option is disregarded if -always is set.
+
+-orphansonly: Specified namespaces. Separate multiple namespace
+ numbers or names with commas.
+ Examples:
+ -ns:0,2,4
+ -ns:Help,MediaWiki
+
+ Note that Main ns can be indicated either with a 0 or a ',':
+ -ns:0,1
+ -ns:,Talk
Usage:
@@ -29,7 +49,7 @@
python pwb.py delete -cat:"To delete" -always
"""
#
-# (C) Pywikibot team, 2014
+# (C) Pywikibot team, 2016
#
# Distributed under the terms of the MIT license.
#
@@ -38,6 +58,8 @@
__version__ = '$Id$'
#
+import collections
+
from warnings import warn
import pywikibot
@@ -45,12 +67,65 @@
from pywikibot import exceptions
from pywikibot import i18n, pagegenerators
from pywikibot.bot import MultipleSitesBot, CurrentPageBot
+from pywikibot.page import Page
+from pywikibot.tools import islice_with_ellipsis
# This is required for the text that is shown when you run this script
# with the parameter -help.
docuReplacements = {
'¶ms;': pagegenerators.parameterHelp,
}
+
+
+class PageWithRefs(Page):
+
+ """
+ A subclass of Page with convenience methods for reference checking.
+
+ Supports the same interface as Page, with some added methods.
+ """
+
+ def __init__(self, source, title='', ns=0):
+ """Constructor."""
+ super(PageWithRefs, self).__init__(source, title, ns)
+ _cache_attrs = list(super(PageWithRefs, self)._cache_attrs)
+ _cache_attrs = tuple(_cache_attrs + ['_ref_table'])
+
+ def get_ref_table(self, *args, **kwargs):
+ """Build mapping table with pages which links the current
page."""
+ ref_table = collections.defaultdict(list)
+ for page in self.getReferences(*args, **kwargs):
+ ref_table[page.namespace()].append(page)
+ return ref_table
+
+ @property
+ def ref_table(self):
+ """
+ Build link reference table lazily.
+
+ This property gives a default table without any parameter set for
+ getReferences(), whereas self.get_ref_table() is able to accept
+ parameters.
+ """
+ if not hasattr(self, '_ref_table'):
+ self._ref_table = self.get_ref_table()
+ return self._ref_table
+
+ def namespaces_with_ref_to_page(self, namespaces=None):
+ """
+ Check if current page has links from pages in namepaces.
+
+ If namespaces is None, all namespaces are checked.
+ Returns a set with namespaces where a ref to page is present.
+
+ @param namespaces: Namespace to check
+ @type namespaces: iterable of Namespace objects
+ @rtype set: namespaces where a ref to page is present
+ """
+ if namespaces is None:
+ namespaces = self.site.namespaces()
+
+ return set(namespaces) & set(self.ref_table)
class DeletionRobot(MultipleSitesBot, CurrentPageBot):
@@ -68,10 +143,43 @@
"""
self.availableOptions.update({
'undelete': False,
+ 'isorphan': 0,
+ 'orphansonly': [],
})
super(DeletionRobot, self).__init__(generator=generator, **kwargs)
self.summary = summary
+ # Upcast pages to PageWithRefs()
+ self.generator = (PageWithRefs(p) for p in self.generator)
+
+ def display_references(self):
+ """
+ Display pages which links the current page, sorted per namespace.
+
+ Number of pages to display per namespace is provided by:
+ - self.getOption('isorphan')
+ """
+ refs = self.current_page.ref_table
+ if refs:
+ total = sum(len(v) for v in refs.values())
+ pywikibot.warning('There are %d pages who link to %s.'
+ % (total, self.current_page))
+ else:
+ return
+
+ show_n_pages = self.getOption('isorphan')
+ width = len(max((ns.canonical_prefix() for ns in refs), key=len))
+ for ns in sorted(refs):
+ n_pages_in_ns = len(refs[ns])
+ plural = '' if n_pages_in_ns == 1 else 's'
+ ns_name = ns.canonical_prefix() if ns != ns.MAIN else 'Main:'
+ ns_id = '[{0}]'.format(ns.id)
+ pywikibot.output(
+ ' {0!s:<{width}} {1:>6} {2:>10} page{pl}'.format(
+ ns_name, ns_id, n_pages_in_ns, width=width, pl=plural))
+ if show_n_pages: # do not show marker if 0 pages are requested.
+ for page in islice_with_ellipsis(refs[ns], show_n_pages):
+ pywikibot.output(' {0!s}'.format(page.title()))
def treat_page(self):
"""Process one page from the generator."""
@@ -83,9 +191,27 @@
self.current_page.undelete(self.summary)
else:
if self.current_page.exists():
+
+ if (self.getOption('isorphan') is not False and
+ not self.getOption('always')):
+ self.display_references()
+
+ if self.getOption('orphansonly'):
+ namespaces = self.getOption('orphansonly')
+ ns_with_ref = self.current_page.namespaces_with_ref_to_page(
+ namespaces)
+ ns_with_ref = sorted(list(ns_with_ref))
+ if ns_with_ref:
+ ns_names = ', '.join(str(ns.id) for ns in ns_with_ref)
+ pywikibot.output(
+ 'Skipping: {0} is not orphan in ns: {1}.'.format(
+ self.current_page, ns_names))
+ return # Not an orphan, do not delete.
+
self.current_page.delete(self.summary,
not self.getOption('always'),
- self.getOption('always'))
+ self.getOption('always'),
+ quit=True)
else:
pywikibot.output(u'Skipping: {0} does not exist.'.format(
self.current_page))
@@ -125,6 +251,16 @@
local_args.append('-imageused' + arg[7:])
elif arg.startswith('-undelete'):
options['undelete'] = True
+ elif arg.startswith('-isorphan'):
+ options['isorphan'] = int(arg[10:]) if arg[10:] != '' else 0
+ if options['isorphan'] < 0:
+ options['isorphan'] = False
+ elif arg.startswith('-orphansonly'):
+ if arg[13:]:
+ namespaces = mysite.namespaces.resolve(arg[13:].split(","))
+ else:
+ namespaces = mysite.namespaces
+ options['orphansonly'] = namespaces
else:
genFactory.handleArg(arg)
found = arg.find(':') + 1
diff --git a/tests/tools_tests.py b/tests/tools_tests.py
index 8f12faa..71f6b63 100644
--- a/tests/tools_tests.py
+++ b/tests/tools_tests.py
@@ -263,6 +263,85 @@
ValueError, '42', tools.merge_unique_dicts, self.dct1, **self.dct1)
+class TestIsSliceWithEllipsis(TestCase):
+
+ """Test islice_with_ellipsis."""
+
+ net = False
+
+ it = ['a', 'b', 'c', 'd', 'f']
+ it_null = []
+
+ def test_show_default_marker(self):
+ """Test marker is shown without kwargs."""
+ stop = 2
+ it = list(tools.islice_with_ellipsis(self.it, stop))
+ self.assertEqual(len(it), stop + 1) # +1 to consider marker.
+ self.assertEqual(it[:-1], self.it[:stop])
+ self.assertEqual(it[-1], '…')
+
+ def test_show_custom_marker(self):
+ """Test correct marker is shown with kwargs.."""
+ stop = 2
+ it = list(tools.islice_with_ellipsis(self.it, stop, marker='new'))
+ self.assertEqual(len(it), stop + 1) # +1 to consider marker.
+ self.assertEqual(it[:-1], self.it[:stop])
+ self.assertNotEqual(it[-1], '…')
+ self.assertEqual(it[-1], 'new')
+
+ def test_show_marker_with_start_stop(self):
+ """Test marker is shown with start and stop without
kwargs."""
+ start = 1
+ stop = 3
+ it = list(tools.islice_with_ellipsis(self.it, start, stop))
+ self.assertEqual(len(it), stop - start + 1) # +1 to consider marker.
+ self.assertEqual(it[:-1], self.it[start:stop])
+ self.assertEqual(it[-1], '…')
+
+ def test_show_custom_marker_with_start_stop(self):
+ """Test marker is shown with start and stop with
kwargs."""
+ start = 1
+ stop = 3
+ it = list(tools.islice_with_ellipsis(self.it, start, stop,
marker='new'))
+ self.assertEqual(len(it), stop - start + 1) # +1 to consider marker.
+ self.assertEqual(it[:-1], self.it[start:stop])
+ self.assertNotEqual(it[-1], '…')
+ self.assertEqual(it[-1], 'new')
+
+ def test_show_marker_with_stop_zero(self):
+ """Test marker is shown with stop for non empty
iterable."""
+ stop = 0
+ it = list(tools.islice_with_ellipsis(self.it, stop))
+ self.assertEqual(len(it), stop + 1) # +1 to consider marker.
+ self.assertEqual(it[-1], '…')
+
+ def test_do_not_show_marker_with_stop_zero(self):
+ """Test marker is shown with stop for empty
iterable."""
+ stop = 0
+ it = list(tools.islice_with_ellipsis(self.it_null, stop))
+ self.assertEqual(len(it), stop)
+
+ def test_do_not_show_marker(self):
+ """Test marker is not shown when no marker is
specified."""
+ import itertools
+ stop = 2
+ it_1 = list(tools.islice_with_ellipsis(self.it, stop, marker=None))
+ it_2 = list(itertools.islice(self.it, stop))
+ self.assertEqual(it_1, it_2) # same behavior as islice().
+
+ def test_do_not_show_marker_when_get_all(self):
+ """Test marker is not shown when all elements are
retrieved."""
+ stop = None
+ it = list(tools.islice_with_ellipsis(self.it, stop))
+ self.assertEqual(len(it), len(self.it))
+ self.assertEqual(it, self.it)
+ self.assertNotEqual(it[-1], '…')
+
+ def test_accept_only_keyword_marker(self):
+ """Test that the only kwargs accepted is
'marker'."""
+ self.assertRaises(TypeError, tools.islice_with_ellipsis(self.it, 1,
t=''))
+
+
def passthrough(x):
"""Return x."""
return x
--
To view, visit
https://gerrit.wikimedia.org/r/266587
To unsubscribe, visit
https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I365a5f9c067bae5637a6c628e308b35f8c4eb1b7
Gerrit-PatchSet: 19
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Owner: Mpaa <mpaa.wiki(a)gmail.com>
Gerrit-Reviewer: John Vandenberg <jayvdb(a)gmail.com>
Gerrit-Reviewer: Mpaa <mpaa.wiki(a)gmail.com>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot <>