1
0
Fork 0

Initial version of the Python scripts to manage l10n using the XLIFF format

Add the following files:

  python3-flightgear/README-l10n.txt
  python3-flightgear/fg-convert-translation-files
  python3-flightgear/fg-new-translations
  python3-flightgear/fg-update-translation-files
  python3-flightgear/flightgear/__init__.py
  python3-flightgear/flightgear/meta/__init__.py
  python3-flightgear/flightgear/meta/exceptions.py
  python3-flightgear/flightgear/meta/i18n.py
  python3-flightgear/flightgear/meta/logging.py
  python3-flightgear/flightgear/meta/misc.py

They should work on Python 3.4 and later (tested with 3.5.3). The folder
structure is chosen so that other FG support modules can insert
themselves here, and possibly be used together. I put all of these
inside 'flightgear.meta', because I don't expect them to be needed at FG
runtime (neither now nor in the future), probably not even by the CMake
build system.

To declare that a string has plural forms, simply set the attribute
'with-plural' to 'true' on the corresponding element of the default
translation (and as in Qt, use %n as a placeholder for the number that
determines which singular or plural form to use).
This commit is contained in:
Florent Rougon 2017-06-26 10:45:57 +02:00
parent 89948603de
commit c6eb59eb42
10 changed files with 2634 additions and 0 deletions

View file

