I found a label whose keyboard accelerator wasn't connected to anything so I wrote a test for that and I found a few more.
From: David Shea dshea@redhat.com
--- tests/glade/check_accelerators.py | 2 +- tests/glade/check_markup.py | 4 +- tests/lib/pangocheck.py | 126 -------------------------------------- tests/lib/translatepo.py | 85 ------------------------- 4 files changed, 3 insertions(+), 214 deletions(-) delete mode 100644 tests/lib/pangocheck.py delete mode 100644 tests/lib/translatepo.py
diff --git a/tests/glade/check_accelerators.py b/tests/glade/check_accelerators.py index ab87ec1..dcf87e2 100755 --- a/tests/glade/check_accelerators.py +++ b/tests/glade/check_accelerators.py @@ -30,7 +30,7 @@ # Import translation methods if needed if ('-t' in sys.argv) or ('--translate' in sys.argv): try: - from translatepo import translate_all + from pocketlint.translatepo import translate_all except ImportError: print("Unable to load po translation module") sys.exit(99) diff --git a/tests/glade/check_markup.py b/tests/glade/check_markup.py index ba639da..3d747d2 100755 --- a/tests/glade/check_markup.py +++ b/tests/glade/check_markup.py @@ -32,12 +32,12 @@ # Import translation methods if needed if ('-t' in sys.argv) or ('--translate' in sys.argv): try: - from translatepo import translate_all + from pocketlint.translatepo import translate_all except ImportError: print("Unable to load po translation module") sys.exit(99)
-from pangocheck import markup_nodes, markup_match, markup_necessary +from pocketlint.pangocheck import markup_nodes, markup_match, markup_necessary
try: from lxml import etree diff --git a/tests/lib/pangocheck.py b/tests/lib/pangocheck.py deleted file mode 100644 index 6944974..0000000 --- a/tests/lib/pangocheck.py +++ /dev/null @@ -1,126 +0,0 @@ -# -# pangocheck.py: data and methods for checking pango markup strings -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: David Shea dshea@redhat.com - -import re -from collections import Counter - -__all__ = ["markup_nodes", "is_markup", "markup_match"] - -# "a" isn't actually pango markup, but GtkLabel uses it -markup_nodes = ["markup", "a", "b", "big", "i", "s", "span", "sub", "sup", "small", "tt", "u"] - -# Check to see if a string looks like Pango markup, no validation -def is_markup(test_string): - return any(re.search(r'<\s*%s(\s|>)' % node_type, test_string) - for node_type in markup_nodes) - -# Verify that the translation of a markup string looks more or less like the original -def markup_match(orig_markup, xlated_markup): - # Look for tags. Create a count of each kind of tag and a list of attributes. - # "Don't parse XML with regular expressions" I can hear you saying, but we're - # not trying to match elements, just pull tag-like substrings out of the string. - # Figuring out if tags are closed or in the right order is someone else's job. - def _parse_markup(markup_string): - name_count = Counter() - attr_count = Counter() - - for tag in re.findall(r'<[^>]*>', markup_string): - # Treat everything up to the first space, / or > as the element name - (name, rest) = re.match(r'<([^\s/>]*)(.*)>', tag).groups() - name_count[name] += 1 - - # Strip the / from the rest of the tag, if present - if rest.endswith('/'): - rest = rest[:-1] - - # Make a list of attributes that need to be contained in the other string - attr_count.update(rest.split()) - - return (name_count, attr_count) - - (name_count1, attr_count1) = _parse_markup(orig_markup) - (name_count2, attr_count2) = _parse_markup(xlated_markup) - - name_list1 = sorted(name_count1.elements()) - name_list2 = sorted(name_count2.elements()) - attr_list1 = sorted(attr_count1.elements()) - attr_list2 = sorted(attr_count2.elements()) - - return (name_list1 == name_list2) and (attr_list1 == attr_list2) - -# Check that the markup is needed at all. -# The input is a parsed ElementTree of the string '<markup>pango markup goes here</markup>' -# The markup is unnecessary if the only markup in the string surrounds the entire rest of -# the string, meaning that the pango attributes apply to the entire string, and thus -# could be expressed using attribute lists. For example, strings like: -# <b>Bold text</b> -# or -# <span foreground="grey"><i>colorful</i></span> -# but not strings like: -# <span size="small">This string contains <b>internal</b> markup</span> -# that contain markup that must be passed to the translators. -# -# This function returns True if the markup is necessary and False if the markup -# can be discarded and expressed as attribute lists. -def markup_necessary(markup_tree): - # If the element has no children at all, there is no markup inside and the - # markup is unnecessary. - if not len(markup_tree): - return False - - # If there is more than one child, the markup is necessary - if len(markup_tree) > 1: - return True - - # QUICK NOTE FOR PEOPLE EXPECTING ElementTree TO ACT KINDA LIKE DOM 'CUZ LOL - # ElementTree is kind of weird with respect to handling multiple text children - # of an Element node. element.text is the text leading up to the first element - # child, and element[child_idx].tail is the text following the child node that - # is actually a child of element but isn't a property of element because Python - # is crazy. - # - # A string like "<markup>word1<i>word2</i>word3<empty/>word4</markup>" will result in - # tree == <Element 'markup' ...> - # tree.text == 'word1' - # tree[0] == <Element 'i' ...> - # tree[0].text == 'word2' - # tree[0].tail == 'word3' - # tree[1] == <Element 'empty' ...> - # tree[1].text == None - # tree[1].text == 'word4' - # - # So elements that contain text before a child markup element will have - # element.text is not None. Elements that have text after a child element - # will have .tail on that child set to not None. - - # If .text is set, there is text before the child node, as in - # <span>text <b>child</b></span> - # and the markup is necessary - if markup_tree.text: - return True - - # If the child (we already know there's only one) has .tail set, then - # there is text between the close of the child and the end of the element - # and the markup is necessary - if markup_tree[0].tail: - return True - - # Recurse on the child node - return markup_necessary(markup_tree[0]) diff --git a/tests/lib/translatepo.py b/tests/lib/translatepo.py deleted file mode 100644 index 56b6f01..0000000 --- a/tests/lib/translatepo.py +++ /dev/null @@ -1,85 +0,0 @@ -# -# translatepo.py: translate strings from data in .po files -# -# Copyright (C) 2013-2014 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: David Shea dshea@redhat.com - -# This module is helpful for testing translated data. The input is the .po files -# in the po directory, so no gettext compilation or LC_MESSAGES directories are -# necessary. - -# Ignore any interruptible calls -# pylint: disable=interruptible-system-call - -import os -import locale -import re - -try: - import polib -except ImportError: - print("You need to install the python-polib package to read translations") - raise - -class PODict(object): - def __init__(self, filename): - """Create a new dictionary of translations from a po file.""" - - self._dict = {} - self._dict[None] = {} - - pofile = polib.pofile(filename) - self.metadata = pofile.metadata - - # Rearrange the entries in the pofile to make it easier to look up contexts. - # _dict will be layed out _dict[msgctxt][msgid] = (translated1, translated2, ...) - # There may be multiple translations because of plurals - for entry in pofile.translated_entries(): - if (entry.msgctxt is not None) and (entry.msgctxt not in self._dict): - self._dict[entry.msgctxt] = {} - - # If this is a plural entry, add entries for both the singular and - # plural forms so that either can be used for a lookup - if entry.msgstr_plural: - xlist = [entry.msgstr_plural[key] for key in entry.msgstr_plural.keys()] - self._dict[entry.msgctxt][entry.msgid] = xlist - self._dict[entry.msgctxt][entry.msgid_plural] = xlist - else: - self._dict[entry.msgctxt][entry.msgid] = (entry.msgstr,) - - def get(self, key, context=None): - return self._dict[context][key] - -# Return a dictionary of PODict objects for each language in a po directory -def translate_all(podir): - # Reset the locale to C before parsing the po file because - # polib has erroneous uses of lower() - saved_locale = locale.setlocale(locale.LC_ALL, None) - locale.setlocale(locale.LC_CTYPE, 'C') - - podicts = {} - - with open(os.path.join(podir, 'LINGUAS')) as linguas: - for line in linguas.readlines(): - if re.match(r'^#', line): - continue - - for lang in line.strip().split(" "): - podicts[lang] = PODict(os.path.join(podir, lang + ".po")) - - locale.setlocale(locale.LC_CTYPE, saved_locale) - return podicts
From: David Shea dshea@redhat.com
Having a bunch of subdirectories with one file each seems kind of silly. --- tests/Makefile.am | 2 +- tests/glade/accelerators/check_accelerators.py | 257 ---------------------- tests/glade/check_accelerators.py | 257 ++++++++++++++++++++++ tests/glade/check_format_string.py | 81 +++++++ tests/glade/check_glade_validity.py | 88 ++++++++ tests/glade/check_icons.py | 75 +++++++ tests/glade/check_invisible_char.py | 82 +++++++ tests/glade/check_markup.py | 138 ++++++++++++ tests/glade/check_pw_visibility.py | 93 ++++++++ tests/glade/check_viewport.py | 79 +++++++ tests/glade/format_string/check_format_string.py | 81 ------- tests/glade/icons/check_icons.py | 75 ------- tests/glade/markup/check_markup.py | 138 ------------ tests/glade/pw_visibility/check_invisible_char.py | 82 ------- tests/glade/pw_visibility/check_pw_visibility.py | 93 -------- tests/glade/run_glade_tests.sh | 6 +- tests/glade/validity/check_glade_validity.py | 88 -------- tests/glade/viewport/check_viewport.py | 79 ------- 18 files changed, 897 insertions(+), 897 deletions(-) delete mode 100755 tests/glade/accelerators/check_accelerators.py create mode 100755 tests/glade/check_accelerators.py create mode 100755 tests/glade/check_format_string.py create mode 100755 tests/glade/check_glade_validity.py create mode 100755 tests/glade/check_icons.py create mode 100755 tests/glade/check_invisible_char.py create mode 100755 tests/glade/check_markup.py create mode 100755 tests/glade/check_pw_visibility.py create mode 100755 tests/glade/check_viewport.py delete mode 100755 tests/glade/format_string/check_format_string.py delete mode 100755 tests/glade/icons/check_icons.py delete mode 100755 tests/glade/markup/check_markup.py delete mode 100755 tests/glade/pw_visibility/check_invisible_char.py delete mode 100755 tests/glade/pw_visibility/check_pw_visibility.py delete mode 100755 tests/glade/validity/check_glade_validity.py delete mode 100755 tests/glade/viewport/check_viewport.py
diff --git a/tests/Makefile.am b/tests/Makefile.am index 26cfc6c..b742e6e 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -34,7 +34,7 @@ AM_TESTS_ENVIRONMENT = top_srcdir="$(top_srcdir)" top_builddir="$(top_builddir)" TEST_EXTENSIONS = .sh
# Test scripts need to be listed both here and in TESTS -dist_check_SCRIPTS = $(srcdir)/glade/*/*.py \ +dist_check_SCRIPTS = $(srcdir)/glade/*.py \ glade/run_glade_tests.sh \ $(srcdir)/lib/*.py \ $(srcdir)/lib/*.sh \ diff --git a/tests/glade/accelerators/check_accelerators.py b/tests/glade/accelerators/check_accelerators.py deleted file mode 100755 index ab87ec1..0000000 --- a/tests/glade/accelerators/check_accelerators.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2013 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: David Shea dshea@redhat.com - -# Ignore any interruptible calls -# pylint: disable=interruptible-system-call - -import sys -import argparse -import re -import os.path -import copy -import locale - -# Import translation methods if needed -if ('-t' in sys.argv) or ('--translate' in sys.argv): - try: - from translatepo import translate_all - except ImportError: - print("Unable to load po translation module") - sys.exit(99) - -try: - from lxml import etree -except ImportError: - print("You need to install the python-lxml package to use check_accelerators.py") - sys.exit(1) - -accel_re = re.compile(r'_(?P<accel>.)') -success = True - -def is_exception(node, conflicting_node, language=None): - # Check for a comment of the form - # <!-- check_accelerators: <conflicting-node-id> --> - # The node passed in is the label property of the widget rather than the - # <object> node itself, so we actually want the id attribute of the parent node. - for comment in node.xpath("./preceding-sibling::comment()[contains(., 'check_accelerators:')]"): - if comment.text.split(":")[1].strip() == conflicting_node.getparent().attrib['id']: - return True - - return False - -def add_check_accel(glade_filename, accels, label, po_map): - """Check whether an accelerator conflicts with existing accelerators. - and add it to the current accelerator context. - """ - global success - - if po_map: - try: - label_texts = po_map.get(label.text, label.get("context")) - except KeyError: - return - - # If there is more than one translation (i.e., a plural string), check - # that the accelerator is the same for all translations - if len(label_texts) > 1: - match = accel_re.search(label_texts[0]) - if match: - accel0 = match.group('accel').lower() - else: - accel0 = None - - for label_text in label_texts[1:]: - match = accel_re.search(label_text) - if match: - accel = match.group('accel').lower() - else: - accel = None - - if accel != accel0: - print("Mismatched accelerator in translations for %s in language %s" % \ - (label.text, po_map.metadata['Language'])) - success = False - - label.text = label_texts[0] - lang_str = " for language %s" % po_map.metadata['Language'] - else: - lang_str = "" - - match = accel_re.search(label.text) - if match: - accel = match.group('accel').lower() - if accel in accels: - # Check for an exception comment - if is_exception(label, accels[accel]): - return - - print(("Accelerator collision for key %s in %s%s\n line %d: %s\n line %d: %s" %\ - (accel, os.path.normpath(glade_filename), lang_str, - accels[accel].sourceline, accels[accel].text, - label.sourceline, label.text)).encode("utf-8")) - success = False - else: - accels[accel] = label - else: - print(("No accelerator defined for %s in %s%s: line %d" %\ - (label.text, os.path.normpath(glade_filename), lang_str, - label.sourceline)).encode("utf-8")) - success = False - -def combine_accels(glade_filename, list_a, list_b, po_map): - if not list_a: - return list_b - if not list_b: - return list_a - - newlist = [] - for accels_a in list_a: - for accels_b in list_b: - new_accels = copy.copy(accels_a) - for accel in accels_b.keys(): - add_check_accel(glade_filename, new_accels, accels_b[accel], po_map) - newlist.append(new_accels) - return newlist - -# GtkNotebook widgets define several child widgets, not all of which are active -# at the same time. To further complicate things, an object can have more than -# one GtkNotebook child, and a GtkNotebook can have GtkNotebook children. -# -# To handle this, GtkNotebook objects are processed separately. -# process_object returns a list of possible accelerator dictionaries, and each of -# these is compared against the list of accelerators returned for the object's -# other GtkNotebook children. - -def process_object(glade_filename, interface_object, po_map): - """Process keyboard shortcuts for a given glade object. - - The return value from this function is a list of accelerator - dictionaries, with each consiting of accelerator shortcut characters - as keys and the corresponding <object> Element objects as values. Each - dictionary represents a set of accelerators that could be active at any - given time. - """ - # Start with an empty context for things that are always active - accels = [{}] - - # Add everything that isn't a child of a GtkNotebook - for label in interface_object.xpath(".//property[@name='label' and ../property[@name='use_underline']/text() = 'True' and not(ancestor::object[@class='GtkNotebook'])]"): - add_check_accel(glade_filename, accels[0], label, po_map) - - # For each GtkNotebook tab that is not a child of another notebook, - # add the tab to the top-level context - for notebook_label in interface_object.xpath(".//object[@class='GtkNotebook' and not(ancestor::object[@class='GtkNotebook'])]/child[@type='tab']//property[@name='label' and ../property[@name='use_underline']/text() = 'True']"): - add_check_accel(glade_filename, accels[0], notebook_label, po_map) - - # Now process each non-tab object in each Gtknotebook that is not a child - # of another notebook. For each Gtk notebook, each non-tab child represents - # a separate potentially-active context. Since each list returned by - # process_object for a GtkNotebook child is independent of each other - # GtkNotebook child, we can just combine all of them into a single list. - # For example, say there's a notebook with two panes. The first pane - # contains another notebook with two panes. Let's call the main pane - # A, and the panes in the child GtkNotebook A_1 and A_2. A contains an - # accelerator for 'a', A_1 contains accelerators for 'b' and 'c', and A_2 - # contains accelerators for 'b' and 'c'. The list returned would look like: - # [{'a': label1, 'b': label2, 'c': label3}, - # {'a': label1, 'b': label4, 'c': label5}] - # Then when we process the other pane in the outermost Notebook (let's call - # it B), we find acclerators for 'a' and 'b': - # [{'a': label6, 'b': label7}]. - # None of these dictionaries are active at the same time. Because - # process_object on A combined the accelerators that are in the top-level - # of A with each of the accelerators in the Notebook children of A, we can - # treat A as if it were actually two panes at the same-level of B and just - # create a list of three dictionaries for the whole notebook. - # - # A deepcopy of each object is taken so that the object can be treated as a - # separate XML document so that the ancestor axis stuff works. - - for notebook in interface_object.xpath(".//object[@class='GtkNotebook' and not(ancestor::object[@class='GtkNotebook'])]"): - # Create the list of dictionaries for the notebook - notebook_list = [] - for child in notebook.xpath("./child[not(@type='tab')]"): - notebook_list.extend(process_object(glade_filename, copy.deepcopy(child), po_map)) - - # Now combine this with our list of accelerators - accels = combine_accels(glade_filename, accels, notebook_list, po_map) - - return accels - -def check_glade(glade_filename, po_map=None): - with open(glade_filename) as glade_file: - # Parse the XML - glade_tree = etree.parse(glade_file) - - # Treat each top-level object as a separate context - for interface_object in glade_tree.xpath("/interface/object"): - process_object(glade_filename, interface_object, po_map) - -def main(argv=None): - if argv is None: - argv = sys.argv - - parser = argparse.ArgumentParser("Check for duplicated accelerators") - parser.add_argument("-t", "--translate", action='store_true', - help="Check translated strings") - parser.add_argument("-p", "--podir", action='store', type=str, - metavar='PODIR', help='Directory containing po files', default='./po') - parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", - help='The glade file to check') - args = parser.parse_args(args=argv) - - # First check the untranslated strings in each file - for glade_file in args.glade_files: - check_glade(glade_file) - - # Now loop over all of the translations - if args.translate: - import langtable - - podicts = translate_all(args.podir) - - for (lang, po_map) in ((key, podicts[key]) for key in podicts.keys()): - # Set the locale so that we can use lower() on accelerator keys. - # If the language is of the form xx_XX, use that as the - # locale name. Otherwise use the first locale that - # langtable returns for the language. If that doesn't work, - # just use C and hope for the best. - if '_' in lang: - locale.setlocale(locale.LC_ALL, lang) - else: - locale_list = langtable.list_locales(languageId=lang) - if locale_list: - try: - locale.setlocale(locale.LC_ALL, locale_list[0]) - except locale.Error: - print("No such locale %s, using C" % locale_list[0]) - locale.setlocale(locale.LC_ALL, 'C') - else: - locale.setlocale(locale.LC_ALL, 'C') - - for glade_file in args.glade_files: - check_glade(glade_file, po_map) - -if __name__ == "__main__": - main(sys.argv[1:]) - - if success: - sys.exit(0) - else: - sys.exit(1) diff --git a/tests/glade/check_accelerators.py b/tests/glade/check_accelerators.py new file mode 100755 index 0000000..ab87ec1 --- /dev/null +++ b/tests/glade/check_accelerators.py @@ -0,0 +1,257 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2013 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author: David Shea dshea@redhat.com + +# Ignore any interruptible calls +# pylint: disable=interruptible-system-call + +import sys +import argparse +import re +import os.path +import copy +import locale + +# Import translation methods if needed +if ('-t' in sys.argv) or ('--translate' in sys.argv): + try: + from translatepo import translate_all + except ImportError: + print("Unable to load po translation module") + sys.exit(99) + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to use check_accelerators.py") + sys.exit(1) + +accel_re = re.compile(r'_(?P<accel>.)') +success = True + +def is_exception(node, conflicting_node, language=None): + # Check for a comment of the form + # <!-- check_accelerators: <conflicting-node-id> --> + # The node passed in is the label property of the widget rather than the + # <object> node itself, so we actually want the id attribute of the parent node. + for comment in node.xpath("./preceding-sibling::comment()[contains(., 'check_accelerators:')]"): + if comment.text.split(":")[1].strip() == conflicting_node.getparent().attrib['id']: + return True + + return False + +def add_check_accel(glade_filename, accels, label, po_map): + """Check whether an accelerator conflicts with existing accelerators. + and add it to the current accelerator context. + """ + global success + + if po_map: + try: + label_texts = po_map.get(label.text, label.get("context")) + except KeyError: + return + + # If there is more than one translation (i.e., a plural string), check + # that the accelerator is the same for all translations + if len(label_texts) > 1: + match = accel_re.search(label_texts[0]) + if match: + accel0 = match.group('accel').lower() + else: + accel0 = None + + for label_text in label_texts[1:]: + match = accel_re.search(label_text) + if match: + accel = match.group('accel').lower() + else: + accel = None + + if accel != accel0: + print("Mismatched accelerator in translations for %s in language %s" % \ + (label.text, po_map.metadata['Language'])) + success = False + + label.text = label_texts[0] + lang_str = " for language %s" % po_map.metadata['Language'] + else: + lang_str = "" + + match = accel_re.search(label.text) + if match: + accel = match.group('accel').lower() + if accel in accels: + # Check for an exception comment + if is_exception(label, accels[accel]): + return + + print(("Accelerator collision for key %s in %s%s\n line %d: %s\n line %d: %s" %\ + (accel, os.path.normpath(glade_filename), lang_str, + accels[accel].sourceline, accels[accel].text, + label.sourceline, label.text)).encode("utf-8")) + success = False + else: + accels[accel] = label + else: + print(("No accelerator defined for %s in %s%s: line %d" %\ + (label.text, os.path.normpath(glade_filename), lang_str, + label.sourceline)).encode("utf-8")) + success = False + +def combine_accels(glade_filename, list_a, list_b, po_map): + if not list_a: + return list_b + if not list_b: + return list_a + + newlist = [] + for accels_a in list_a: + for accels_b in list_b: + new_accels = copy.copy(accels_a) + for accel in accels_b.keys(): + add_check_accel(glade_filename, new_accels, accels_b[accel], po_map) + newlist.append(new_accels) + return newlist + +# GtkNotebook widgets define several child widgets, not all of which are active +# at the same time. To further complicate things, an object can have more than +# one GtkNotebook child, and a GtkNotebook can have GtkNotebook children. +# +# To handle this, GtkNotebook objects are processed separately. +# process_object returns a list of possible accelerator dictionaries, and each of +# these is compared against the list of accelerators returned for the object's +# other GtkNotebook children. + +def process_object(glade_filename, interface_object, po_map): + """Process keyboard shortcuts for a given glade object. + + The return value from this function is a list of accelerator + dictionaries, with each consiting of accelerator shortcut characters + as keys and the corresponding <object> Element objects as values. Each + dictionary represents a set of accelerators that could be active at any + given time. + """ + # Start with an empty context for things that are always active + accels = [{}] + + # Add everything that isn't a child of a GtkNotebook + for label in interface_object.xpath(".//property[@name='label' and ../property[@name='use_underline']/text() = 'True' and not(ancestor::object[@class='GtkNotebook'])]"): + add_check_accel(glade_filename, accels[0], label, po_map) + + # For each GtkNotebook tab that is not a child of another notebook, + # add the tab to the top-level context + for notebook_label in interface_object.xpath(".//object[@class='GtkNotebook' and not(ancestor::object[@class='GtkNotebook'])]/child[@type='tab']//property[@name='label' and ../property[@name='use_underline']/text() = 'True']"): + add_check_accel(glade_filename, accels[0], notebook_label, po_map) + + # Now process each non-tab object in each Gtknotebook that is not a child + # of another notebook. For each Gtk notebook, each non-tab child represents + # a separate potentially-active context. Since each list returned by + # process_object for a GtkNotebook child is independent of each other + # GtkNotebook child, we can just combine all of them into a single list. + # For example, say there's a notebook with two panes. The first pane + # contains another notebook with two panes. Let's call the main pane + # A, and the panes in the child GtkNotebook A_1 and A_2. A contains an + # accelerator for 'a', A_1 contains accelerators for 'b' and 'c', and A_2 + # contains accelerators for 'b' and 'c'. The list returned would look like: + # [{'a': label1, 'b': label2, 'c': label3}, + # {'a': label1, 'b': label4, 'c': label5}] + # Then when we process the other pane in the outermost Notebook (let's call + # it B), we find acclerators for 'a' and 'b': + # [{'a': label6, 'b': label7}]. + # None of these dictionaries are active at the same time. Because + # process_object on A combined the accelerators that are in the top-level + # of A with each of the accelerators in the Notebook children of A, we can + # treat A as if it were actually two panes at the same-level of B and just + # create a list of three dictionaries for the whole notebook. + # + # A deepcopy of each object is taken so that the object can be treated as a + # separate XML document so that the ancestor axis stuff works. + + for notebook in interface_object.xpath(".//object[@class='GtkNotebook' and not(ancestor::object[@class='GtkNotebook'])]"): + # Create the list of dictionaries for the notebook + notebook_list = [] + for child in notebook.xpath("./child[not(@type='tab')]"): + notebook_list.extend(process_object(glade_filename, copy.deepcopy(child), po_map)) + + # Now combine this with our list of accelerators + accels = combine_accels(glade_filename, accels, notebook_list, po_map) + + return accels + +def check_glade(glade_filename, po_map=None): + with open(glade_filename) as glade_file: + # Parse the XML + glade_tree = etree.parse(glade_file) + + # Treat each top-level object as a separate context + for interface_object in glade_tree.xpath("/interface/object"): + process_object(glade_filename, interface_object, po_map) + +def main(argv=None): + if argv is None: + argv = sys.argv + + parser = argparse.ArgumentParser("Check for duplicated accelerators") + parser.add_argument("-t", "--translate", action='store_true', + help="Check translated strings") + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help='Directory containing po files', default='./po') + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=argv) + + # First check the untranslated strings in each file + for glade_file in args.glade_files: + check_glade(glade_file) + + # Now loop over all of the translations + if args.translate: + import langtable + + podicts = translate_all(args.podir) + + for (lang, po_map) in ((key, podicts[key]) for key in podicts.keys()): + # Set the locale so that we can use lower() on accelerator keys. + # If the language is of the form xx_XX, use that as the + # locale name. Otherwise use the first locale that + # langtable returns for the language. If that doesn't work, + # just use C and hope for the best. + if '_' in lang: + locale.setlocale(locale.LC_ALL, lang) + else: + locale_list = langtable.list_locales(languageId=lang) + if locale_list: + try: + locale.setlocale(locale.LC_ALL, locale_list[0]) + except locale.Error: + print("No such locale %s, using C" % locale_list[0]) + locale.setlocale(locale.LC_ALL, 'C') + else: + locale.setlocale(locale.LC_ALL, 'C') + + for glade_file in args.glade_files: + check_glade(glade_file, po_map) + +if __name__ == "__main__": + main(sys.argv[1:]) + + if success: + sys.exit(0) + else: + sys.exit(1) diff --git a/tests/glade/check_format_string.py b/tests/glade/check_format_string.py new file mode 100755 index 0000000..b730a94 --- /dev/null +++ b/tests/glade/check_format_string.py @@ -0,0 +1,81 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author: David Shea dshea@redhat.com +# + +""" +Python script to ensure that translatable format strings are not present +in Glade files. + +Since format substitution is language-dependent, gettext is unable to check +the validity of format string translations for strings within glade. Instead, +the format string constant, the translation substitution, and the format +substitution should all happen outside of glade. Untranslated placeholder +strings are allowable within glade. +""" + +# Ignore any interruptible calls +# pylint: disable=interruptible-system-call + +import sys +import argparse +import re + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to use check_format_string.py") + sys.exit(1) + +def check_glade_file(glade_file_path): + global success + + with open(glade_file_path) as glade_file: + # Parse the XML + glade_tree = etree.parse(glade_file) + + # Check any property with translatable="yes" + for translatable in glade_tree.xpath(".//*[@translatable='yes']"): + # Look for % followed by an open parenthesis (indicating %(name) + # style substitution), one of the python format conversion flags + # (#0- +hlL), or one of the python conversion types + # (diouxXeEfFgGcrs) + if re.search(r'%[-(#0 +hlLdiouxXeEfFgGcrs]', translatable.text): + print("Translatable format string found in glade at %s:%d" % \ + (glade_file_path, translatable.sourceline)) + success = False + +if __name__ == "__main__": + success = True + parser = argparse.ArgumentParser("Check that password entries have visibility set to False") + + # Ignore translation arguments + parser.add_argument("-t", "--translate", action='store_true', + help=argparse.SUPPRESS) + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help=argparse.SUPPRESS, default='./po') + + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=sys.argv[1:]) + + success = True + for file_path in args.glade_files: + check_glade_file(file_path) + + sys.exit(0 if success else 1) diff --git a/tests/glade/check_glade_validity.py b/tests/glade/check_glade_validity.py new file mode 100755 index 0000000..1101472 --- /dev/null +++ b/tests/glade/check_glade_validity.py @@ -0,0 +1,88 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author: David Shea dshea@redhat.com + +import sys +import argparse + +from collections import Counter + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to run the glade checks") + sys.exit(99) + +success = True + +def main(argv): + global success + + for glade_file in argv: + # Parse the glade file to ensure it's well-formed + try: + glade_tree = etree.parse(glade_file) + except etree.XMLSyntaxError: + print("%s is not a valid XML file" % glade_file) + success = False + continue + + # Check for duplicate IDs + # Build a Counter from a list of all ids, extracts the ones with count > 1 + # Fun fact: glade uses <col id="<number>"> in GtkListStore data, so ids + # aren't actually unique and getting an object with a particular ID + # isn't as simple as document.getElementById. Only check the IDs on objects. + for glade_id in [c for c in Counter(glade_tree.xpath(".//object/@id")).most_common() \ + if c[1] > 1]: + print("%s: ID %s appears %d times" % (glade_file, glade_id[0], glade_id[1])) + success = False + + # Check for ID references + # mnemonic_widget properties and action-widget elements need to refer to + # valid object ids. + for mnemonic_widget in glade_tree.xpath(".//property[@name='mnemonic_widget']"): + if not glade_tree.xpath(".//object[@id='%s']" % mnemonic_widget.text): + print("mnemonic_widget reference to invalid ID %s at line %d of %s" % \ + (mnemonic_widget.text, mnemonic_widget.sourceline, glade_file)) + success = False + + for action_widget in glade_tree.xpath(".//action-widget"): + if not glade_tree.xpath(".//object[@id='%s']" % action_widget.text): + print("action-widget reference to invalid ID %s at line %d of %s" % \ + (action_widget.text, action_widget.sourceline, glade_file)) + success = False + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Check glade file validity") + + # Ignore translation arguments + parser.add_argument("-t", "--translate", action='store_true', + help=argparse.SUPPRESS) + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help=argparse.SUPPRESS, default='./po') + + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=sys.argv[1:]) + + main(args.glade_files) + + if success: + sys.exit(0) + else: + sys.exit(1) diff --git a/tests/glade/check_icons.py b/tests/glade/check_icons.py new file mode 100755 index 0000000..bfeac6d --- /dev/null +++ b/tests/glade/check_icons.py @@ -0,0 +1,75 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author: David Shea dshea@redhat.com +# +""" +Check that all icons referenced from glade files are valid in the gnome icon theme. +""" + +# Ignore any interruptible calls +# pylint: disable=interruptible-system-call + +import argparse +import sys + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to use check_pw_visibility.py") + sys.exit(1) + +from iconcheck import icon_exists + +def check_glade_file(glade_file_path): + glade_success = True + with open(glade_file_path) as glade_file: + # Parse the XML + glade_tree = etree.parse(glade_file) + + # Stock image names are deprecated + for element in glade_tree.xpath("//property[@name='stock' or @name='stock_id']"): + glade_success = False + print("Deprecated stock icon found at %s:%d" % (glade_file_path, element.sourceline)) + + # Check whether named icons exist + for element in glade_tree.xpath("//property[@name='icon_name']"): + if not icon_exists(element.text): + glade_success = False + print("Invalid icon name %s found at %s:%d" % (element.text, glade_file_path, element.sourceline)) + + return glade_success + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Check that password entries have visibility set to False") + + # Ignore translation arguments + parser.add_argument("-t", "--translate", action='store_true', + help=argparse.SUPPRESS) + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help=argparse.SUPPRESS, default='./po') + + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=sys.argv[1:]) + + success = True + for file_path in args.glade_files: + if not check_glade_file(file_path): + success = False + + sys.exit(0 if success else 1) diff --git a/tests/glade/check_invisible_char.py b/tests/glade/check_invisible_char.py new file mode 100755 index 0000000..72d8486 --- /dev/null +++ b/tests/glade/check_invisible_char.py @@ -0,0 +1,82 @@ +#!/usr/bin/python2 +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author: David Shea dshea@redhat.com +# + +""" +Check that the invisible_char in glade files is actually a char. + +The invisible char is often non-ASCII and sometimes that gets clobbered. +""" + +# Ignore any interruptible calls +# pylint: disable=interruptible-system-call + +import argparse +import sys + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to use check_pw_visibility.py") + sys.exit(1) + +def check_glade_file(glade_file_path): + succ = True + + with open(glade_file_path, "r") as glade_file: + tree = etree.parse(glade_file) + # Only look for entries with an invisible_char property + for entry in tree.xpath("//object[@class='GtkEntry' and ./property[@name='invisible_char']]"): + # Check the contents of the invisible_char property + invis = entry.xpath("./property[@name='invisible_char']")[0] + if len(invis.text) != 1: + print("invisible_char at %s:%s not a character" % (glade_file_path, invis.sourceline)) + succ = False + + # If the char is '?' that's probably also bad + if invis.text == '?': + print("invisible_char at %s:%s is not what you want" % (glade_file_path, invis.sourceline)) + + # Check that invisible_char even does anything: visibility should be False + if not entry.xpath("./property[@name='visibility' and ./text() = 'False']"): + print("Pointless invisible_char found at %s:%s" % (glade_file_path, invis.sourceline)) + succ = False + + return succ + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Check that invisible character properties are set correctly") + + # Ignore translation arguments + parser.add_argument("-t", "--translate", action='store_true', + help=argparse.SUPPRESS) + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help=argparse.SUPPRESS, default='./po') + + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=sys.argv[1:]) + + success = True + for file_path in args.glade_files: + if not check_glade_file(file_path): + success = False + + sys.exit(0 if success else 1) diff --git a/tests/glade/check_markup.py b/tests/glade/check_markup.py new file mode 100755 index 0000000..ba639da --- /dev/null +++ b/tests/glade/check_markup.py @@ -0,0 +1,138 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author: David Shea dshea@redhat.com +# + +""" +Python script to check that properties in glade using Pango markup contain +valid markup. +""" + +# Ignore any interruptible calls +# pylint: disable=interruptible-system-call + +import sys +import argparse + +# Import translation methods if needed +if ('-t' in sys.argv) or ('--translate' in sys.argv): + try: + from translatepo import translate_all + except ImportError: + print("Unable to load po translation module") + sys.exit(99) + +from pangocheck import markup_nodes, markup_match, markup_necessary + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to use check_markup.py") + sys.exit(99) + +class PangoElementException(Exception): + def __init__(self, element): + Exception.__init__(self) + self.element = element + + def __str__(self): + return "Invalid element %s" % self.element + +def _validate_pango_markup(root): + """Validate parsed pango markup. + + :param etree.ElementTree root: The pango markup parsed as an XML ElementTree + :raises PangoElementException: If the pango markup contains unknown elements + """ + if root.tag not in markup_nodes: + raise PangoElementException(root.tag) + + for child in root: + _validate_pango_markup(child) + +def check_glade_file(glade_file_path, po_map=None): + glade_success = True + with open(glade_file_path) as glade_file: + # Parse the XML + glade_tree = etree.parse(glade_file) + + # Search for label properties on objects that have use_markup set to True + for label in glade_tree.xpath(".//property[@name='label' and ../property[@name='use_markup']/text() = 'True']"): + if po_map: + try: + label_texts = po_map.get(label.text, label.get("context")) + except KeyError: + continue + lang_str = " for language %s" % po_map.metadata['Language'] + else: + label_texts = (label.text,) + lang_str = "" + + # Wrap the label text in <markup> tags and parse the tree + for label_text in label_texts: + try: + # pylint: disable=unescaped-markup + pango_tree = etree.fromstring("<markup>%s</markup>" % label_text) + _validate_pango_markup(pango_tree) + + # Check if the markup is necessary + if not markup_necessary(pango_tree): + print("Markup could be expressed as attributes at %s%s:%d" % \ + (glade_file_path, lang_str, label.sourceline)) + glade_success = False + except etree.XMLSyntaxError: + print("Unable to parse pango markup at %s%s:%d" % \ + (glade_file_path, lang_str, label.sourceline)) + glade_success = False + except PangoElementException as px: + print("Invalid pango element %s at %s%s:%d" % \ + (px.element, glade_file_path, lang_str, label.sourceline)) + glade_success = False + else: + if po_map: + # Check that translated markup has the same elements and attributes + if not markup_match(label.text, label_text): + print("Translated markup does not contain the same elements and attributes at %s%s:%d" % \ + (glade_file_path, lang_str, label.sourceline)) + glade_success = False + return glade_success + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Check Pango markup validity") + parser.add_argument("-t", "--translate", action='store_true', + help="Check translated strings") + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help='Directory containing po files', default='./po') + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=sys.argv[1:]) + + success = True + for file_path in args.glade_files: + if not check_glade_file(file_path): + success = False + + # Now loop over all of the translations + if args.translate: + podicts = translate_all(args.podir) + for po_dict in podicts.values(): + for file_path in args.glade_files: + if not check_glade_file(file_path, po_dict): + success = False + + sys.exit(0 if success else 1) diff --git a/tests/glade/check_pw_visibility.py b/tests/glade/check_pw_visibility.py new file mode 100755 index 0000000..f48f875 --- /dev/null +++ b/tests/glade/check_pw_visibility.py @@ -0,0 +1,93 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2013 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author: Vratislav Podzimek vpodzime@redhat.com +# + +""" +Simple python script checking that password GtkEntries in the given .glade files +have the visibility set to False. + +""" + +# Ignore any interruptible calls +# pylint: disable=interruptible-system-call + +import argparse +import sys + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to use check_pw_visibility.py") + sys.exit(1) + +PW_ID_INDICATORS = ("pw", "password", "passwd", "passphrase") + +def check_glade_file(glade_file_path): + def check_entry(entry, fpath): + succ = True + + entry_id = entry.attrib.get("id", "UNKNOWN ID") + visibility_props = entry.xpath("./property[@name='visibility']") + + # no entry should have visibility specified multiple times + if len(visibility_props) > 1: + print("Visibility specified multiple times for the entry %s (%s)" % (entry_id, fpath)) + succ = False + + # password entry should have visibility set to False + if any(ind in entry_id.lower() for ind in PW_ID_INDICATORS): + if not visibility_props: + print("Visibility not specified for the password entry %s (%s)" % (entry_id, fpath)) + succ = False + elif visibility_props[0].text.strip() != "False": + print("Visibility not set properly for the password entry %s (%s)" % (entry_id, fpath)) + succ = False + # only password entries should have the visibility set to False + elif visibility_props and visibility_props[0].text.strip() == "False": + print("Non-password entry %s (%s) has the visibility set to False (bad id?)" % (entry_id, fpath)) + succ = False + + return succ + + succ = True + with open(glade_file_path, "r") as glade_file: + tree = etree.parse(glade_file) + for entry in tree.xpath("//object[@class='GtkEntry']"): + succ = succ and check_entry(entry, glade_file_path) + + return succ + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Check that password entries have visibility set to False") + + # Ignore translation arguments + parser.add_argument("-t", "--translate", action='store_true', + help=argparse.SUPPRESS) + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help=argparse.SUPPRESS, default='./po') + + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=sys.argv[1:]) + + success = True + for file_path in args.glade_files: + success = success and check_glade_file(file_path) + + sys.exit(0 if success else 1) diff --git a/tests/glade/check_viewport.py b/tests/glade/check_viewport.py new file mode 100755 index 0000000..bd9d771 --- /dev/null +++ b/tests/glade/check_viewport.py @@ -0,0 +1,79 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author: David Shea dshea@redhat.com +# + +""" +Check that widgets that implement GtkScrollable are not placed within a +GtkViewport. If a widget knows how to scroll itself we probably don't want +to add an extra layer. +""" + +# Ignore any interruptible calls +# pylint: disable=interruptible-system-call + +import argparse +import sys + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to use check_pw_visibility.py") + sys.exit(1) + +# I guess we could look at the introspected classes and see if they implement the Scrollable +# interface but that sounds like kind of a pain +SCROLLABLES = ["GtkIconView", "GtkLayout", "GtkTextView", "GtkToolPalette", + "GtkTreeView", "GtkViewport"] + +def check_glade_file(glade_file_path): + glade_success = True + with open(glade_file_path) as glade_file: + # Parse the XML + glade_tree = etree.parse(glade_file) + + # Look for something like: + # <object class="GtkViewport"> + # <child> + # <object class="GtkTreeView"> + for scrollable in SCROLLABLES: + for element in glade_tree.xpath(".//object[@class='GtkViewport']/child/object[@class='%s']" % scrollable): + glade_success = False + print("%s contained in GtkViewport at %s:%d" % (scrollable, glade_file_path, + element.sourceline)) + return glade_success + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Check that password entries have visibility set to False") + + # Ignore translation arguments + parser.add_argument("-t", "--translate", action='store_true', + help=argparse.SUPPRESS) + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help=argparse.SUPPRESS, default='./po') + + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=sys.argv[1:]) + + success = True + for file_path in args.glade_files: + if not check_glade_file(file_path): + success = False + + sys.exit(0 if success else 1) diff --git a/tests/glade/format_string/check_format_string.py b/tests/glade/format_string/check_format_string.py deleted file mode 100755 index b730a94..0000000 --- a/tests/glade/format_string/check_format_string.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: David Shea dshea@redhat.com -# - -""" -Python script to ensure that translatable format strings are not present -in Glade files. - -Since format substitution is language-dependent, gettext is unable to check -the validity of format string translations for strings within glade. Instead, -the format string constant, the translation substitution, and the format -substitution should all happen outside of glade. Untranslated placeholder -strings are allowable within glade. -""" - -# Ignore any interruptible calls -# pylint: disable=interruptible-system-call - -import sys -import argparse -import re - -try: - from lxml import etree -except ImportError: - print("You need to install the python-lxml package to use check_format_string.py") - sys.exit(1) - -def check_glade_file(glade_file_path): - global success - - with open(glade_file_path) as glade_file: - # Parse the XML - glade_tree = etree.parse(glade_file) - - # Check any property with translatable="yes" - for translatable in glade_tree.xpath(".//*[@translatable='yes']"): - # Look for % followed by an open parenthesis (indicating %(name) - # style substitution), one of the python format conversion flags - # (#0- +hlL), or one of the python conversion types - # (diouxXeEfFgGcrs) - if re.search(r'%[-(#0 +hlLdiouxXeEfFgGcrs]', translatable.text): - print("Translatable format string found in glade at %s:%d" % \ - (glade_file_path, translatable.sourceline)) - success = False - -if __name__ == "__main__": - success = True - parser = argparse.ArgumentParser("Check that password entries have visibility set to False") - - # Ignore translation arguments - parser.add_argument("-t", "--translate", action='store_true', - help=argparse.SUPPRESS) - parser.add_argument("-p", "--podir", action='store', type=str, - metavar='PODIR', help=argparse.SUPPRESS, default='./po') - - parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", - help='The glade file to check') - args = parser.parse_args(args=sys.argv[1:]) - - success = True - for file_path in args.glade_files: - check_glade_file(file_path) - - sys.exit(0 if success else 1) diff --git a/tests/glade/icons/check_icons.py b/tests/glade/icons/check_icons.py deleted file mode 100755 index bfeac6d..0000000 --- a/tests/glade/icons/check_icons.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: David Shea dshea@redhat.com -# -""" -Check that all icons referenced from glade files are valid in the gnome icon theme. -""" - -# Ignore any interruptible calls -# pylint: disable=interruptible-system-call - -import argparse -import sys - -try: - from lxml import etree -except ImportError: - print("You need to install the python-lxml package to use check_pw_visibility.py") - sys.exit(1) - -from iconcheck import icon_exists - -def check_glade_file(glade_file_path): - glade_success = True - with open(glade_file_path) as glade_file: - # Parse the XML - glade_tree = etree.parse(glade_file) - - # Stock image names are deprecated - for element in glade_tree.xpath("//property[@name='stock' or @name='stock_id']"): - glade_success = False - print("Deprecated stock icon found at %s:%d" % (glade_file_path, element.sourceline)) - - # Check whether named icons exist - for element in glade_tree.xpath("//property[@name='icon_name']"): - if not icon_exists(element.text): - glade_success = False - print("Invalid icon name %s found at %s:%d" % (element.text, glade_file_path, element.sourceline)) - - return glade_success - -if __name__ == "__main__": - parser = argparse.ArgumentParser("Check that password entries have visibility set to False") - - # Ignore translation arguments - parser.add_argument("-t", "--translate", action='store_true', - help=argparse.SUPPRESS) - parser.add_argument("-p", "--podir", action='store', type=str, - metavar='PODIR', help=argparse.SUPPRESS, default='./po') - - parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", - help='The glade file to check') - args = parser.parse_args(args=sys.argv[1:]) - - success = True - for file_path in args.glade_files: - if not check_glade_file(file_path): - success = False - - sys.exit(0 if success else 1) diff --git a/tests/glade/markup/check_markup.py b/tests/glade/markup/check_markup.py deleted file mode 100755 index ba639da..0000000 --- a/tests/glade/markup/check_markup.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: David Shea dshea@redhat.com -# - -""" -Python script to check that properties in glade using Pango markup contain -valid markup. -""" - -# Ignore any interruptible calls -# pylint: disable=interruptible-system-call - -import sys -import argparse - -# Import translation methods if needed -if ('-t' in sys.argv) or ('--translate' in sys.argv): - try: - from translatepo import translate_all - except ImportError: - print("Unable to load po translation module") - sys.exit(99) - -from pangocheck import markup_nodes, markup_match, markup_necessary - -try: - from lxml import etree -except ImportError: - print("You need to install the python-lxml package to use check_markup.py") - sys.exit(99) - -class PangoElementException(Exception): - def __init__(self, element): - Exception.__init__(self) - self.element = element - - def __str__(self): - return "Invalid element %s" % self.element - -def _validate_pango_markup(root): - """Validate parsed pango markup. - - :param etree.ElementTree root: The pango markup parsed as an XML ElementTree - :raises PangoElementException: If the pango markup contains unknown elements - """ - if root.tag not in markup_nodes: - raise PangoElementException(root.tag) - - for child in root: - _validate_pango_markup(child) - -def check_glade_file(glade_file_path, po_map=None): - glade_success = True - with open(glade_file_path) as glade_file: - # Parse the XML - glade_tree = etree.parse(glade_file) - - # Search for label properties on objects that have use_markup set to True - for label in glade_tree.xpath(".//property[@name='label' and ../property[@name='use_markup']/text() = 'True']"): - if po_map: - try: - label_texts = po_map.get(label.text, label.get("context")) - except KeyError: - continue - lang_str = " for language %s" % po_map.metadata['Language'] - else: - label_texts = (label.text,) - lang_str = "" - - # Wrap the label text in <markup> tags and parse the tree - for label_text in label_texts: - try: - # pylint: disable=unescaped-markup - pango_tree = etree.fromstring("<markup>%s</markup>" % label_text) - _validate_pango_markup(pango_tree) - - # Check if the markup is necessary - if not markup_necessary(pango_tree): - print("Markup could be expressed as attributes at %s%s:%d" % \ - (glade_file_path, lang_str, label.sourceline)) - glade_success = False - except etree.XMLSyntaxError: - print("Unable to parse pango markup at %s%s:%d" % \ - (glade_file_path, lang_str, label.sourceline)) - glade_success = False - except PangoElementException as px: - print("Invalid pango element %s at %s%s:%d" % \ - (px.element, glade_file_path, lang_str, label.sourceline)) - glade_success = False - else: - if po_map: - # Check that translated markup has the same elements and attributes - if not markup_match(label.text, label_text): - print("Translated markup does not contain the same elements and attributes at %s%s:%d" % \ - (glade_file_path, lang_str, label.sourceline)) - glade_success = False - return glade_success - -if __name__ == "__main__": - parser = argparse.ArgumentParser("Check Pango markup validity") - parser.add_argument("-t", "--translate", action='store_true', - help="Check translated strings") - parser.add_argument("-p", "--podir", action='store', type=str, - metavar='PODIR', help='Directory containing po files', default='./po') - parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", - help='The glade file to check') - args = parser.parse_args(args=sys.argv[1:]) - - success = True - for file_path in args.glade_files: - if not check_glade_file(file_path): - success = False - - # Now loop over all of the translations - if args.translate: - podicts = translate_all(args.podir) - for po_dict in podicts.values(): - for file_path in args.glade_files: - if not check_glade_file(file_path, po_dict): - success = False - - sys.exit(0 if success else 1) diff --git a/tests/glade/pw_visibility/check_invisible_char.py b/tests/glade/pw_visibility/check_invisible_char.py deleted file mode 100755 index 72d8486..0000000 --- a/tests/glade/pw_visibility/check_invisible_char.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/python2 -# -# Copyright (C) 2015 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: David Shea dshea@redhat.com -# - -""" -Check that the invisible_char in glade files is actually a char. - -The invisible char is often non-ASCII and sometimes that gets clobbered. -""" - -# Ignore any interruptible calls -# pylint: disable=interruptible-system-call - -import argparse -import sys - -try: - from lxml import etree -except ImportError: - print("You need to install the python-lxml package to use check_pw_visibility.py") - sys.exit(1) - -def check_glade_file(glade_file_path): - succ = True - - with open(glade_file_path, "r") as glade_file: - tree = etree.parse(glade_file) - # Only look for entries with an invisible_char property - for entry in tree.xpath("//object[@class='GtkEntry' and ./property[@name='invisible_char']]"): - # Check the contents of the invisible_char property - invis = entry.xpath("./property[@name='invisible_char']")[0] - if len(invis.text) != 1: - print("invisible_char at %s:%s not a character" % (glade_file_path, invis.sourceline)) - succ = False - - # If the char is '?' that's probably also bad - if invis.text == '?': - print("invisible_char at %s:%s is not what you want" % (glade_file_path, invis.sourceline)) - - # Check that invisible_char even does anything: visibility should be False - if not entry.xpath("./property[@name='visibility' and ./text() = 'False']"): - print("Pointless invisible_char found at %s:%s" % (glade_file_path, invis.sourceline)) - succ = False - - return succ - - -if __name__ == "__main__": - parser = argparse.ArgumentParser("Check that invisible character properties are set correctly") - - # Ignore translation arguments - parser.add_argument("-t", "--translate", action='store_true', - help=argparse.SUPPRESS) - parser.add_argument("-p", "--podir", action='store', type=str, - metavar='PODIR', help=argparse.SUPPRESS, default='./po') - - parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", - help='The glade file to check') - args = parser.parse_args(args=sys.argv[1:]) - - success = True - for file_path in args.glade_files: - if not check_glade_file(file_path): - success = False - - sys.exit(0 if success else 1) diff --git a/tests/glade/pw_visibility/check_pw_visibility.py b/tests/glade/pw_visibility/check_pw_visibility.py deleted file mode 100755 index f48f875..0000000 --- a/tests/glade/pw_visibility/check_pw_visibility.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2013 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: Vratislav Podzimek vpodzime@redhat.com -# - -""" -Simple python script checking that password GtkEntries in the given .glade files -have the visibility set to False. - -""" - -# Ignore any interruptible calls -# pylint: disable=interruptible-system-call - -import argparse -import sys - -try: - from lxml import etree -except ImportError: - print("You need to install the python-lxml package to use check_pw_visibility.py") - sys.exit(1) - -PW_ID_INDICATORS = ("pw", "password", "passwd", "passphrase") - -def check_glade_file(glade_file_path): - def check_entry(entry, fpath): - succ = True - - entry_id = entry.attrib.get("id", "UNKNOWN ID") - visibility_props = entry.xpath("./property[@name='visibility']") - - # no entry should have visibility specified multiple times - if len(visibility_props) > 1: - print("Visibility specified multiple times for the entry %s (%s)" % (entry_id, fpath)) - succ = False - - # password entry should have visibility set to False - if any(ind in entry_id.lower() for ind in PW_ID_INDICATORS): - if not visibility_props: - print("Visibility not specified for the password entry %s (%s)" % (entry_id, fpath)) - succ = False - elif visibility_props[0].text.strip() != "False": - print("Visibility not set properly for the password entry %s (%s)" % (entry_id, fpath)) - succ = False - # only password entries should have the visibility set to False - elif visibility_props and visibility_props[0].text.strip() == "False": - print("Non-password entry %s (%s) has the visibility set to False (bad id?)" % (entry_id, fpath)) - succ = False - - return succ - - succ = True - with open(glade_file_path, "r") as glade_file: - tree = etree.parse(glade_file) - for entry in tree.xpath("//object[@class='GtkEntry']"): - succ = succ and check_entry(entry, glade_file_path) - - return succ - -if __name__ == "__main__": - parser = argparse.ArgumentParser("Check that password entries have visibility set to False") - - # Ignore translation arguments - parser.add_argument("-t", "--translate", action='store_true', - help=argparse.SUPPRESS) - parser.add_argument("-p", "--podir", action='store', type=str, - metavar='PODIR', help=argparse.SUPPRESS, default='./po') - - parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", - help='The glade file to check') - args = parser.parse_args(args=sys.argv[1:]) - - success = True - for file_path in args.glade_files: - success = success and check_glade_file(file_path) - - sys.exit(0 if success else 1) diff --git a/tests/glade/run_glade_tests.sh b/tests/glade/run_glade_tests.sh index 9ff3a2f..0a4f9f9 100755 --- a/tests/glade/run_glade_tests.sh +++ b/tests/glade/run_glade_tests.sh @@ -6,13 +6,13 @@ if ! type parallel 2>&1 > /dev/null; then fi
if ! rpm -q gnome-icon-theme &> /dev/null; then - # used in icons/check_icons.py;tests/lib/iconcheck.py + # used in check_icons.py;tests/lib/iconcheck.py echo "gnome-icon-theme must be installed" exit 99 fi
if ! rpm -q gnome-icon-theme-symbolic &> /dev/null; then - # used in icons/check_icons.py;tests/lib/iconcheck.py + # used in check_icons.py;tests/lib/iconcheck.py echo "gnome-icon-theme-symbolic must be installed" exit 99 fi @@ -38,7 +38,7 @@ if [ "$translate_set" -eq 1 -a "$podir_set" -eq 0 ]; then fi
status=0 -for check in ${srcdir}/*/check_*.py ; do +for check in ${srcdir}/check_*.py ; do findtestfiles -name '*.glade' | parallel --no-notice --gnu -j0 "${check}" "$@" {} if [ "$?" -ne 0 ]; then status=1 diff --git a/tests/glade/validity/check_glade_validity.py b/tests/glade/validity/check_glade_validity.py deleted file mode 100755 index 1101472..0000000 --- a/tests/glade/validity/check_glade_validity.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: David Shea dshea@redhat.com - -import sys -import argparse - -from collections import Counter - -try: - from lxml import etree -except ImportError: - print("You need to install the python-lxml package to run the glade checks") - sys.exit(99) - -success = True - -def main(argv): - global success - - for glade_file in argv: - # Parse the glade file to ensure it's well-formed - try: - glade_tree = etree.parse(glade_file) - except etree.XMLSyntaxError: - print("%s is not a valid XML file" % glade_file) - success = False - continue - - # Check for duplicate IDs - # Build a Counter from a list of all ids, extracts the ones with count > 1 - # Fun fact: glade uses <col id="<number>"> in GtkListStore data, so ids - # aren't actually unique and getting an object with a particular ID - # isn't as simple as document.getElementById. Only check the IDs on objects. - for glade_id in [c for c in Counter(glade_tree.xpath(".//object/@id")).most_common() \ - if c[1] > 1]: - print("%s: ID %s appears %d times" % (glade_file, glade_id[0], glade_id[1])) - success = False - - # Check for ID references - # mnemonic_widget properties and action-widget elements need to refer to - # valid object ids. - for mnemonic_widget in glade_tree.xpath(".//property[@name='mnemonic_widget']"): - if not glade_tree.xpath(".//object[@id='%s']" % mnemonic_widget.text): - print("mnemonic_widget reference to invalid ID %s at line %d of %s" % \ - (mnemonic_widget.text, mnemonic_widget.sourceline, glade_file)) - success = False - - for action_widget in glade_tree.xpath(".//action-widget"): - if not glade_tree.xpath(".//object[@id='%s']" % action_widget.text): - print("action-widget reference to invalid ID %s at line %d of %s" % \ - (action_widget.text, action_widget.sourceline, glade_file)) - success = False - -if __name__ == "__main__": - parser = argparse.ArgumentParser("Check glade file validity") - - # Ignore translation arguments - parser.add_argument("-t", "--translate", action='store_true', - help=argparse.SUPPRESS) - parser.add_argument("-p", "--podir", action='store', type=str, - metavar='PODIR', help=argparse.SUPPRESS, default='./po') - - parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", - help='The glade file to check') - args = parser.parse_args(args=sys.argv[1:]) - - main(args.glade_files) - - if success: - sys.exit(0) - else: - sys.exit(1) diff --git a/tests/glade/viewport/check_viewport.py b/tests/glade/viewport/check_viewport.py deleted file mode 100755 index bd9d771..0000000 --- a/tests/glade/viewport/check_viewport.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see http://www.gnu.org/licenses/. -# -# Author: David Shea dshea@redhat.com -# - -""" -Check that widgets that implement GtkScrollable are not placed within a -GtkViewport. If a widget knows how to scroll itself we probably don't want -to add an extra layer. -""" - -# Ignore any interruptible calls -# pylint: disable=interruptible-system-call - -import argparse -import sys - -try: - from lxml import etree -except ImportError: - print("You need to install the python-lxml package to use check_pw_visibility.py") - sys.exit(1) - -# I guess we could look at the introspected classes and see if they implement the Scrollable -# interface but that sounds like kind of a pain -SCROLLABLES = ["GtkIconView", "GtkLayout", "GtkTextView", "GtkToolPalette", - "GtkTreeView", "GtkViewport"] - -def check_glade_file(glade_file_path): - glade_success = True - with open(glade_file_path) as glade_file: - # Parse the XML - glade_tree = etree.parse(glade_file) - - # Look for something like: - # <object class="GtkViewport"> - # <child> - # <object class="GtkTreeView"> - for scrollable in SCROLLABLES: - for element in glade_tree.xpath(".//object[@class='GtkViewport']/child/object[@class='%s']" % scrollable): - glade_success = False - print("%s contained in GtkViewport at %s:%d" % (scrollable, glade_file_path, - element.sourceline)) - return glade_success - -if __name__ == "__main__": - parser = argparse.ArgumentParser("Check that password entries have visibility set to False") - - # Ignore translation arguments - parser.add_argument("-t", "--translate", action='store_true', - help=argparse.SUPPRESS) - parser.add_argument("-p", "--podir", action='store', type=str, - metavar='PODIR', help=argparse.SUPPRESS, default='./po') - - parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", - help='The glade file to check') - args = parser.parse_args(args=sys.argv[1:]) - - success = True - for file_path in args.glade_files: - if not check_glade_file(file_path): - success = False - - sys.exit(0 if success else 1)
From: David Shea dshea@redhat.com
This test searches glade files for GtkLabels that contain a keyboard accelerator but are not connected to another widget. --- tests/glade/check_mnemonics.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100755 tests/glade/check_mnemonics.py
diff --git a/tests/glade/check_mnemonics.py b/tests/glade/check_mnemonics.py new file mode 100755 index 0000000..28ac2eb --- /dev/null +++ b/tests/glade/check_mnemonics.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author: David Shea dshea@redhat.com +# pylint: disable=interruptible-system-call + +# Look for widgets with keyboard accelerators but no mnemonic + + +import sys +import argparse + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to run the glade checks") + sys.exit(99) + +def check_glade_file(glade_file_path): + glade_success = True + + with open(glade_file_path) as glade_file: + # Parse the XML + glade_tree = etree.parse(glade_file) + + # Look for labels with use-underline=True and no mnemonic-widget + for label in glade_tree.xpath(".//object[@class='GtkLabel' and ./property[@name='use_underline' and ./text() = 'True'] and not(./property[@name='mnemonic_widget'])]"): + # And now filter out the cases where the label actually does have a mnemonic. + # This list is not comprehensive, probably. + + parent = label.getparent() + + # Is the label the child of a GtkButton? The button might be pretty far up there. + # Assume widgets names that end in "Button" are subclasses of GtkButton + if parent.tag == 'child' and \ + label.xpath("ancestor::object[substring(@class, string-length(@class) - string-length('Button') + 1) = 'Button']"): + continue + + # Is the label a GtkNotebook tab? + if parent.tag == 'child' and parent.get('type') == 'tab' and \ + parent.getparent().get('class') == 'GtkNotebook': + continue + + print("Label with accelerator and no mnemonic at %s:%d" % (glade_file_path, label.sourceline)) + glade_success = False + + return glade_success + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Check glade file validity") + + # Ignore translation arguments + parser.add_argument("-t", "--translate", action='store_true', + help=argparse.SUPPRESS) + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help=argparse.SUPPRESS, default='./po') + + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=sys.argv[1:]) + + success = True + for file_path in args.glade_files: + if not check_glade_file(file_path): + success = False + + if success: + sys.exit(0) + else: + sys.exit(1)
From: David Shea dshea@redhat.com
--- pyanaconda/ui/gui/spokes/advanced_user.glade | 3 ++- pyanaconda/ui/gui/spokes/custom.glade | 1 + pyanaconda/ui/gui/spokes/filter.glade | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/pyanaconda/ui/gui/spokes/advanced_user.glade b/pyanaconda/ui/gui/spokes/advanced_user.glade index 93470aa..0bf041f 100644 --- a/pyanaconda/ui/gui/spokes/advanced_user.glade +++ b/pyanaconda/ui/gui/spokes/advanced_user.glade @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.18.3 --> +<!-- Generated with glade 3.19.0 --> <interface> <requires lib="gtk+" version="3.0"/> <object class="GtkAdjustment" id="gid"> @@ -259,6 +259,7 @@ <property name="can_focus">False</property> <property name="label" translatable="yes" context="GUI|Advanced User">_Add user to the following groups:</property> <property name="use_underline">True</property> + <property name="mnemonic_widget">t_groups</property> <property name="xalign">0</property> </object> <packing> diff --git a/pyanaconda/ui/gui/spokes/custom.glade b/pyanaconda/ui/gui/spokes/custom.glade index ec6d173..0b5988d 100644 --- a/pyanaconda/ui/gui/spokes/custom.glade +++ b/pyanaconda/ui/gui/spokes/custom.glade @@ -1084,6 +1084,7 @@ <property name="can_focus">False</property> <property name="label" translatable="yes" context="GUI|Custom Partitioning|Encrypted">_Passphrase:</property> <property name="use_underline">True</property> + <property name="mnemonic_widget">passphraseEntry</property> <attributes> <attribute name="font-desc" value="Cantarell 11"/> </attributes> diff --git a/pyanaconda/ui/gui/spokes/filter.glade b/pyanaconda/ui/gui/spokes/filter.glade index f7acb1c..00d3338 100644 --- a/pyanaconda/ui/gui/spokes/filter.glade +++ b/pyanaconda/ui/gui/spokes/filter.glade @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.18.3 --> +<!-- Generated with glade 3.19.0 --> <interface> <requires lib="gtk+" version="3.0"/> <requires lib="AnacondaWidgets" version="1.0"/> @@ -332,6 +332,7 @@ <property name="can_focus">False</property> <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|Search">Search Res_ults:</property> <property name="use_underline">True</property> + <property name="mnemonic_widget">searchTreeView</property> <property name="xalign">0</property> </object> <packing>
From: David Shea dshea@redhat.com
This whole box is an unshown placeholder of some sort and it's messing up my shiny new test. --- pyanaconda/ui/gui/spokes/network.glade | 1 - 1 file changed, 1 deletion(-)
diff --git a/pyanaconda/ui/gui/spokes/network.glade b/pyanaconda/ui/gui/spokes/network.glade index 2a7c19a..177ce80 100644 --- a/pyanaconda/ui/gui/spokes/network.glade +++ b/pyanaconda/ui/gui/spokes/network.glade @@ -2175,7 +2175,6 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="label" translatable="yes" context="GUI|Network">_Airplane Mode</property> - <property name="use_underline">True</property> </object> <packing> <property name="expand">False</property>
Shouldn't the underscore from the label be removed too?
Eh, I guess. It's not shown at all and I didn't know if @rvykydal had any plans for it so I was trying not mess with it too much. I think I'll just stick a GtkSwitch somewhere in there to connect the label to, since I assume that's the eventual goal.
Added label: ACK.
Looks good to me other than the question above
Pushed.
Closed.
anaconda-patches@lists.fedorahosted.org