Initial commit: Core Teletext Editor functionality
This commit is contained in:
20
venv/lib/python3.12/site-packages/PyQt6/lupdate/__init__.py
Normal file
20
venv/lib/python3.12/site-packages/PyQt6/lupdate/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
# The public API.
|
||||
from .lupdate import lupdate
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,68 @@
|
||||
# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
from ..uic import UIFile
|
||||
|
||||
from .source_file import SourceFile
|
||||
from .translations import Context, Message
|
||||
from .user import User, UserException
|
||||
|
||||
|
||||
class DesignerSource(SourceFile, User):
|
||||
""" Encapsulate a Designer source file. """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Initialise the object. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Read the source file.
|
||||
self.progress("Reading {0}...".format(self.filename))
|
||||
|
||||
try:
|
||||
ui_file = UIFile(self.filename)
|
||||
except Exception as e:
|
||||
raise UserException(str(e))
|
||||
|
||||
if ui_file.widget is not None:
|
||||
context = Context(ui_file.class_name)
|
||||
|
||||
# Get each <string> element. Note that we don't support the
|
||||
# <stringlist> element which seems to provide defaults for the
|
||||
# attributes of any child <string> elements.
|
||||
for string_el in ui_file.widget.iter('string'):
|
||||
if string_el.get('notr', 'false') == 'true':
|
||||
continue
|
||||
|
||||
# This can be None or an empty string depending on the exact
|
||||
# XML.
|
||||
if not string_el.text:
|
||||
continue
|
||||
|
||||
message = Message(self.filename, 0, string_el.text,
|
||||
string_el.get('comment', ''), False)
|
||||
|
||||
extra_comment = string_el.get('extracomment')
|
||||
if extra_comment:
|
||||
message.embedded_comments.extra_comments.append(
|
||||
extra_comment)
|
||||
|
||||
context.messages.append(message)
|
||||
|
||||
if context.messages:
|
||||
self.contexts.append(context)
|
||||
98
venv/lib/python3.12/site-packages/PyQt6/lupdate/lupdate.py
Normal file
98
venv/lib/python3.12/site-packages/PyQt6/lupdate/lupdate.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
import fnmatch
|
||||
import os
|
||||
|
||||
from .designer_source import DesignerSource
|
||||
from .python_source import PythonSource
|
||||
from .translation_file import TranslationFile
|
||||
from .user import UserException
|
||||
|
||||
|
||||
def lupdate(sources, translation_files, no_obsolete=False, no_summary=True,
|
||||
verbose=False, excludes=None):
|
||||
""" Update a sequence of translation (.ts) files from a sequence of Python
|
||||
source (.py) files, Designer source (.ui) files or directories containing
|
||||
source files.
|
||||
"""
|
||||
|
||||
if excludes is None:
|
||||
excludes = ()
|
||||
|
||||
# Read the .ts files.
|
||||
translations = [TranslationFile(ts, no_obsolete=no_obsolete,
|
||||
no_summary=no_summary, verbose=verbose)
|
||||
for ts in translation_files]
|
||||
|
||||
# Read the sources.
|
||||
source_files = []
|
||||
for source in sources:
|
||||
if os.path.isdir(source):
|
||||
for dirpath, dirnames, filenames in os.walk(source):
|
||||
_remove_excludes(dirnames, excludes)
|
||||
_remove_excludes(filenames, excludes)
|
||||
|
||||
for fn in filenames:
|
||||
filename = os.path.join(dirpath, fn)
|
||||
|
||||
if filename.endswith('.py'):
|
||||
source_files.append(
|
||||
PythonSource(filename=filename,
|
||||
verbose=verbose))
|
||||
|
||||
elif filename.endswith('.ui'):
|
||||
source_files.append(
|
||||
DesignerSource(filename=filename,
|
||||
verbose=verbose))
|
||||
|
||||
elif verbose:
|
||||
print("Ignoring", filename)
|
||||
|
||||
elif source.endswith('.py'):
|
||||
source_files.append(
|
||||
PythonSource(filename=source, verbose=verbose))
|
||||
|
||||
elif source.endswith('.ui'):
|
||||
source_files.append(
|
||||
DesignerSource(filename=source, verbose=verbose))
|
||||
|
||||
else:
|
||||
raise UserException(
|
||||
"{0} must be a directory or a .py or a .ui file".format(
|
||||
source))
|
||||
|
||||
# Update each translation for each source.
|
||||
for t in translations:
|
||||
for s in source_files:
|
||||
t.update(s)
|
||||
|
||||
t.write()
|
||||
|
||||
|
||||
def _remove_excludes(names, excludes):
|
||||
""" Remove all implicitly and explicitly excluded names from a list. """
|
||||
|
||||
for name in list(names):
|
||||
if name.startswith('.'):
|
||||
names.remove(name)
|
||||
else:
|
||||
for exclude in excludes:
|
||||
if fnmatch.fnmatch(name, exclude):
|
||||
names.remove(name)
|
||||
break
|
||||
87
venv/lib/python3.12/site-packages/PyQt6/lupdate/pylupdate.py
Normal file
87
venv/lib/python3.12/site-packages/PyQt6/lupdate/pylupdate.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from .lupdate import lupdate
|
||||
|
||||
|
||||
def main():
|
||||
""" Update a .ts file from a .py file. """
|
||||
|
||||
import argparse
|
||||
|
||||
from PyQt6.QtCore import PYQT_VERSION_STR
|
||||
|
||||
from .user import UserException
|
||||
|
||||
# The program name.
|
||||
PROGRAM_NAME = 'pylupdate6'
|
||||
|
||||
# Parse the command line.
|
||||
parser = argparse.ArgumentParser(prog=PROGRAM_NAME,
|
||||
description="Python Language Update Tool")
|
||||
|
||||
parser.add_argument('-V', '--version', action='version',
|
||||
version=PYQT_VERSION_STR)
|
||||
parser.add_argument('--exclude', action='append', metavar="PATTERN",
|
||||
help="exclude matching files when reading a directory")
|
||||
parser.add_argument('--no-obsolete', '-no-obsolete', action='store_true',
|
||||
help="remove any obsolete translated messages")
|
||||
parser.add_argument('--no-summary', action='store_true',
|
||||
help="suppress the summary")
|
||||
parser.add_argument('--ts', '-ts', action='append', metavar="FILE",
|
||||
required=True,
|
||||
help="a .ts file to update or create")
|
||||
parser.add_argument('--verbose', action='store_true',
|
||||
help="show progress messages")
|
||||
parser.add_argument('file', nargs='+',
|
||||
help="the .py or .ui file, or directory to be read")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Update the translation files.
|
||||
try:
|
||||
lupdate(args.file, args.ts, args.no_obsolete, args.no_summary,
|
||||
args.verbose, args.exclude)
|
||||
except UserException as e:
|
||||
print("{0}: {1}".format(PROGRAM_NAME, e), file=sys.stderr)
|
||||
return 1
|
||||
except:
|
||||
if args.verbose:
|
||||
import traceback
|
||||
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
else:
|
||||
print("""An unexpected error occurred.
|
||||
Check that you are using the latest version of {name} and send an error
|
||||
report to the PyQt mailing list and include the following information:
|
||||
|
||||
- the version of {name} ({version})
|
||||
- the .py or .ui file that caused the error (as an attachment)
|
||||
- the verbose output of {name} (use the --verbose flag when calling
|
||||
{name})""".format(name=PROGRAM_NAME, version=PYQT_VERSION_STR),
|
||||
file=sys.stderr)
|
||||
|
||||
return 2
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
358
venv/lib/python3.12/site-packages/PyQt6/lupdate/python_source.py
Normal file
358
venv/lib/python3.12/site-packages/PyQt6/lupdate/python_source.py
Normal file
@@ -0,0 +1,358 @@
|
||||
# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
import ast
|
||||
import re
|
||||
import tokenize
|
||||
|
||||
from .source_file import SourceFile
|
||||
from .translations import Context, EmbeddedComments, Message
|
||||
from .user import User, UserException
|
||||
|
||||
|
||||
class PythonSource(SourceFile, User):
|
||||
""" Encapsulate a Python source file. """
|
||||
|
||||
# The regular expression to extract a PEP 263 encoding.
|
||||
_PEP_263 = re.compile(rb'^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Initialise the object. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Read the source file.
|
||||
self.progress("Reading {0}...".format(self.filename))
|
||||
with open(self.filename, 'rb') as f:
|
||||
source = f.read()
|
||||
|
||||
# Implement universal newlines.
|
||||
source = source.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
|
||||
|
||||
# Try and extract a PEP 263 encoding.
|
||||
encoding = 'UTF-8'
|
||||
|
||||
for line_nr, line in enumerate(source.split(b'\n')):
|
||||
if line_nr > 1:
|
||||
break
|
||||
|
||||
match = re.match(self._PEP_263, line)
|
||||
if match:
|
||||
encoding = match.group(1).decode('ascii')
|
||||
break
|
||||
|
||||
# Decode the source according to the encoding.
|
||||
try:
|
||||
source = source.decode(encoding)
|
||||
except LookupError:
|
||||
raise UserException("Unsupported encoding '{0}'".format(encoding))
|
||||
|
||||
# Parse the source file.
|
||||
self.progress("Parsing {0}...".format(self.filename))
|
||||
|
||||
try:
|
||||
tree = ast.parse(source, filename=self.filename)
|
||||
except SyntaxError as e:
|
||||
raise UserException(
|
||||
"Invalid syntax at line {0} of {1}:\n{2}".format(
|
||||
e.lineno, e.filename, e.text.rstrip()))
|
||||
|
||||
# Look for translation contexts and their contents.
|
||||
visitor = Visitor(self)
|
||||
visitor.visit(tree)
|
||||
|
||||
# Read the file again as a sequence of tokens so that we see the
|
||||
# comments.
|
||||
with open(self.filename, 'rb') as f:
|
||||
current = None
|
||||
|
||||
for token in tokenize.tokenize(f.readline):
|
||||
if token.type == tokenize.COMMENT:
|
||||
# See if it is an embedded comment.
|
||||
parts = token.string.split(' ', maxsplit=1)
|
||||
if len(parts) == 2:
|
||||
if parts[0] == '#:':
|
||||
if current is None:
|
||||
current = EmbeddedComments()
|
||||
|
||||
current.extra_comments.append(parts[1])
|
||||
elif parts[0] == '#=':
|
||||
if current is None:
|
||||
current = EmbeddedComments()
|
||||
|
||||
current.message_id = parts[1]
|
||||
elif parts[0] == '#~':
|
||||
parts = parts[1].split(' ', maxsplit=1)
|
||||
if len(parts) == 1:
|
||||
parts.append('')
|
||||
|
||||
if current is None:
|
||||
current = EmbeddedComments()
|
||||
|
||||
current.extras.append(parts)
|
||||
|
||||
elif token.type == tokenize.NL:
|
||||
continue
|
||||
|
||||
elif current is not None:
|
||||
# Associate the embedded comment with the line containing
|
||||
# this token.
|
||||
line_nr = token.start[0]
|
||||
|
||||
# See if there is a message on that line.
|
||||
for context in self.contexts:
|
||||
for message in context.messages:
|
||||
if message.line_nr == line_nr:
|
||||
break
|
||||
else:
|
||||
message = None
|
||||
|
||||
if message is not None:
|
||||
message.embedded_comments = current
|
||||
break
|
||||
|
||||
current = None
|
||||
|
||||
|
||||
class Visitor(ast.NodeVisitor):
|
||||
""" A visitor that extracts translation contexts. """
|
||||
|
||||
def __init__(self, source):
|
||||
""" Initialise the visitor. """
|
||||
|
||||
self._source = source
|
||||
self._context_stack = []
|
||||
|
||||
super().__init__()
|
||||
|
||||
def visit_Call(self, node):
|
||||
""" Visit a call. """
|
||||
|
||||
# Parse the arguments if a translation function is being called.
|
||||
call_args = None
|
||||
|
||||
if isinstance(node.func, ast.Attribute):
|
||||
name = node.func.attr
|
||||
|
||||
elif isinstance(node.func, ast.Name):
|
||||
name = node.func.id
|
||||
|
||||
if name == 'QT_TR_NOOP':
|
||||
call_args = self._parse_QT_TR_NOOP(node)
|
||||
elif name == 'QT_TRANSLATE_NOOP':
|
||||
call_args = self._parse_QT_TRANSLATE_NOOP(node)
|
||||
else:
|
||||
name = ''
|
||||
|
||||
# Allow these to be either methods or functions.
|
||||
if name == 'tr':
|
||||
call_args = self._parse_tr(node)
|
||||
elif name == 'translate':
|
||||
call_args = self._parse_translate(node)
|
||||
|
||||
# Update the context if the arguments are usable.
|
||||
if call_args is not None and call_args.source != '':
|
||||
call_args.context.messages.append(
|
||||
Message(self._source.filename, node.lineno,
|
||||
call_args.source, call_args.disambiguation,
|
||||
(call_args.numerus)))
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
""" Visit a class. """
|
||||
|
||||
try:
|
||||
name = self._context_stack[-1].name + '.' + node.name
|
||||
except IndexError:
|
||||
name = node.name
|
||||
|
||||
self._context_stack.append(Context(name))
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
context = self._context_stack.pop()
|
||||
|
||||
if context.messages:
|
||||
self._source.contexts.append(context)
|
||||
|
||||
def _get_current_context(self):
|
||||
""" Return the current Context object if there is one. """
|
||||
|
||||
return self._context_stack[-1] if self._context_stack else None
|
||||
|
||||
@classmethod
|
||||
def _get_first_str(cls, args):
|
||||
""" Get the first of a list of arguments as a str. """
|
||||
|
||||
# Check that there is at least one argument.
|
||||
if not args:
|
||||
return None
|
||||
|
||||
return cls._get_str(args[0])
|
||||
|
||||
def _get_or_create_context(self, name):
|
||||
""" Return the Context object for a name, creating it if necessary. """
|
||||
|
||||
for context in self._source.contexts:
|
||||
if context.name == name:
|
||||
return context
|
||||
|
||||
context = Context(name)
|
||||
self._source.contexts.append(context)
|
||||
|
||||
return context
|
||||
|
||||
@staticmethod
|
||||
def _get_str(node, allow_none=False):
|
||||
""" Return the str from a node or None if it wasn't an appropriate
|
||||
node.
|
||||
"""
|
||||
|
||||
if isinstance(node, ast.Constant):
|
||||
if isinstance(node.value, str):
|
||||
return node.value
|
||||
|
||||
if allow_none and node.value is None:
|
||||
return ''
|
||||
|
||||
return None
|
||||
|
||||
def _parse_QT_TR_NOOP(self, node):
|
||||
""" Parse the arguments to QT_TR_NOOP(). """
|
||||
|
||||
# Ignore unless there is a current context.
|
||||
context = self._get_current_context()
|
||||
if context is None:
|
||||
return None
|
||||
|
||||
call_args = self._parse_noop_without_context(node.args, node.keywords)
|
||||
if call_args is None:
|
||||
return None
|
||||
|
||||
call_args.context = context
|
||||
|
||||
return call_args
|
||||
|
||||
def _parse_QT_TRANSLATE_NOOP(self, node):
|
||||
""" Parse the arguments to QT_TRANSLATE_NOOP(). """
|
||||
|
||||
# Get the context.
|
||||
name = self._get_first_str(node.args)
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
call_args = self._parse_noop_without_context(node.args[1:],
|
||||
node.keywords)
|
||||
if call_args is None:
|
||||
return None
|
||||
|
||||
call_args.context = self._get_or_create_context(name)
|
||||
|
||||
return call_args
|
||||
|
||||
def _parse_tr(self, node):
|
||||
""" Parse the arguments to tr(). """
|
||||
|
||||
# Ignore unless there is a current context.
|
||||
context = self._get_current_context()
|
||||
if context is None:
|
||||
return None
|
||||
|
||||
call_args = self._parse_without_context(node.args, node.keywords)
|
||||
if call_args is None:
|
||||
return None
|
||||
|
||||
call_args.context = context
|
||||
|
||||
return call_args
|
||||
|
||||
def _parse_translate(self, node):
|
||||
""" Parse the arguments to translate(). """
|
||||
|
||||
# Get the context.
|
||||
name = self._get_first_str(node.args)
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
call_args = self._parse_without_context(node.args[1:], node.keywords)
|
||||
if call_args is None:
|
||||
return None
|
||||
|
||||
call_args.context = self._get_or_create_context(name)
|
||||
|
||||
return call_args
|
||||
|
||||
def _parse_without_context(self, args, keywords):
|
||||
""" Parse arguments for a message source and optional disambiguation
|
||||
and n.
|
||||
"""
|
||||
|
||||
# The source is required.
|
||||
source = self._get_first_str(args)
|
||||
if source is None:
|
||||
return None
|
||||
|
||||
if len(args) > 1:
|
||||
disambiguation = self._get_str(args[1], allow_none=True)
|
||||
else:
|
||||
for kw in keywords:
|
||||
if kw.arg == 'disambiguation':
|
||||
disambiguation = self._get_str(kw.value, allow_none=True)
|
||||
break
|
||||
else:
|
||||
disambiguation = ''
|
||||
|
||||
# Ignore if the disambiguation is specified but isn't a string.
|
||||
if disambiguation is None:
|
||||
return None
|
||||
|
||||
if len(args) > 2:
|
||||
numerus = True
|
||||
else:
|
||||
numerus = 'n' in keywords
|
||||
|
||||
if len(args) > 3:
|
||||
return None
|
||||
|
||||
return CallArguments(source, disambiguation, numerus)
|
||||
|
||||
def _parse_noop_without_context(self, args, keywords):
|
||||
""" Parse arguments for a message source. """
|
||||
|
||||
# There must be exactly one positional argument.
|
||||
if len(args) != 1 or len(keywords) != 0:
|
||||
return None
|
||||
|
||||
source = self._get_str(args[0])
|
||||
if source is None:
|
||||
return None
|
||||
|
||||
return CallArguments(source)
|
||||
|
||||
|
||||
class CallArguments:
|
||||
""" Encapsulate the possible arguments of a translation function. """
|
||||
|
||||
def __init__(self, source, disambiguation='', numerus=False):
|
||||
""" Initialise the object. """
|
||||
|
||||
self.context = None
|
||||
self.source = source
|
||||
self.disambiguation = disambiguation
|
||||
self.numerus = numerus
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
class SourceFile:
|
||||
""" The base class for any source file that provides translation contexts.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, **kwargs):
|
||||
""" Initialise the object. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.filename = filename
|
||||
self.contexts = []
|
||||
@@ -0,0 +1,414 @@
|
||||
# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
import os
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from .user import User, UserException
|
||||
|
||||
|
||||
class TranslationFile(User):
|
||||
""" Encapsulate a translation file. """
|
||||
|
||||
def __init__(self, ts_file, no_obsolete, no_summary, **kwargs):
|
||||
""" Initialise the translation file. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if os.path.isfile(ts_file):
|
||||
self.progress("Reading {0}...".format(ts_file))
|
||||
|
||||
try:
|
||||
self._root = ElementTree.parse(ts_file).getroot()
|
||||
except Exception as e:
|
||||
raise UserException(
|
||||
"{}: {}: {}".format(ts_file,
|
||||
"invalid translation file", str(e)))
|
||||
else:
|
||||
self._root = ElementTree.fromstring(_EMPTY_TS)
|
||||
|
||||
self._ts_file = ts_file
|
||||
self._no_obsolete = no_obsolete
|
||||
self._no_summary = no_summary
|
||||
self._updated_contexts = {}
|
||||
|
||||
# Create a dict of contexts keyed by the context name and having the
|
||||
# list of message elements as the value.
|
||||
self._contexts = {}
|
||||
|
||||
# Also create a dict of existing translations so that they can be
|
||||
# re-used.
|
||||
self._translations = {}
|
||||
|
||||
context_els = []
|
||||
for context_el in self._root:
|
||||
if context_el.tag != 'context':
|
||||
continue
|
||||
|
||||
context_els.append(context_el)
|
||||
|
||||
name = ''
|
||||
message_els = []
|
||||
|
||||
for el in context_el:
|
||||
if el.tag == 'name':
|
||||
name = el.text
|
||||
elif el.tag == 'message':
|
||||
message_els.append(el)
|
||||
|
||||
if name:
|
||||
self._contexts[name] = message_els
|
||||
|
||||
for message_el in message_els:
|
||||
source_el = message_el.find('source')
|
||||
if source_el is None or not source_el.text:
|
||||
continue
|
||||
|
||||
translation_el = message_el.find('translation')
|
||||
if translation_el is None or not translation_el.text:
|
||||
continue
|
||||
|
||||
self._translations[source_el.text] = translation_el.text
|
||||
|
||||
# Remove the context elements but keep everything else in the root
|
||||
# (probably set by Linguist).
|
||||
for context_el in context_els:
|
||||
self._root.remove(context_el)
|
||||
|
||||
# Clear the summary statistics.
|
||||
self._nr_new = 0
|
||||
self._nr_new_duplicates = 0
|
||||
self._nr_new_using_existing_translation = 0
|
||||
self._nr_existing = 0
|
||||
self._nr_kept_obsolete = 0
|
||||
self._nr_discarded_obsolete = 0
|
||||
self._nr_discarded_untranslated = 0
|
||||
|
||||
# Remember all new messages so we can make the summary less confusing
|
||||
# than it otherwise might be.
|
||||
self._new_message_els = []
|
||||
|
||||
def update(self, source):
|
||||
""" Update the translation file from a SourceFile object. """
|
||||
|
||||
self.progress(
|
||||
"Updating {0} from {1}...".format(self._ts_file,
|
||||
source.filename))
|
||||
|
||||
for context in source.contexts:
|
||||
# Get the messages that we already know about for this context.
|
||||
try:
|
||||
message_els = self._contexts[context.name]
|
||||
except KeyError:
|
||||
message_els = []
|
||||
|
||||
# Get the messages that have already been updated.
|
||||
updated_message_els = self._get_updated_message_els(context.name)
|
||||
|
||||
for message in context.messages:
|
||||
message_el = self._find_message(message, message_els)
|
||||
|
||||
if message_el is not None:
|
||||
# Move the message to the updated list.
|
||||
message_els.remove(message_el)
|
||||
self._add_message_el(message_el, updated_message_els)
|
||||
else:
|
||||
# See if this is a new message. If not then we just have
|
||||
# another location for an existing message.
|
||||
message_el = self._find_message(message,
|
||||
updated_message_els)
|
||||
|
||||
if message_el is None:
|
||||
message_el = self._make_message_el(message)
|
||||
updated_message_els.append(message_el)
|
||||
|
||||
self.progress(
|
||||
"Added new message '{0}'".format(
|
||||
self.pretty(message.source)))
|
||||
self._nr_new += 1
|
||||
else:
|
||||
self.progress(
|
||||
"Updated message '{0}'".format(
|
||||
self.pretty(message.source)))
|
||||
|
||||
# Go through any translations making sure they are not
|
||||
# 'vanished' which might happen if we have restored a
|
||||
# previously obsolete message.
|
||||
for translation_el in message_el.findall('translation'):
|
||||
if translation_el.get('type') == 'vanished':
|
||||
if translation_el.text:
|
||||
del translation_el.attrib['type']
|
||||
else:
|
||||
translation_el.set('type', 'unfinished')
|
||||
|
||||
# Don't count another copy of a new message as an existing
|
||||
# one.
|
||||
if message_el in self._new_message_els:
|
||||
self._nr_new_duplicates += 1
|
||||
else:
|
||||
self._nr_existing += 1
|
||||
|
||||
message_el.insert(0, self._make_location_el(message))
|
||||
|
||||
def write(self):
|
||||
""" Write the translation file back to the filesystem. """
|
||||
|
||||
# If we are keeping obsolete messages then add them to the updated
|
||||
# message elements list.
|
||||
for name, message_els in self._contexts.items():
|
||||
updated_message_els = None
|
||||
|
||||
for message_el in message_els:
|
||||
source = self.pretty(message_el.find('source').text)
|
||||
|
||||
translation_el = message_el.find('translation')
|
||||
if translation_el is not None and translation_el.text:
|
||||
if self._no_obsolete:
|
||||
self.progress(
|
||||
"Discarded obsolete message '{0}'".format(
|
||||
source))
|
||||
self._nr_discarded_obsolete += 1
|
||||
else:
|
||||
translation_el.set('type', 'vanished')
|
||||
|
||||
if updated_message_els is None:
|
||||
updated_message_els = self._get_updated_message_els(
|
||||
name)
|
||||
|
||||
self._add_message_el(message_el, updated_message_els)
|
||||
|
||||
self.progress(
|
||||
"Kept obsolete message '{0}'".format(source))
|
||||
self._nr_kept_obsolete += 1
|
||||
else:
|
||||
self.progress(
|
||||
"Discarded untranslated message '{0}'".format(
|
||||
source))
|
||||
self._nr_discarded_untranslated += 1
|
||||
|
||||
# Created the sorted context elements.
|
||||
for name in sorted(self._updated_contexts.keys()):
|
||||
context_el = ElementTree.Element('context')
|
||||
|
||||
name_el = ElementTree.Element('name')
|
||||
name_el.text = name
|
||||
context_el.append(name_el)
|
||||
|
||||
context_el.extend(self._updated_contexts[name])
|
||||
|
||||
self._root.append(context_el)
|
||||
|
||||
self.progress("Writing {0}...".format(self._ts_file))
|
||||
|
||||
# Replicate the indentation used by Qt Linguist. Note that there are
|
||||
# still differences in the way elements are closed.
|
||||
for el in self._root:
|
||||
ElementTree.indent(el, space=' ')
|
||||
|
||||
with open(self._ts_file, 'w', encoding='utf-8', newline='\n') as f:
|
||||
f.write('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
f.write('<!DOCTYPE TS>\n')
|
||||
ElementTree.ElementTree(self._root).write(f, encoding='unicode')
|
||||
f.write('\n')
|
||||
|
||||
if not self._no_summary:
|
||||
self._summary()
|
||||
|
||||
@staticmethod
|
||||
def _add_message_el(message_el, updated_message_els):
|
||||
""" Add a message element to a list of updated message elements. """
|
||||
|
||||
# Remove all the location elements.
|
||||
for location_el in message_el.findall('location'):
|
||||
message_el.remove(location_el)
|
||||
|
||||
# Add the message to the updated list.
|
||||
updated_message_els.append(message_el)
|
||||
|
||||
@classmethod
|
||||
def _find_message(cls, message, message_els):
|
||||
""" Return the message element for a message from a list. """
|
||||
|
||||
for message_el in message_els:
|
||||
source = ''
|
||||
comment = ''
|
||||
extra_comment = ''
|
||||
extras = []
|
||||
|
||||
# Extract the data from the element.
|
||||
for el in message_el:
|
||||
if el.tag == 'source':
|
||||
source = el.text
|
||||
elif el.tag == 'comment':
|
||||
comment = el.text
|
||||
elif el.tag == 'extracomment':
|
||||
extra_comment = el.text
|
||||
elif el.tag.startswith('extra-'):
|
||||
extras.append([el.tag[6:], el.text])
|
||||
|
||||
# Compare with the message.
|
||||
if source != message.source:
|
||||
continue
|
||||
|
||||
if comment != message.comment:
|
||||
continue
|
||||
|
||||
if extra_comment != cls._get_message_extra_comments(message):
|
||||
continue
|
||||
|
||||
if extras != message.embedded_comments.extras:
|
||||
continue
|
||||
|
||||
return message_el
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_message_extra_comments(message):
|
||||
""" Return a message's extra comments as they appear in a .ts file. """
|
||||
|
||||
return ' '.join(message.embedded_comments.extra_comments)
|
||||
|
||||
def _get_updated_message_els(self, name):
|
||||
""" Return the list of updated message elements for a context. """
|
||||
|
||||
try:
|
||||
updated_message_els = self._updated_contexts[name]
|
||||
except KeyError:
|
||||
updated_message_els = []
|
||||
self._updated_contexts[name] = updated_message_els
|
||||
|
||||
return updated_message_els
|
||||
|
||||
def _make_location_el(self, message):
|
||||
""" Return a 'location' element. """
|
||||
|
||||
return ElementTree.Element('location',
|
||||
filename=os.path.relpath(message.filename,
|
||||
start=os.path.dirname(os.path.abspath(self._ts_file))),
|
||||
line=str(message.line_nr))
|
||||
|
||||
def _make_message_el(self, message):
|
||||
""" Return a 'message' element. """
|
||||
|
||||
attrs = {}
|
||||
|
||||
if message.embedded_comments.message_id:
|
||||
attrs['id'] = message.embedded_comments.message_id
|
||||
|
||||
if message.numerus:
|
||||
attrs['numerus'] = 'yes'
|
||||
|
||||
message_el = ElementTree.Element('message', attrs)
|
||||
|
||||
source_el = ElementTree.Element('source')
|
||||
source_el.text = message.source
|
||||
message_el.append(source_el)
|
||||
|
||||
if message.comment:
|
||||
comment_el = ElementTree.Element('comment')
|
||||
comment_el.text = message.comment
|
||||
message_el.append(comment_el)
|
||||
|
||||
if message.embedded_comments.extra_comments:
|
||||
extracomment_el = ElementTree.Element('extracomment')
|
||||
extracomment_el.text = self._get_message_extra_comments(message)
|
||||
message_el.append(extracomment_el)
|
||||
|
||||
translation_el = ElementTree.Element('translation',
|
||||
type='unfinished')
|
||||
|
||||
# Try and find another message with the same source and use its
|
||||
# translation if it has one.
|
||||
translation = self._translations.get(message.source)
|
||||
if translation:
|
||||
translation_el.text = translation
|
||||
|
||||
self.progress(
|
||||
"Reused existing translation for '{0}'".format(
|
||||
self.pretty(message.source)))
|
||||
self._nr_new_using_existing_translation += 1
|
||||
|
||||
if message.numerus:
|
||||
translation_el.append(ElementTree.Element(
|
||||
'numerusform'))
|
||||
|
||||
message_el.append(translation_el)
|
||||
|
||||
for field, value in message.embedded_comments.extras:
|
||||
el = ElementTree.Element('extra-' + field)
|
||||
el.text = value
|
||||
message_el.append(el)
|
||||
|
||||
self._new_message_els.append(message_el)
|
||||
|
||||
return message_el
|
||||
|
||||
def _summary(self):
|
||||
""" Display the summary of changes to the user. """
|
||||
|
||||
summary_lines = []
|
||||
|
||||
# Display a line of the summary and the heading if not already done.
|
||||
def summary(line):
|
||||
nonlocal summary_lines
|
||||
|
||||
if not summary_lines:
|
||||
summary_lines.append(
|
||||
"Summary of changes to {ts}:".format(ts=self._ts_file))
|
||||
|
||||
summary_lines.append(" " + line)
|
||||
|
||||
if self._nr_new:
|
||||
if self._nr_new_duplicates:
|
||||
summary("{0} new messages were added (and {1} duplicates)".format(
|
||||
self._nr_new, self._nr_new_duplicates))
|
||||
else:
|
||||
summary("{0} new messages were added".format(self._nr_new))
|
||||
|
||||
if self._nr_new_using_existing_translation:
|
||||
summary("{0} messages reused existing translations".format(
|
||||
self._nr_new_using_existing_translation))
|
||||
|
||||
if self._nr_existing:
|
||||
summary("{0} existing messages were found".format(
|
||||
self._nr_existing))
|
||||
|
||||
if self._nr_kept_obsolete:
|
||||
summary("{0} obsolete messages were kept".format(
|
||||
self._nr_kept_obsolete))
|
||||
|
||||
if self._nr_discarded_obsolete:
|
||||
summary("{0} obsolete messages were discarded".format(
|
||||
self._nr_discarded_obsolete))
|
||||
|
||||
if self._nr_discarded_untranslated:
|
||||
summary("{0} untranslated messages were discarded".format(
|
||||
self._nr_discarded_untranslated))
|
||||
|
||||
if not summary_lines:
|
||||
summary_lines.append("{ts} was unchanged".format(ts=self._ts_file))
|
||||
|
||||
print(os.linesep.join(summary_lines))
|
||||
|
||||
|
||||
# The XML of an empty .ts file. This is what a current lupdate will create
|
||||
# with an empty C++ source file.
|
||||
_EMPTY_TS = '''<TS version="2.1">
|
||||
</TS>
|
||||
'''
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
class Context:
|
||||
""" Encapsulate a message context. """
|
||||
|
||||
def __init__(self, name):
|
||||
""" Initialise the context. """
|
||||
|
||||
self.name = name
|
||||
self.messages = []
|
||||
|
||||
|
||||
class EmbeddedComments:
|
||||
""" Encapsulate information for a translator embedded in comments. """
|
||||
|
||||
def __init__(self):
|
||||
""" Initialise the object. """
|
||||
|
||||
self.message_id = ''
|
||||
self.extra_comments = []
|
||||
self.extras = []
|
||||
|
||||
|
||||
class Message:
|
||||
""" Encapsulate a message. """
|
||||
|
||||
def __init__(self, filename, line_nr, source, comment, numerus):
|
||||
""" Initialise the message. """
|
||||
|
||||
self.filename = filename
|
||||
self.line_nr = line_nr
|
||||
self.source = source
|
||||
self.comment = comment
|
||||
self.numerus = numerus
|
||||
self.embedded_comments = EmbeddedComments()
|
||||
47
venv/lib/python3.12/site-packages/PyQt6/lupdate/user.py
Normal file
47
venv/lib/python3.12/site-packages/PyQt6/lupdate/user.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
class UserException(Exception):
|
||||
""" Encapsulate an exception ultimate caused by the user. """
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class User:
|
||||
""" A mixin that provides methods for communicating with the user. """
|
||||
|
||||
def __init__(self, verbose, **kwargs):
|
||||
""" Initialise the object. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._verbose = verbose
|
||||
|
||||
@staticmethod
|
||||
def pretty(text):
|
||||
""" Returns a pretty-fied version of some text suitable for displaying
|
||||
to the user.
|
||||
"""
|
||||
|
||||
return text.replace('\n', '\\n')
|
||||
|
||||
def progress(self, message):
|
||||
""" Display a progress message. """
|
||||
|
||||
if self._verbose:
|
||||
print(message)
|
||||
Reference in New Issue
Block a user