@ -0,0 +1,83 @@
Quick start for the localization (l10n) scripts
===============================================
The following assumes that all of these are in present in
$FG_ROOT/Translations:
- the default translation (default/*.xml);
- the legacy FlightGear XML localization files (<language_code>/*.xml);
- except for 'fg-convert-translation-files' which creates them, existing
XLIFF 1.2 files (<language_code>/FlightGear-nonQt.xlf).
Note: the legacy FlightGear XML localization files are only needed by
'fg-convert-translation-files' when migrating to the XLIFF format. The
other scripts only need the default translation and obviously, for
'fg-update-translation-files', the current XLIFF files.
To get the initial XLIFF files (generated from the default translation in
$FG_ROOT/Translations/default as well as the legacy FlightGear XML
localization files in $FG_ROOT/Translations/<language_code>):
languages="de en_US es fr it nl pl pt zh_CN"
# Your shell must expand $languages as several words. POSIX shell does that,
# but not zsh for instance. Otherwise, don't use a shell variable.
fg-convert-translation-files --transl-dir="$FG_ROOT/Translations" $languages
# Add strings found in the default translation but missing in the legacy FG
# XML l10n files
fg-update-translation-files --transl-dir="$FG_ROOT/Translations" \
merge-new-master $languages
When master strings[1] have changed (in a large sense, i.e.: strings added,
modified or removed, or categories added or removed[2]):
fg-update-translation-files --transl-dir="$FG_ROOT/Translations" \
merge-new-master $languages
To remove unused translated strings (not to be done too often in my opinion):
fg-update-translation-files --transl-dir="$FG_ROOT/Translations" \
remove-unused $languages
(you may replace 'remove-unused' with 'mark-unused' to just mark the strings
as not-to-be-translated, however 'merge-new-master' presented above already
does that)
To create skeleton translations for new languages (e.g., for fr_BE, en_AU and
ca):
1) Check (add if necessary) that flightgear/meta/i18n.py knows the plural
forms used in the new languages. This is done by editing PLURAL_FORMS
towards the top of this i18n.py file (very easy). If the existing entry
for, e.g., "zh" is sufficient for zh_TW or zh_HK, just let "zh" handle
them: it will be tried as fallback if there is no perfect match on
language and territory.
2) Run a command such as:
fg-new-translations --transl-dir="$FG_ROOT/Translations" fr_BE en_AU ca
(if you do this for only one language at a time, you can use the -o
option to precisely control where the output goes, otherwise
fg-new-translations chooses an appropriate place based on the value
specified for --transl-dir)
fg-convert-translation-files, fg-update-translation-files and
fg-new-translations all support the --help option for more detailed
information.
Footnotes
---------
[1] Strings in the default translation.
[2] Only empty categories are removed by this command. An obsolete category
can be made empty by manual editing (easy, just locate the right
<group>) or this way:
fg-update-translation-files --transl-dir=... mark-unused
fg-update-translation-files --transl-dir=... remove-unused
(note that this will remove *all* strings marked as unused in the first
step, not only those in some particular category!)

View file

@ -0,0 +1,186 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# fg-convert-translation-files --- Convert FlightGear's translation files
# Copyright (C) 2017 Florent Rougon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import argparse
import collections
import locale
import os
import sys
try:
import xml.etree.ElementTree as et
except ImportError:
import elementtree.ElementTree as et
import flightgear.meta.logging
import flightgear.meta.i18n as fg_i18n
PROGNAME = os.path.basename(sys.argv[0])
# Only messages with severity >= info will be printed to the terminal (it's
# possible to also log all messages to a file regardless of their level, see
# the Logger class). Of course, there is also the standard logging module...
logger = flightgear.meta.logging.Logger(
progname=PROGNAME,
logLevel=flightgear.meta.logging.LogLevel.info,
defaultOutputStream=sys.stderr)
debug = logger.debug
info = logger.info
notice = logger.notice
warning = logger.warning
error = logger.error
critical = logger.critical
# We could use Translation.__str__(): not as readable (for now) but more
# accurate on metadata
def printPlainText(l10nResPoolMgr, translations):
"""Print output suitable for a quick review (by the programmer)."""
firstLang = True
for langCode, (transl, nbWhitespacePbs) in translations.items():
# 'transl' is a Translation instance
if firstLang:
firstLang = False
else:
print()
print("-" * 78 + "\n" + langCode + "\n" + "-" * 78)
print("\nNumber of leading and/or trailing whitespace problems: {}"
.format(nbWhitespacePbs))
for cat in transl:
print("\nCategory: {cat}\n{underline}".format(
cat=cat, underline="~"*(len("Category: ") + len(cat))))
t = transl[cat]
for tid, translUnit in sorted(t.items()):
# - Using '{master!r}' and '{transl!r}' prints stuff such as
# \xa0 for nobreak spaces, which can lead to the erroneous
# conclusion that there was an encoding problem.
# - Only printing the first target text here (no plural forms)
print("\n{id}\n '{sourceText}'\n '{targetText}'"
.format(id=tid.id(), sourceText=translUnit.sourceText,
targetText=translUnit.targetTexts[0]))
def writeXliff(l10nResPoolMgr, translations):
formatHandler = fg_i18n.XliffFormatHandler()
for langCode, translData in translations.items():
translation = translData.transl # Translation instance
if params.output_dir is None:
# Use default locations for the written xliff files
l10nResPoolMgr.writeTranslation(formatHandler, translation)
else:
basename = "{}-{}.{}".format(
formatHandler.defaultFileStem(langCode),
langCode,
formatHandler.standardExtension)
filePath = os.path.join(params.output_dir, basename)
formatHandler.writeTranslation(translation, filePath)
def processCommandLine():
params = argparse.Namespace()
parser = argparse.ArgumentParser(
usage="""\
%(prog)s [OPTION ...] LANGUAGE_CODE...
Convert FlightGear's old XML translation files into other formats.""",
description="""\
Most notably, XLIFF format can be chosen for output. The script performs
a few automated checks on the input files too.""",
formatter_class=argparse.RawDescriptionHelpFormatter,
# I want --help but not -h (it might be useful for something else)
add_help=False)
parser.add_argument("-t", "--transl-dir",
help="""\
directory containing all translation subdirs (such as
{default!r}, 'en_GB', 'fr_FR', 'de', 'it'...). This
"option" MUST be specified.""".format(
default=fg_i18n.DEFAULT_LANG_DIR))
parser.add_argument("lang_code", metavar="LANGUAGE_CODE", nargs="+",
help="""\
codes of languages to read translations for (don't
specify {default!r} this way, it is special and not a
language code)"""
.format(default=fg_i18n.DEFAULT_LANG_DIR))
parser.add_argument("-o", "--output-dir",
help="""\
output directory for written XLIFF files
(default: for each output file, use a suitable location
under TRANSL_DIR)""")
parser.add_argument("-f", "--output-format", default="xliff",
choices=("xliff", "text"), help="""\
format to use for the output files""")
parser.add_argument("--help", action="help",
help="display this message and exit")
params = parser.parse_args(namespace=params)
if params.transl_dir is None:
error("--transl-dir must be given, aborting")
sys.exit(1)
return params
def main():
global params
locale.setlocale(locale.LC_ALL, '')
params = processCommandLine()
l10nResPoolMgr = fg_i18n.L10NResourcePoolManager(params.transl_dir, logger)
# English version of all translatable strings
masterTransl, nbWhitespaceProblemsInMaster = \
l10nResPoolMgr.readFgMasterTranslation()
translations = collections.OrderedDict()
# Sort elements of 'translations' according to language code (= the keys)
for langCode in sorted(params.lang_code):
translationData = l10nResPoolMgr.readFgTranslation(masterTransl,
langCode)
translations[translationData.transl.targetLanguage] = translationData
if params.output_format == "xliff":
writeFunc = writeXliff # write to files
elif params.output_format == "text":
writeFunc = printPlainText # print to stdout
else:
assert False, \
"Unexpected output format: '{}'".format(params.output_format)
writeFunc(l10nResPoolMgr, translations)
nbWhitespaceProblemsInTransl = sum(
(translData.nbWhitespacePbs for translData in translations.values() ))
info("total number of leading and/or trailing whitespace problems: {}"
.format(nbWhitespaceProblemsInMaster + nbWhitespaceProblemsInTransl))
sys.exit(0)
if __name__ == "__main__": main()

View file

@ -0,0 +1,125 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# fg-new-translations --- Create new translations for FlightGear
# Copyright (C) 2017 Florent Rougon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import argparse
import collections
import locale
import os
import sys
try:
import xml.etree.ElementTree as et
except ImportError:
import elementtree.ElementTree as et
import flightgear.meta.logging
import flightgear.meta.i18n as fg_i18n
PROGNAME = os.path.basename(sys.argv[0])
# Only messages with severity >= info will be printed to the terminal (it's
# possible to also log all messages to a file regardless of their level, see
# the Logger class). Of course, there is also the standard logging module...
logger = flightgear.meta.logging.Logger(
progname=PROGNAME,
logLevel=flightgear.meta.logging.LogLevel.info,
defaultOutputStream=sys.stderr)
def processCommandLine():
params = argparse.Namespace()
parser = argparse.ArgumentParser(
usage="""\
%(prog)s [OPTION ...] LANGUAGE_CODE...
Write the skeleton of XLIFF translation files.""",
description="""\
This program writes XLIFF translation files with the strings to translate
for the specified languages (target strings are empty). This is what you need
to start a translation for a new language.""",
formatter_class=argparse.RawDescriptionHelpFormatter,
# I want --help but not -h (it might be useful for something else)
add_help=False)
parser.add_argument("-t", "--transl-dir",
help="""\
directory containing all translation subdirs (such as
{default!r}, 'en_GB', 'fr_FR', 'de', 'it'...). This
"option" MUST be specified.""".format(
default=fg_i18n.DEFAULT_LANG_DIR))
parser.add_argument("lang_code", metavar="LANGUAGE_CODE", nargs="+",
help="""\
codes of languages to create translations for (e.g., fr,
fr_BE, en_GB, it, es_ES...)""")
parser.add_argument("-o", "--output-file",
help="""\
where to write the output to (use '-' for standard
output); if not specified, a suitable file under
TRANSL_DIR will be chosen for each LANGUAGE_CODE.
Note: this option can only be given when exactly one
LANGUAGE_CODE has been specified on the command
line (it doesn't make sense otherwise).""")
parser.add_argument("--output-format", default="xliff",
choices=fg_i18n.FORMAT_HANDLERS_NAMES,
help="format to use for the output files")
parser.add_argument("--help", action="help",
help="display this message and exit")
params = parser.parse_args(namespace=params)
if params.transl_dir is None:
logger.error("--transl-dir must be given, aborting")
sys.exit(1)
if params.output_file is not None and len(params.lang_code) > 1:
logger.error("--output-file can only be given when exactly one "
"LANGUAGE_CODE has been specified on the command line "
"(it doesn't make sense otherwise)")
sys.exit(1)
return params
def main():
global params
locale.setlocale(locale.LC_ALL, '')
params = processCommandLine()
l10nResPoolMgr = fg_i18n.L10NResourcePoolManager(params.transl_dir, logger)
xliffFormatHandler = fg_i18n.FORMAT_HANDLERS_MAP[params.output_format]()
if params.output_file is not None:
assert len(params.lang_code) == 1, params.lang_code
# Output to one file or to stdout
l10nResPoolMgr.writeSkeletonTranslation(
xliffFormatHandler, params.lang_code[0],
filePath=params.output_file)
else:
# Output to several files
for langCode in params.lang_code:
l10nResPoolMgr.writeSkeletonTranslation(xliffFormatHandler,
langCode)
sys.exit(0)
if __name__ == "__main__": main()

View file

@ -0,0 +1,184 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# fg-update-translation-files --- Merge new default translation,
# remove obsolete strings from a translation
# Copyright (C) 2017 Florent Rougon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import argparse
import enum
import locale
import os
import sys
try:
import xml.etree.ElementTree as et
except ImportError:
import elementtree.ElementTree as et
import flightgear.meta.logging
import flightgear.meta.i18n as fg_i18n
PROGNAME = os.path.basename(sys.argv[0])
# Only messages with severity >= info will be printed to the terminal (it's
# possible to also log all messages to a file regardless of their level, see
# the Logger class). Of course, there is also the standard logging module...
logger = flightgear.meta.logging.Logger(
progname=PROGNAME,
logLevel=flightgear.meta.logging.LogLevel.info,
defaultOutputStream=sys.stderr)
def processCommandLine():
params = argparse.Namespace()
parser = argparse.ArgumentParser(
usage="""\
%(prog)s [OPTION ...] ACTION LANGUAGE_CODE...
Update FlightGear XLIFF localization files.""",
description="""\
This program performs the following operations (actions) on FlightGear XLIFF
translation files (*.xlf):
- [merge-new-master]
Read the default translation[1], add new translated strings it contains to
the XLIFF localization files corresponding to the specified language(s),
mark the translated strings in said files that need review (modified in
the default translation) as well as those that are not used anymore
(disappeared in the default translation, or marked in a way that says they
don't need to be translated);
- [mark-unused]
Read the default translation and mark translated strings (in the XLIFF
localization files corresponding to the specified language(s)) that are
not used anymore;
- [remove-unused]
In the XLIFF localization files corresponding to the specified
language(s), remove all translated strings that are marked as unused.
A translated string that is marked as unused is still present in the XLIFF
localization file; it is just presented in a way that tells translators they
don't need to worry about it. On the other hand, when a translated string is
removed, translators don't see it anymore and the translation is lost, except
if rescued by external means such as backups or version control systems (Git,
Subversion, etc.)
Note that the 'remove-unused' action does *not* imply 'mark-unused'. It only
removes translation units that are already marked as unused (i.e., with
translate="no"). Thus, it makes sense to do 'mark-unused' followed by
'remove-unused' if you really want to get rid of old translations (you need to
invoke the program twice, or make a small change for this). Leaving unused
translated strings marked as such in XLIFF files shouldn't harm much in
general on the short or mid-term: they only take some space.
[1] FlightGear XML files in $FG_ROOT/Translations/default containing strings
used for the default locale (English).""",
formatter_class=argparse.RawDescriptionHelpFormatter,
# I want --help but not -h (it might be useful for something else)
add_help=False)
parser.add_argument("-t", "--transl-dir",
help="""\
directory containing all translation subdirs (such as
{default!r}, 'en_GB', 'fr_FR', 'de', 'it'...). This
"option" MUST be specified.""".format(
default=fg_i18n.DEFAULT_LANG_DIR))
parser.add_argument("action", metavar="ACTION",
choices=("merge-new-master",
"mark-unused",
"remove-unused"),
help="""\
what to do: merge a new default (= master)
translation, or mark unused translation units, or
remove those already marked as unused from the XLIFF
files corresponding to each given LANGUAGE_CODE (i.e.,
those that are not in the default translation)""")
parser.add_argument("lang_code", metavar="LANGUAGE_CODE", nargs="+",
help="""\
codes of languages to operate on (e.g., fr, en_GB, it,
es_ES...)""")
parser.add_argument("--help", action="help",
help="display this message and exit")
params = parser.parse_args(namespace=params)
if params.transl_dir is None:
logger.error("--transl-dir must be given, aborting")
sys.exit(1)
return params
class MarkOrRemoveUnusedAction(enum.Enum):
mark, remove = range(2)
def markOrRemoveUnused(l10nResPoolMgr, action):
formatHandler = fg_i18n.XliffFormatHandler()
masterTransl = l10nResPoolMgr.readFgMasterTranslation().transl
for langCode in params.lang_code:
xliffPath = formatHandler.defaultFilePath(params.transl_dir, langCode)
transl = formatHandler.readTranslation(xliffPath)
if action == MarkOrRemoveUnusedAction.mark:
transl.markObsoleteOrVanished(masterTransl, logger=logger)
elif action == MarkOrRemoveUnusedAction.remove:
transl.removeObsoleteOrVanished(logger=logger)
else:
assert False, "unexpected action: {!r}".format(action)
l10nResPoolMgr.writeTranslation(formatHandler, transl,
filePath=xliffPath)
def mergeNewMaster(l10nResPoolMgr):
formatHandler = fg_i18n.XliffFormatHandler()
masterTransl = l10nResPoolMgr.readFgMasterTranslation().transl
for langCode in params.lang_code:
xliffPath = formatHandler.defaultFilePath(params.transl_dir, langCode)
transl = formatHandler.readTranslation(xliffPath)
transl.mergeMasterTranslation(masterTransl, logger=logger)
l10nResPoolMgr.writeTranslation(formatHandler, transl,
filePath=xliffPath)
def main():
global params
locale.setlocale(locale.LC_ALL, '')
params = processCommandLine()
l10nResPoolMgr = fg_i18n.L10NResourcePoolManager(params.transl_dir, logger)
if params.action == "mark-unused":
markOrRemoveUnused(l10nResPoolMgr, MarkOrRemoveUnusedAction.mark)
elif params.action == "remove-unused":
markOrRemoveUnused(l10nResPoolMgr, MarkOrRemoveUnusedAction.remove)
elif params.action == "merge-new-master":
mergeNewMaster(l10nResPoolMgr)
else:
assert False, "Bug: unexpected action: {!r}".format(params.action)
sys.exit(0)
if __name__ == "__main__": main()

View file

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# exceptions.py --- Simple, general-purpose subclass of Exception
#
# Copyright (C) 2015, 2017 Florent Rougon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Simple, general-purpose Exception subclass."""
class FGPyException(Exception):
def __init__(self, message=None, *, mayCapitalizeMsg=True):
"""Initialize an FGPyException instance.
Except in cases where 'message' starts with a proper noun or
something like that, its first character should be given in
lower case. Automated treatments of this exception may print the
message with its first character changed to upper case, unless
'mayCapitalizeMsg' is False. In other words, if the case of the
first character of 'message' must not be changed under any
circumstances, set 'mayCapitalizeMsg' to False.
"""
self.message = message
self.mayCapitalizeMsg = mayCapitalizeMsg
def __str__(self):
return self.completeMessage()
def __repr__(self):
return "{}.{}({!r})".format(__name__, type(self).__name__, self.message)
# Typically overridden by subclasses with a custom constructor
def detail(self):
return self.message
def completeMessage(self):
if self.message:
return "{shortDesc}: {detail}".format(
shortDesc=self.ExceptionShortDescription,
detail=self.detail())
else:
return self.ExceptionShortDescription
ExceptionShortDescription = "FlightGear Python generic exception"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
# logging.py --- Simple logging infrastructure (mostly taken from FFGo)
# Copyright (C) 2015, 2017 Florent Rougon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import sys
from . import misc
class LogLevel(misc.OrderedEnum):
debug, info, notice, warning, error, critical = range(6)
# List containing the above log levels as strings in increasing priority order
allLogLevels = [member.name for member in LogLevel]
allLogLevels.sort(key=lambda n: LogLevel[n].value)
def _logFuncFactory(level):
def logFunc(self, *args, **kwargs):
self.log(LogLevel[level], True, *args, **kwargs)
def logFunc_noPrefix(self, *args, **kwargs):
self.log(LogLevel[level], False, *args, **kwargs)
return (logFunc, logFunc_noPrefix)
class Logger:
def __init__(self, progname=None, logLevel=LogLevel.notice,
defaultOutputStream=sys.stdout, logFile=None):
self.progname = progname
self.logLevel = logLevel
self.defaultOutputStream = defaultOutputStream
self.logFile = logFile
def setLogFile(self, *args, **kwargs):
self.logFile = open(*args, **kwargs)
def log(self, level, printLogLevel, *args, **kwargs):
if printLogLevel and level >= LogLevel.warning and args:
args = [level.name.upper() + ": " + args[0]] + list(args[1:])
if level >= self.logLevel:
if (self.progname is not None) and args:
tArgs = [self.progname + ": " + args[0]] + list(args[1:])
else:
tArgs = args
kwargs["file"] = self.defaultOutputStream
print(*tArgs, **kwargs)
if self.logFile is not None:
kwargs["file"] = self.logFile
print(*args, **kwargs)
# Don't overload log() with too many tests or too much indirection for
# little use
def logToFile(self, *args, **kwargs):
kwargs["file"] = self.logFile
print(*args, **kwargs)
# NP functions are “no prefix” variants which never prepend the log level
# (otherwise, it is only prepended for warning and higher levels).
debug, debugNP = _logFuncFactory("debug")
info, infoNP = _logFuncFactory("info")
notice, noticeNP = _logFuncFactory("notice")
warning, warningNP = _logFuncFactory("warning")
error, errorNP = _logFuncFactory("error")
critical, criticalNP = _logFuncFactory("critical")
class DummyLogger(Logger):
def setLogFile(self, *args, **kwargs):
pass
def log(self, *args, **kwargs):
pass
def logToFile(self, *args, **kwargs):
pass

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# misc.py --- Miscellaneous classes and/or functions
# Copyright (C) 2015-2017 Florent Rougon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import enum
# Based on an example from the 'enum' documentation
class OrderedEnum(enum.Enum):
"""Base class for enumerations whose members can be ordered.
Contrary to enum.IntEnum, this class maintains normal enum.Enum
invariants, such as members not being comparable to members of other
enumerations (nor of any other class, actually).
"""
def __ge__(self, other):
if self.__class__ is other.__class__:
return self.value >= other.value
return NotImplemented
def __gt__(self, other):
if self.__class__ is other.__class__:
return self.value > other.value
return NotImplemented
def __le__(self, other):
if self.__class__ is other.__class__:
return self.value <= other.value
return NotImplemented
def __lt__(self, other):
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented
def __eq__(self, other):
if self.__class__ is other.__class__:
return self.value == other.value
return NotImplemented
def __ne__(self, other):
if self.__class__ is other.__class__:
return self.value != other.value
return NotImplemented
# Taken from <http://effbot.org/zone/element-lib.htm#prettyprint> and modified
# by Florent Rougon
def indentXmlTree(elem, level=0, basicOffset=2, lastChild=False):
def indentation(level):
return "\n" + level*basicOffset*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = indentation(level+1)
for e in elem[:-1]:
indentXmlTree(e, level+1, basicOffset, False)
if len(elem):
indentXmlTree(elem[-1], level+1, basicOffset, True)
if level and (not elem.tail or not elem.tail.strip()):
if lastChild:
elem.tail = indentation(level-1)
else:
elem.tail = indentation(level)