jenkins-bot has submitted this change and it was merged.
Change subject: Move logging to new pywikibot.logging module
......................................................................
Move logging to new pywikibot.logging module
config2 and tools now use logging layer.
pywikibot.__url__ introduced as base url to
online documentation.
Generic logging tools moved into tools._logging
Change-Id: I8f1e2c1d09b5d39c5facadb0690ee71bdbf5de1b
---
M pywikibot/__init__.py
M pywikibot/backports.py
M pywikibot/bot.py
M pywikibot/config2.py
M pywikibot/family.py
M pywikibot/flow.py
M pywikibot/i18n.py
A pywikibot/logging.py
M pywikibot/page.py
M pywikibot/tools/__init__.py
A pywikibot/tools/_logging.py
M pywikibot/userinterfaces/gui.py
M pywikibot/userinterfaces/terminal_interface_base.py
M tox.ini
14 files changed, 393 insertions(+), 314 deletions(-)
Approvals:
XZise: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/__init__.py b/pywikibot/__init__.py
index febe973..fc32f30 100644
--- a/pywikibot/__init__.py
+++ b/pywikibot/__init__.py
@@ -9,6 +9,7 @@
__release__ = '2.0b3'
__version__ = '$Id$'
+__url__ = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Pywikibot'
import datetime
import math
diff --git a/pywikibot/backports.py b/pywikibot/backports.py
index cb44b6e..39b904c 100644
--- a/pywikibot/backports.py
+++ b/pywikibot/backports.py
@@ -12,7 +12,7 @@
# Distributed under the terms of the PSF license.
#
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
__license__ = """
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
@@ -62,6 +62,7 @@
agrees to be bound by the terms and conditions of this License
Agreement.
"""
+
import logging
import warnings
diff --git a/pywikibot/bot.py b/pywikibot/bot.py
index 104a45a..63be6b0 100644
--- a/pywikibot/bot.py
+++ b/pywikibot/bot.py
@@ -49,7 +49,7 @@
#
# Distributed under the terms of the MIT license.
#
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
__version__ = '$Id$'
@@ -75,12 +75,6 @@
_logger = "bot"
-# logging levels
-from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
-STDOUT = 16
-VERBOSE = 18
-INPUT = 25
-
import pywikibot
from pywikibot import backports
@@ -92,7 +86,18 @@
ListOption, HighlightContextOption,
ChoiceException, QuitKeyboardInterrupt,
)
+from pywikibot.logging import CRITICAL, ERROR, INFO, WARNING # noqa: unused
+from pywikibot.logging import DEBUG, INPUT, STDOUT, VERBOSE
+from pywikibot.logging import (
+ add_init_routine,
+ debug, error, exception, log, output, stdout, warning,
+)
+from pywikibot.logging import critical # noqa: unused
from pywikibot.tools import deprecated, deprecated_args, PY2, PYTHON_VERSION
+from pywikibot.tools._logging import (
+ LoggingFormatter as _LoggingFormatter,
+ RotatingFileHandler,
+)
if not PY2:
unicode = str
@@ -130,119 +135,13 @@
self.stop = stop
-# Logging module configuration
-class RotatingFileHandler(logging.handlers.RotatingFileHandler):
+class LoggingFormatter(_LoggingFormatter):
- """Modified RotatingFileHandler supporting unlimited amount of
backups."""
+ """Logging formatter that uses
config.console_encoding."""
- def doRollover(self):
- """
- Modified naming system for logging files.
-
- Overwrites the default Rollover renaming by inserting the count number
- between file name root and extension. If backupCount is >= 1, the system
- will successively create new files with the same pathname as the base
- file, but with inserting ".1", ".2" etc. in front of the
filename
- suffix. For example, with a backupCount of 5 and a base file name of
- "app.log", you would get "app.log", "app.1.log",
"app.2.log", ...
- through to "app.5.log". The file being written to is always
"app.log" -
- when it gets filled up, it is closed and renamed to "app.1.log", and
if
- files "app.1.log", "app.2.log" etc. already exist, then they
are
- renamed to "app.2.log", "app.3.log" etc. respectively.
- If backupCount is == -1 do not rotate but create new numbered filenames.
- The newest file has the highest number except some older numbered files
- where deleted and the bot was restarted. In this case the ordering
- starts from the lowest available (unused) number.
-
- """
- if self.stream:
- self.stream.close()
- self.stream = None
- root, ext = os.path.splitext(self.baseFilename)
- if self.backupCount > 0:
- for i in range(self.backupCount - 1, 0, -1):
- sfn = "%s.%d%s" % (root, i, ext)
- dfn = "%s.%d%s" % (root, i + 1, ext)
- if os.path.exists(sfn):
- if os.path.exists(dfn):
- os.remove(dfn)
- os.rename(sfn, dfn)
- dfn = "%s.1%s" % (root, ext)
- if os.path.exists(dfn):
- os.remove(dfn)
- os.rename(self.baseFilename, dfn)
- elif self.backupCount == -1:
- if not hasattr(self, '_lastNo'):
- self._lastNo = 1
- while True:
- fn = "%s.%d%s" % (root, self._lastNo, ext)
- self._lastNo += 1
- if not os.path.exists(fn):
- break
- os.rename(self.baseFilename, fn)
- self.mode = 'w'
- self.stream = self._open()
-
- def format(self, record):
- """Strip trailing newlines before outputting text to
file."""
- # Warnings captured from the warnings system are not processed by
- # logoutput(), so the 'context' variables are missing.
- # The same context details are provided by Python 3.X, but need to
- # be extracted from the warning message for Python <= 2.7.
- if record.name == 'py.warnings' and 'caller_file' not in
record.__dict__:
- assert len(record.args) == 1, \
- 'Arguments for record is not correctly set'
- msg = record.args[0]
-
- if PY2:
- record.pathname = msg.partition(':')[0]
- record.lineno = msg.partition(':')[2].partition(':')[0]
- record.module =
msg.rpartition('/')[2].rpartition('.')[0]
- else:
- assert msg.startswith(record.pathname + ':'), \
- 'Record argument should start with path'
-
- record.__dict__['caller_file'] = record.pathname
- record.__dict__['caller_name'] = record.module
- record.__dict__['caller_line'] = record.lineno
-
- # Remove the path and the line number, and strip the extra space
- msg = msg.partition(':')[2].partition(':')[2].lstrip()
- record.args = (msg,)
-
- text = logging.handlers.RotatingFileHandler.format(self, record)
- return text.rstrip("\r\n")
-
-
-class LoggingFormatter(logging.Formatter):
-
- """Format LogRecords for output to file.
-
- This formatter *ignores* the 'newline' key of the LogRecord, because
- every record written to a file must end with a newline, regardless of
- whether the output to the user's console does.
-
- """
-
- def formatException(self, ei):
- r"""
- Convert exception trace to unicode if necessary.
-
- Make sure that the exception trace is converted to unicode.
-
- L{exceptions.Error} traces are encoded in our console encoding, which
- is needed for plainly printing them. However, when logging them
- using logging.exception, the Python logging module will try to use
- these traces, and it will fail if they are console encoded strings.
-
- Formatter.formatException also strips the trailing \n, which we need.
- """
- strExc = logging.Formatter.formatException(self, ei)
-
- if PY2 and isinstance(strExc, bytes):
- return strExc.decode(config.console_encoding) + '\n'
- else:
- return strExc + '\n'
+ def __init__(self, fmt=None, datefmt=None):
+ """Constructor setting underlying encoding to
console_encoding."""
+ _LoggingFormatter.__init__(self, fmt, datefmt, config.console_encoding)
# Initialize the handlers and formatters for the logging system.
@@ -456,162 +355,7 @@
log(u'=== ' * 14)
-# User output/logging functions
-
-# Six output functions are defined. Each requires a unicode or string
-# argument. All of these functions generate a message to the log file if
-# logging is enabled ("-log" or "-debug" command line arguments).
-
-# The functions output(), stdout(), warning(), and error() all display a
-# message to the user through the logger object; the only difference is the
-# priority level, which can be used by the application layer to alter the
-# display. The stdout() function should be used only for data that is
-# the "result" of a script, as opposed to information messages to the
-# user.
-
-# The function log() by default does not display a message to the user, but
-# this can be altered by using the "-verbose" command line option.
-
-# The function debug() only logs its messages, they are never displayed on
-# the user console. debug() takes a required second argument, which is a
-# string indicating the debugging layer.
-
-def logoutput(text, decoder=None, newline=True, _level=INFO, _logger="",
- **kwargs):
- """Format output and send to the logging module.
-
- Helper function used by all the user-output convenience functions.
-
- """
- if _logger:
- logger = logging.getLogger("pywiki." + _logger)
- else:
- logger = logging.getLogger("pywiki")
-
- # make sure logging system has been initialized
- if not _handlers_initialized:
- init_handlers()
-
- # frame 0 is logoutput() in this module,
- # frame 1 is the convenience function (output(), etc.)
- # frame 2 is whatever called the convenience function
- frame = sys._getframe(2)
-
- module = os.path.basename(frame.f_code.co_filename)
- context = {'caller_name': frame.f_code.co_name,
- 'caller_file': module,
- 'caller_line': frame.f_lineno,
- 'newline': ("\n" if newline else "")}
-
- if decoder:
- text = text.decode(decoder)
- elif not isinstance(text, unicode):
- if not isinstance(text, str):
- # looks like text is a non-text object.
- # Maybe it has a __unicode__ builtin ?
- # (allows to print Page, Site...)
- text = unicode(text)
- else:
- try:
- text = text.decode('utf-8')
- except UnicodeDecodeError:
- text = text.decode('iso8859-1')
-
- logger.log(_level, text, extra=context, **kwargs)
-
-
-def output(text, decoder=None, newline=True, toStdout=False, **kwargs):
- r"""Output a message to the user via the userinterface.
-
- Works like print, but uses the encoding used by the user's console
- (console_encoding in the configuration file) instead of ASCII.
-
- If decoder is None, text should be a unicode string. Otherwise it
- should be encoded in the given encoding.
-
- If newline is True, a line feed will be added after printing the text.
-
- If toStdout is True, the text will be sent to standard output,
- so that it can be piped to another process. All other text will
- be sent to stderr. See:
https://en.wikipedia.org/wiki/Pipeline_%28Unix%29
-
- text can contain special sequences to create colored output. These
- consist of the escape character \03 and the color name in curly braces,
- e. g. \03{lightpurple}. \03{default} resets the color.
-
- Other keyword arguments are passed unchanged to the logger; so far, the
- only argument that is useful is "exc_info=True", which causes the
- log message to include an exception traceback.
-
- """
- if toStdout: # maintained for backwards-compatibity only
- logoutput(text, decoder, newline, STDOUT, **kwargs)
- else:
- logoutput(text, decoder, newline, INFO, **kwargs)
-
-
-def stdout(text, decoder=None, newline=True, **kwargs):
- """Output script results to the user via the
userinterface."""
- logoutput(text, decoder, newline, STDOUT, **kwargs)
-
-
-def warning(text, decoder=None, newline=True, **kwargs):
- """Output a warning message to the user via the
userinterface."""
- logoutput(text, decoder, newline, WARNING, **kwargs)
-
-
-def error(text, decoder=None, newline=True, **kwargs):
- """Output an error message to the user via the
userinterface."""
- logoutput(text, decoder, newline, ERROR, **kwargs)
-
-
-def log(text, decoder=None, newline=True, **kwargs):
- """Output a record to the log file."""
- logoutput(text, decoder, newline, VERBOSE, **kwargs)
-
-
-def critical(text, decoder=None, newline=True, **kwargs):
- """Output a critical record to the log file."""
- logoutput(text, decoder, newline, CRITICAL, **kwargs)
-
-
-def debug(text, layer, decoder=None, newline=True, **kwargs):
- """Output a debug record to the log file.
-
- @param layer: The name of the logger that text will be sent to.
- """
- logoutput(text, decoder, newline, DEBUG, layer, **kwargs)
-
-
-def exception(msg=None, decoder=None, newline=True, tb=False, **kwargs):
- """Output an error traceback to the user via the userinterface.
-
- Use directly after an 'except' statement::
-
- ...
- except:
- pywikibot.exception()
- ...
-
- or alternatively::
-
- ...
- except Exception as e:
- pywikibot.exception(e)
- ...
-
- @param tb: Set to True in order to output traceback also.
- """
- if isinstance(msg, BaseException):
- exc_info = 1
- else:
- exc_info = sys.exc_info()
- msg = u'%s: %s' % (repr(exc_info[1]).split('(')[0],
- unicode(exc_info[1]).strip())
- if tb:
- kwargs['exc_info'] = exc_info
- logoutput(msg, decoder, newline, ERROR, **kwargs)
-
+add_init_routine(init_handlers)
# User input functions
diff --git a/pywikibot/config2.py b/pywikibot/config2.py
index 774a818..a19def2 100644
--- a/pywikibot/config2.py
+++ b/pywikibot/config2.py
@@ -56,6 +56,9 @@
else:
import _winreg as winreg
+from pywikibot import __url__
+from pywikibot.logging import error, output, warning
+
# This frozen set should contain all imported modules/variables, so it must
# occur directly after the imports. At that point globals() only contains the
# names and some magic variables (like __name__)
@@ -317,7 +320,7 @@
exc_text = "No user-config.py found in directory '%s'.\n" %
base_dir
if _no_user_config:
if _no_user_config != '2':
- print(exc_text)
+ output(exc_text)
else:
exc_text += " Please check that user-config.py is stored in the correct
location.\n"
exc_text += " Directory where user-config.py is searched is determined
as follows:\n\n"
@@ -333,7 +336,7 @@
for arg in sys.argv[1:]:
if arg.startswith(str('-verbose')) or arg == str('-v'):
- print("The base directory is %s" % base_dir)
+ output('The base directory is {0}'.format(base_dir))
break
family_files = {}
@@ -871,8 +874,8 @@
return cmd[:-1].strip()
except WindowsError as e:
# Catch any key lookup errors
- print('WARNING: Unable to find editor for files *.' + extension)
- print(e)
+ output('Unable to detect program for file extension "{0}":
{1}'.format(
+ extension, e))
def _detect_win32_editor():
@@ -922,7 +925,7 @@
_thislevel = 0
if _no_user_config:
if _no_user_config != '2':
- print("WARNING: Skipping loading of user-config.py.")
+ warning('Skipping loading of user-config.py.')
_fns = []
else:
_fns = [os.path.join(_base_dir, "user-config.py")]
@@ -937,11 +940,11 @@
with open(_filename, 'rb') as f:
exec(compile(f.read(), _filename, 'exec'), _uc)
else:
- print("WARNING: Skipped '%(fn)s': writeable by
others."
- % {'fn': _filename})
+ warning("Skipped '%(fn)s': writeable by others."
+ % {'fn': _filename})
else:
- print("WARNING: Skipped '%(fn)s': owned by someone else."
- % {'fn': _filename})
+ warning("Skipped '%(fn)s': owned by someone else."
+ % {'fn': _filename})
class _DifferentTypeError(UserWarning, TypeError):
@@ -1031,10 +1034,9 @@
if transliteration_target == 'not set':
if sys.platform == 'win32':
transliteration_target = console_encoding
- print("WARNING: Running on Windows and transliteration_target is not "
- "set.")
- print("Please see "
-
"https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Pywikibot/Windows")
+ warning(
+ 'Running on Windows and transliteration_target is not set.\n'
+ 'Please see {0}/Windows'.format(__url__))
else:
transliteration_target = None
elif transliteration_target in ('None', 'none'):
@@ -1049,23 +1051,24 @@
#
https://docs.python.org/2/reference/lexical_analysis.html#string-literals
# encode('unicode-escape') also changes Unicode characters
if set(editor) & set('\a\b\f\n\r\t\v'):
- print('WARNING: The editor path contains probably invalid escaped '
- 'characters. Make sure to use a raw-string (r"..." or
r\'...\'), '
- 'forward slashs as a path delimiter or to escape the normal '
- 'path delimiter.')
+ warning(
+ 'The editor path contains probably invalid escaped '
+ 'characters. Make sure to use a raw-string (r"..." or
r\'...\'), '
+ 'forward slashs as a path delimiter or to escape the normal '
+ 'path delimiter.')
# Fix up default site
if family == 'wikipedia' and mylang == 'language':
if _no_user_config != '2':
- print("WARNING: family and mylang are not set.\n"
- "Defaulting to family='test' and
mylang='test'.")
+ warning('family and mylang are not set.\n'
+ "Defaulting to family='test' and
mylang='test'.")
family = mylang = 'test'
# SECURITY WARNINGS
if (not ignore_file_security_warnings and
private_files_permission & (stat.S_IRWXG | stat.S_IRWXO) != 0):
- print("CRITICAL SECURITY WARNING: 'private_files_permission' is
set"
+ error("CRITICAL SECURITY WARNING: 'private_files_permission' is
set"
" to allow access from the group/others which"
" could give them access to the sensitive files."
" To avoid giving others access to sensitive files, pywikibot"
@@ -1083,7 +1086,7 @@
if _arg == "modified":
_all = 0
else:
- print("Unknown arg %(_arg)s ignored" % locals())
+ warning('Unknown arg %(_arg)s ignored' % locals())
_k = list(globals().keys())
_k.sort()
for _name in _k:
@@ -1102,7 +1105,7 @@
_value = repr('xxxxxxxx')
else:
_value = repr(_value)
- print("%s=%s" % (_name, _value))
+ output('{0}={1}'.format(_name, _value))
# cleanup all locally-defined variables
for __var in list(globals().keys()):
diff --git a/pywikibot/family.py b/pywikibot/family.py
index 148a539..72753f0 100644
--- a/pywikibot/family.py
+++ b/pywikibot/family.py
@@ -5,7 +5,7 @@
#
# Distributed under the terms of the MIT license.
#
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
__version__ = '$Id$'
#
diff --git a/pywikibot/flow.py b/pywikibot/flow.py
index 46dfac9..accc5d4 100644
--- a/pywikibot/flow.py
+++ b/pywikibot/flow.py
@@ -5,7 +5,7 @@
#
# Distributed under the terms of the MIT license.
#
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
__version__ = '$Id$'
diff --git a/pywikibot/i18n.py b/pywikibot/i18n.py
index c8c516c..0a2a5d2 100644
--- a/pywikibot/i18n.py
+++ b/pywikibot/i18n.py
@@ -37,6 +37,7 @@
import pywikibot
+from pywikibot import __url__
from pywikibot import Error
from pywikibot import config
from pywikibot.plural import plural_rules
@@ -477,8 +478,8 @@
raise TranslationError(
'Unable to load messages package %s for bundle %s'
'\nIt can happen due to lack of i18n submodule or files. '
- 'Read
https://mediawiki.org/wiki/PWB/i18n'
- % (_messages_package_name, twtitle))
+ 'Read %s/i18n'
+ % (_messages_package_name, twtitle, __url__))
code_needed = False
# If a site is given instead of a code, use its language
diff --git a/pywikibot/logging.py b/pywikibot/logging.py
new file mode 100644
index 0000000..ed82a98
--- /dev/null
+++ b/pywikibot/logging.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+"""Logging functions."""
+#
+# (C) Pywikibot team, 2010-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import absolute_import, unicode_literals
+
+__version__ = '$Id$'
+
+import logging
+import os
+import sys
+
+# logging levels
+from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
+STDOUT = 16
+VERBOSE = 18
+INPUT = 25
+
+if sys.version_info[0] > 2:
+ unicode = str
+
+_init_routines = []
+_inited_routines = []
+
+
+def add_init_routine(routine):
+ """Add a routine to be run as soon as possible."""
+ _init_routines.append(routine)
+
+
+def _init():
+ """Init any routines which have not already been
called."""
+ for init_routine in _init_routines:
+ if init_routine not in _inited_routines:
+ init_routine()
+ _inited_routines.append(init_routine)
+
+ # Clear the list of routines to be inited
+ _init_routines[:] = []
+
+
+# User output/logging functions
+
+# Six output functions are defined. Each requires a unicode or string
+# argument. All of these functions generate a message to the log file if
+# logging is enabled ("-log" or "-debug" command line arguments).
+
+# The functions output(), stdout(), warning(), and error() all display a
+# message to the user through the logger object; the only difference is the
+# priority level, which can be used by the application layer to alter the
+# display. The stdout() function should be used only for data that is
+# the "result" of a script, as opposed to information messages to the
+# user.
+
+# The function log() by default does not display a message to the user, but
+# this can be altered by using the "-verbose" command line option.
+
+# The function debug() only logs its messages, they are never displayed on
+# the user console. debug() takes a required second argument, which is a
+# string indicating the debugging layer.
+
+
+def logoutput(text, decoder=None, newline=True, _level=INFO, _logger="",
+ **kwargs):
+ """Format output and send to the logging module.
+
+ Helper function used by all the user-output convenience functions.
+
+ """
+ if _logger:
+ logger = logging.getLogger("pywiki." + _logger)
+ else:
+ logger = logging.getLogger("pywiki")
+
+ # invoke any init routines
+ if _init_routines:
+ _init()
+
+ # frame 0 is logoutput() in this module,
+ # frame 1 is the convenience function (output(), etc.)
+ # frame 2 is whatever called the convenience function
+ frame = sys._getframe(2)
+
+ module = os.path.basename(frame.f_code.co_filename)
+ context = {'caller_name': frame.f_code.co_name,
+ 'caller_file': module,
+ 'caller_line': frame.f_lineno,
+ 'newline': ("\n" if newline else "")}
+
+ if decoder:
+ text = text.decode(decoder)
+ elif not isinstance(text, unicode):
+ if not isinstance(text, str):
+ # looks like text is a non-text object.
+ # Maybe it has a __unicode__ builtin ?
+ # (allows to print Page, Site...)
+ text = unicode(text)
+ else:
+ try:
+ text = text.decode('utf-8')
+ except UnicodeDecodeError:
+ text = text.decode('iso8859-1')
+
+ logger.log(_level, text, extra=context, **kwargs)
+
+
+def output(text, decoder=None, newline=True, toStdout=False, **kwargs):
+ r"""Output a message to the user via the userinterface.
+
+ Works like print, but uses the encoding used by the user's console
+ (console_encoding in the configuration file) instead of ASCII.
+
+ If decoder is None, text should be a unicode string. Otherwise it
+ should be encoded in the given encoding.
+
+ If newline is True, a line feed will be added after printing the text.
+
+ If toStdout is True, the text will be sent to standard output,
+ so that it can be piped to another process. All other text will
+ be sent to stderr. See:
https://en.wikipedia.org/wiki/Pipeline_%28Unix%29
+
+ text can contain special sequences to create colored output. These
+ consist of the escape character \03 and the color name in curly braces,
+ e. g. \03{lightpurple}. \03{default} resets the color.
+
+ Other keyword arguments are passed unchanged to the logger; so far, the
+ only argument that is useful is "exc_info=True", which causes the
+ log message to include an exception traceback.
+
+ """
+ if toStdout: # maintained for backwards-compatibity only
+ logoutput(text, decoder, newline, STDOUT, **kwargs)
+ else:
+ logoutput(text, decoder, newline, INFO, **kwargs)
+
+
+def stdout(text, decoder=None, newline=True, **kwargs):
+ """Output script results to the user via the
userinterface."""
+ logoutput(text, decoder, newline, STDOUT, **kwargs)
+
+
+def warning(text, decoder=None, newline=True, **kwargs):
+ """Output a warning message to the user via the
userinterface."""
+ logoutput(text, decoder, newline, WARNING, **kwargs)
+
+
+def error(text, decoder=None, newline=True, **kwargs):
+ """Output an error message to the user via the
userinterface."""
+ logoutput(text, decoder, newline, ERROR, **kwargs)
+
+
+def log(text, decoder=None, newline=True, **kwargs):
+ """Output a record to the log file."""
+ logoutput(text, decoder, newline, VERBOSE, **kwargs)
+
+
+def critical(text, decoder=None, newline=True, **kwargs):
+ """Output a critical record to the log file."""
+ logoutput(text, decoder, newline, CRITICAL, **kwargs)
+
+
+def debug(text, layer, decoder=None, newline=True, **kwargs):
+ """Output a debug record to the log file.
+
+ @param layer: The name of the logger that text will be sent to.
+ """
+ logoutput(text, decoder, newline, DEBUG, layer, **kwargs)
+
+
+def exception(msg=None, decoder=None, newline=True, tb=False, **kwargs):
+ """Output an error traceback to the user via the userinterface.
+
+ Use directly after an 'except' statement::
+
+ ...
+ except:
+ pywikibot.exception()
+ ...
+
+ or alternatively::
+
+ ...
+ except Exception as e:
+ pywikibot.exception(e)
+ ...
+
+ @param tb: Set to True in order to output traceback also.
+ """
+ if isinstance(msg, BaseException):
+ exc_info = 1
+ else:
+ exc_info = sys.exc_info()
+ msg = u'%s: %s' % (repr(exc_info[1]).split('(')[0],
+ unicode(exc_info[1]).strip())
+ if tb:
+ kwargs['exc_info'] = exc_info
+ logoutput(msg, decoder, newline, ERROR, **kwargs)
diff --git a/pywikibot/page.py b/pywikibot/page.py
index 081c252..2c697c0 100644
--- a/pywikibot/page.py
+++ b/pywikibot/page.py
@@ -16,7 +16,7 @@
#
# Distributed under the terms of the MIT license.
#
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
__version__ = '$Id$'
#
diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py
index fa07047..2787b89 100644
--- a/pywikibot/tools/__init__.py
+++ b/pywikibot/tools/__init__.py
@@ -32,15 +32,7 @@
else:
import Queue
-
-def print_debug(msg, *args, **kwargs):
- """Simple debug routine."""
- print(msg)
-
-
-# This variable uses the builtin print function.
-# pywikibot updates it to use logging in bot.init_handlers()
-debug = print_debug
+from pywikibot.logging import debug
_logger = 'tools'
diff --git a/pywikibot/tools/_logging.py b/pywikibot/tools/_logging.py
new file mode 100644
index 0000000..3b07379
--- /dev/null
+++ b/pywikibot/tools/_logging.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+"""Logging tools."""
+#
+# (C) Pywikibot team, 2009-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import absolute_import, unicode_literals
+
+__version__ = '$Id$'
+
+import logging
+import os
+
+from pywikibot.tools import PY2
+
+
+# Logging module configuration
+class RotatingFileHandler(logging.handlers.RotatingFileHandler):
+
+ """Modified RotatingFileHandler supporting unlimited amount of
backups."""
+
+ def doRollover(self):
+ """
+ Modified naming system for logging files.
+
+ Overwrites the default Rollover renaming by inserting the count number
+ between file name root and extension. If backupCount is >= 1, the system
+ will successively create new files with the same pathname as the base
+ file, but with inserting ".1", ".2" etc. in front of the
filename
+ suffix. For example, with a backupCount of 5 and a base file name of
+ "app.log", you would get "app.log", "app.1.log",
"app.2.log", ...
+ through to "app.5.log". The file being written to is always
"app.log" -
+ when it gets filled up, it is closed and renamed to "app.1.log", and
if
+ files "app.1.log", "app.2.log" etc. already exist, then they
are
+ renamed to "app.2.log", "app.3.log" etc. respectively.
+ If backupCount is == -1 do not rotate but create new numbered filenames.
+ The newest file has the highest number except some older numbered files
+ where deleted and the bot was restarted. In this case the ordering
+ starts from the lowest available (unused) number.
+
+ """
+ if self.stream:
+ self.stream.close()
+ self.stream = None
+ root, ext = os.path.splitext(self.baseFilename)
+ if self.backupCount > 0:
+ for i in range(self.backupCount - 1, 0, -1):
+ sfn = "%s.%d%s" % (root, i, ext)
+ dfn = "%s.%d%s" % (root, i + 1, ext)
+ if os.path.exists(sfn):
+ if os.path.exists(dfn):
+ os.remove(dfn)
+ os.rename(sfn, dfn)
+ dfn = "%s.1%s" % (root, ext)
+ if os.path.exists(dfn):
+ os.remove(dfn)
+ os.rename(self.baseFilename, dfn)
+ elif self.backupCount == -1:
+ if not hasattr(self, '_lastNo'):
+ self._lastNo = 1
+ while True:
+ fn = "%s.%d%s" % (root, self._lastNo, ext)
+ self._lastNo += 1
+ if not os.path.exists(fn):
+ break
+ os.rename(self.baseFilename, fn)
+ self.mode = 'w'
+ self.stream = self._open()
+
+ def format(self, record):
+ """Strip trailing newlines before outputting text to
file."""
+ # Warnings captured from the warnings system are not processed by
+ # logoutput(), so the 'context' variables are missing.
+ # The same context details are provided by Python 3.X, but need to
+ # be extracted from the warning message for Python <= 2.7.
+ if record.name == 'py.warnings' and 'caller_file' not in
record.__dict__:
+ assert len(record.args) == 1, \
+ 'Arguments for record is not correctly set'
+ msg = record.args[0]
+
+ if PY2:
+ record.pathname = msg.partition(':')[0]
+ record.lineno = msg.partition(':')[2].partition(':')[0]
+ record.module =
msg.rpartition('/')[2].rpartition('.')[0]
+ else:
+ assert msg.startswith(record.pathname + ':'), \
+ 'Record argument should start with path'
+
+ record.__dict__['caller_file'] = record.pathname
+ record.__dict__['caller_name'] = record.module
+ record.__dict__['caller_line'] = record.lineno
+
+ # Remove the path and the line number, and strip the extra space
+ msg = msg.partition(':')[2].partition(':')[2].lstrip()
+ record.args = (msg,)
+
+ text = logging.handlers.RotatingFileHandler.format(self, record)
+ return text.rstrip("\r\n")
+
+
+class LoggingFormatter(logging.Formatter):
+
+ """Format LogRecords for output to file.
+
+ This formatter *ignores* the 'newline' key of the LogRecord, because
+ every record written to a file must end with a newline, regardless of
+ whether the output to the user's console does.
+
+ """
+
+ def __init__(self, fmt=None, datefmt=None, encoding=None):
+ """Constructor with additional encoding
parameter."""
+ logging.Formatter.__init__(self, fmt, datefmt)
+ self._encoding = encoding
+
+ def formatException(self, ei):
+ r"""
+ Convert exception trace to unicode if necessary.
+
+ Make sure that the exception trace is converted to unicode.
+
+ L{exceptions.Error} traces are encoded in our console encoding, which
+ is needed for plainly printing them. However, when logging them
+ using logging.exception, the Python logging module will try to use
+ these traces, and it will fail if they are console encoded strings.
+
+ Formatter.formatException also strips the trailing \n, which we need.
+ """
+ strExc = logging.Formatter.formatException(self, ei)
+
+ if PY2 and isinstance(strExc, bytes):
+ return strExc.decode(self._encoding) + '\n'
+ else:
+ return strExc + '\n'
diff --git a/pywikibot/userinterfaces/gui.py b/pywikibot/userinterfaces/gui.py
index 6ca260b..59faacf 100644
--- a/pywikibot/userinterfaces/gui.py
+++ b/pywikibot/userinterfaces/gui.py
@@ -33,6 +33,8 @@
import pywikibot
+from pywikibot import __url__
+
class TextEditor(ScrolledText):
@@ -517,8 +519,7 @@
except ImportError:
pywikibot.warning('This script requires ImageTk from the'
'Python Imaging Library (PIL).\n'
- 'See:
https://www.mediawiki.org/wiki/'
- 'Manual:Pywikibot/flickrripper.py')
+ 'See: {0}/flickrripper.py'.format(__url__))
raise
image = Image.open(photo)
diff --git a/pywikibot/userinterfaces/terminal_interface_base.py
b/pywikibot/userinterfaces/terminal_interface_base.py
index 783feb3..3049041 100755
--- a/pywikibot/userinterfaces/terminal_interface_base.py
+++ b/pywikibot/userinterfaces/terminal_interface_base.py
@@ -5,7 +5,7 @@
#
# Distributed under the terms of the MIT license.
#
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
__version__ = '$Id$'
diff --git a/tox.ini b/tox.ini
index e4ece8e..6f623b0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -63,6 +63,7 @@
pywikibot/fixes.py \
pywikibot/i18n.py \
pywikibot/logentries.py \
+ pywikibot/logging.py \
pywikibot/login.py \
pywikibot/page.py \
pywikibot/pagegenerators.py \
--
To view, visit
https://gerrit.wikimedia.org/r/236502
To unsubscribe, visit
https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I8f1e2c1d09b5d39c5facadb0690ee71bdbf5de1b
Gerrit-PatchSet: 14
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Owner: John Vandenberg <jayvdb(a)gmail.com>
Gerrit-Reviewer: John Vandenberg <jayvdb(a)gmail.com>
Gerrit-Reviewer: Ladsgroup <ladsgroup(a)gmail.com>
Gerrit-Reviewer: Mattflaschen <mflaschen(a)wikimedia.org>
Gerrit-Reviewer: XZise <CommodoreFabianus(a)gmx.de>
Gerrit-Reviewer: jenkins-bot <>