From e77ea6b0a9ec36000abbae49fe471eb86ba4ed8d Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 08:01:45 -0500 Subject: [PATCH 1/9] Add our configuration to be more configurable. --- configure.py | 58 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/configure.py b/configure.py index 73a5b47..e1ed4ee 100644 --- a/configure.py +++ b/configure.py @@ -15,12 +15,14 @@ import shutil import subprocess import sys +from pathlib import Path -home = os.path.dirname(os.path.realpath(__file__)) -dist = os.path.join(home, 'dist') -template_dir = os.path.join(home, 'template') -theme_dir = os.path.join(home, 'theme') -extension_dir = os.path.join(home, 'extension') +home_dir = os.path.dirname(os.path.realpath(__file__)) +dist_dir = os.path.join(home_dir, 'dist') +resources_dir = os.path.join(home_dir, 'resources') +template_dir = os.path.join(home_dir, 'template') +theme_dir = os.path.join(home_dir, 'theme') +extension_dir = os.path.join(home_dir, 'extension') def parse_args(argv=None): @@ -51,7 +53,13 @@ def parse_args(argv=None): parser.add_argument( '--no-qrc', help='do not build QRC resources.', - action='store_true' + action='store_true', + ) + parser.add_argument( + '--output-dir', + help='the default output directory path', + default=Path(dist_dir), + type=Path, ) parser.add_argument( '--qt-framework', @@ -304,7 +312,7 @@ def configure_stylesheet(config, style, qt_dist, style_prefix): file.write(contents) -def configure_style(config, style): +def configure_style(config, style, qt_dist): '''Configure the icons and stylesheet for a given style.''' def configure_qt(qt_dist, style_prefix): @@ -317,17 +325,20 @@ def configure_qt(qt_dist, style_prefix): # assets. This uses the resource system, AKA, # `url(:/dark/path/to/resource)`. if not config['no_qrc']: - configure_qt(dist, f':/{style}/') + configure_qt(qt_dist, f':/{style}/') -def write_qrc(config): +def write_qrc(config, qt_dist): '''Simple QRC writer.''' resources = [] for style in config['themes'].keys(): - files = os.listdir(f'{dist}/{style}') + files = os.listdir(f'{qt_dist}/{style}') resources += [f'{style}/{i}' for i in files] - with open(f'{dist}/{config["resource"]}', 'w') as file: + qrc_path = config['resource'] + if not os.path.isabs(qrc_path): + qrc_path = f'{qt_dist}/{qrc_path}' + with open(qrc_path, 'w') as file: print('', file=file) print(' ', file=file) for resource in sorted(resources): @@ -340,7 +351,7 @@ def configure(args): '''Configure all styles and write the files to a QRC file.''' if args.clean: - shutil.rmtree(dist, ignore_errors=True) + shutil.rmtree(args.output_dir, ignore_errors=True) # Need to convert our styles accordingly. config = { @@ -355,22 +366,23 @@ def configure(args): for extension in args.extensions: config['templates'].append(read_template_dir(f'{extension_dir}/{extension}')) + args.output_dir.mkdir(parents=True, exist_ok=True) for style in config['themes'].keys(): - configure_style(config, style) + configure_style(config, style, str(args.output_dir)) # Create and compile our resource files. if not args.no_qrc: - write_qrc(config) + write_qrc(config, str(args.output_dir)) if args.compiled_resource is not None: rcc = parse_rcc(args) - - command = [ - rcc, - f'{dist}/{args.resource}', - '-o', - f'{home}/{args.compiled_resource}' - ] - + resource_path = args.resource + compiled_resource_path = args.compiled_resource + if not os.path.isabs(resource_path): + resource_path = f'{args.output_dir}/{resource_path}' + if not os.path.isabs(compiled_resource_path): + compiled_resource_path = f'{resources_dir}/{compiled_resource_path}' + + command = [rcc, resource_path, '-o', compiled_resource_path] try: subprocess.check_output( command, @@ -398,7 +410,7 @@ def configure(args): raise SystemExit if args.qt_framework == "pyqt6": - fix_qt6_import(f'{home}/{args.compiled_resource}') + fix_qt6_import(compiled_resource_path) def fix_qt6_import(compiled_file): From 639c2a083fbea20a0da127ba71aff55bb2aee350 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 08:07:10 -0500 Subject: [PATCH 2/9] Add script to configure for all frameworks. --- ci/configure_all.sh | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 ci/configure_all.sh diff --git a/ci/configure_all.sh b/ci/configure_all.sh new file mode 100644 index 0000000..adafbf2 --- /dev/null +++ b/ci/configure_all.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# Run each configure for all supported frameworks, and store them in `dist/ci`. +# + +set -eux pipefail + +ci_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +project_home="$(dirname "${ci_home}")" +mkdir -p "${project_home}/dist/ci" +cd "${project_home}" + +# pop them into dist since it's ignored anyway +if [[ ! -v PYTHON ]]; then + PYTHON=python +fi +frameworks=("pyqt5" "pyqt6" "pyside6") +have_pyside=$(PYTHON -c 'import sys; print(sys.version_info < (3, 11))') +if [[ "${have_pyside}" == "True" ]]; then + frameworks+=("pyside2") +fi + +# NOTE: We need to make sure the scripts directory is added to the path +python_home=$(PYTHON -c 'import site; print(site.getsitepackages()[0])') +scripts_dir="${python_home}/scripts" +uname_s="$(uname -s)" +if [[ "${uname_s}" == MINGW* ]]; then + # want to convert C:/... to /c/... + scripts_dir=$(echo "/$scripts_dir" | sed -e 's/\\/\//g' -e 's/://') +fi +export PATH="${scripts_dir}:${PATH}" +for framework in "${frameworks[@]}"; do + "${PYTHON}" "${project_home}/configure.py" \ + --styles=all \ + --extensions=all \ + --qt-framework "${framework}" \ + --output-dir "${project_home}/dist/ci" \ + --resource "breeze_${framework}.qrc" \ + --compiled-resource "${project_home}/dist/ci/breeze_${framework}.py" + # this will auto-fail due to pipefail, checks the imports work + "${PYTHON}" -c "import os; os.chdir('dist/ci'); import breeze_${framework}" +done \ No newline at end of file From 9a627697f67fb875fb358797af4bff428e07eec3 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 08:30:28 -0500 Subject: [PATCH 3/9] Add tests for checking the theme. --- ci/configure_all.sh | 7 ++++++- ci/theme.sh | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) mode change 100644 => 100755 ci/configure_all.sh create mode 100755 ci/theme.sh diff --git a/ci/configure_all.sh b/ci/configure_all.sh old mode 100644 new mode 100755 index adafbf2..b80ff8f --- a/ci/configure_all.sh +++ b/ci/configure_all.sh @@ -1,7 +1,12 @@ #!/usr/bin/env bash # # Run each configure for all supported frameworks, and store them in `dist/ci`. -# +# This requires the correct frameworks to be installed: +# - PyQt5 +# - PyQt6 +# - PySide6 +# And if using Python 3.10 or earlier: +# - PySide2 set -eux pipefail diff --git a/ci/theme.sh b/ci/theme.sh new file mode 100755 index 0000000..63ec3ce --- /dev/null +++ b/ci/theme.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# +# Use scripts to check if the theme determination works. + +set -eux pipefail + +ci_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +project_home="$(dirname "${ci_home}")" +cd "${project_home}/example" + +if [[ ! -v PYTHON ]]; then + PYTHON=python +fi +theme=$("${PYTHON}" -c "import breeze_theme; print(breeze_theme.get_theme())") +if [[ "${theme}" != Theme.* ]]; then + >&2 echo "Unable to get the correct theme." + exit 1 +fi +"${PYTHON}" -c "import breeze_theme; print(breeze_theme.is_light())" +"${PYTHON}" -c "import breeze_theme; print(breeze_theme.is_dark())" From 074edc642662093efd7154b17d518f2147381818 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 12:46:39 -0500 Subject: [PATCH 4/9] Add Github workflows. --- .github/workflows/lint.yml | 44 ++ .github/workflows/theme.yml | 27 + ci/theme.sh | 20 - configure.py | 168 +++--- example/advanced-dock.py | 13 +- example/branchless/application.py | 38 +- example/breeze_theme.py | 173 +++--- example/dial.py | 23 +- example/lcd.py | 11 +- example/placeholder_text.py | 18 +- example/shared.py | 46 +- example/slider.py | 15 +- example/standard_icons.py | 55 +- example/titlebar.py | 169 +++--- example/url.py | 16 +- example/whatsthis.py | 7 +- example/widgets.py | 36 +- pyproject.toml | 581 ++++++++++++++++++++ ci/configure_all.sh => scripts/configure.sh | 18 +- scripts/fmt.sh | 14 + scripts/lint.sh | 17 + scripts/shared.sh | 8 + scripts/theme.sh | 24 + setup.cfg | 6 +- test/ui.py | 2 +- vcs.py | 43 +- 26 files changed, 1177 insertions(+), 415 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/theme.yml delete mode 100755 ci/theme.sh create mode 100644 pyproject.toml rename ci/configure_all.sh => scripts/configure.sh (71%) create mode 100755 scripts/fmt.sh create mode 100755 scripts/lint.sh create mode 100755 scripts/shared.sh create mode 100755 scripts/theme.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..73ba172 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,44 @@ +name: Linters + +on: [push] + +jobs: + lint-version-python: + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.11", "3.12"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint flake8 pyright + - name: Analysing the code with pylint + run: | + scripts/lint.sh + + lint-os-python: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint flake8 pyright + - name: Analysing the code with pylint + run: | + scripts/lint.sh diff --git a/.github/workflows/theme.yml b/.github/workflows/theme.yml new file mode 100644 index 0000000..893d6a5 --- /dev/null +++ b/.github/workflows/theme.yml @@ -0,0 +1,27 @@ +name: Theme + +on: [push] + +jobs: + theme-python: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + shell: bash + run: | + python -m pip install --upgrade pip + if [[ "$RUNNER_OS" == "Windows" ]]; then + python -m pip install winrt-Windows.UI.ViewManagement winrt-Windows.UI + fi + - name: Checking our Python imports. + run: | + scripts/theme.sh diff --git a/ci/theme.sh b/ci/theme.sh deleted file mode 100755 index 63ec3ce..0000000 --- a/ci/theme.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -# -# Use scripts to check if the theme determination works. - -set -eux pipefail - -ci_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" -project_home="$(dirname "${ci_home}")" -cd "${project_home}/example" - -if [[ ! -v PYTHON ]]; then - PYTHON=python -fi -theme=$("${PYTHON}" -c "import breeze_theme; print(breeze_theme.get_theme())") -if [[ "${theme}" != Theme.* ]]; then - >&2 echo "Unable to get the correct theme." - exit 1 -fi -"${PYTHON}" -c "import breeze_theme; print(breeze_theme.is_light())" -"${PYTHON}" -c "import breeze_theme; print(breeze_theme.is_dark())" diff --git a/configure.py b/configure.py index e1ed4ee..786631e 100644 --- a/configure.py +++ b/configure.py @@ -29,12 +29,7 @@ def parse_args(argv=None): '''Parse the command-line options.''' parser = argparse.ArgumentParser(description='Styles to configure for a Qt application.') - parser.add_argument( - '-v', - '--version', - action='version', - version=f'%(prog)s {__version__}' - ) + parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version__}') parser.add_argument( '--styles', help='comma-separate list of styles to configure. pass `all` to build all themes', @@ -68,12 +63,10 @@ def parse_args(argv=None): 'Note: building for PyQt6 requires PySide6-rcc to be installed.' ), choices=['pyqt5', 'pyqt6', 'pyside2', 'pyside6'], - default='pyqt5' + default='pyqt5', ) parser.add_argument( - '--clean', - help='clean dist directory prior to configuring themes.', - action='store_true' + '--clean', help='clean dist directory prior to configuring themes.', action='store_true' ) parser.add_argument( '--rcc', @@ -81,7 +74,7 @@ def parse_args(argv=None): 'path to the rcc executable. ' 'Overrides rcc of chosen framework. ' 'Only use if system cannot find the rcc exe.' - ) + ), ) parser.add_argument( '--compiled-resource', @@ -102,7 +95,7 @@ def load_json(path): # we don't want to prevent code from working without # a complex parser, so we do something very simple: # only remove lines starting with '//'. - with open(path) as file: + with open(path, encoding='utf-8') as file: lines = file.read().splitlines() lines = [i for i in lines if not i.strip().startswith('//')] return json.loads('\n'.join(lines)) @@ -115,7 +108,8 @@ def read_template_dir(directory): stylesheet = '' stylesheet_path = f'{directory}/stylesheet.qss.in' if os.path.exists(stylesheet_path): - stylesheet = open(f'{directory}/stylesheet.qss.in').read() + with open(f'{directory}/stylesheet.qss.in', encoding='utf-8') as style_file: + stylesheet = style_file.read() data = { 'stylesheet': stylesheet, 'icons': [], @@ -125,7 +119,8 @@ def read_template_dir(directory): else: icon_data = {} for file in glob.glob(f'{directory}/*.svg.in'): - svg = open(file).read() + with open(file, encoding='utf-8') as svg_file: + svg = svg_file.read() name = os.path.splitext(os.path.splitext(os.path.basename(file))[0])[0] if name in icon_data: replacements = icon_data[name] @@ -133,11 +128,13 @@ def read_template_dir(directory): # Need to find all the values inside the image. keys = re.findall(r'\^[0-9a-zA-Z_-]+\^', svg) replacements = [i[1:-1] for i in keys] - data['icons'].append({ - 'name': name, - 'svg': svg, - 'replacements': replacements, - }) + data['icons'].append( + { + 'name': name, + 'svg': svg, + 'replacements': replacements, + } + ) return data @@ -216,7 +213,7 @@ def parse_color(color): if color.startswith('#'): return parse_hexcolor(color) - elif color.startswith('rgb'): + if color.startswith('rgb'): return parse_rgba(color) raise NotImplementedError @@ -260,11 +257,11 @@ def replace_by_index(contents, theme, colors): # parse the color, get the correct value, and use only that # for the replacement. if key.endswith(':hex'): - color = theme[key[:-len(':hex')]] - rgb = [f"{i:02x}" for i in parse_color(color)[:3]] + color = theme[key[: -len(':hex')]] + rgb = [f'{i:02x}' for i in parse_color(color)[:3]] value = f'#{"".join(rgb)}' elif key.endswith(':opacity'): - color = theme[key[:-len(':opacity')]] + color = theme[key[: -len(':opacity')]] value = str(parse_color(color)[3]) else: value = theme[key] @@ -288,7 +285,7 @@ def configure_icons(config, style, qt_dist): for ext, colors in replacements.items(): contents = replace_by_index(icon['svg'], theme, colors) filename = f'{qt_dist}/{style}/{icon_basename(name, ext)}.svg' - with open(filename, 'w') as file: + with open(filename, 'w', encoding='utf-8') as file: file.write(contents) else: # Then we just have a list of replacements for the @@ -297,7 +294,7 @@ def configure_icons(config, style, qt_dist): assert isinstance(replacements, list) contents = replace_by_name(icon['svg'], theme, replacements) filename = f'{qt_dist}/{style}/{name}.svg' - with open(filename, 'w') as file: + with open(filename, 'w', encoding='utf-8') as file: file.write(contents) @@ -308,7 +305,7 @@ def configure_stylesheet(config, style, qt_dist, style_prefix): contents = replace_by_name(contents, config['themes'][style]) contents = contents.replace('^style^', style_prefix) - with open(f'{qt_dist}/{style}/stylesheet.qss', 'w') as file: + with open(f'{qt_dist}/{style}/stylesheet.qss', 'w', encoding='utf-8') as file: file.write(contents) @@ -338,7 +335,7 @@ def write_qrc(config, qt_dist): qrc_path = config['resource'] if not os.path.isabs(qrc_path): qrc_path = f'{qt_dist}/{qrc_path}' - with open(qrc_path, 'w') as file: + with open(qrc_path, 'w', encoding='utf-8') as file: print('', file=file) print(' ', file=file) for resource in sorted(resources): @@ -347,6 +344,51 @@ def write_qrc(config, qt_dist): print('', file=file) +def compile_resource(args): + '''Compile our resource file to a standalone Python file.''' + + rcc = parse_rcc(args) + resource_path = args.resource + compiled_resource_path = args.compiled_resource + if not os.path.isabs(resource_path): + resource_path = f'{args.output_dir}/{resource_path}' + if not os.path.isabs(compiled_resource_path): + compiled_resource_path = f'{resources_dir}/{compiled_resource_path}' + + command = [rcc, resource_path, '-o', compiled_resource_path] + try: + subprocess.check_output( + command, + stdin=subprocess.DEVNULL, + stderr=subprocess.PIPE, + shell=False, + ) + except subprocess.CalledProcessError as error: + if b'File does not exist' in error.stderr: + print('ERROR: Ensure qrc file exists or deselect "no-qrc" option!', file=sys.stderr) + else: + print(f'ERROR: Got an unknown error of "{error.stderr.decode("utf-8")}"!', file=sys.stderr) + raise SystemExit from error + except FileNotFoundError as error: + if args.rcc: + print('ERROR: rcc path invalid!', file=sys.stderr) + else: + print('ERROR: Ensure rcc executable exists for chosen framework!', file=sys.stderr) + print( + 'Required rcc for PyQt5: pyrcc5', + 'Required rcc for PySide6 & PyQt6: PySide6-rcc', + 'Required rcc for PySide2: PySide2-rcc', + '', + 'if using venv, activate it or provide path to rcc.', + sep='\n', + file=sys.stderr, + ) + raise SystemExit from error + + if args.qt_framework == 'pyqt6': + fix_qt6_import(compiled_resource_path) + + def configure(args): '''Configure all styles and write the files to a QRC file.''' @@ -354,12 +396,7 @@ def configure(args): shutil.rmtree(args.output_dir, ignore_errors=True) # Need to convert our styles accordingly. - config = { - 'themes': {}, - 'templates': [], - 'no_qrc': args.no_qrc, - 'resource': args.resource - } + config = {'themes': {}, 'templates': [], 'no_qrc': args.no_qrc, 'resource': args.resource} config['templates'].append(read_template_dir(template_dir)) for style in args.styles: config['themes'][style] = load_json(f'{theme_dir}/{style}.json') @@ -367,59 +404,23 @@ def configure(args): config['templates'].append(read_template_dir(f'{extension_dir}/{extension}')) args.output_dir.mkdir(parents=True, exist_ok=True) - for style in config['themes'].keys(): + for style in config['themes']: configure_style(config, style, str(args.output_dir)) # Create and compile our resource files. if not args.no_qrc: write_qrc(config, str(args.output_dir)) if args.compiled_resource is not None: - rcc = parse_rcc(args) - resource_path = args.resource - compiled_resource_path = args.compiled_resource - if not os.path.isabs(resource_path): - resource_path = f'{args.output_dir}/{resource_path}' - if not os.path.isabs(compiled_resource_path): - compiled_resource_path = f'{resources_dir}/{compiled_resource_path}' - - command = [rcc, resource_path, '-o', compiled_resource_path] - try: - subprocess.check_output( - command, - stdin=subprocess.DEVNULL, - stderr=subprocess.PIPE, - shell=False, - ) - except subprocess.CalledProcessError as error: - if b'File does not exist' in error.stderr: - print('ERROR: Ensure qrc file exists or deselect "no-qrc" option!', file=sys.stderr) - else: - print(f'ERROR: Got an unknown errir of "{error.stderr.decode("utf-8")}"!', file=sys.stderr) - raise SystemExit - except FileNotFoundError: - if args.rcc: - print("ERROR: rcc path invalid!", file=sys.stderr) - else: - print('ERROR: Ensure rcc executable exists for chosen framework!', file=sys.stderr) - print( - 'Required rcc for PyQt5: pyrcc5', - 'Required rcc for PySide6 & PyQt6: PySide6-rcc', - 'Required rcc for PySide2: PySide2-rcc', - '', - 'if using venv, activate it or provide path to rcc.', sep='\n', file=sys.stderr) - raise SystemExit - - if args.qt_framework == "pyqt6": - fix_qt6_import(compiled_resource_path) + compile_resource(args) def fix_qt6_import(compiled_file): '''Fix import after using PySide6-rcc to compile for PyQt6''' - with open(compiled_file, "r") as file: + with open(compiled_file, 'r', encoding='utf-8') as file: text = file.read() - text = text.replace("PySide6", "PyQt6") - with open(compiled_file, "w") as file: + text = text.replace('PySide6', 'PyQt6') + with open(compiled_file, 'w', encoding='utf-8') as file: file.write(text) @@ -427,16 +428,15 @@ def parse_rcc(args): '''Get rcc required for chosen framework''' if args.rcc: - rcc = args.rcc - else: - if args.qt_framework == 'pyqt6' or args.qt_framework == 'pyside6': - rcc = 'pyside6-rcc' - elif args.qt_framework == "pyqt5": - rcc = 'pyrcc5' - elif args.qt_framework == 'pyside2': - rcc = 'pyside2-rcc' - - return rcc + return args.rcc + if args.qt_framework in ('pyqt6', 'pyside6'): + return 'pyside6-rcc' + if args.qt_framework == 'pyqt5': + return 'pyrcc5' + if args.qt_framework == 'pyside2': + return 'pyside2-rcc' + + raise ValueError(f'Got an unsupported Qt framework of "{args.qt_framework}".') def main(argv=None): diff --git a/example/advanced-dock.py b/example/advanced-dock.py index 687606c..29b585b 100644 --- a/example/advanced-dock.py +++ b/example/advanced-dock.py @@ -29,20 +29,21 @@ Simple PyQt application using the advanced-docking-system. ''' -import shared +# pylint: disable=no-name-in-module,import-error + import sys +import shared + parser = shared.create_parser() parser.add_argument( - '--use-internal', - help='''use the dock manager internal stylesheet.''', - action='store_true' + '--use-internal', help='''use the dock manager internal stylesheet.''', action='store_true' ) # https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/blob/master/doc/user-guide.md#configuration-flags parser.add_argument( '--focus-highlighting', help='''use the focus highlighting (and other configuration flags).''', - action='store_true' + action='store_true', ) # setConfigFlag args, unknown = shared.parse_args(parser) @@ -114,7 +115,7 @@ def main(): # run window.setWindowState(compat.WindowMaximized) shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/branchless/application.py b/example/branchless/application.py index deccd06..94fbeef 100644 --- a/example/branchless/application.py +++ b/example/branchless/application.py @@ -22,23 +22,30 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +''' + branchless + ========== + + Simple PyQt application without branches for our QTreeViews. +''' + import os import sys HOME = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, os.path.dirname(HOME)) -import widgets -import shared +import shared # noqa # pylint: disable=wrong-import-position,import-error +import widgets # noqa # pylint: disable=wrong-import-position,import-error parser = shared.create_parser() args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) compat = shared.get_compat_definitions(args) -ICON_MAP = shared.get_icon_map(args, compat) +ICON_MAP = shared.get_icon_map(compat) -def set_stylesheet(args, app, compat): +def set_stylesheet(app): '''Set the application stylesheet.''' if args.stylesheet != 'native': @@ -46,11 +53,13 @@ def set_stylesheet(args, app, compat): qt_path = shared.get_stylesheet(resource_format) ext_path = os.path.join(HOME, 'stylesheet.qss.in') stylesheet = shared.read_qtext_file(qt_path, compat) - stylesheet += '\n' + open(ext_path, 'r').read() + with open(ext_path, 'r', encoding='utf-8') as file: + stylesheet += '\n' + file.read() app.setStyleSheet(stylesheet) def get_treeviews(parent, depth=1000): + '''Recursively get all tree views.''' for child in parent.children(): if isinstance(child, QtWidgets.QTreeView): yield child @@ -68,18 +77,9 @@ def main(): # setup ui ui = widgets.Ui() ui.setup(window) - ui.bt_delay_popup.addActions([ - ui.actionAction, - ui.actionAction_C - ]) - ui.bt_instant_popup.addActions([ - ui.actionAction, - ui.actionAction_C - ]) - ui.bt_menu_button_popup.addActions([ - ui.actionAction, - ui.actionAction_C - ]) + ui.bt_delay_popup.addActions([ui.actionAction, ui.actionAction_C]) + ui.bt_instant_popup.addActions([ui.actionAction, ui.actionAction_C]) + ui.bt_menu_button_popup.addActions([ui.actionAction, ui.actionAction_C]) window.setWindowTitle('Sample BreezeStyleSheets application.') # Add event triggers @@ -93,8 +93,8 @@ def main(): for tree in get_treeviews(window): tree.setObjectName("branchless") - set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + set_stylesheet(app) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/breeze_theme.py b/example/breeze_theme.py index a2230a4..817af58 100644 --- a/example/breeze_theme.py +++ b/example/breeze_theme.py @@ -41,6 +41,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ''' +# pylint: disable=import-error,no-member + import typing import ctypes import ctypes.util @@ -75,7 +77,7 @@ def from_string(value: str | None) -> 'Theme': value = value.lower() if value == 'dark': return Theme.DARK - elif value == 'light': + if value == 'light': return Theme.LIGHT raise ValueError(f'Got an invalid theme value of "{value}".') @@ -85,9 +87,9 @@ def to_string(self) -> str: # NOTE: This is for Py3.10 and earlier support. if self == Theme.DARK: return 'Dark' - elif self == Theme.LIGHT: + if self == Theme.LIGHT: return 'Light' - elif self == Theme.UNKNOWN: + if self == Theme.UNKNOWN: return 'Unknown' raise ValueError(f'Got an invalid theme value of "{self}".') @@ -104,7 +106,8 @@ def is_light_color(r: int, g: int, b: int) -> bool: Returns: If the color is perceived as light. ''' - return (((5 * g) + (2 * r) + b) > (8 * 128)) + return ((5 * g) + (2 * r) + b) > (8 * 128) + # region windows @@ -112,7 +115,7 @@ def is_light_color(r: int, g: int, b: int) -> bool: def _get_theme_windows() -> Theme: '''Get the current theme, as light or dark, for the system on Windows.''' - from winreg import HKEY_CURRENT_USER, OpenKey, QueryValueEx + from winreg import HKEY_CURRENT_USER, OpenKey, QueryValueEx # pyright: ignore[reportAttributeAccessIssue] # In HKEY_CURRENT_USER, get the Personalisation Key. try: @@ -125,10 +128,10 @@ def _get_theme_windows() -> Theme: # some headless Windows instances (e.g. GitHub Actions or Docker images) do not have this key # this is also not present if the user has never set the value. however, more recent Windows # installs will have this, starting at `10.0.10240.0`: - # https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/ui/apply-windows-themes#know-when-dark-mode-is-enabled + # https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/ui/apply-windows-themes#know-when-dark-mode-is-enabled # noqa # pylint: disable=line-too-long # # Note that the documentation is inverted: if the foreground is light, we are using DARK mode. - winver = sys.getwindowsversion() + winver = sys.getwindowsversion() # pyright: ignore[reportAttributeAccessIssue] if winver[:4] < (10, 0, 10240, 0): return Theme.UNKNOWN try: @@ -144,7 +147,7 @@ def _get_theme_windows() -> Theme: if use_light == 0: return Theme.DARK - elif use_light == 1: + if use_light == 1: return Theme.LIGHT return Theme.UNKNOWN @@ -152,13 +155,14 @@ def _get_theme_windows() -> Theme: def _listener_windows(callback: CallbackFn) -> None: '''Register an event listener for dark/light theme changes.''' - import ctypes.wintypes # pyright: ignore[reportMissingImports] + import ctypes.wintypes # pyright: ignore[reportMissingImports] # pylint: disable=redefined-outer-name global _advapi32 if _advapi32 is None: _advapi32 = _initialize_advapi32() advapi32 = _advapi32 + assert advapi32 is not None hkey = ctypes.wintypes.HKEY() advapi32.RegOpenKeyExA( @@ -169,16 +173,16 @@ def _listener_windows(callback: CallbackFn) -> None: ctypes.byref(hkey), ) - dwSize = ctypes.wintypes.DWORD(ctypes.sizeof(ctypes.wintypes.DWORD)) - queryValueLast = ctypes.wintypes.DWORD() - queryValue = ctypes.wintypes.DWORD() + size = ctypes.wintypes.DWORD(ctypes.sizeof(ctypes.wintypes.DWORD)) + query_last_value = ctypes.wintypes.DWORD() + query_value = ctypes.wintypes.DWORD() advapi32.RegQueryValueExA( hkey, ctypes.wintypes.LPCSTR(b'AppsUseLightTheme'), ctypes.wintypes.LPDWORD(), ctypes.wintypes.LPDWORD(), - ctypes.cast(ctypes.byref(queryValueLast), ctypes.wintypes.LPBYTE), - ctypes.byref(dwSize), + ctypes.cast(ctypes.byref(query_last_value), ctypes.wintypes.LPBYTE), + ctypes.byref(size), ) while True: @@ -194,20 +198,20 @@ def _listener_windows(callback: CallbackFn) -> None: ctypes.wintypes.LPCSTR(b'AppsUseLightTheme'), ctypes.wintypes.LPDWORD(), ctypes.wintypes.LPDWORD(), - ctypes.cast(ctypes.byref(queryValue), ctypes.wintypes.LPBYTE), - ctypes.byref(dwSize), + ctypes.cast(ctypes.byref(query_value), ctypes.wintypes.LPBYTE), + ctypes.byref(size), ) - if queryValueLast.value != queryValue.value: - queryValueLast.value = queryValue.value - callback(Theme.LIGHT if queryValue.value else Theme.DARK) + if query_last_value.value != query_value.value: + query_last_value.value = query_value.value + callback(Theme.LIGHT if query_value.value else Theme.DARK) -def _initialize_advapi32() -> 'ctypes.WinDLL': +def _initialize_advapi32() -> ctypes.CDLL: '''Initialize our advapi32 library.''' - import ctypes.wintypes # pyright: ignore[reportMissingImports] + import ctypes.wintypes # pyright: ignore[reportMissingImports] # pylint: disable=redefined-outer-name - advapi32 = ctypes.windll.advapi32 + advapi32 = ctypes.windll.advapi32 # pyright: ignore[reportAttributeAccessIssue] # LSTATUS RegOpenKeyExA( # HKEY hKey, @@ -262,7 +266,7 @@ def _initialize_advapi32() -> 'ctypes.WinDLL': return advapi32 -_advapi32: typing.Optional['ctypes.WinDLL'] = None +_advapi32: typing.Optional['ctypes.CDLL'] = None # endregion @@ -277,7 +281,7 @@ def macos_supported_version() -> bool: major = int(sysver.split('.')[0]) if major < 10: return False - elif major >= 11: + if major >= 11: return True # have a macOS10 version @@ -288,29 +292,25 @@ def macos_supported_version() -> bool: def _get_theme_macos() -> Theme: '''Get the current theme, as light or dark, for the system on macOS.''' - global _theme_macos_impl - if _theme_macos_impl is None: - _theme_macos_impl = _get_theme_macos_impl() - return _theme_macos_impl() - - -def _as_utf8(value: bytes | str) -> bytes: - '''Encode a value to UTF-8''' - return value if isinstance(value, bytes) else value.encode('utf-8') - - -def _register_name(objc: ctypes.CDLL, name: bytes | str) -> None: - '''Register a name within our DLLs.''' - return objc.sel_registerName(_as_utf8(name)) - - -def _get_class(objc: ctypes.CDLL, name: bytes | str) -> 'ctypes._NamedFuncPointer': - '''Get a class by the registered name.''' - return objc.objc_getClass(_as_utf8(name)) - - -def _get_theme_macos_impl() -> ThemeFn: - '''Create the theme callback for macOS.''' + # NOTE: This can segfault on M1 and M2 Macs on Big Sur 11.4+. So, we also + # try reading directly using subprocess. + try: + command = ['defaults', 'read', '-globalDomain', 'AppleInterfaceStyle'] + process = subprocess.run(command, capture_output=True, check=True) + try: + result = process.stdout.decode('utf-8').strip() + return Theme.DARK if result == 'Dark' else Theme.LIGHT + except UnicodeDecodeError: + return Theme.LIGHT + except subprocess.CalledProcessError as error: + # If this keypair does not exist, then it's a specific error because the style + # hasn't been set before, so then it specifically is a light theme. this can + # affect no-UI systems like CI. + not_exist = b'does not exist' in error.stderr + any_app = b'kCFPreferencesAnyApplication' in error.stderr + interface_style = b'AppleInterfaceStyle' in error.stderr + if not_exist and any_app and interface_style: + return Theme.LIGHT # NOTE: We do this so we don't need imports at the global level. try: @@ -318,36 +318,55 @@ def _get_theme_macos_impl() -> ThemeFn: objc = ctypes.cdll.LoadLibrary('libobjc.dylib') except OSError: # revert to full path for older OS versions and hardened programs - objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) + obc_name = ctypes.util.find_library('objc') + assert obc_name is not None + objc = ctypes.cdll.LoadLibrary(obc_name) # See https://docs.python.org/3/library/ctypes.html#function-prototypes for arguments description msg_prototype = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p) msg = msg_prototype(('objc_msgSend', objc), ((1, '', None), (1, '', None), (1, '', None))) - auto_release_pool = _get_class(objc, 'NSAutoreleasePool') + user_defaults = _get_class(objc, 'NSUserDefaults') + ns_string = _get_class(objc, 'NSString') + pool = msg(auto_release_pool, _register_name(objc, 'alloc')) pool = msg(pool, _register_name(objc, 'init')) - - user_defaults = _get_class(objc, 'NSUserDefaults') std_user_defaults = msg(user_defaults, _register_name(objc, 'standardUserDefaults')) - ns_string = _get_class(objc, 'NSString') key = msg(ns_string, _register_name(objc, "stringWithUTF8String:"), _as_utf8('AppleInterfaceStyle')) appearance_ns = msg(std_user_defaults, _register_name(objc, 'stringForKey:'), ctypes.c_void_p(key)) appearance_c = msg(appearance_ns, _register_name(objc, 'UTF8String')) out = ctypes.string_at(appearance_c) if appearance_c is not None else None msg(pool, _register_name(objc, 'release')) + return Theme.from_string(out.decode('utf-8')) if out is not None else Theme.LIGHT +def _as_utf8(value: bytes | str) -> bytes: + '''Encode a value to UTF-8''' + return value if isinstance(value, bytes) else value.encode('utf-8') + + +def _register_name(objc: ctypes.CDLL, name: bytes | str) -> None: + '''Register a name within our DLLs.''' + return objc.sel_registerName(_as_utf8(name)) + + +def _get_class(objc: ctypes.CDLL, name: bytes | str) -> 'ctypes._NamedFuncPointer': + '''Get a class by the registered name.''' + return objc.objc_getClass(_as_utf8(name)) + + def _listener_macos(callback: CallbackFn) -> None: '''Register an event listener for dark/light theme changes.''' try: - from Foundation import NSKeyValueObservingOptionNew as _ # noqa # pyright: ignore[reportMissingImports] - except (ImportError, ModuleNotFoundError): - raise RuntimeError('Missing the required Foundation modules: cannot listen.') + from Foundation import ( # noqa # pyright: ignore[reportMissingImports] # pylint: disable + NSKeyValueObservingOptionNew as _, + ) + except (ImportError, ModuleNotFoundError) as error: + raise RuntimeError('Missing the required Foundation modules: cannot listen.') from error # now need to register a child event path = Path(__file__) @@ -358,7 +377,7 @@ def _listener_macos(callback: CallbackFn) -> None: universal_newlines=True, cwd=path.parent, ) as process: - for line in process.stdout: + for line in typing.cast(str, process.stdout): callback(Theme.from_string(line.strip())) @@ -368,20 +387,26 @@ def _listen_child_macos() -> None: # NOTE: We do this so we don't need imports at the global level. try: from Foundation import ( # pyright: ignore[reportMissingImports] - NSObject, NSKeyValueObservingOptionNew, NSKeyValueChangeNewKey, NSUserDefaults + NSKeyValueChangeNewKey, + NSKeyValueObservingOptionNew, + NSObject, + NSUserDefaults, ) from PyObjCTools import AppHelper # pyright: ignore[reportMissingImports] - except ModuleNotFoundError: - raise RuntimeError('Missing the required Foundation modules: cannot listen.') + except ModuleNotFoundError as error: + raise RuntimeError('Missing the required Foundation modules: cannot listen.') from error signal.signal(signal.SIGINT, signal.SIG_IGN) class Observer(NSObject): - def observeValueForKeyPath_ofObject_change_context_( - self, path, object, changeDescription, context + '''Custom namespace key observer.''' + + def observeValueForKeyPath_ofObject_change_context_( # pylint: disable=invalid-name + self, path, obj, changeDescription, context ): + '''Observe our key to detect the light/dark status.''' _ = path - _ = object + _ = obj _ = context result = changeDescription[NSKeyValueChangeNewKey] try: @@ -403,8 +428,6 @@ def observeValueForKeyPath_ofObject_change_context_( AppHelper.runConsoleEventLoop() -_theme_macos_impl: ThemeFn | None = None - # endregion # region linux @@ -431,7 +454,7 @@ def _listener_linux(callback: CallbackFn) -> None: command = [gsettings, 'monitor', 'org.gnome.desktop.interface', schema] # this has rhe same restrictions as above with subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True) as process: - for line in process.stdout: + for line in typing.cast(str, process.stdout): value = line.removeprefix(f"{schema}: '").removesuffix("'") callback(Theme.DARK if '-dark' in value.lower() else Theme.LIGHT) @@ -439,23 +462,22 @@ def _listener_linux(callback: CallbackFn) -> None: def _get_gsettings_schema() -> tuple[str, str]: '''Get the schema to use when monitoring via gsettings.''' # This follows the gsettings followed here: - # https://github.com/GNOME/gsettings-desktop-schemas/blob/master/schemas/org.gnome.desktop.interface.gschema.xml.in + # https://github.com/GNOME/gsettings-desktop-schemas/blob/master/schemas/org.gnome.desktop.interface.gschema.xml.in # noqa # pylint: disable=line-too-long gsettings = _get_gsettings() command = [gsettings, 'get', 'org.gnome.desktop.interface'] # using the freedesktop specifications for checking dark mode # this will return something like `prefer-dark`, which is the true value. # valid values are 'default', 'prefer-dark', 'prefer-light'. - process = subprocess.run(command + ['color-scheme'], capture_output=True) + process = subprocess.run(command + ['color-scheme'], capture_output=True, check=False) if process.returncode == 0: return ('color-scheme', process.stdout.decode('utf-8')) - elif b'No such key' not in process.stderr: + if b'No such key' not in process.stderr: raise RuntimeError('Unable to get our color-scheme from our gsettings.') # if not found then trying older gtk-theme method # this relies on the theme not lying to you: if the theme is dark, it ends in `-dark`. - process = subprocess.run(command + ['gtk-theme'], capture_output=True) - process.check_returncode() + process = subprocess.run(command + ['gtk-theme'], capture_output=True, check=True) return ('gtk-theme', process.stdout.decode('utf-8')) @@ -489,6 +511,7 @@ def _listener_dummy(callback: CallbackFn) -> None: '''Register an event listener for dark/light theme changes (always unimplemented).''' _ = callback + # endregion @@ -517,19 +540,17 @@ def register_functions() -> tuple[ThemeFn, ListenerFn]: if sys.platform == 'darwin' and macos_supported_version(): return (_get_theme_macos, _listener_macos) - elif sys.platform == 'win32' and platform.release().isdigit() and int(platform.release()) >= 10: + if sys.platform == 'win32' and platform.release().isdigit() and int(platform.release()) >= 10: # Checks if running Windows 10 version 10.0.14393 (Anniversary Update) OR HIGHER. # The getwindowsversion method returns a tuple. The third item is the build number # that we can use to check if the user has a new enough version of Windows. winver = int(platform.version().split('.')[2]) if winver >= 14393: return (_get_theme_windows, _listener_windows) - else: - return (_get_theme_dummy, _listener_dummy) - elif sys.platform == "linux": - return (_get_theme_linux, _listener_linux) - else: return (_get_theme_dummy, _listener_dummy) + if sys.platform == "linux": + return (_get_theme_linux, _listener_linux) + return (_get_theme_dummy, _listener_dummy) # register these callbacks once diff --git a/example/dial.py b/example/dial.py index 71f3b95..3a385cb 100644 --- a/example/dial.py +++ b/example/dial.py @@ -32,14 +32,13 @@ ''' import math -import shared import sys +import shared + parser = shared.create_parser() parser.add_argument( - '--no-align', - help='''allow larger widgets without forcing alignment.''', - action='store_true' + '--no-align', help='''allow larger widgets without forcing alignment.''', action='store_true' ) args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) @@ -71,11 +70,11 @@ def circle_percent(dial): return offset / distance -def circle_position(dial, groove_rect, position, r): +def circle_position(dial, rect, position, r): '''Calculate the (x, y) coordinates based on the position on a circle.''' # Get our center and the percent we've gone alone the dial. - center = groove_rect.center() + center = rect.center() x0 = center.x() y0 = center.y() distance = dial.maximum - dial.minimum @@ -100,9 +99,9 @@ def circle_position(dial, groove_rect, position, r): return x0 - r * math.cos(theta), y0 - r * math.sin(theta) -def handle_position(dial, groove_rect, r): +def handle_position(dial, rect, r): '''Calculate the position of the handle.''' - return circle_position(dial, groove_rect, dial.sliderPosition, r) + return circle_position(dial, rect, dial.sliderPosition, r) def default_pen(color, width): @@ -160,7 +159,7 @@ def __init__(self, widget=None): self.handle = (0, 0) self.is_hovered = False - def paintEvent(self, event): + def paintEvent(self, event): # pylint: disable=too-many-locals '''Override the paint event to ensure the ticks are painted.''' if args.stylesheet == 'native': @@ -240,6 +239,8 @@ def paintEvent(self, event): handle_pos = QtCore.QPointF(hx, hy) painter.drawEllipse(handle_pos, self.handle_radius, self.handle_radius) + return None + def eventFilter(self, obj, event): '''Override the color when we have a hover event.''' @@ -275,6 +276,8 @@ class Ui: '''Main class for the user interface.''' def setup(self, MainWindow): + '''Setup our main window for the UI.''' + MainWindow.setObjectName('MainWindow') MainWindow.resize(1068, 824) self.centralwidget = QtWidgets.QWidget(MainWindow) @@ -308,7 +311,7 @@ def main(): window.setWindowTitle('QDial') shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/lcd.py b/example/lcd.py index aa3b9a7..98a2ad7 100644 --- a/example/lcd.py +++ b/example/lcd.py @@ -31,14 +31,13 @@ supports highlighting the handle on the active or hovered dial. ''' -import shared import sys +import shared + parser = shared.create_parser() parser.add_argument( - '--no-align', - help='''allow larger widgets without forcing alignment.''', - action='store_true' + '--no-align', help='''allow larger widgets without forcing alignment.''', action='store_true' ) args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) @@ -72,6 +71,8 @@ class Ui: '''Main class for the user interface.''' def setup(self, MainWindow): + '''Setup our main window for the UI.''' + MainWindow.setObjectName('MainWindow') MainWindow.resize(1068, 824) self.centralwidget = QtWidgets.QWidget(MainWindow) @@ -118,7 +119,7 @@ def main(): window.setWindowTitle('QLCDNumber') shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/placeholder_text.py b/example/placeholder_text.py index 8e435f6..a1cec31 100644 --- a/example/placeholder_text.py +++ b/example/placeholder_text.py @@ -33,19 +33,20 @@ and palette edits correctly affect styles in Qt5, but not Qt6. ''' -import shared +# pylint: disable=duplicate-code + import sys +import shared + parser = shared.create_parser() parser.add_argument( - '--set-app-palette', - help='''set the placeholder text palette globally.''', - action='store_true' + '--set-app-palette', help='''set the placeholder text palette globally.''', action='store_true' ) parser.add_argument( '--set-widget-palette', help='''set the placeholder text palette for the affected widgets.''', - action='store_true' + action='store_true', ) args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) @@ -54,7 +55,7 @@ def set_palette(widget, role, color): - '''Set the palette for the placeholder text. This only works in Qt5.''' + '''Set the palette for a widget.''' palette = widget.palette() palette.setColor(role, color) @@ -62,6 +63,7 @@ def set_palette(widget, role, color): def set_placeholder_palette(widget): + '''Set the palette for the placeholder text. This only works in Qt5.''' set_palette(widget, compat.PlaceholderText, colors.PlaceholderColor) @@ -69,6 +71,8 @@ class Ui: '''Main class for the user interface.''' def setup(self, MainWindow): + '''Setup our main window for the UI.''' + MainWindow.setObjectName('MainWindow') MainWindow.resize(1068, 824) self.centralwidget = QtWidgets.QWidget(MainWindow) @@ -112,7 +116,7 @@ def main(): window.setWindowTitle('Stylized Placeholder Text.') shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/shared.py b/example/shared.py index f40d27a..a305385 100644 --- a/example/shared.py +++ b/example/shared.py @@ -5,6 +5,8 @@ Shared imports and compatibility definitions between Qt5 and Qt6. ''' +# pylint: disable=import-error + import argparse import importlib import logging @@ -22,9 +24,7 @@ def create_parser(): '''Create an argparser with the base settings for all Qt applications.''' - parser = argparse.ArgumentParser( - description='Configurations for the Qt5 application.' - ) + parser = argparse.ArgumentParser(description='Configurations for the Qt5 application.') parser.add_argument( '--stylesheet', help='stylesheet name (`dark`, `light`, `native`, `auto`, ...)', @@ -38,16 +38,8 @@ def create_parser(): help='application style (`Fusion`, `Windows`, `native`, ...)', default='native', ) - parser.add_argument( - '--font-size', - help='font size for the application', - type=float, - default=-1 - ) - parser.add_argument( - '--font-family', - help='the font family' - ) + parser.add_argument('--font-size', help='font size for the application', type=float, default=-1) + parser.add_argument('--font-family', help='the font family') parser.add_argument( '--scale', help='scale factor for the UI', @@ -61,14 +53,10 @@ def create_parser(): 'Note: building for PyQt6 requires PySide6-rcc to be installed.' ), choices=['pyqt5', 'pyqt6', 'pyside2', 'pyside6'], - default='pyqt5' + default='pyqt5', ) # Linux or Unix-like only. - parser.add_argument( - '--use-x11', - help='force the use of x11 on compatible systems.', - action='store_true' - ) + parser.add_argument('--use-x11', help='force the use of x11 on compatible systems.', action='store_true') return parser @@ -102,7 +90,7 @@ def parse_args(parser): def is_qt6(args): '''Get if we're using Qt6 and not Qt5.''' - return args.qt_framework == 'pyqt6' or args.qt_framework == 'pyside6' + return args.qt_framework in ('pyqt6', 'pyside6') def import_qt(args, load_resources=True): @@ -116,6 +104,8 @@ def import_qt(args, load_resources=True): from PyQt5 import QtCore, QtGui, QtWidgets # pyright: ignore[reportMissingImports] elif args.qt_framework == 'pyside2': from PySide2 import QtCore, QtGui, QtWidgets # pyright: ignore[reportMissingImports] + else: + raise ValueError(f'Got an invalid Qt framework of "{args.qt_framework}".') if load_resources: sys.path.insert(0, f'{home}/resources') @@ -135,16 +125,16 @@ def get_stylesheet(resource_format): def get_version(args): + '''Get the current version of the Qt library.''' QtCore, _, __ = import_qt(args, load_resources=False) - if args.qt_framework == 'pyqt5' or args.qt_framework == 'pyqt6': + if args.qt_framework in ('pyqt5', 'pyqt6'): # QT_VERSION is stored in 0xMMmmpp, each in 8 bit pairs. # Goes major, minor, patch. 393984 is "6.3.0" return (QtCore.QT_VERSION >> 16, (QtCore.QT_VERSION >> 8) & 0xFF, QtCore.QT_VERSION & 0xFF) - else: - return QtCore.__version_info__[:3] + return QtCore.__version_info__[:3] -def get_compat_definitions(args): +def get_compat_definitions(args): # pylint: disable=too-many-statements '''Create our compatibility definitions.''' ns = argparse.Namespace() @@ -154,7 +144,7 @@ def get_compat_definitions(args): ns.QtWidgets = QtWidgets # ensure we store the QT_VERSION - if args.qt_framework == 'pyqt5' or args.qt_framework == 'pyqt6': + if args.qt_framework in ('pyqt5', 'pyqt6'): # QT_VERSION is stored in 0xMMmmpp, each in 8 bit pairs. # Goes major, minor, patch. 393984 is "6.3.0" ns.QT_VERSION = (QtCore.QT_VERSION >> 16, (QtCore.QT_VERSION >> 8) & 0xFF, QtCore.QT_VERSION & 0xFF) @@ -854,7 +844,7 @@ def get_colors(args, compat): return ns -def get_icon_map(args, compat): +def get_icon_map(compat): '''Create a map of standard icons to resource paths.''' icon_map = { @@ -1006,6 +996,8 @@ def is_dark_mode(compat, reinitialize=False): def read_qtext_file(path, compat): + '''Read the Qt text resource.''' + file = compat.QtCore.QFile(path) file.open(compat.ReadOnly | compat.Text) stream = compat.QtCore.QTextStream(file) @@ -1021,7 +1013,7 @@ def set_stylesheet(args, app, compat): app.setStyleSheet(read_qtext_file(stylesheet, compat)) -def exec_app(args, app, window, compat): +def exec_app(args, app, window): '''Show and execute the Qt application.''' window.show() diff --git a/example/slider.py b/example/slider.py index 0e28b74..33c9aa3 100644 --- a/example/slider.py +++ b/example/slider.py @@ -31,9 +31,10 @@ get customized styling behavior with a QSlider. ''' -import shared import sys +import shared + parser = shared.create_parser() args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) @@ -44,10 +45,10 @@ class Slider(QtWidgets.QSlider): '''QSlider with a custom paint event.''' - def __init__(self, *args, **kwds): + def __init__(self, *args, **kwds): # pylint: disable=useless-parent-delegation,redefined-outer-name super().__init__(*args, **kwds) - def paintEvent(self, event): + def paintEvent(self, event): # pylint: disable=unused-argument,(too-many-locals '''Override the paint event to ensure the ticks are painted.''' painter = QtWidgets.QStylePainter(self) @@ -73,10 +74,10 @@ def paintEvent(self, event): width = (self.width() - handle.width()) + handle.width() / 2 x = int(percent * width) h = 4 - if position == compat.TicksBothSides or position == compat.TicksAbove: + if position in (compat.TicksBothSides, compat.TicksAbove): y = self.rect().top() painter.drawLine(x, y, x, y + h) - if position == compat.TicksBothSides or position == compat.TicksBelow: + if position in (compat.TicksBothSides, compat.TicksBelow): y = self.rect().bottom() painter.drawLine(x, y, x, y - h) @@ -91,6 +92,8 @@ class Ui: '''Main class for the user interface.''' def setup(self, MainWindow): + '''Setup our main window for the UI.''' + MainWindow.setObjectName('MainWindow') MainWindow.resize(1068, 824) self.centralwidget = QtWidgets.QWidget(MainWindow) @@ -119,7 +122,7 @@ def main(): window.setWindowTitle('QSlider with Ticks.') shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/standard_icons.py b/example/standard_icons.py index 85cd13b..b00db10 100644 --- a/example/standard_icons.py +++ b/example/standard_icons.py @@ -29,21 +29,25 @@ Example overriding QCommonStyle for custom standard icons. ''' -import shared import sys +import shared + parser = shared.create_parser() args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) compat = shared.get_compat_definitions(args) -ICON_MAP = shared.get_icon_map(args, compat) +ICON_MAP = shared.get_icon_map(compat) def style_icon(style, icon, option=None, widget=None): + '''Helper to provide arguments for setting a style icon.''' return shared.style_icon(args, style, icon, ICON_MAP, option, widget) class ApplicationStyle(QtWidgets.QCommonStyle): + '''A custom application style overriding standard icons.''' + def __init__(self, style): super().__init__() self.style = style @@ -74,6 +78,7 @@ def add_standard_button(ui, layout, icon, index): def add_standard_buttons(ui, page, icons): '''Create and add QToolButtons with standard icons to the UI.''' + _ = ui for icon_name in icons: icon_enum = getattr(compat, icon_name) icon = style_icon(page.style(), icon_enum, widget=page) @@ -84,7 +89,9 @@ def add_standard_buttons(ui, page, icons): class Ui: '''Main class for the user interface.''' - def setup(self, MainWindow): + def setup(self, MainWindow): # pylint: disable=too-many-statements + '''Setup our main window for the UI.''' + MainWindow.setObjectName('MainWindow') MainWindow.resize(1068, 824) self.centralwidget = QtWidgets.QWidget(MainWindow) @@ -99,22 +106,26 @@ def setup(self, MainWindow): self.tool_box.addItem(self.page1, 'Overwritten Icons') self.layout.addWidget(self.tool_box) - add_standard_buttons(self, self.page1, [ - 'SP_ArrowLeft', - 'SP_ArrowDown', - 'SP_ArrowRight', - 'SP_ArrowUp', - 'SP_DockWidgetCloseButton', - 'SP_DialogCancelButton', - 'SP_DialogCloseButton', - 'SP_DialogDiscardButton', - 'SP_DialogHelpButton', - 'SP_DialogNoButton', - 'SP_DialogOkButton', - 'SP_DialogOpenButton', - 'SP_DialogResetButton', - 'SP_DialogSaveButton', - ]) + add_standard_buttons( + self, + self.page1, + [ + 'SP_ArrowLeft', + 'SP_ArrowDown', + 'SP_ArrowRight', + 'SP_ArrowUp', + 'SP_DockWidgetCloseButton', + 'SP_DialogCancelButton', + 'SP_DialogCloseButton', + 'SP_DialogDiscardButton', + 'SP_DialogHelpButton', + 'SP_DialogNoButton', + 'SP_DialogOkButton', + 'SP_DialogOpenButton', + 'SP_DialogResetButton', + 'SP_DialogSaveButton', + ], + ) self.page2 = QtWidgets.QListWidget() self.tool_box.addItem(self.page2, 'Default Icons') @@ -248,6 +259,8 @@ def setup(self, MainWindow): self.retranslateUi(MainWindow) def retranslateUi(self, MainWindow): + '''Retranslate our UI after initializing some of our base modules.''' + _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate('MainWindow', 'MainWindow')) self.menuMenu.setTitle(_translate('MainWindow', '&Menu')) @@ -255,9 +268,11 @@ def retranslateUi(self, MainWindow): self.actionAction_C.setText(_translate('MainWindow', 'Action &C')) def about(self): + '''Load our Qt about window.''' QtWidgets.QMessageBox.aboutQt(self.centralwidget, 'About Menu') def critical(self): + '''Launch a critical message box.''' QtWidgets.QMessageBox.critical(self.centralwidget, 'Error', 'Critical Error') @@ -276,7 +291,7 @@ def main(): ui.actionAction_C.triggered.connect(ui.critical) shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/titlebar.py b/example/titlebar.py index 1ea126c..95db87b 100644 --- a/example/titlebar.py +++ b/example/titlebar.py @@ -107,13 +107,15 @@ - Windows 10 ''' +# pylint: disable=protected-access + import enum import os -import shared import sys - from pathlib import Path +import shared + parser = shared.create_parser() parser.add_argument( '--minimize-location', @@ -157,7 +159,7 @@ QtCore, QtGui, QtWidgets = shared.import_qt(args) compat = shared.get_compat_definitions(args) colors = shared.get_colors(args, compat) -ICON_MAP = shared.get_icon_map(args, compat) +ICON_MAP = shared.get_icon_map(compat) # 100ms between repaints, so we avoid over-repainting. # Allows us to avoid glitchy motion during drags/ REPAINT_TIMER = 100 @@ -262,6 +264,7 @@ def close_icon(widget): def transparent_icon(widget): '''Create a transparent icon.''' + _ = widget return QtGui.QIcon() @@ -286,6 +289,7 @@ def size_less(x, y): '''Compare 2 sizes, determining if any bounds of x are less than y.''' return x.width() < y.width() or x.height() < y.height() + # UI WIDGETS # These are just to populate the views: these could be anything. @@ -341,7 +345,7 @@ def __init__(self, parent=None): class SettingTabs(QtWidgets.QTabWidget): '''Sample setting widget with a tab view.''' - def __init__(self, parent=None): + def __init__(self, parent=None): # pylint: disable=too-many-statements super().__init__(parent) self.setTabPosition(compat.North) @@ -421,12 +425,14 @@ def launch_filedialog(self): self.data_folder.setText(dialog.selectedFiles()[0]) def launch_fontdialog(self, edit): + '''Launch our font selection disablog.''' initial = QtGui.QFont() initial.setFamily(edit.text()) font, ok = QtWidgets.QFontDialog.getFont(initial) if ok: edit.setText(font.family()) + # RESIZE HELPERS @@ -587,7 +593,7 @@ def start_resize(self, window, window_type): # and track hover events outside the app. This doesn't # work on Wayland or on macOS. # https://doc.qt.io/qt-5/qwidget.html#grabMouse - if not IS_TRUE_WAYLAND and not sys.platform == 'darwin': + if not IS_TRUE_WAYLAND and sys.platform != 'darwin': self.window().grabMouse() @@ -605,7 +611,7 @@ def end_resize(self, window_type): setattr(self, f'_{window_type}_resize', None) window.window().unsetCursor() - if not IS_TRUE_WAYLAND and not sys.platform == 'darwin': + if not IS_TRUE_WAYLAND and sys.platform != 'darwin': self.window().releaseMouse() @@ -634,6 +640,7 @@ def end_frame(self, window_type): '''End the window frame resize state.''' setattr(self, f'_{window_type}_frame', None) + # EVENT HANDLES @@ -679,13 +686,12 @@ def window_mouse_double_click_event(self, event): if not widget.underMouse() or event.button() != compat.LeftButton: return super(type(self), self).mouseDoubleClickEvent(event) if widget._is_shaded: - widget.unshade() - elif widget.isMinimized() or widget.isMaximized(): - widget.restore() - elif widget._has_shade: - widget.shade() - else: - widget.maximize() + return widget.unshade() + if widget.isMinimized() or widget.isMaximized(): + return widget.restore() + if widget._has_shade: + return widget.shade() + return widget.maximize() def window_mouse_press_event(self, event, window, window_type): @@ -714,7 +720,7 @@ def window_mouse_move_event(self, event, window, window_type): if getattr(window, f'_{window_type}_frame') is not None: end_drag(window, window_type) if getattr(window, f'_{window_type}_drag') is not None: - handle_drag(window, event, self, window_type) + handle_drag(self, event, window, window_type) return super(type(self), self).mouseMoveEvent(event) @@ -724,6 +730,7 @@ def window_mouse_release_event(self, event, window, window_type): end_drag(window, window_type) return super(type(self), self).mouseReleaseEvent(event) + # WINDOW WIDGETS @@ -763,6 +770,7 @@ def elideMode(self): return self._elide def setElideMode(self, elide): + '''Set the elide mode for the label.''' self._elide = elide def elide(self): @@ -783,6 +791,7 @@ class TitleButton(QtWidgets.QToolButton): def __init__(self, icon, parent=None): super().__init__() + _ = parent self.setIcon(icon) self.setAutoRaise(True) @@ -790,7 +799,7 @@ def __init__(self, icon, parent=None): class TitleBar(QtWidgets.QFrame): '''Custom instance of a QTitlebar''' - def __init__(self, window, parent=None, flags=None): + def __init__(self, window, parent=None, flags=None): # pylint: disable=(too-many-statements super().__init__(parent) # Get and set some properties. @@ -847,14 +856,16 @@ def __init__(self, window, parent=None, flags=None): self._top_action.toggled.connect(self.toggle_keep_above) self._close_action = action('&Close', self, close_icon(self)) self._close_action.triggered.connect(self._window.close) - self._main_menu.addActions([ - self._restore_action, - self._move_action, - self._size_action, - self._min_action, - self._max_action, - self._top_action, - ]) + self._main_menu.addActions( + [ + self._restore_action, + self._move_action, + self._size_action, + self._min_action, + self._max_action, + self._top_action, + ] + ) self._main_menu.addSeparator() self._main_menu.addAction(self._close_action) self._menu.setMenu(self._main_menu) @@ -1248,93 +1259,93 @@ def is_active(self): def is_on_top(self, pos, rect): '''Determine if the cursor is on the top of the widget.''' return ( - pos.x() >= rect.x() + self._border_width and - pos.x() <= rect.x() + rect.width() - self._border_width and - pos.y() >= rect.y() and - pos.y() <= rect.y() + self._border_width + pos.x() >= rect.x() + self._border_width + and pos.x() <= rect.x() + rect.width() - self._border_width + and pos.y() >= rect.y() + and pos.y() <= rect.y() + self._border_width ) def is_on_bottom(self, pos, rect): '''Determine if the cursor is on the bottom of the widget.''' return ( - pos.x() >= rect.x() + self._border_width and - pos.x() <= rect.x() + rect.width() - self._border_width and - pos.y() >= rect.y() + rect.height() - self._border_width and - pos.y() <= rect.y() + rect.height() + pos.x() >= rect.x() + self._border_width + and pos.x() <= rect.x() + rect.width() - self._border_width + and pos.y() >= rect.y() + rect.height() - self._border_width + and pos.y() <= rect.y() + rect.height() ) def is_on_left(self, pos, rect): '''Determine if the cursor is on the left of the widget.''' return ( - pos.x() >= rect.x() - self._border_width and - pos.x() <= rect.x() + self._border_width and - pos.y() >= rect.y() + self._border_width and - pos.y() <= rect.y() + rect.height() - self._border_width + pos.x() >= rect.x() - self._border_width + and pos.x() <= rect.x() + self._border_width + and pos.y() >= rect.y() + self._border_width + and pos.y() <= rect.y() + rect.height() - self._border_width ) def is_on_right(self, pos, rect): '''Determine if the cursor is on the right of the widget.''' return ( - pos.x() >= rect.x() + rect.width() - self._border_width and - pos.x() <= rect.x() + rect.width() and - pos.y() >= rect.y() + self._border_width and - pos.y() <= rect.y() + rect.height() - self._border_width + pos.x() >= rect.x() + rect.width() - self._border_width + and pos.x() <= rect.x() + rect.width() + and pos.y() >= rect.y() + self._border_width + and pos.y() <= rect.y() + rect.height() - self._border_width ) def is_on_top_left(self, pos, rect): '''Determine if the cursor is on the top left of the widget.''' return ( - pos.x() >= rect.x() and - pos.x() <= rect.x() + self._border_width and - pos.y() >= rect.y() and - pos.y() <= rect.y() + self._border_width + pos.x() >= rect.x() + and pos.x() <= rect.x() + self._border_width + and pos.y() >= rect.y() + and pos.y() <= rect.y() + self._border_width ) def is_on_top_right(self, pos, rect): '''Determine if the cursor is on the top right of the widget.''' return ( - pos.x() >= rect.x() + rect.width() - self._border_width and - pos.x() <= rect.x() + rect.width() and - pos.y() >= rect.y() and - pos.y() <= rect.y() + self._border_width + pos.x() >= rect.x() + rect.width() - self._border_width + and pos.x() <= rect.x() + rect.width() + and pos.y() >= rect.y() + and pos.y() <= rect.y() + self._border_width ) def is_on_bottom_left(self, pos, rect): '''Determine if the cursor is on the bottom left of the widget.''' return ( - pos.x() >= rect.x() and - pos.x() <= rect.x() + self._border_width and - pos.y() >= rect.y() + rect.height() - self._border_width and - pos.y() <= rect.y() + rect.height() + pos.x() >= rect.x() + and pos.x() <= rect.x() + self._border_width + and pos.y() >= rect.y() + rect.height() - self._border_width + and pos.y() <= rect.y() + rect.height() ) def is_on_bottom_right(self, pos, rect): '''Determine if the cursor is on the bottom right of the widget.''' return ( - pos.x() >= rect.x() + rect.width() - self._border_width and - pos.x() <= rect.x() + rect.width() and - pos.y() >= rect.y() + rect.height() - self._border_width and - pos.y() <= rect.y() + rect.height() + pos.x() >= rect.x() + rect.width() - self._border_width + and pos.x() <= rect.x() + rect.width() + and pos.y() >= rect.y() + rect.height() - self._border_width + and pos.y() <= rect.y() + rect.height() ) - def cursor_position(self, pos, rect): + def cursor_position(self, pos, rect): # pylint: disable=too-many-return-statements) '''Calculate the cursor position inside the window.''' if self.is_on_left(pos, rect): return WindowEdge.Left - elif self.is_on_right(pos, rect): + if self.is_on_right(pos, rect): return WindowEdge.Right - elif self.is_on_bottom(pos, rect): + if self.is_on_bottom(pos, rect): return WindowEdge.Bottom - elif self.is_on_top(pos, rect): + if self.is_on_top(pos, rect): return WindowEdge.Top - elif self.is_on_bottom_left(pos, rect): + if self.is_on_bottom_left(pos, rect): return WindowEdge.BottomLeft - elif self.is_on_bottom_right(pos, rect): + if self.is_on_bottom_right(pos, rect): return WindowEdge.BottomRight - elif self.is_on_top_right(pos, rect): + if self.is_on_top_right(pos, rect): return WindowEdge.TopRight - elif self.is_on_top_left(pos, rect): + if self.is_on_top_left(pos, rect): return WindowEdge.TopLeft return WindowEdge.NoEdge @@ -1390,27 +1401,27 @@ def update_cursor(self, position): self._window.setCursor(self._cursor) - def resize(self, position, rect): + def resize(self, position, rect): # pylint: disable=too-many-branches '''Resize our window to the adjusted dimensions.''' # Get our new frame dimensions. if self._press_edge == WindowEdge.NoEdge: return - elif self._press_edge == WindowEdge.Top: + if self._press_edge == WindowEdge.Top: rect.setTop(position.y()) - elif self._press_edge == WindowEdge.Bottom: + if self._press_edge == WindowEdge.Bottom: rect.setBottom(position.y()) - elif self._press_edge == WindowEdge.Left: + if self._press_edge == WindowEdge.Left: rect.setLeft(position.x()) - elif self._press_edge == WindowEdge.Right: + if self._press_edge == WindowEdge.Right: rect.setRight(position.x()) - elif self._press_edge == WindowEdge.TopLeft: + if self._press_edge == WindowEdge.TopLeft: rect.setTopLeft(position) - elif self._press_edge == WindowEdge.TopRight: + if self._press_edge == WindowEdge.TopRight: rect.setTopRight(position) - elif self._press_edge == WindowEdge.BottomLeft: + if self._press_edge == WindowEdge.BottomLeft: rect.setBottomLeft(position) - elif self._press_edge == WindowEdge.BottomRight: + if self._press_edge == WindowEdge.BottomRight: rect.setBottomRight(position) # Ensure we don't drag the widgets if we go below min sizes. @@ -1507,7 +1518,7 @@ def enter(self, event): def leave(self, event): '''Handle the leaveEvent of the window.''' - + _ = event if not self._pressed: self.unset_cursor() @@ -1564,6 +1575,7 @@ def __init__( flags=QtCore.Qt.WindowType(0), sizegrip=False, ): + _ = sizegrip super().__init__(parent, flags=flags) @@ -1810,7 +1822,7 @@ def unminimize(self, subwindow): self._minimized.remove(subwindow) self.move_minimized() - def move_minimized(self): + def move_minimized(self): # pylint: disable=too-many-locals '''Move the minimized windows.''' # No need to set the geometry of our minimized windows. @@ -1863,9 +1875,8 @@ def move_minimized(self): # Now, need to place them accordingly. # Need to handle unshifts, if they occur, due to the - for index in range(len(self._minimized)): + for index, window in enumerate(self._minimized): # Calculate our new column, only storing if it is a new column. - window = self._minimized[index] is_new_column = index % row_count == 0 if index != 0 and is_new_column: point = new_column(point) @@ -2023,6 +2034,7 @@ def move_event(self, _, event, window_type): def resize_event(self, obj, event, window_type): '''Handle window resize events.''' + _ = obj # NOTE: If we're on Wayland, we cant' track hover events outside the # main widget, and we can't guess intermittently since if the mouse # doesn't move, we won't get an `Enter` or `HoverEnter` event, and @@ -2245,7 +2257,8 @@ def restore(self, _): '''Restore the window, showing the main widget and size grip.''' self.showNormal() - def showNormal(self): + def showNormal(self): # pylint: disable=useless-parent-delegation + '''Show the normal titlebar view.''' super().showNormal() def shade(self, size): @@ -2321,7 +2334,7 @@ def main(): app.installEventFilter(window) shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/url.py b/example/url.py index 910694e..4256744 100644 --- a/example/url.py +++ b/example/url.py @@ -30,19 +30,20 @@ cannot be modified in stylesheets. ''' -import shared +# pylint: disable=duplicate-code + import sys +import shared + parser = shared.create_parser() parser.add_argument( - '--set-app-palette', - help='''set the placeholder text palette globally.''', - action='store_true' + '--set-app-palette', help='''set the placeholder text palette globally.''', action='store_true' ) parser.add_argument( '--set-widget-palette', help='''set the placeholder text palette for the affected widgets.''', - action='store_true' + action='store_true', ) args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) @@ -59,6 +60,7 @@ def set_palette(widget, role, color): def set_link_palette(widget): + '''Set the palette for a link type.''' set_palette(widget, compat.Link, colors.LinkColor) set_palette(widget, compat.LinkVisited, colors.LinkVisitedColor) @@ -67,6 +69,8 @@ class Ui: '''Main class for the user interface.''' def setup(self, MainWindow): + '''Setup our main window for the UI.''' + url = 'https://github.com/Alexhuszagh/BreezeStyleSheets' MainWindow.setObjectName('MainWindow') MainWindow.resize(400, 200) @@ -121,7 +125,7 @@ def main(): window.setWindowTitle('Stylized URL colors.') shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/whatsthis.py b/example/whatsthis.py index 153f816..1d7c78e 100644 --- a/example/whatsthis.py +++ b/example/whatsthis.py @@ -30,9 +30,10 @@ since it cannot be modified via stylesheets. ''' -import shared import sys +import shared + parser = shared.create_parser() args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) @@ -44,6 +45,8 @@ class Ui: '''Main class for the user interface.''' def setup(self, MainWindow): + '''Setup our main window for the UI.''' + MainWindow.setObjectName('MainWindow') MainWindow.resize(1068, 824) self.centralwidget = QtWidgets.QWidget(MainWindow) @@ -78,7 +81,7 @@ def main(): window.setWindowTitle('Stylized QWhatsThis.') shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/example/widgets.py b/example/widgets.py index dcfab3f..3705b3d 100644 --- a/example/widgets.py +++ b/example/widgets.py @@ -30,14 +30,17 @@ Simple example showing numerous built-in widgets. ''' -import shared +# pylint: disable=duplicate-code + import sys +import shared + parser = shared.create_parser() args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) compat = shared.get_compat_definitions(args) -ICON_MAP = shared.get_icon_map(args, compat) +ICON_MAP = shared.get_icon_map(compat) def close_icon(widget): @@ -48,7 +51,10 @@ def close_icon(widget): class Ui: '''Main class for the user interface.''' - def setup(self, MainWindow): + def setup(self, MainWindow): # pylint: disable=too-many-statements,too-many-locals + '''Setup our main window for the UI.''' + + # pylint: disable=unused-variable MainWindow.setObjectName('MainWindow') MainWindow.resize(1068, 824) self.centralwidget = QtWidgets.QWidget(MainWindow) @@ -421,8 +427,11 @@ def setup(self, MainWindow): MainWindow.setTabOrder(self.verticalSlider, self.tabWidget) MainWindow.setTabOrder(self.tabWidget, self.lineEdit) MainWindow.setTabOrder(self.lineEdit, self.listWidget) + # pylint: enable=unused-variable + + def retranslateUi(self, MainWindow): # pylint: disable=too-many-statements + '''Retranslate our UI after initializing some of our base modules.''' - def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate('MainWindow', 'MainWindow')) self.groupBox.setTitle(_translate('MainWindow', 'ToolBox')) @@ -502,9 +511,11 @@ def retranslateUi(self, MainWindow): self.actionAction_C.setText(_translate('MainWindow', 'Action &C')) def about(self): + '''Load our Qt about window.''' QtWidgets.QMessageBox.aboutQt(self.centralwidget, 'About Menu') def critical(self): + '''Launch a critical message box.''' QtWidgets.QMessageBox.critical(self.centralwidget, 'Error', 'Critical Error') @@ -516,18 +527,9 @@ def main(): # setup ui ui = Ui() ui.setup(window) - ui.bt_delay_popup.addActions([ - ui.actionAction, - ui.actionAction_C - ]) - ui.bt_instant_popup.addActions([ - ui.actionAction, - ui.actionAction_C - ]) - ui.bt_menu_button_popup.addActions([ - ui.actionAction, - ui.actionAction_C - ]) + ui.bt_delay_popup.addActions([ui.actionAction, ui.actionAction_C]) + ui.bt_instant_popup.addActions([ui.actionAction, ui.actionAction_C]) + ui.bt_menu_button_popup.addActions([ui.actionAction, ui.actionAction_C]) window.setWindowTitle('Sample BreezeStyleSheets application.') # Add event triggers @@ -538,7 +540,7 @@ def main(): window.tabifyDockWidget(ui.dockWidget1, ui.dockWidget2) shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window, compat) + return shared.exec_app(args, app, window) if __name__ == '__main__': diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fdc39fa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,581 @@ +# NOTE: This is not a true pyproject.toml, this is for configurations with black. + +[tool.black] +target-version = ["py310"] +line-length = 110 +color = true +skip-string-normalization = true + +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | env + | venv +)/ +''' + +[tool.isort] +py_version = 310 +line_length = 110 +include_trailing_comma = true +profile = "black" +multi_line_output = 3 +indent = 4 +color_output = true +known_typing = ["typing", "types", "typing_extensions", "mypy", "mypy_extensions"] +sections = ["FUTURE", "TYPING", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +no_lines_before = ["TYPING", "STDLIB", "FIRSTPARTY", "LOCALFOLDER"] + +[tool.pylint.main] +# Analyse import fallback blocks. This can be used to support both Python 2 and 3 +# compatible code, which means that the block might have code that exists only in +# one or another interpreter, leading to false positives when analysed. +# analyse-fallback-blocks = + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint in +# a server-like mode. +# clear-cache-post-run = + +# Always return a 0 (non-error) status code, even if lint errors are found. This +# is primarily useful in continuous integration scripts. +# exit-zero = + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +# extension-pkg-allow-list = + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +# extension-pkg-whitelist = + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +# fail-on = + +# Specify a score threshold under which the program will exit with error. +fail-under = 10.0 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +# from-stdin = + +# Files or directories to be skipped. They should be base names, not paths. +ignore = ["CVS"] + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, it +# can't be used as an escape character. +# ignore-paths = + +# Files or directories matching the regular expression patterns are skipped. The +# regex matches against base names, not paths. The default value ignores Emacs +# file locks +ignore-patterns = ["^\\.#"] + +# List of module names for which member attributes should not be checked and will +# not be imported (useful for modules/projects where namespaces are manipulated +# during runtime and thus existing member attributes cannot be deduced by static +# analysis). It supports qualified module names, as well as Unix pattern +# matching. +# ignored-modules = + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +# init-hook = + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs = 1 + +# Control the amount of potential inferred values when inferring a single object. +# This can help the performance when dealing with large functions or complex, +# nested conditions. +limit-inference-results = 100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +# load-plugins = + +# Pickle collected data for later comparisons. +persistent = true + +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. +# prefer-stubs = + +# Minimum Python version to use for version dependent checks. Will default to the +# version used to run pylint. +py-version = "3.10" + +# Discover python modules and packages in the file system subtree. +# recursive = + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +# source-roots = + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode = true + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +# unsafe-load-any-extension = + +[tool.pylint.basic] +# Naming style matching correct argument names. +argument-naming-style = "snake_case" + +# Regular expression matching correct argument names. Overrides argument-naming- +# style. If left empty, argument names will be checked with the set naming style. +# argument-rgx = + +# Naming style matching correct attribute names. +attr-naming-style = "snake_case" + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +# attr-rgx = + +# Bad variable names which should always be refused, separated by a comma. +bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +# bad-names-rgxs = + +# Naming style matching correct class attribute names. +class-attribute-naming-style = "any" + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +# class-attribute-rgx = + +# Naming style matching correct class constant names. +class-const-naming-style = "UPPER_CASE" + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +# class-const-rgx = + +# Naming style matching correct class names. +class-naming-style = "PascalCase" + +# Regular expression matching correct class names. Overrides class-naming-style. +# If left empty, class names will be checked with the set naming style. +# class-rgx = + +# Naming style matching correct constant names. +const-naming-style = "UPPER_CASE" + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming style. +# const-rgx = + +# Minimum line length for functions/classes that require docstrings, shorter ones +# are exempt. +docstring-min-length = -1 + +# Naming style matching correct function names. +function-naming-style = "snake_case" + +# Regular expression matching correct function names. Overrides function-naming- +# style. If left empty, function names will be checked with the set naming style. +# function-rgx = + +# Good variable names which should always be accepted, separated by a comma. +good-names = ["i", "j", "k", "ex", "Run", "_"] + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +# good-names-rgxs = + +# Include a hint for the correct naming format with invalid-name. +# include-naming-hint = + +# Naming style matching correct inline iteration names. +inlinevar-naming-style = "any" + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +# inlinevar-rgx = + +# Naming style matching correct method names. +method-naming-style = "snake_case" + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +# method-rgx = + +# Naming style matching correct module names. +module-naming-style = "snake_case" + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +# module-rgx = + +# Colon-delimited sets of names that determine each other's naming style when the +# name regexes allow several styles. +# name-group = + +# Regular expression which should only match function or class names that do not +# require a docstring. +no-docstring-rgx = "^_" + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. These +# decorators are taken in consideration only for invalid-name. +property-classes = ["abc.abstractproperty"] + +# Regular expression matching correct type alias names. If left empty, type alias +# names will be checked with the set naming style. +# typealias-rgx = + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +# typevar-rgx = + +# Naming style matching correct variable names. +variable-naming-style = "snake_case" + +# Regular expression matching correct variable names. Overrides variable-naming- +# style. If left empty, variable names will be checked with the set naming style. +# variable-rgx = + +[tool.pylint.classes] +# Warn about protected attribute access inside special methods +# check-protected-access-in-special-methods = + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg = ["cls"] + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg = ["mcs"] + +[tool.pylint.design] +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +# exclude-too-few-public-methods = + +# List of qualified class names to ignore when counting class parents (see R0901) +# ignored-parents = + +# Maximum number of arguments for function / method. +max-args = 15 + +# Maximum number of attributes for a class (see R0902). +max-attributes = 12 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr = 5 + +# Maximum number of branch for function / method body. +max-branches = 12 + +# Maximum number of locals for function / method body. +max-locals = 20 + +# Maximum number of parents for a class (see R0901). +max-parents = 7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods = 40 + +# Maximum number of return / yield for function / method body. +max-returns = 6 + +# Maximum number of statements in function / method body. +max-statements = 50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods = 1 + +[tool.pylint.exceptions] +# Exceptions that will emit a warning when caught. +overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] + +[tool.pylint.format] +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +# expected-line-ending-format = + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines = "^\\s*(# )??$" + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren = 4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string = " " + +# Maximum number of characters on a single line. +max-line-length = 110 + +# Maximum number of lines in a module. +max-module-lines = 2500 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +# single-line-class-stmt = + +# Allow the body of an if to be on the same line as the test if there is no else. +# single-line-if-stmt = + +[tool.pylint.imports] +# List of modules that can be imported at any level, not just the top level one. +# allow-any-import-level = + +# Allow explicit reexports by alias from a package __init__. +# allow-reexport-from-package = + +# Allow wildcard imports from modules that define __all__. +# allow-wildcard-with-all = + +# Deprecated modules which should not be used, separated by a comma. +# deprecated-modules = + +# Output a graph (.gv or any supported image format) of external dependencies to +# the given file (report RP0402 must not be disabled). +# ext-import-graph = + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be disabled). +# import-graph = + +# Output a graph (.gv or any supported image format) of internal dependencies to +# the given file (report RP0402 must not be disabled). +# int-import-graph = + +# Force import order to recognize a module as part of the standard compatibility +# libraries. +# known-standard-library = + +# Force import order to recognize a module as part of a third party library. +known-third-party = ["enchant"] + +# Couples of modules and preferred modules, separated by a comma. +# preferred-modules = + +[tool.pylint.logging] +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style = "old" + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules = ["logging"] + +[tool.pylint."messages control"] +# Only show warnings with the listed confidence levels. Leave empty to show all. +# Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] + +# Disable the message, report, category or checker with the given id(s). You can +# either give multiple identifiers separated by comma (,) or put this option +# multiple times (only on the command line, not in the configuration file where +# it should appear only once). You can also use "--disable=all" to disable +# everything first and then re-enable specific checks. For example, if you want +# to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-symbolic-message-instead", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", "import-outside-toplevel", "broad-exception-caught", "too-few-public-methods", "global-statement", "c-extension-no-member", "too-many-instance-attributes", "invalid-name", "attribute-defined-outside-init", "unnecessary-lambda-assignment"] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where it +# should appear only once). See also the "--disable" option for examples. +# enable = + +[tool.pylint.method_args] +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods = ["requests.api.delete", "requests.api.get", "requests.api.head", "requests.api.options", "requests.api.patch", "requests.api.post", "requests.api.put", "requests.api.request"] + +[tool.pylint.miscellaneous] +# List of note tags to take in consideration, separated by a comma. +notes = ["FIXME", "XXX", "TODO"] + +# Regular expression of note tags to take in consideration. +# notes-rgx = + +[tool.pylint.refactoring] +# Maximum number of nested blocks for function / method body +max-nested-blocks = 5 + +# Complete name of functions that never returns. When checking for inconsistent- +# return-statements if a never returning function is called then it will be +# considered as an explicit return statement and no message will be printed. +never-returning-functions = ["sys.exit", "argparse.parse_error"] + +# Let 'consider-using-join' be raised when the separator to join on would be non- +# empty (resulting in expected fixes of the type: ``"- " + " - ".join(items)``) +suggest-join-with-non-empty-separator = true + +[tool.pylint.reports] +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each category, +# as well as 'statement' which is the total number of statements analyzed. This +# score is used by the global evaluation report (RP0004). +evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))" + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +# msg-template = + +# Set the output format. Available formats are: text, parseable, colorized, json2 +# (improved json format), json (old json format) and msvs (visual studio). You +# can also give a reporter class, e.g. mypackage.mymodule.MyReporterClass. +# output-format = + +# Tells whether to display a full report or only the messages. +# reports = + +# Activate the evaluation score. +score = true + +[tool.pylint.similarities] +# Comments are removed from the similarity computation +ignore-comments = true + +# Docstrings are removed from the similarity computation +ignore-docstrings = true + +# Imports are removed from the similarity computation +ignore-imports = true + +# Signatures are removed from the similarity computation +ignore-signatures = true + +# Minimum lines number of a similarity. +min-similarity-lines = 15 + +[tool.pylint.spelling] +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions = 4 + +# Spelling dictionary name. No available dictionaries : You need to install both +# the python package and the system dependency for enchant to work. +# spelling-dict = + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:" + +# List of comma separated words that should not be checked. +# spelling-ignore-words = + +# A path to a file that contains the private dictionary; one word per line. +# spelling-private-dict-file = + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +# spelling-store-unknown-words = + +[tool.pylint.typecheck] +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators = ["contextlib.contextmanager"] + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +# generated-members = + +# Tells whether missing members accessed in mixin class should be ignored. A +# class is considered mixin if its name matches the mixin-class-rgx option. +# Tells whether to warn about missing members when the owner of the attribute is +# inferred to be None. +ignore-none = true + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference can +# return multiple potential results while evaluating a Python object, but some +# branches might not be evaluated, which results in partial inference. In that +# case, it might be useful to still emit no-member and other checks for the rest +# of the inferred objects. +ignore-on-opaque-inference = true + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"] + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes = ["optparse.Values", "thread._local", "_thread._local", "argparse.Namespace"] + +# Show a hint with possible names when a member name was not found. The aspect of +# finding the hint is based on edit distance. +missing-member-hint = true + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance = 1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices = 1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx = ".*[Mm]ixin" + +# List of decorators that change the signature of a decorated function. +# signature-mutators = + +[tool.pylint.variables] +# List of additional names supposed to be defined in builtins. Remember that you +# should avoid defining new builtins when possible. +# additional-builtins = + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables = true + +# List of names allowed to shadow builtins +# allowed-redefined-builtins = + +# List of strings which can identify a callback function by name. A callback name +# must start or end with one of those strings. +callbacks = ["cb_", "_cb"] + +# A regular expression matching the name of dummy variables (i.e. expected to not +# be used). +dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" + +# Argument names that match this expression will be ignored. +ignored-argument-names = "_.*|^ignored_|^unused_" + +# Tells whether we should check for unused import in __init__ files. +# init-import = + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules = ["six.moves", "past.builtins", "future.builtins", "builtins", "io"] diff --git a/ci/configure_all.sh b/scripts/configure.sh similarity index 71% rename from ci/configure_all.sh rename to scripts/configure.sh index b80ff8f..230c683 100755 --- a/ci/configure_all.sh +++ b/scripts/configure.sh @@ -10,23 +10,25 @@ set -eux pipefail -ci_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" -project_home="$(dirname "${ci_home}")" +scripts_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +project_home="$(dirname "${scripts_home}")" mkdir -p "${project_home}/dist/ci" cd "${project_home}" +# shellcheck source=/dev/null +. "${scripts_home}/shared.sh" # pop them into dist since it's ignored anyway -if [[ ! -v PYTHON ]]; then +if ! is-set PYTHON; then PYTHON=python fi frameworks=("pyqt5" "pyqt6" "pyside6") -have_pyside=$(PYTHON -c 'import sys; print(sys.version_info < (3, 11))') +have_pyside=$(${PYTHON} -c 'import sys; print(sys.version_info < (3, 11))') if [[ "${have_pyside}" == "True" ]]; then frameworks+=("pyside2") fi # NOTE: We need to make sure the scripts directory is added to the path -python_home=$(PYTHON -c 'import site; print(site.getsitepackages()[0])') +python_home=$(${PYTHON} -c 'import site; print(site.getsitepackages()[0])') scripts_dir="${python_home}/scripts" uname_s="$(uname -s)" if [[ "${uname_s}" == MINGW* ]]; then @@ -35,7 +37,7 @@ if [[ "${uname_s}" == MINGW* ]]; then fi export PATH="${scripts_dir}:${PATH}" for framework in "${frameworks[@]}"; do - "${PYTHON}" "${project_home}/configure.py" \ + ${PYTHON} "${project_home}/configure.py" \ --styles=all \ --extensions=all \ --qt-framework "${framework}" \ @@ -43,5 +45,5 @@ for framework in "${frameworks[@]}"; do --resource "breeze_${framework}.qrc" \ --compiled-resource "${project_home}/dist/ci/breeze_${framework}.py" # this will auto-fail due to pipefail, checks the imports work - "${PYTHON}" -c "import os; os.chdir('dist/ci'); import breeze_${framework}" -done \ No newline at end of file + ${PYTHON} -c "import os; os.chdir('dist/ci'); import breeze_${framework}" +done diff --git a/scripts/fmt.sh b/scripts/fmt.sh new file mode 100755 index 0000000..8a8cda6 --- /dev/null +++ b/scripts/fmt.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# +# Run our automatic code formatters. +# +# This requires black and isort to be installed. + +set -eux pipefail + +scripts_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +project_home="$(dirname "${scripts_home}")" +cd "${project_home}" + +isort ./*.py example/*.py example/**/*.py +black --config pyproject.toml example/ ./*.py \ No newline at end of file diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 0000000..2793f1a --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# +# Run our code linters, including type checking. +# Since we have 0 dependencies, we don't use securit checks. + +set -eux pipefail + +scripts_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +project_home="$(dirname "${scripts_home}")" +cd "${project_home}" + +# run our python lint checks +pylint ./*.py example/*.py example/**/*.py +pyright example/breeze_theme.py +flake8 + +# run our C++ lint checks \ No newline at end of file diff --git a/scripts/shared.sh b/scripts/shared.sh new file mode 100755 index 0000000..3dc5d14 --- /dev/null +++ b/scripts/shared.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Shared logic for our bash scripts. + +# We have to use this because macOS does not have bash 4.2 support. +is-set() +{ + declare -p "${1}" &>/dev/null +} diff --git a/scripts/theme.sh b/scripts/theme.sh new file mode 100755 index 0000000..a3794da --- /dev/null +++ b/scripts/theme.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# Use scripts to check if the theme determination works. + +set -eux pipefail + +scripts_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +project_home="$(dirname "${scripts_home}")" +cd "${project_home}/example" +# shellcheck source=/dev/null +. "${scripts_home}/shared.sh" + +if ! is-set PYTHON; then + PYTHON=python +fi +# Check the import first, then calling the function for easier debugging. +${PYTHON} -c "import breeze_theme" +theme=$(${PYTHON} -c "import breeze_theme; print(breeze_theme.get_theme())") +if [[ "${theme}" != Theme.* ]]; then + >&2 echo "Unable to get the correct theme." + exit 1 +fi +${PYTHON} -c "import breeze_theme; print(breeze_theme.is_light())" +${PYTHON} -c "import breeze_theme; print(breeze_theme.is_dark())" diff --git a/setup.cfg b/setup.cfg index 00ed8e1..c7c01a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,8 +2,10 @@ exclude = .git, __pycache__, build, dist max-line-length = 110 per-file-ignores = - # Widgets need to be created but not used. + # widgets need to be created but not used. example/widgets.py: F841 test/ui.py: F841 - # Lambdas are way cleaner here + # lambdas are way cleaner here example/titlebar.py: E731 + # these are auto-generated files + resources/*.py: E302 E305 \ No newline at end of file diff --git a/test/ui.py b/test/ui.py index a993b52..3b35c4f 100644 --- a/test/ui.py +++ b/test/ui.py @@ -81,7 +81,7 @@ args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) compat = shared.get_compat_definitions(args) -ICON_MAP = shared.get_icon_map(args, compat) +ICON_MAP = shared.get_icon_map(compat) layout = { 'vertical': QtWidgets.QVBoxLayout, diff --git a/vcs.py b/vcs.py index 93486d5..2a747b2 100644 --- a/vcs.py +++ b/vcs.py @@ -233,12 +233,7 @@ def parse_args(argv=None): '''Parse the command-line options.''' parser = argparse.ArgumentParser(description='Git configuration changes.') - parser.add_argument( - '-v', - '--version', - action='version', - version=f'%(prog)s {__version__}' - ) + parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version__}') dist = parser.add_mutually_exclusive_group() dist.add_argument( '--track-dist', @@ -278,35 +273,41 @@ def call(command, ignore_errors=True): except subprocess.CalledProcessError as error: if b'Unable to mark file' not in error.stderr or not ignore_errors: raise + return None def assume_unchanged(git, file): '''Assume a version-controlled file is unchanged.''' - return call([ - git, - 'update-index', - '--assume-unchanged', - file, - ]) + return call( + [ + git, + 'update-index', + '--assume-unchanged', + file, + ] + ) def no_assume_unchanged(git, file): '''No longer assume a version-controlled file is unchanged.''' - return call([ - git, - 'update-index', - '--no-assume-unchanged', - file, - ]) + return call( + [ + git, + 'update-index', + '--no-assume-unchanged', + file, + ] + ) def write_gitignore(entries): '''Write to ignore ignore file using the provided entries.''' - with open(os.path.join(home, '.gitignore'), 'w') as file: - file.write(f'{"\n".join(entries)}\n{PYTHON_GITIGNORE}\n{CPP_GITIGNORE}\n') + with open(os.path.join(home, '.gitignore'), 'w', encoding='utf-8') as file: + custom = '\n'.join(entries) + file.write(f'{custom}\n{PYTHON_GITIGNORE}\n{CPP_GITIGNORE}\n') def main(argv=None): @@ -361,7 +362,7 @@ def update_dist_index(file): dist_files = [] dist_dirs = [f'{home}/dist', f'{home}/resources'] for dist_dir in dist_dirs: - for root, dirs, files in os.walk(dist_dir): + for root, _, files in os.walk(dist_dir): relpath = os.path.relpath(root, home) for file in files: dist_files.append(f'{relpath}/{file}') From 68a35db1e14f5482da2bb683df09d93773d040a6 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 13:07:07 -0500 Subject: [PATCH 5/9] Add clang-tidy lint action. --- .github/workflows/lint.yml | 102 +-- .github/workflows/theme.yml | 54 +- example/breeze_theme.hpp | 2 +- pyproject.toml | 1162 +++++++++++++++++------------------ 4 files changed, 667 insertions(+), 653 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 73ba172..fe703d1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,44 +1,58 @@ -name: Linters - -on: [push] - -jobs: - lint-version-python: - strategy: - matrix: - os: [ubuntu-latest] - python-version: ["3.11", "3.12"] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint flake8 pyright - - name: Analysing the code with pylint - run: | - scripts/lint.sh - - lint-os-python: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.10"] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint flake8 pyright - - name: Analysing the code with pylint - run: | - scripts/lint.sh +name: Linters + +on: [push] + +jobs: + lint-version-python: + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.11", "3.12"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint flake8 pyright + - name: Analysing the code with pylint + run: | + scripts/lint.sh + + lint-os-python: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint flake8 pyright + - name: Analysing the code with pylint + run: | + scripts/lint.sh + + lint-cpp: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo apt update + sudo apt install clang-tidy -y + - name: Analysing the code with clang-tidy + shell: bash + run: | + set -eux pipefail + clang-tidy -checks=-*,clang-analyzer-*,-clang-analyzer-cplusplus* example/breeze_theme.hpp -- diff --git a/.github/workflows/theme.yml b/.github/workflows/theme.yml index 893d6a5..348f632 100644 --- a/.github/workflows/theme.yml +++ b/.github/workflows/theme.yml @@ -1,27 +1,27 @@ -name: Theme - -on: [push] - -jobs: - theme-python: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.10"] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - shell: bash - run: | - python -m pip install --upgrade pip - if [[ "$RUNNER_OS" == "Windows" ]]; then - python -m pip install winrt-Windows.UI.ViewManagement winrt-Windows.UI - fi - - name: Checking our Python imports. - run: | - scripts/theme.sh +name: Theme + +on: [push] + +jobs: + theme-python: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + shell: bash + run: | + python -m pip install --upgrade pip + if [[ "$RUNNER_OS" == "Windows" ]]; then + python -m pip install winrt-Windows.UI.ViewManagement winrt-Windows.UI + fi + - name: Checking our Python imports. + run: | + scripts/theme.sh diff --git a/example/breeze_theme.hpp b/example/breeze_theme.hpp index 0d9e8b9..f27e349 100644 --- a/example/breeze_theme.hpp +++ b/example/breeze_theme.hpp @@ -461,4 +461,4 @@ namespace breeze_stylesheets { return ::breeze_stylesheets::get_theme() == ::breeze_stylesheets::theme::light; } -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index fdc39fa..4e6ed30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,581 +1,581 @@ -# NOTE: This is not a true pyproject.toml, this is for configurations with black. - -[tool.black] -target-version = ["py310"] -line-length = 110 -color = true -skip-string-normalization = true - -exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - | env - | venv -)/ -''' - -[tool.isort] -py_version = 310 -line_length = 110 -include_trailing_comma = true -profile = "black" -multi_line_output = 3 -indent = 4 -color_output = true -known_typing = ["typing", "types", "typing_extensions", "mypy", "mypy_extensions"] -sections = ["FUTURE", "TYPING", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] -no_lines_before = ["TYPING", "STDLIB", "FIRSTPARTY", "LOCALFOLDER"] - -[tool.pylint.main] -# Analyse import fallback blocks. This can be used to support both Python 2 and 3 -# compatible code, which means that the block might have code that exists only in -# one or another interpreter, leading to false positives when analysed. -# analyse-fallback-blocks = - -# Clear in-memory caches upon conclusion of linting. Useful if running pylint in -# a server-like mode. -# clear-cache-post-run = - -# Always return a 0 (non-error) status code, even if lint errors are found. This -# is primarily useful in continuous integration scripts. -# exit-zero = - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -# extension-pkg-allow-list = - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. (This is an alternative name to extension-pkg-allow-list -# for backward compatibility.) -# extension-pkg-whitelist = - -# Return non-zero exit code if any of these messages/categories are detected, -# even if score is above --fail-under value. Syntax same as enable. Messages -# specified are enabled, while categories only check already-enabled messages. -# fail-on = - -# Specify a score threshold under which the program will exit with error. -fail-under = 10.0 - -# Interpret the stdin as a python script, whose filename needs to be passed as -# the module_or_package argument. -# from-stdin = - -# Files or directories to be skipped. They should be base names, not paths. -ignore = ["CVS"] - -# Add files or directories matching the regular expressions patterns to the -# ignore-list. The regex matches against paths and can be in Posix or Windows -# format. Because '\\' represents the directory delimiter on Windows systems, it -# can't be used as an escape character. -# ignore-paths = - -# Files or directories matching the regular expression patterns are skipped. The -# regex matches against base names, not paths. The default value ignores Emacs -# file locks -ignore-patterns = ["^\\.#"] - -# List of module names for which member attributes should not be checked and will -# not be imported (useful for modules/projects where namespaces are manipulated -# during runtime and thus existing member attributes cannot be deduced by static -# analysis). It supports qualified module names, as well as Unix pattern -# matching. -# ignored-modules = - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -# init-hook = - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use, and will cap the count on Windows to -# avoid hangs. -jobs = 1 - -# Control the amount of potential inferred values when inferring a single object. -# This can help the performance when dealing with large functions or complex, -# nested conditions. -limit-inference-results = 100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -# load-plugins = - -# Pickle collected data for later comparisons. -persistent = true - -# Resolve imports to .pyi stubs if available. May reduce no-member messages and -# increase not-an-iterable messages. -# prefer-stubs = - -# Minimum Python version to use for version dependent checks. Will default to the -# version used to run pylint. -py-version = "3.10" - -# Discover python modules and packages in the file system subtree. -# recursive = - -# Add paths to the list of the source roots. Supports globbing patterns. The -# source root is an absolute path or a path relative to the current working -# directory used to determine a package namespace for modules located under the -# source root. -# source-roots = - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode = true - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -# unsafe-load-any-extension = - -[tool.pylint.basic] -# Naming style matching correct argument names. -argument-naming-style = "snake_case" - -# Regular expression matching correct argument names. Overrides argument-naming- -# style. If left empty, argument names will be checked with the set naming style. -# argument-rgx = - -# Naming style matching correct attribute names. -attr-naming-style = "snake_case" - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. If left empty, attribute names will be checked with the set naming -# style. -# attr-rgx = - -# Bad variable names which should always be refused, separated by a comma. -bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -# bad-names-rgxs = - -# Naming style matching correct class attribute names. -class-attribute-naming-style = "any" - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. If left empty, class attribute names will be checked -# with the set naming style. -# class-attribute-rgx = - -# Naming style matching correct class constant names. -class-const-naming-style = "UPPER_CASE" - -# Regular expression matching correct class constant names. Overrides class- -# const-naming-style. If left empty, class constant names will be checked with -# the set naming style. -# class-const-rgx = - -# Naming style matching correct class names. -class-naming-style = "PascalCase" - -# Regular expression matching correct class names. Overrides class-naming-style. -# If left empty, class names will be checked with the set naming style. -# class-rgx = - -# Naming style matching correct constant names. -const-naming-style = "UPPER_CASE" - -# Regular expression matching correct constant names. Overrides const-naming- -# style. If left empty, constant names will be checked with the set naming style. -# const-rgx = - -# Minimum line length for functions/classes that require docstrings, shorter ones -# are exempt. -docstring-min-length = -1 - -# Naming style matching correct function names. -function-naming-style = "snake_case" - -# Regular expression matching correct function names. Overrides function-naming- -# style. If left empty, function names will be checked with the set naming style. -# function-rgx = - -# Good variable names which should always be accepted, separated by a comma. -good-names = ["i", "j", "k", "ex", "Run", "_"] - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -# good-names-rgxs = - -# Include a hint for the correct naming format with invalid-name. -# include-naming-hint = - -# Naming style matching correct inline iteration names. -inlinevar-naming-style = "any" - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. If left empty, inline iteration names will be checked -# with the set naming style. -# inlinevar-rgx = - -# Naming style matching correct method names. -method-naming-style = "snake_case" - -# Regular expression matching correct method names. Overrides method-naming- -# style. If left empty, method names will be checked with the set naming style. -# method-rgx = - -# Naming style matching correct module names. -module-naming-style = "snake_case" - -# Regular expression matching correct module names. Overrides module-naming- -# style. If left empty, module names will be checked with the set naming style. -# module-rgx = - -# Colon-delimited sets of names that determine each other's naming style when the -# name regexes allow several styles. -# name-group = - -# Regular expression which should only match function or class names that do not -# require a docstring. -no-docstring-rgx = "^_" - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. These -# decorators are taken in consideration only for invalid-name. -property-classes = ["abc.abstractproperty"] - -# Regular expression matching correct type alias names. If left empty, type alias -# names will be checked with the set naming style. -# typealias-rgx = - -# Regular expression matching correct type variable names. If left empty, type -# variable names will be checked with the set naming style. -# typevar-rgx = - -# Naming style matching correct variable names. -variable-naming-style = "snake_case" - -# Regular expression matching correct variable names. Overrides variable-naming- -# style. If left empty, variable names will be checked with the set naming style. -# variable-rgx = - -[tool.pylint.classes] -# Warn about protected attribute access inside special methods -# check-protected-access-in-special-methods = - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg = ["cls"] - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg = ["mcs"] - -[tool.pylint.design] -# List of regular expressions of class ancestor names to ignore when counting -# public methods (see R0903) -# exclude-too-few-public-methods = - -# List of qualified class names to ignore when counting class parents (see R0901) -# ignored-parents = - -# Maximum number of arguments for function / method. -max-args = 15 - -# Maximum number of attributes for a class (see R0902). -max-attributes = 12 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr = 5 - -# Maximum number of branch for function / method body. -max-branches = 12 - -# Maximum number of locals for function / method body. -max-locals = 20 - -# Maximum number of parents for a class (see R0901). -max-parents = 7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods = 40 - -# Maximum number of return / yield for function / method body. -max-returns = 6 - -# Maximum number of statements in function / method body. -max-statements = 50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods = 1 - -[tool.pylint.exceptions] -# Exceptions that will emit a warning when caught. -overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] - -[tool.pylint.format] -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -# expected-line-ending-format = - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines = "^\\s*(# )??$" - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren = 4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string = " " - -# Maximum number of characters on a single line. -max-line-length = 110 - -# Maximum number of lines in a module. -max-module-lines = 2500 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -# single-line-class-stmt = - -# Allow the body of an if to be on the same line as the test if there is no else. -# single-line-if-stmt = - -[tool.pylint.imports] -# List of modules that can be imported at any level, not just the top level one. -# allow-any-import-level = - -# Allow explicit reexports by alias from a package __init__. -# allow-reexport-from-package = - -# Allow wildcard imports from modules that define __all__. -# allow-wildcard-with-all = - -# Deprecated modules which should not be used, separated by a comma. -# deprecated-modules = - -# Output a graph (.gv or any supported image format) of external dependencies to -# the given file (report RP0402 must not be disabled). -# ext-import-graph = - -# Output a graph (.gv or any supported image format) of all (i.e. internal and -# external) dependencies to the given file (report RP0402 must not be disabled). -# import-graph = - -# Output a graph (.gv or any supported image format) of internal dependencies to -# the given file (report RP0402 must not be disabled). -# int-import-graph = - -# Force import order to recognize a module as part of the standard compatibility -# libraries. -# known-standard-library = - -# Force import order to recognize a module as part of a third party library. -known-third-party = ["enchant"] - -# Couples of modules and preferred modules, separated by a comma. -# preferred-modules = - -[tool.pylint.logging] -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style = "old" - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules = ["logging"] - -[tool.pylint."messages control"] -# Only show warnings with the listed confidence levels. Leave empty to show all. -# Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] - -# Disable the message, report, category or checker with the given id(s). You can -# either give multiple identifiers separated by comma (,) or put this option -# multiple times (only on the command line, not in the configuration file where -# it should appear only once). You can also use "--disable=all" to disable -# everything first and then re-enable specific checks. For example, if you want -# to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-symbolic-message-instead", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", "import-outside-toplevel", "broad-exception-caught", "too-few-public-methods", "global-statement", "c-extension-no-member", "too-many-instance-attributes", "invalid-name", "attribute-defined-outside-init", "unnecessary-lambda-assignment"] - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where it -# should appear only once). See also the "--disable" option for examples. -# enable = - -[tool.pylint.method_args] -# List of qualified names (i.e., library.method) which require a timeout -# parameter e.g. 'requests.api.get,requests.api.post' -timeout-methods = ["requests.api.delete", "requests.api.get", "requests.api.head", "requests.api.options", "requests.api.patch", "requests.api.post", "requests.api.put", "requests.api.request"] - -[tool.pylint.miscellaneous] -# List of note tags to take in consideration, separated by a comma. -notes = ["FIXME", "XXX", "TODO"] - -# Regular expression of note tags to take in consideration. -# notes-rgx = - -[tool.pylint.refactoring] -# Maximum number of nested blocks for function / method body -max-nested-blocks = 5 - -# Complete name of functions that never returns. When checking for inconsistent- -# return-statements if a never returning function is called then it will be -# considered as an explicit return statement and no message will be printed. -never-returning-functions = ["sys.exit", "argparse.parse_error"] - -# Let 'consider-using-join' be raised when the separator to join on would be non- -# empty (resulting in expected fixes of the type: ``"- " + " - ".join(items)``) -suggest-join-with-non-empty-separator = true - -[tool.pylint.reports] -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'fatal', 'error', 'warning', 'refactor', -# 'convention', and 'info' which contain the number of messages in each category, -# as well as 'statement' which is the total number of statements analyzed. This -# score is used by the global evaluation report (RP0004). -evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))" - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -# msg-template = - -# Set the output format. Available formats are: text, parseable, colorized, json2 -# (improved json format), json (old json format) and msvs (visual studio). You -# can also give a reporter class, e.g. mypackage.mymodule.MyReporterClass. -# output-format = - -# Tells whether to display a full report or only the messages. -# reports = - -# Activate the evaluation score. -score = true - -[tool.pylint.similarities] -# Comments are removed from the similarity computation -ignore-comments = true - -# Docstrings are removed from the similarity computation -ignore-docstrings = true - -# Imports are removed from the similarity computation -ignore-imports = true - -# Signatures are removed from the similarity computation -ignore-signatures = true - -# Minimum lines number of a similarity. -min-similarity-lines = 15 - -[tool.pylint.spelling] -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions = 4 - -# Spelling dictionary name. No available dictionaries : You need to install both -# the python package and the system dependency for enchant to work. -# spelling-dict = - -# List of comma separated words that should be considered directives if they -# appear at the beginning of a comment and should not be checked. -spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:" - -# List of comma separated words that should not be checked. -# spelling-ignore-words = - -# A path to a file that contains the private dictionary; one word per line. -# spelling-private-dict-file = - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -# spelling-store-unknown-words = - -[tool.pylint.typecheck] -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators = ["contextlib.contextmanager"] - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -# generated-members = - -# Tells whether missing members accessed in mixin class should be ignored. A -# class is considered mixin if its name matches the mixin-class-rgx option. -# Tells whether to warn about missing members when the owner of the attribute is -# inferred to be None. -ignore-none = true - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference can -# return multiple potential results while evaluating a Python object, but some -# branches might not be evaluated, which results in partial inference. In that -# case, it might be useful to still emit no-member and other checks for the rest -# of the inferred objects. -ignore-on-opaque-inference = true - -# List of symbolic message names to ignore for Mixin members. -ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"] - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes = ["optparse.Values", "thread._local", "_thread._local", "argparse.Namespace"] - -# Show a hint with possible names when a member name was not found. The aspect of -# finding the hint is based on edit distance. -missing-member-hint = true - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance = 1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices = 1 - -# Regex pattern to define which classes are considered mixins. -mixin-class-rgx = ".*[Mm]ixin" - -# List of decorators that change the signature of a decorated function. -# signature-mutators = - -[tool.pylint.variables] -# List of additional names supposed to be defined in builtins. Remember that you -# should avoid defining new builtins when possible. -# additional-builtins = - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables = true - -# List of names allowed to shadow builtins -# allowed-redefined-builtins = - -# List of strings which can identify a callback function by name. A callback name -# must start or end with one of those strings. -callbacks = ["cb_", "_cb"] - -# A regular expression matching the name of dummy variables (i.e. expected to not -# be used). -dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" - -# Argument names that match this expression will be ignored. -ignored-argument-names = "_.*|^ignored_|^unused_" - -# Tells whether we should check for unused import in __init__ files. -# init-import = - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules = ["six.moves", "past.builtins", "future.builtins", "builtins", "io"] +# NOTE: This is not a true pyproject.toml, this is for configurations with black. + +[tool.black] +target-version = ["py310"] +line-length = 110 +color = true +skip-string-normalization = true + +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | env + | venv +)/ +''' + +[tool.isort] +py_version = 310 +line_length = 110 +include_trailing_comma = true +profile = "black" +multi_line_output = 3 +indent = 4 +color_output = true +known_typing = ["typing", "types", "typing_extensions", "mypy", "mypy_extensions"] +sections = ["FUTURE", "TYPING", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +no_lines_before = ["TYPING", "STDLIB", "FIRSTPARTY", "LOCALFOLDER"] + +[tool.pylint.main] +# Analyse import fallback blocks. This can be used to support both Python 2 and 3 +# compatible code, which means that the block might have code that exists only in +# one or another interpreter, leading to false positives when analysed. +# analyse-fallback-blocks = + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint in +# a server-like mode. +# clear-cache-post-run = + +# Always return a 0 (non-error) status code, even if lint errors are found. This +# is primarily useful in continuous integration scripts. +# exit-zero = + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +# extension-pkg-allow-list = + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +# extension-pkg-whitelist = + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +# fail-on = + +# Specify a score threshold under which the program will exit with error. +fail-under = 10.0 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +# from-stdin = + +# Files or directories to be skipped. They should be base names, not paths. +ignore = ["CVS"] + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, it +# can't be used as an escape character. +# ignore-paths = + +# Files or directories matching the regular expression patterns are skipped. The +# regex matches against base names, not paths. The default value ignores Emacs +# file locks +ignore-patterns = ["^\\.#"] + +# List of module names for which member attributes should not be checked and will +# not be imported (useful for modules/projects where namespaces are manipulated +# during runtime and thus existing member attributes cannot be deduced by static +# analysis). It supports qualified module names, as well as Unix pattern +# matching. +# ignored-modules = + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +# init-hook = + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs = 1 + +# Control the amount of potential inferred values when inferring a single object. +# This can help the performance when dealing with large functions or complex, +# nested conditions. +limit-inference-results = 100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +# load-plugins = + +# Pickle collected data for later comparisons. +persistent = true + +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. +# prefer-stubs = + +# Minimum Python version to use for version dependent checks. Will default to the +# version used to run pylint. +py-version = "3.10" + +# Discover python modules and packages in the file system subtree. +# recursive = + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +# source-roots = + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode = true + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +# unsafe-load-any-extension = + +[tool.pylint.basic] +# Naming style matching correct argument names. +argument-naming-style = "snake_case" + +# Regular expression matching correct argument names. Overrides argument-naming- +# style. If left empty, argument names will be checked with the set naming style. +# argument-rgx = + +# Naming style matching correct attribute names. +attr-naming-style = "snake_case" + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +# attr-rgx = + +# Bad variable names which should always be refused, separated by a comma. +bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +# bad-names-rgxs = + +# Naming style matching correct class attribute names. +class-attribute-naming-style = "any" + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +# class-attribute-rgx = + +# Naming style matching correct class constant names. +class-const-naming-style = "UPPER_CASE" + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +# class-const-rgx = + +# Naming style matching correct class names. +class-naming-style = "PascalCase" + +# Regular expression matching correct class names. Overrides class-naming-style. +# If left empty, class names will be checked with the set naming style. +# class-rgx = + +# Naming style matching correct constant names. +const-naming-style = "UPPER_CASE" + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming style. +# const-rgx = + +# Minimum line length for functions/classes that require docstrings, shorter ones +# are exempt. +docstring-min-length = -1 + +# Naming style matching correct function names. +function-naming-style = "snake_case" + +# Regular expression matching correct function names. Overrides function-naming- +# style. If left empty, function names will be checked with the set naming style. +# function-rgx = + +# Good variable names which should always be accepted, separated by a comma. +good-names = ["i", "j", "k", "ex", "Run", "_"] + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +# good-names-rgxs = + +# Include a hint for the correct naming format with invalid-name. +# include-naming-hint = + +# Naming style matching correct inline iteration names. +inlinevar-naming-style = "any" + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +# inlinevar-rgx = + +# Naming style matching correct method names. +method-naming-style = "snake_case" + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +# method-rgx = + +# Naming style matching correct module names. +module-naming-style = "snake_case" + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +# module-rgx = + +# Colon-delimited sets of names that determine each other's naming style when the +# name regexes allow several styles. +# name-group = + +# Regular expression which should only match function or class names that do not +# require a docstring. +no-docstring-rgx = "^_" + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. These +# decorators are taken in consideration only for invalid-name. +property-classes = ["abc.abstractproperty"] + +# Regular expression matching correct type alias names. If left empty, type alias +# names will be checked with the set naming style. +# typealias-rgx = + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +# typevar-rgx = + +# Naming style matching correct variable names. +variable-naming-style = "snake_case" + +# Regular expression matching correct variable names. Overrides variable-naming- +# style. If left empty, variable names will be checked with the set naming style. +# variable-rgx = + +[tool.pylint.classes] +# Warn about protected attribute access inside special methods +# check-protected-access-in-special-methods = + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg = ["cls"] + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg = ["mcs"] + +[tool.pylint.design] +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +# exclude-too-few-public-methods = + +# List of qualified class names to ignore when counting class parents (see R0901) +# ignored-parents = + +# Maximum number of arguments for function / method. +max-args = 15 + +# Maximum number of attributes for a class (see R0902). +max-attributes = 12 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr = 5 + +# Maximum number of branch for function / method body. +max-branches = 12 + +# Maximum number of locals for function / method body. +max-locals = 20 + +# Maximum number of parents for a class (see R0901). +max-parents = 7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods = 40 + +# Maximum number of return / yield for function / method body. +max-returns = 6 + +# Maximum number of statements in function / method body. +max-statements = 50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods = 1 + +[tool.pylint.exceptions] +# Exceptions that will emit a warning when caught. +overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] + +[tool.pylint.format] +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +# expected-line-ending-format = + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines = "^\\s*(# )??$" + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren = 4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string = " " + +# Maximum number of characters on a single line. +max-line-length = 110 + +# Maximum number of lines in a module. +max-module-lines = 2500 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +# single-line-class-stmt = + +# Allow the body of an if to be on the same line as the test if there is no else. +# single-line-if-stmt = + +[tool.pylint.imports] +# List of modules that can be imported at any level, not just the top level one. +# allow-any-import-level = + +# Allow explicit reexports by alias from a package __init__. +# allow-reexport-from-package = + +# Allow wildcard imports from modules that define __all__. +# allow-wildcard-with-all = + +# Deprecated modules which should not be used, separated by a comma. +# deprecated-modules = + +# Output a graph (.gv or any supported image format) of external dependencies to +# the given file (report RP0402 must not be disabled). +# ext-import-graph = + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be disabled). +# import-graph = + +# Output a graph (.gv or any supported image format) of internal dependencies to +# the given file (report RP0402 must not be disabled). +# int-import-graph = + +# Force import order to recognize a module as part of the standard compatibility +# libraries. +# known-standard-library = + +# Force import order to recognize a module as part of a third party library. +known-third-party = ["enchant"] + +# Couples of modules and preferred modules, separated by a comma. +# preferred-modules = + +[tool.pylint.logging] +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style = "old" + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules = ["logging"] + +[tool.pylint."messages control"] +# Only show warnings with the listed confidence levels. Leave empty to show all. +# Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] + +# Disable the message, report, category or checker with the given id(s). You can +# either give multiple identifiers separated by comma (,) or put this option +# multiple times (only on the command line, not in the configuration file where +# it should appear only once). You can also use "--disable=all" to disable +# everything first and then re-enable specific checks. For example, if you want +# to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-symbolic-message-instead", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", "import-outside-toplevel", "broad-exception-caught", "too-few-public-methods", "global-statement", "c-extension-no-member", "too-many-instance-attributes", "invalid-name", "attribute-defined-outside-init", "unnecessary-lambda-assignment"] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where it +# should appear only once). See also the "--disable" option for examples. +# enable = + +[tool.pylint.method_args] +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods = ["requests.api.delete", "requests.api.get", "requests.api.head", "requests.api.options", "requests.api.patch", "requests.api.post", "requests.api.put", "requests.api.request"] + +[tool.pylint.miscellaneous] +# List of note tags to take in consideration, separated by a comma. +notes = ["FIXME", "XXX", "TODO"] + +# Regular expression of note tags to take in consideration. +# notes-rgx = + +[tool.pylint.refactoring] +# Maximum number of nested blocks for function / method body +max-nested-blocks = 5 + +# Complete name of functions that never returns. When checking for inconsistent- +# return-statements if a never returning function is called then it will be +# considered as an explicit return statement and no message will be printed. +never-returning-functions = ["sys.exit", "argparse.parse_error"] + +# Let 'consider-using-join' be raised when the separator to join on would be non- +# empty (resulting in expected fixes of the type: ``"- " + " - ".join(items)``) +suggest-join-with-non-empty-separator = true + +[tool.pylint.reports] +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each category, +# as well as 'statement' which is the total number of statements analyzed. This +# score is used by the global evaluation report (RP0004). +evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))" + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +# msg-template = + +# Set the output format. Available formats are: text, parseable, colorized, json2 +# (improved json format), json (old json format) and msvs (visual studio). You +# can also give a reporter class, e.g. mypackage.mymodule.MyReporterClass. +# output-format = + +# Tells whether to display a full report or only the messages. +# reports = + +# Activate the evaluation score. +score = true + +[tool.pylint.similarities] +# Comments are removed from the similarity computation +ignore-comments = true + +# Docstrings are removed from the similarity computation +ignore-docstrings = true + +# Imports are removed from the similarity computation +ignore-imports = true + +# Signatures are removed from the similarity computation +ignore-signatures = true + +# Minimum lines number of a similarity. +min-similarity-lines = 15 + +[tool.pylint.spelling] +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions = 4 + +# Spelling dictionary name. No available dictionaries : You need to install both +# the python package and the system dependency for enchant to work. +# spelling-dict = + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:" + +# List of comma separated words that should not be checked. +# spelling-ignore-words = + +# A path to a file that contains the private dictionary; one word per line. +# spelling-private-dict-file = + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +# spelling-store-unknown-words = + +[tool.pylint.typecheck] +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators = ["contextlib.contextmanager"] + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +# generated-members = + +# Tells whether missing members accessed in mixin class should be ignored. A +# class is considered mixin if its name matches the mixin-class-rgx option. +# Tells whether to warn about missing members when the owner of the attribute is +# inferred to be None. +ignore-none = true + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference can +# return multiple potential results while evaluating a Python object, but some +# branches might not be evaluated, which results in partial inference. In that +# case, it might be useful to still emit no-member and other checks for the rest +# of the inferred objects. +ignore-on-opaque-inference = true + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"] + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes = ["optparse.Values", "thread._local", "_thread._local", "argparse.Namespace"] + +# Show a hint with possible names when a member name was not found. The aspect of +# finding the hint is based on edit distance. +missing-member-hint = true + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance = 1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices = 1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx = ".*[Mm]ixin" + +# List of decorators that change the signature of a decorated function. +# signature-mutators = + +[tool.pylint.variables] +# List of additional names supposed to be defined in builtins. Remember that you +# should avoid defining new builtins when possible. +# additional-builtins = + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables = true + +# List of names allowed to shadow builtins +# allowed-redefined-builtins = + +# List of strings which can identify a callback function by name. A callback name +# must start or end with one of those strings. +callbacks = ["cb_", "_cb"] + +# A regular expression matching the name of dummy variables (i.e. expected to not +# be used). +dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" + +# Argument names that match this expression will be ignored. +ignored-argument-names = "_.*|^ignored_|^unused_" + +# Tells whether we should check for unused import in __init__ files. +# init-import = + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules = ["six.moves", "past.builtins", "future.builtins", "builtins", "io"] From 02bfbea40bf30dd4ab840f14f0bd1bf22d1309fe Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 14:12:25 -0500 Subject: [PATCH 6/9] Add configure script checks. --- .github/workflows/configure.yml | 21 +++++++++++++++++++++ .github/workflows/lint.yml | 26 ++++++++++++++++++++++---- .github/workflows/theme.yml | 4 ++-- example/breeze_theme.hpp | 9 +++++---- 4 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/configure.yml diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml new file mode 100644 index 0000000..89bf51d --- /dev/null +++ b/.github/workflows/configure.yml @@ -0,0 +1,21 @@ +name: Configure + +on: [push] + +jobs: + theme-python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + shell: bash + run: | + python -m pip install --upgrade pip + pip install PySide2 PySide6 PyQt5 PyQt6 + - name: Checking our configuration scripts. + run: | + scripts/configure.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fe703d1..13f87e6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -44,15 +44,33 @@ jobs: scripts/lint.sh lint-cpp: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Install dependencies run: | - sudo apt update - sudo apt install clang-tidy -y + python -m pip install --upgrade pip + pip install clang-tidy - name: Analysing the code with clang-tidy shell: bash run: | set -eux pipefail - clang-tidy -checks=-*,clang-analyzer-*,-clang-analyzer-cplusplus* example/breeze_theme.hpp -- + + # Windows oddly requires C++20 support due to internal bugs. + if [[ "${RUNNER_OS}" == "Windows" ]]; then + extra_args="-extra-arg=-std=c++20" + passthrough="" + elif [[ "${RUNNER_OS}" == "macOS" ]]; then + # NOTE: The search paths aren't added by default, and we need C then C++ by default + # for our search. This makes the process easier. + extra_args="-extra-arg=-std=c++17 -extra-arg=--stdlib=libc++" + location="$(xcrun --show-sdk-path)" + passthrough="-I${location}/usr/include/c++/v1 -I${location}/usr/include" + else + extra_args="-extra-arg=-std=c++17" + passthrough="" + fi + clang-tidy -checks=-*,clang-analyzer-*,-clang-analyzer-cplusplus* ${extra_args} example/breeze_theme.hpp -- ${passthrough} diff --git a/.github/workflows/theme.yml b/.github/workflows/theme.yml index 348f632..7a8906e 100644 --- a/.github/workflows/theme.yml +++ b/.github/workflows/theme.yml @@ -19,8 +19,8 @@ jobs: shell: bash run: | python -m pip install --upgrade pip - if [[ "$RUNNER_OS" == "Windows" ]]; then - python -m pip install winrt-Windows.UI.ViewManagement winrt-Windows.UI + if [[ "${RUNNER_OS}" == "Windows" ]]; then + python -m pip install winrt-Windows.UI.ViewManagement winrt-Windows.UI fi - name: Checking our Python imports. run: | diff --git a/example/breeze_theme.hpp b/example/breeze_theme.hpp index f27e349..75c61d7 100644 --- a/example/breeze_theme.hpp +++ b/example/breeze_theme.hpp @@ -423,17 +423,18 @@ namespace breeze_stylesheets // this will return something like `prefer - dark`, which is the true value. // valid values are 'default', 'prefer-dark', 'prefer-light'. const ::std::string command = "gsettings get org.gnome.desktop.interface "; - auto [stdout, code] = ::breeze_stylesheets::_run_command(command + "color-scheme"); - if (code != EXIT_SUCCESS) + auto result = ::breeze_stylesheets::_run_command(command + "color-scheme"); + if (::std::get<1>(result) != EXIT_SUCCESS) { // NOTE: We always assume this is due to invalid key, which might not be true // since we don't check if gsettings exists first. // if not found then trying older gtk-theme method // this relies on the theme not lying to you : if the theme is dark, it ends in `- dark`. - auto [stdout, code] = ::breeze_stylesheets::_run_command(command + "gtk-theme"); + result = ::breeze_stylesheets::_run_command(command + "gtk-theme"); } - if (code != EXIT_SUCCESS || !stdout.has_value()) + auto stdout = ::std::get<0>(result); + if (::std::get<1>(result) != EXIT_SUCCESS || !stdout.has_value()) throw new ::std::runtime_error("Unable to get response for the current system theme."); auto value = stdout.value(); From 8dc05a7fd1543a3ef2d9161e0b45f65eb97345c7 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 15:23:58 -0500 Subject: [PATCH 7/9] Add UI checks in headless mode. --- .github/workflows/lint.yml | 2 +- .github/workflows/ui.yml | 27 ++++++++++++++ example/shared.py | 2 ++ scripts/headless.sh | 57 +++++++++++++++++++++++++++++ test/ui.py | 73 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ui.yml create mode 100755 scripts/headless.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 13f87e6..10c7bf9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,7 +46,7 @@ jobs: lint-cpp: strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest] # TODO: Restore macos-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml new file mode 100644 index 0000000..6c9a86b --- /dev/null +++ b/.github/workflows/ui.yml @@ -0,0 +1,27 @@ +name: Ui + +on: [push] + +jobs: + ui: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + shell: bash + run: | + python -m pip install --upgrade pip + pip install PySide2 PySide6 PyQt5 PyQt6 + sudo apt-get update + sudo apt-get install xvfb + sudo apt-get install build-essential libgl1-mesa-dev libgstreamer-gl1.0-0 libpulse-dev \ + libxcb-glx0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \ + libxcb-render0 libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-util1 libxcb-xfixes0 \ + libxcb-xinerama0 libxcb1 libxkbcommon-dev libxkbcommon-x11-0 libxcb-xkb-dev + - name: Checking our Python imports. + run: | + scripts/headless.sh diff --git a/example/shared.py b/example/shared.py index a305385..709de21 100644 --- a/example/shared.py +++ b/example/shared.py @@ -1017,6 +1017,8 @@ def exec_app(args, app, window): '''Show and execute the Qt application.''' window.show() + if os.environ.get('QT_QPA_PLATFORM') == 'offscreen': + return app.quit() return execute(args, app) diff --git a/scripts/headless.sh b/scripts/headless.sh new file mode 100755 index 0000000..eaf797a --- /dev/null +++ b/scripts/headless.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2086,2068 +# +# Run each configure for all supported frameworks, and store them in `dist/ci`. +# This requires the correct frameworks to be installed: +# - PyQt5 +# - PyQt6 +# - PySide6 +# And if using Python 3.10 or earlier: +# - PySide2 + +set -eux pipefail + +scripts_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +project_home="$(dirname "${scripts_home}")" +mkdir -p "${project_home}/dist/ci" +cd "${project_home}" +# shellcheck source=/dev/null +. "${scripts_home}/shared.sh" + +# we xcb installed for our headless running, so exit if we don't have it +if ! hash xvfb-run &>/dev/null; then + >&2 echo "Do not have xvfb installed..." + exit 1 +fi + +# pop them into dist since it's ignored anyway +if ! is-set PYTHON; then + PYTHON=python +fi +frameworks=("pyqt5" "pyqt6" "pyside6") +have_pyside=$(${PYTHON} -c 'import sys; print(sys.version_info < (3, 11))') +if [[ "${have_pyside}" == "True" ]]; then + frameworks+=("pyside2") +fi + +# need to run everything in headless mode. +# note: our shared libraries can be run without issues +export QT_QPA_PLATFORM=offscreen +for script in example/*.py; do + if [[ "${script}" == "example/advanced-dock.py" ]]; then + continue + fi + for framework in "${frameworks[@]}"; do + echo "Running '${script}' for framework '${framework}'." + xvfb-run -a "${PYTHON}" "${script}" --qt-framework "${framework}" + done +done + +# now we need to run our tests +widgets=$(${PYTHON} -c "import os; os.chdir('test'); import ui; print(' '.join([i[5:] for i in dir(ui) if i.startswith('test_')]))") +for widget in ${widgets[@]}; do + for framework in "${frameworks[@]}"; do + echo "Running test for widget '${widget}' for framework '${framework}'." + xvfb-run -a "${PYTHON}" test/ui.py --widget "${widget}" --qt-framework "${framework}" + done +done diff --git a/test/ui.py b/test/ui.py index 3b35c4f..df876eb 100644 --- a/test/ui.py +++ b/test/ui.py @@ -99,6 +99,11 @@ } +def is_headless(): + '''Get if the scripts are running in test mode, that is offscreen.''' + return os.environ.get('QT_QPA_PLATFORM') == 'offscreen' + + def add_widgets(layout, children): '''Add 1 or more widgets to the layout.''' @@ -1731,6 +1736,8 @@ def test_file_icon_provider(widget, *_): def test_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QDialog(window) dialog.setMinimumSize(100, 100) shared.execute(args, dialog) @@ -1739,6 +1746,8 @@ def test_dialog(_, window, *__): def test_modal_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QDialog(window) dialog.setMinimumSize(100, 100) dialog.setModal(True) @@ -1748,6 +1757,8 @@ def test_modal_dialog(_, window, *__): def test_sizegrip_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QDialog(window) dialog.setMinimumSize(100, 100) dialog.setSizeGripEnabled(True) @@ -1757,6 +1768,8 @@ def test_sizegrip_dialog(_, window, *__): def test_colordialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QColor() QtWidgets.QColorDialog.getColor(initial) @@ -1764,6 +1777,8 @@ def test_colordialog(*_): def test_alpha_colordialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QColor() QtWidgets.QColorDialog.getColor(initial, options=compat.ColorShowAlphaChannel) @@ -1771,6 +1786,8 @@ def test_alpha_colordialog(*_): def test_nobuttons_colordialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QColor() QtWidgets.QColorDialog.getColor(initial, options=compat.ColorNoButtons) @@ -1778,6 +1795,8 @@ def test_nobuttons_colordialog(*_): def test_qt_colordialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QColor() QtWidgets.QColorDialog.getColor(initial, options=compat.ColorDontUseNativeDialog) @@ -1785,6 +1804,8 @@ def test_qt_colordialog(*_): def test_fontdialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QFont() QtWidgets.QFontDialog.getFont(initial) @@ -1792,6 +1813,8 @@ def test_fontdialog(*_): def test_nobuttons_fontdialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QFont() QtWidgets.QFontDialog.getFont(initial, options=compat.FontNoButtons) @@ -1799,6 +1822,8 @@ def test_nobuttons_fontdialog(*_): def test_qt_fontdialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QFont() QtWidgets.QFontDialog.getFont(initial, options=compat.FontDontUseNativeDialog) @@ -1806,6 +1831,8 @@ def test_qt_fontdialog(*_): def test_filedialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QFileDialog(window) dialog.setFileMode(compat.Directory) shared.execute(args, dialog) @@ -1814,6 +1841,8 @@ def test_filedialog(_, window, *__): def test_qt_filedialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QFileDialog(window) dialog.setOption(compat.FileDontUseNativeDialog) shared.execute(args, dialog) @@ -1822,6 +1851,8 @@ def test_qt_filedialog(_, window, *__): def test_error_message(widget, *_): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QErrorMessage(widget) dialog.showMessage('Error message') shared.execute(args, dialog) @@ -1834,7 +1865,8 @@ def test_progress_dialog(_, window, __, ___, ____, app): dialog.setMinimumDuration(0) dialog.setMinimumSize(300, 100) dialog.show() - for i in range(1, 101): + count = 5 if is_headless() else 100 + for i in range(1, count + 1): dialog.setValue(i) app.processEvents() time.sleep(0.02) @@ -1846,6 +1878,8 @@ def test_progress_dialog(_, window, __, ___, ____, app): def test_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) shared.execute(args, dialog) @@ -1853,6 +1887,8 @@ def test_input_dialog(_, window, *__): def test_int_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setInputMode(compat.IntInput) shared.execute(args, dialog) @@ -1861,6 +1897,8 @@ def test_int_input_dialog(_, window, *__): def test_double_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setInputMode(compat.DoubleInput) shared.execute(args, dialog) @@ -1869,6 +1907,8 @@ def test_double_input_dialog(_, window, *__): def test_combobox_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setComboBoxItems(['Item 1', 'Item 2']) shared.execute(args, dialog) @@ -1877,6 +1917,8 @@ def test_combobox_input_dialog(_, window, *__): def test_list_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setComboBoxItems(['Item 1', 'Item 2']) dialog.setOption(compat.UseListViewForComboBoxItems) @@ -1886,6 +1928,8 @@ def test_list_input_dialog(_, window, *__): def test_nobuttons_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setComboBoxItems(['Item 1', 'Item 2']) dialog.setOption(compat.InputNoButtons) @@ -1939,6 +1983,8 @@ def _wizard(widget): def test_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) shared.execute(args, wizard) @@ -1946,6 +1992,8 @@ def test_wizard(widget, *_): def test_classic_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) wizard.setWizardStyle(compat.ClassicStyle) shared.execute(args, wizard) @@ -1954,6 +2002,8 @@ def test_classic_wizard(widget, *_): def test_modern_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) wizard.setWizardStyle(compat.ModernStyle) shared.execute(args, wizard) @@ -1962,6 +2012,8 @@ def test_modern_wizard(widget, *_): def test_mac_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) wizard.setWizardStyle(compat.MacStyle) shared.execute(args, wizard) @@ -1970,6 +2022,8 @@ def test_mac_wizard(widget, *_): def test_aero_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) wizard.setWizardStyle(compat.AeroStyle) shared.execute(args, wizard) @@ -1978,6 +2032,8 @@ def test_aero_wizard(widget, *_): def test_system_tray(widget, window, *_): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QErrorMessage(widget) dialog.showMessage('Hey! System tray icon.') @@ -1993,6 +2049,8 @@ def test_system_tray(widget, window, *_): def _test_standard_button(window, app, button): + if is_headless(): + return None, None, False, False message = QtWidgets.QMessageBox(window) message.addButton(button) message.setMinimumSize(100, 100) @@ -2065,6 +2123,8 @@ def test_discard_button(_, window, __, ___, ____, app): def _test_standard_icon(window, app, icon): + if is_headless(): + return None, None, False, False message = QtWidgets.QMessageBox(window) message.setIcon(icon) message.setMinimumSize(100, 100) @@ -2190,6 +2250,8 @@ def test_disabled_menubar(widget, window, font, width, *_): def test_issue25(widget, window, font, width, *_): + if is_headless(): + return None, None, False, False def launch_filedialog(folder): dialog = QtWidgets.QFileDialog() @@ -2303,6 +2365,8 @@ def launch_fontdialog(value): def test_issue28(_, window, *__): + if is_headless(): + return dialog = QtWidgets.QFileDialog(window) dialog.setFileMode(compat.Directory) shared.execute(args, dialog) @@ -2369,9 +2433,12 @@ def test(args, test_widget): # run if show_window: window.show() - if quit: + if is_headless(): + window.close() + if quit or is_headless(): return app.quit() - return shared.execute(args, app) + elif not is_headless(): + return shared.execute(args, app) def main(): From deba9be463f34de1f68fc820bbc922e7b0116307 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 16:06:20 -0500 Subject: [PATCH 8/9] Add test actions for C++ theme detection. --- .github/workflows/theme.yml | 32 ++++++++++++++++++++++++++++++++ .github/workflows/ui.yml | 2 +- example/breeze_theme.hpp | 2 +- scripts/headless.sh | 2 ++ scripts/test_theme.cpp | 15 +++++++++++++++ 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 scripts/test_theme.cpp diff --git a/.github/workflows/theme.yml b/.github/workflows/theme.yml index 7a8906e..49e58d3 100644 --- a/.github/workflows/theme.yml +++ b/.github/workflows/theme.yml @@ -25,3 +25,35 @@ jobs: - name: Checking our Python imports. run: | scripts/theme.sh + + theme-cpp: + strategy: + matrix: + os: [ubuntu-latest] # TODO: Restore macos-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Checking our C++ theme detection. + shell: bash + run: | + mkdir -p dist + cd dist + if [[ "${RUNNER_OS}" == "macOS" ]]; then + clang++ ../scripts/test_theme.cpp -o test_theme -std=c++17 + ./test_theme + else + g++ ../scripts/test_theme.cpp -o test_theme -std=c++17 + ./test_theme + fi + + theme-cpp-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: ilammy/msvc-dev-cmd@v1 + - name: Checking our C++ theme detection. + run: | + mkdir dist -ErrorAction SilentlyContinue + cd dist + cl ..\scripts\test_theme.cpp /std:c++17 /EHsc /link Advapi32.lib OleAut32.lib + .\test_theme.exe diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index 6c9a86b..855b7a2 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -1,6 +1,6 @@ name: Ui -on: [push] +on: [workflow_dispatch] jobs: ui: diff --git a/example/breeze_theme.hpp b/example/breeze_theme.hpp index 75c61d7..f51b964 100644 --- a/example/breeze_theme.hpp +++ b/example/breeze_theme.hpp @@ -6,7 +6,7 @@ * * This code has been minimally tested but should be useful on most platforms. * This makes extensive use of C++17 features. On Windows, this requires adding - * `OleAut32.lib` to the linker. + * `OleAut32.lib` and `Advapi32.lib` to the linker. * * This currently supports: * - Windows diff --git a/scripts/headless.sh b/scripts/headless.sh index eaf797a..1c8dd37 100755 --- a/scripts/headless.sh +++ b/scripts/headless.sh @@ -48,6 +48,8 @@ for script in example/*.py; do done # now we need to run our tests +# NOTE: We run each test separately just because it simplifies the logic. +# Some tests don't work in headless mode so we skip them. widgets=$(${PYTHON} -c "import os; os.chdir('test'); import ui; print(' '.join([i[5:] for i in dir(ui) if i.startswith('test_')]))") for widget in ${widgets[@]}; do for framework in "${frameworks[@]}"; do diff --git a/scripts/test_theme.cpp b/scripts/test_theme.cpp new file mode 100644 index 0000000..a9f2fae --- /dev/null +++ b/scripts/test_theme.cpp @@ -0,0 +1,15 @@ +/** + * Example to test our theme detection works at the C++ level. +*/ + +#include +#include "../example/breeze_theme.hpp" + +int main() +{ + std::cout << "Theme: " << static_cast(breeze_stylesheets::get_theme()) << std::endl; + std::cout << "Is Dark: " << breeze_stylesheets::is_dark() << std::endl; + std::cout << "Is Light: " << breeze_stylesheets::is_light() << std::endl; + + return 0; +} From 6e28e67070ffce8cc4b08ca54396add4fed325b6 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 16:10:22 -0500 Subject: [PATCH 9/9] Ensure our flows are run on PRs. --- .github/workflows/configure.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/theme.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml index 89bf51d..a65658b 100644 --- a/.github/workflows/configure.yml +++ b/.github/workflows/configure.yml @@ -1,6 +1,6 @@ name: Configure -on: [push] +on: [push, pull_request] jobs: theme-python: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 10c7bf9..6b30919 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,6 @@ name: Linters -on: [push] +on: [push, pull_request] jobs: lint-version-python: diff --git a/.github/workflows/theme.yml b/.github/workflows/theme.yml index 49e58d3..5e3cf73 100644 --- a/.github/workflows/theme.yml +++ b/.github/workflows/theme.yml @@ -1,6 +1,6 @@ name: Theme -on: [push] +on: [push, pull_request] jobs: theme-python: