From 2c00901aa6ab7f1a0ae94822dc718abff10bcc3a Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Thu, 28 Nov 2019 13:47:37 +0100 Subject: [PATCH 01/18] scripts: Add qsscheck.py script --- scripts/qsscheck.py | 141 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100755 scripts/qsscheck.py diff --git a/scripts/qsscheck.py b/scripts/qsscheck.py new file mode 100755 index 000000000000..2b345531b27f --- /dev/null +++ b/scripts/qsscheck.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import argparse +import fnmatch +import os +import os.path +import re +import sys +import tinycss.css21 +import PyQt5.QtWidgets + + +def get_skins(mixxx_path): + skins_path = os.path.join(mixxx_path, 'res', 'skins') + for entry in os.scandir(skins_path): + if entry.is_dir(): + yield entry.name + + +def make_glob(name): + return re.sub(r'<.*>', '*', name) + + +def get_global_names(mixxx_path): + classnames = set() + objectnames = set() + for root, dirs, fnames in os.walk(os.path.join(mixxx_path, 'src')): + for fname in fnames: + ext = os.path.splitext(fname)[1] + if ext in ('.h', '.cpp'): + fpath = os.path.join(root, fname) + with open(fpath, mode='r') as f: + for line in f: + classnames.update(set( + re.findall(r'^\s*class\s+([\w_]+)', line))) + objectnames.update(set( + re.findall(r'setObjectName\(.*"([^"]+)"', line))) + elif ext == '.ui': + fpath = os.path.join(root, fname) + with open(fpath, mode='r') as f: + objectnames.update(set( + re.findall(r']+name="([^"]+)"', f.read()))) + return classnames, objectnames + + +def get_skin_objectnames(mixxx_path, skin): + skin_path = os.path.join(mixxx_path, 'res', 'skins', skin) + for root, dirs, fnames in os.walk(skin_path): + for fname in fnames: + if os.path.splitext(fname)[1] == '.xml': + fpath = os.path.join(root, fname) + with open(fpath, mode='r') as f: + for line in f: + yield from re.findall( + r'(.*)', line) + yield from re.findall( + r'' + r'(.*)', line) + + +def get_skin_stylesheets(mixxx_path, skin): + cssparser = tinycss.css21.CSS21Parser() + skin_path = os.path.join(mixxx_path, 'res', 'skins', skin) + for filename in os.listdir(skin_path): + if os.path.splitext(filename)[1] != '.qss': + continue + qss_path = os.path.join(skin_path, filename) + stylesheet = cssparser.parse_stylesheet_file(qss_path) + yield qss_path, stylesheet + + +def check_stylesheet(stylesheet, classnames, objectnames): + for rule in stylesheet.rules: + if not isinstance(rule, tinycss.css21.RuleSet): + continue + for token in rule.selector: + if token.type == 'IDENT': + if not re.match(r'[A-Z]\w+', token.value): + continue + if token.value in classnames: + continue + if token.value in dir(PyQt5.QtWidgets): + continue + yield (token, 'Unknown widget class "%s"' % token.value) + + elif token.type == 'HASH': + value = token.value[1:] + if value in objectnames: + continue + + if any(fnmatch.fnmatchcase(value, make_glob(objname)) + for objname in objectnames if '<' in objname): + continue + + yield (token, 'Unknown object name "%s"' % token.value) + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('-s', '--skin', help='skin name') + parser.add_argument('-i', '--ignore', default='', + help='glob pattern to ignore') + parser.add_argument('mixxx_path', help='Mixxx path') + args = parser.parse_args(argv) + + mixxx_path = args.mixxx_path + ignore_pattern = args.ignore.split(',') + skins = set(get_skins(mixxx_path)) + if args.skin: + skins = set(skin for skin in skins if skin == args.skin) + + if not skins: + print('No skins to check') + return 1 + + status = 0 + classnames, objectnames = get_global_names(mixxx_path) + for skin in sorted(skins): + skin_objectnames = objectnames.union(set( + get_skin_objectnames(mixxx_path, skin))) + for qss_path, stylesheet in get_skin_stylesheets(mixxx_path, skin): + for error in stylesheet.errors: + status = 2 + print('%s:%d:%d: %s - %s' % ( + qss_path, error.line, error.column, + error.__class__.__name__, error.reason, + )) + for token, message in check_stylesheet( + stylesheet, classnames, skin_objectnames): + if any(fnmatch.fnmatchcase(token.value, pattern) + for pattern in ignore_pattern): + continue + status = 2 + print('%s:%d:%d: %s' % ( + qss_path, token.line, token.column, message, + )) + return status + + +if __name__ == '__main__': + sys.exit(main()) From 151fe9e221d78170042c070eadc99da8af8ff36f Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Thu, 28 Nov 2019 13:56:22 +0100 Subject: [PATCH 02/18] .travis.yml: Run qsscheck.py on Linux CI builds --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0f1c6a4a42f9..fc351bc47b8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,6 +54,10 @@ addons: - qtscript5-dev - qt5keychain-dev - scons + - python3 + - python3-pip + - python3-pyqt5 + - python3-setuptools homebrew: update: true packages: @@ -82,6 +86,9 @@ addons: - taglib - wavpack +before_install: + - pip3 install tinycss + install: # Build flags common to OS X and Linux. # Parallel builds are important for avoiding OSX build timeouts. @@ -120,6 +127,7 @@ script: # lldb doesn't provide an easy way to exit 1 on error: # https://bugs.llvm.org/show_bug.cgi?id=27326 - if [ "$TRAVIS_OS_NAME" = "osx" ]; then lldb ./mixxx-test --batch -o run -o quit -k 'thread backtrace all' -k "script import os; os._exit(1)"; fi + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then ./scripts/qsscheck.py . ; fi notifications: webhooks: From 66c5ab408d466e676c2bd557e41568da98ec252b Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 29 Nov 2019 12:18:55 +0100 Subject: [PATCH 03/18] .travis.yml: Cleanup travis config and run linter as separate job --- .travis.yml | 85 ++++++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc351bc47b8f..0804cb091736 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,54 @@ language: c++ + +# Build flags common to OS X and Linux. +# Parallel builds are important for avoiding OSX build timeouts. +# We turn off verbose output to avoid going over the 4MB output limit. +# TODO(2019-07-21): Add "ffmpeg=1" if FFmpeg 4.x becomes available in Ubuntu +env: COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0" + matrix: include: - os: linux dist: xenial - sudo: required + before_install: + - pip3 install tinycss + script: + - ./scripts/qsscheck.py . + + - os: linux + dist: xenial compiler: gcc + # Ubuntu Xenial build prerequisites + env: EXTRA_FLAGS="localecompare=1" + install: + - scons $COMMON_FLAGS $EXTRA_FLAGS + script: + # NOTE(sblaisot): 2018-01-02 removing gdb wrapper on linux due to a bug in + # return code in order to avoid having a successful build when a test fail. + # https://bugs.launchpad.net/mixxx/+bug/1699689 + - ./mixxx-test + + - os: osx compiler: clang + # Workaround for bug in libopus's opus.h including + # instead of . + # Virtual X (Xvfb) is needed for analyzer waveform tests + env: >- + CFLAGS="-isystem /usr/local/include/opus" + CXXFLAGS="-isystem /usr/local/include/opus" + DISPLAY=:99.0 + before_install: + - export QTDIR="$(find /usr/local/Cellar/qt -d 1 | tail -n 1)" + install: + - scons $COMMON_FLAGS $EXTRA_FLAGS + before_script: + - export + script: + # lldb doesn't provide an easy way to exit 1 on error: + # https://bugs.llvm.org/show_bug.cgi?id=27326 + - lldb ./mixxx-test --batch -o run -o quit -k 'thread backtrace all' -k "script import os; os._exit(1)" git: depth: 1 @@ -86,48 +127,6 @@ addons: - taglib - wavpack -before_install: - - pip3 install tinycss - -install: - # Build flags common to OS X and Linux. - # Parallel builds are important for avoiding OSX build timeouts. - # We turn off verbose output to avoid going over the 4MB output limit. - # TODO(2019-07-21): Add "ffmpeg=1" if FFmpeg 4.x becomes available in Ubuntu - - export COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0" - - # Ubuntu Xenial build prerequisites - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then export EXTRA_FLAGS="localecompare=1"; fi - - # Define QTDIR. - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then export QTDIR=$(find /usr/local/Cellar/qt -d 1 | tail -n 1); fi - - # Workaround for bug in libopus's opus.h including - # instead of . - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then export CXXFLAGS="-isystem /usr/local/include/opus"; fi - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then export CFLAGS="-isystem /usr/local/include/opus"; fi - - # NOTE(rryan): 2016-11-15 we are experiencing Travis timeouts for the OSX - # build. Turning off optimizations to see if that speeds up compile times. - # TODO(rryan): localecompare doesn't work on Travis with qt5 for some reason. - # TODO(2019-07-21): Move "ffmpeg=1" into COMMON_FLAGS if FFmpeg 4.x becomes available in Ubuntu - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then export EXTRA_FLAGS="ffmpeg=1 optimize=none asan=0 localecompare=0"; fi - - - scons $COMMON_FLAGS $EXTRA_FLAGS - -before_script: - # Virtual X (Xvfb) is needed for analyzer waveform tests - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then export DISPLAY=:99.0; fi - -script: - # NOTE(sblaisot): 2018-01-02 removing gdb wrapper on linux due to a bug in - # return code in order to avoid having a successful build when a test fail. - # https://bugs.launchpad.net/mixxx/+bug/1699689 - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then ./mixxx-test; fi - # lldb doesn't provide an easy way to exit 1 on error: - # https://bugs.llvm.org/show_bug.cgi?id=27326 - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then lldb ./mixxx-test --batch -o run -o quit -k 'thread backtrace all' -k "script import os; os._exit(1)"; fi - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then ./scripts/qsscheck.py . ; fi notifications: webhooks: From dceea542cff314fa4b00953f707a4909a067ed88 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 29 Nov 2019 12:30:53 +0100 Subject: [PATCH 04/18] .travis.yml: Add job names --- .travis.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0804cb091736..a78e831410bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,14 +9,16 @@ env: COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtu matrix: include: - - os: linux + - name: qsscheck + os: linux dist: xenial before_install: - pip3 install tinycss script: - ./scripts/qsscheck.py . - - os: linux + - name: Ubuntu/gcc build + os: linux dist: xenial compiler: gcc # Ubuntu Xenial build prerequisites @@ -29,8 +31,8 @@ matrix: # https://bugs.launchpad.net/mixxx/+bug/1699689 - ./mixxx-test - - - os: osx + - name: OSX/clang build + os: osx compiler: clang # Workaround for bug in libopus's opus.h including # instead of . From 44f970e0d9cb00525f9b42d355b7ead6265bd87d Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 29 Nov 2019 13:49:52 +0100 Subject: [PATCH 05/18] .travis.yml: Fix setting global env variables --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a78e831410bc..3a5ba246fa9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,9 @@ language: c++ # Parallel builds are important for avoiding OSX build timeouts. # We turn off verbose output to avoid going over the 4MB output limit. # TODO(2019-07-21): Add "ffmpeg=1" if FFmpeg 4.x becomes available in Ubuntu -env: COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0" +env: + global: + - COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0" matrix: include: From 4acbcfb1e995c5cbbe5a614b52393e8a78b8652a Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 29 Nov 2019 13:12:38 +0100 Subject: [PATCH 06/18] .travis.yml: Remove unused before_script for osx builds --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3a5ba246fa9b..696babbf6a99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,8 +47,6 @@ matrix: - export QTDIR="$(find /usr/local/Cellar/qt -d 1 | tail -n 1)" install: - scons $COMMON_FLAGS $EXTRA_FLAGS - before_script: - - export script: # lldb doesn't provide an easy way to exit 1 on error: # https://bugs.llvm.org/show_bug.cgi?id=27326 From deb4c23423b6e4d4cdc89d96ec3386c1a913b25f Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 29 Nov 2019 13:13:43 +0100 Subject: [PATCH 07/18] .travis.yml: Print QTDIR on osx builds --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 696babbf6a99..6a4d4501e354 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,6 +45,7 @@ matrix: DISPLAY=:99.0 before_install: - export QTDIR="$(find /usr/local/Cellar/qt -d 1 | tail -n 1)" + - echo "QTDIR=$QTDIR" install: - scons $COMMON_FLAGS $EXTRA_FLAGS script: From 65724d57c0c143ab5e961afdf99c0348d7a8aea1 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 29 Nov 2019 13:40:56 +0100 Subject: [PATCH 08/18] .travis.yml: Use python 3.7 by default on OSX --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6a4d4501e354..a7b362f67446 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: c++ - # Build flags common to OS X and Linux. # Parallel builds are important for avoiding OSX build timeouts. # We turn off verbose output to avoid going over the 4MB output limit. @@ -36,6 +35,7 @@ matrix: - name: OSX/clang build os: osx compiler: clang + python: 3.7 # Workaround for bug in libopus's opus.h including # instead of . # Virtual X (Xvfb) is needed for analyzer waveform tests From 91c706417e20f1c5e566741a3f3ba7db182622a7 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 29 Nov 2019 13:41:34 +0100 Subject: [PATCH 09/18] .travis.yml: Replace "matrix" alias with more descriptive "jobs" --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a7b362f67446..43ad114e2b68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ env: global: - COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0" -matrix: +jobs: include: - name: qsscheck os: linux From 4a075fb31333396fd2aca81bc3ee6b83e8dc006c Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 29 Nov 2019 14:27:34 +0100 Subject: [PATCH 10/18] Revert ".travis.yml: Use python 3.7 by default on OSX" This reverts commit 65724d57c0c143ab5e961afdf99c0348d7a8aea1. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 43ad114e2b68..da271c205883 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: c++ + # Build flags common to OS X and Linux. # Parallel builds are important for avoiding OSX build timeouts. # We turn off verbose output to avoid going over the 4MB output limit. @@ -35,7 +36,6 @@ jobs: - name: OSX/clang build os: osx compiler: clang - python: 3.7 # Workaround for bug in libopus's opus.h including # instead of . # Virtual X (Xvfb) is needed for analyzer waveform tests From 35e4801d475e0f05a5a6745d8051304bc868419c Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 29 Nov 2019 14:29:56 +0100 Subject: [PATCH 11/18] .travis.yml: Only install python3 on qsscheck --- .travis.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index da271c205883..6d651b39928b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,14 @@ jobs: - pip3 install tinycss script: - ./scripts/qsscheck.py . + addons: + apt: + packages: + - python3 + - python3-pip + - python3-pyqt5 + - python3-setuptools + - python3-wheel - name: Ubuntu/gcc build os: linux @@ -98,10 +106,6 @@ addons: - qtscript5-dev - qt5keychain-dev - scons - - python3 - - python3-pip - - python3-pyqt5 - - python3-setuptools homebrew: update: true packages: From 779fd9f8fc06ae4a8151f0688f9dcef1f936cd45 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 1 Dec 2019 17:33:10 +0100 Subject: [PATCH 12/18] scripts/qsscheck: Pre-compile regexes --- scripts/qsscheck.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/scripts/qsscheck.py b/scripts/qsscheck.py index 2b345531b27f..4d368541b503 100755 --- a/scripts/qsscheck.py +++ b/scripts/qsscheck.py @@ -10,6 +10,16 @@ import PyQt5.QtWidgets +RE_CPP_CLASSNAME = re.compile(r'^\s*class\s+([\w_]+)') +RE_CPP_OBJNAME = re.compile(r'setObjectName\(.*"([^"]+)"') +RE_UI_OBJNAME = re.compile(r']+name="([^"]+)"') +RE_XML_OBJNAME = re.compile(r'(.*)') +RE_XML_OBJNAME_SETVAR = re.compile( + r'(.*)') +RE_CLASSNAME = re.compile(r'^[A-Z]\w+$') +RE_OBJNAME_VARTAG = re.compile(r'<.*>') + + def get_skins(mixxx_path): skins_path = os.path.join(mixxx_path, 'res', 'skins') for entry in os.scandir(skins_path): @@ -18,7 +28,7 @@ def get_skins(mixxx_path): def make_glob(name): - return re.sub(r'<.*>', '*', name) + return RE_OBJNAME_VARTAG.sub('*', name) def get_global_names(mixxx_path): @@ -31,15 +41,12 @@ def get_global_names(mixxx_path): fpath = os.path.join(root, fname) with open(fpath, mode='r') as f: for line in f: - classnames.update(set( - re.findall(r'^\s*class\s+([\w_]+)', line))) - objectnames.update(set( - re.findall(r'setObjectName\(.*"([^"]+)"', line))) + classnames.update(set(RE_CPP_CLASSNAME.findall(line))) + objectnames.update(set(RE_CPP_OBJNAME.findall(line))) elif ext == '.ui': fpath = os.path.join(root, fname) with open(fpath, mode='r') as f: - objectnames.update(set( - re.findall(r']+name="([^"]+)"', f.read()))) + objectnames.update(set(RE_UI_OBJNAME.findall(f.read()))) return classnames, objectnames @@ -51,11 +58,8 @@ def get_skin_objectnames(mixxx_path, skin): fpath = os.path.join(root, fname) with open(fpath, mode='r') as f: for line in f: - yield from re.findall( - r'(.*)', line) - yield from re.findall( - r'' - r'(.*)', line) + yield from RE_XML_OBJNAME.findall(line) + yield from RE_XML_OBJNAME_SETVAR.findall(line) def get_skin_stylesheets(mixxx_path, skin): @@ -75,7 +79,7 @@ def check_stylesheet(stylesheet, classnames, objectnames): continue for token in rule.selector: if token.type == 'IDENT': - if not re.match(r'[A-Z]\w+', token.value): + if not RE_CLASSNAME.match(token.value): continue if token.value in classnames: continue From d85fadd9d77b4f49dadc5f65d910953f46a362b7 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 1 Dec 2019 17:36:04 +0100 Subject: [PATCH 13/18] scripts/qsscheck: Reduce indention depth using continue statement --- scripts/qsscheck.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/qsscheck.py b/scripts/qsscheck.py index 4d368541b503..daa8ea084b83 100755 --- a/scripts/qsscheck.py +++ b/scripts/qsscheck.py @@ -54,12 +54,14 @@ def get_skin_objectnames(mixxx_path, skin): skin_path = os.path.join(mixxx_path, 'res', 'skins', skin) for root, dirs, fnames in os.walk(skin_path): for fname in fnames: - if os.path.splitext(fname)[1] == '.xml': - fpath = os.path.join(root, fname) - with open(fpath, mode='r') as f: - for line in f: - yield from RE_XML_OBJNAME.findall(line) - yield from RE_XML_OBJNAME_SETVAR.findall(line) + if os.path.splitext(fname)[1] != '.xml': + continue + + fpath = os.path.join(root, fname) + with open(fpath, mode='r') as f: + for line in f: + yield from RE_XML_OBJNAME.findall(line) + yield from RE_XML_OBJNAME_SETVAR.findall(line) def get_skin_stylesheets(mixxx_path, skin): From 6360f9fd7579ac2f04200cd9b5e49e23b7eabed8 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 1 Dec 2019 17:47:28 +0100 Subject: [PATCH 14/18] scripts/qsscheck: Only run glob-matching for actual glob objectnames --- scripts/qsscheck.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/qsscheck.py b/scripts/qsscheck.py index daa8ea084b83..5ced325ab92d 100755 --- a/scripts/qsscheck.py +++ b/scripts/qsscheck.py @@ -27,10 +27,6 @@ def get_skins(mixxx_path): yield entry.name -def make_glob(name): - return RE_OBJNAME_VARTAG.sub('*', name) - - def get_global_names(mixxx_path): classnames = set() objectnames = set() @@ -75,7 +71,7 @@ def get_skin_stylesheets(mixxx_path, skin): yield qss_path, stylesheet -def check_stylesheet(stylesheet, classnames, objectnames): +def check_stylesheet(stylesheet, classnames, objectnames, objectnames_fuzzy): for rule in stylesheet.rules: if not isinstance(rule, tinycss.css21.RuleSet): continue @@ -94,8 +90,8 @@ def check_stylesheet(stylesheet, classnames, objectnames): if value in objectnames: continue - if any(fnmatch.fnmatchcase(value, make_glob(objname)) - for objname in objectnames if '<' in objname): + if any(fnmatch.fnmatchcase(value, objname) + for objname in objectnames_fuzzy): continue yield (token, 'Unknown object name "%s"' % token.value) @@ -122,8 +118,17 @@ def main(argv=None): status = 0 classnames, objectnames = get_global_names(mixxx_path) for skin in sorted(skins): - skin_objectnames = objectnames.union(set( - get_skin_objectnames(mixxx_path, skin))) + # If the skin objectname is something like 'Deck', + # then replace it with 'Deck*' and use glob-like matching + skin_objectnames = set(objectnames) + skin_objectnames_fuzzy = set() + for objname in set(get_skin_objectnames(mixxx_path, skin)): + new_objname = RE_OBJNAME_VARTAG.sub('*', objname) + if '*' in new_objname: + skin_objectnames_fuzzy.add(new_objname) + else: + skin_objectnames.add(new_objname) + for qss_path, stylesheet in get_skin_stylesheets(mixxx_path, skin): for error in stylesheet.errors: status = 2 @@ -132,7 +137,8 @@ def main(argv=None): error.__class__.__name__, error.reason, )) for token, message in check_stylesheet( - stylesheet, classnames, skin_objectnames): + stylesheet, classnames, + skin_objectnames, skin_objectnames_fuzzy): if any(fnmatch.fnmatchcase(token.value, pattern) for pattern in ignore_pattern): continue From af9f3507c18814e2407d33eff8f296e004bf1db7 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 1 Dec 2019 18:40:09 +0100 Subject: [PATCH 15/18] scripts/qsscheck: Improve qscheck script and add -p parameter --- scripts/qsscheck.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/scripts/qsscheck.py b/scripts/qsscheck.py index 5ced325ab92d..5a375a86d2f4 100755 --- a/scripts/qsscheck.py +++ b/scripts/qsscheck.py @@ -20,11 +20,11 @@ RE_OBJNAME_VARTAG = re.compile(r'<.*>') -def get_skins(mixxx_path): - skins_path = os.path.join(mixxx_path, 'res', 'skins') - for entry in os.scandir(skins_path): +def get_skins(path): + for entry in os.scandir(path): if entry.is_dir(): - yield entry.name + yield entry.name, os.path.join(path, entry.name) + def get_global_names(mixxx_path): @@ -46,8 +46,7 @@ def get_global_names(mixxx_path): return classnames, objectnames -def get_skin_objectnames(mixxx_path, skin): - skin_path = os.path.join(mixxx_path, 'res', 'skins', skin) +def get_skin_objectnames(skin_path): for root, dirs, fnames in os.walk(skin_path): for fname in fnames: if os.path.splitext(fname)[1] != '.xml': @@ -60,9 +59,8 @@ def get_skin_objectnames(mixxx_path, skin): yield from RE_XML_OBJNAME_SETVAR.findall(line) -def get_skin_stylesheets(mixxx_path, skin): +def get_skin_stylesheets(skin_path): cssparser = tinycss.css21.CSS21Parser() - skin_path = os.path.join(mixxx_path, 'res', 'skins', skin) for filename in os.listdir(skin_path): if os.path.splitext(filename)[1] != '.qss': continue @@ -98,18 +96,27 @@ def check_stylesheet(stylesheet, classnames, objectnames, objectnames_fuzzy): def main(argv=None): - parser = argparse.ArgumentParser() - parser.add_argument('-s', '--skin', help='skin name') + parser = argparse.ArgumentParser('qsscheck', description='Check Mixxx QSS stylesheets for non-existing object/class names') + parser.add_argument('-p', '--extra-skins-path', + help='Additonal skin path, to check (.e.g. ' + '"~/.mixxx/skins")') + parser.add_argument('-s', '--skin', help='Only check skin with this name') parser.add_argument('-i', '--ignore', default='', - help='glob pattern to ignore') - parser.add_argument('mixxx_path', help='Mixxx path') + help='Glob pattern of class/object names to ignore ' + '(e.g. "#Test*"), separated by commas') + parser.add_argument('mixxx_path', help='Path of Mixxx sources/git repo') args = parser.parse_args(argv) mixxx_path = args.mixxx_path ignore_pattern = args.ignore.split(',') - skins = set(get_skins(mixxx_path)) + + skins_path = os.path.join(mixxx_path, 'res', 'skins') + skins = set(get_skins(skins_path)) + if args.extra_skins_path: + skins.update(set(get_skins(args.extra_skins_path))) + if args.skin: - skins = set(skin for skin in skins if skin == args.skin) + skins = set((name, path) for name, path in skins if name == args.skin) if not skins: print('No skins to check') @@ -117,19 +124,19 @@ def main(argv=None): status = 0 classnames, objectnames = get_global_names(mixxx_path) - for skin in sorted(skins): + for skin_name, skin_path in sorted(skins): # If the skin objectname is something like 'Deck', # then replace it with 'Deck*' and use glob-like matching - skin_objectnames = set(objectnames) + skin_objectnames = objectnames.copy() skin_objectnames_fuzzy = set() - for objname in set(get_skin_objectnames(mixxx_path, skin)): + for objname in get_skin_objectnames(skin_path): new_objname = RE_OBJNAME_VARTAG.sub('*', objname) if '*' in new_objname: skin_objectnames_fuzzy.add(new_objname) else: skin_objectnames.add(new_objname) - for qss_path, stylesheet in get_skin_stylesheets(mixxx_path, skin): + for qss_path, stylesheet in get_skin_stylesheets(skin_path): for error in stylesheet.errors: status = 2 print('%s:%d:%d: %s - %s' % ( From b723e5fba16a0ecc5b591c2a899b5518ef59d4ff Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 1 Dec 2019 19:03:48 +0100 Subject: [PATCH 16/18] scripts/qsscheck: Move core logic from main() into separate function --- scripts/qsscheck.py | 65 ++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/scripts/qsscheck.py b/scripts/qsscheck.py index 5a375a86d2f4..edf78df5c5ed 100755 --- a/scripts/qsscheck.py +++ b/scripts/qsscheck.py @@ -95,6 +95,37 @@ def check_stylesheet(stylesheet, classnames, objectnames, objectnames_fuzzy): yield (token, 'Unknown object name "%s"' % token.value) +def check_skins(mixxx_path, skins, ignore_patterns=()): + classnames, objectnames = get_global_names(mixxx_path) + for skin_name, skin_path in sorted(skins): + # If the skin objectname is something like 'Deck', + # then replace it with 'Deck*' and use glob-like matching + skin_objectnames = objectnames.copy() + skin_objectnames_fuzzy = set() + for objname in get_skin_objectnames(skin_path): + new_objname = RE_OBJNAME_VARTAG.sub('*', objname) + if '*' in new_objname: + skin_objectnames_fuzzy.add(new_objname) + else: + skin_objectnames.add(new_objname) + + for qss_path, stylesheet in get_skin_stylesheets(skin_path): + for error in stylesheet.errors: + yield '%s:%d:%d: %s - %s' % ( + qss_path, error.line, error.column, + error.__class__.__name__, error.reason, + ) + for token, message in check_stylesheet( + stylesheet, classnames, + skin_objectnames, skin_objectnames_fuzzy): + if any(fnmatch.fnmatchcase(token.value, pattern) + for pattern in ignore_patterns): + continue + yield '%s:%d:%d: %s' % ( + qss_path, token.line, token.column, message, + ) + + def main(argv=None): parser = argparse.ArgumentParser('qsscheck', description='Check Mixxx QSS stylesheets for non-existing object/class names') parser.add_argument('-p', '--extra-skins-path', @@ -108,7 +139,6 @@ def main(argv=None): args = parser.parse_args(argv) mixxx_path = args.mixxx_path - ignore_pattern = args.ignore.split(',') skins_path = os.path.join(mixxx_path, 'res', 'skins') skins = set(get_skins(skins_path)) @@ -123,36 +153,9 @@ def main(argv=None): return 1 status = 0 - classnames, objectnames = get_global_names(mixxx_path) - for skin_name, skin_path in sorted(skins): - # If the skin objectname is something like 'Deck', - # then replace it with 'Deck*' and use glob-like matching - skin_objectnames = objectnames.copy() - skin_objectnames_fuzzy = set() - for objname in get_skin_objectnames(skin_path): - new_objname = RE_OBJNAME_VARTAG.sub('*', objname) - if '*' in new_objname: - skin_objectnames_fuzzy.add(new_objname) - else: - skin_objectnames.add(new_objname) - - for qss_path, stylesheet in get_skin_stylesheets(skin_path): - for error in stylesheet.errors: - status = 2 - print('%s:%d:%d: %s - %s' % ( - qss_path, error.line, error.column, - error.__class__.__name__, error.reason, - )) - for token, message in check_stylesheet( - stylesheet, classnames, - skin_objectnames, skin_objectnames_fuzzy): - if any(fnmatch.fnmatchcase(token.value, pattern) - for pattern in ignore_pattern): - continue - status = 2 - print('%s:%d:%d: %s' % ( - qss_path, token.line, token.column, message, - )) + for message in check_skins(mixxx_path, skins, args.ignore.split(',')): + print(message) + status = 2 return status From 8e29cf2667302f98a3529faa6bcc953351d35993 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 1 Dec 2019 19:04:45 +0100 Subject: [PATCH 17/18] scripts/qsscheck: Fix line exceeding maximum line lengths --- scripts/qsscheck.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/qsscheck.py b/scripts/qsscheck.py index edf78df5c5ed..de4868112d7e 100755 --- a/scripts/qsscheck.py +++ b/scripts/qsscheck.py @@ -127,7 +127,9 @@ def check_skins(mixxx_path, skins, ignore_patterns=()): def main(argv=None): - parser = argparse.ArgumentParser('qsscheck', description='Check Mixxx QSS stylesheets for non-existing object/class names') + parser = argparse.ArgumentParser('qsscheck', description='Check Mixxx QSS ' + 'stylesheets for non-existing ' + 'object/class names') parser.add_argument('-p', '--extra-skins-path', help='Additonal skin path, to check (.e.g. ' '"~/.mixxx/skins")') From f60bdd8db7621833cdab977504c68b783a2fec74 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 1 Dec 2019 19:05:28 +0100 Subject: [PATCH 18/18] scripts/qsscheck: Add docstrings for all functions --- scripts/qsscheck.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/qsscheck.py b/scripts/qsscheck.py index de4868112d7e..c0d331815216 100755 --- a/scripts/qsscheck.py +++ b/scripts/qsscheck.py @@ -21,13 +21,14 @@ def get_skins(path): + """Yields (skin_name, skin_path) tuples for each skin directory in path.""" for entry in os.scandir(path): if entry.is_dir(): yield entry.name, os.path.join(path, entry.name) - def get_global_names(mixxx_path): + """Returns 2 sets with all class and object names in the Mixx codebase.""" classnames = set() objectnames = set() for root, dirs, fnames in os.walk(os.path.join(mixxx_path, 'src')): @@ -47,6 +48,12 @@ def get_global_names(mixxx_path): def get_skin_objectnames(skin_path): + """ + Yields all object names in the skin_path. + + Note the names may contain one or more tags, so it's + not enough to check if a name CSS object name is in this list using "in". + """ for root, dirs, fnames in os.walk(skin_path): for fname in fnames: if os.path.splitext(fname)[1] != '.xml': @@ -60,6 +67,7 @@ def get_skin_objectnames(skin_path): def get_skin_stylesheets(skin_path): + """Yields (qss_path, stylesheet) tuples for each qss file in skin_path).""" cssparser = tinycss.css21.CSS21Parser() for filename in os.listdir(skin_path): if os.path.splitext(filename)[1] != '.qss': @@ -70,6 +78,7 @@ def get_skin_stylesheets(skin_path): def check_stylesheet(stylesheet, classnames, objectnames, objectnames_fuzzy): + """Yields (token, problem) tuples for each problem found in stylesheet.""" for rule in stylesheet.rules: if not isinstance(rule, tinycss.css21.RuleSet): continue @@ -96,6 +105,12 @@ def check_stylesheet(stylesheet, classnames, objectnames, objectnames_fuzzy): def check_skins(mixxx_path, skins, ignore_patterns=()): + """ + Yields error messages for skins using class/object names from mixxx_path. + + By providing a list of ignore_patterns, you can ignore certain class or + object names (e.g. #Test, #*Debug). + """ classnames, objectnames = get_global_names(mixxx_path) for skin_name, skin_path in sorted(skins): # If the skin objectname is something like 'Deck', @@ -127,6 +142,7 @@ def check_skins(mixxx_path, skins, ignore_patterns=()): def main(argv=None): + """Main method for handling command line arguments.""" parser = argparse.ArgumentParser('qsscheck', description='Check Mixxx QSS ' 'stylesheets for non-existing ' 'object/class names')