diff options
author | Árni Dagur <agudmundsson@fc-md.umd.edu> | 2020-08-03 01:50:09 +0000 |
---|---|---|
committer | Árni Dagur <arni@dagur.eu> | 2020-12-19 20:24:06 +0000 |
commit | 68b9960a67158dceb7a50f4152074abf9696046b (patch) | |
tree | f52599e81a4b807dcf5f517bcdd177efd916386d /scripts | |
parent | 36831af853e7df59c55f07005ded015b47c5e4e1 (diff) | |
parent | c04ab823a84b974fd26f5bbb1f9e6a6a175c038a (diff) | |
download | qutebrowser-68b9960a67158dceb7a50f4152074abf9696046b.tar.gz qutebrowser-68b9960a67158dceb7a50f4152074abf9696046b.zip |
Merge branch 'master' into more-sophisticated-adblock
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/asciidoc2html.py | 80 | ||||
-rwxr-xr-x | scripts/dev/build_release.py | 13 | ||||
-rw-r--r-- | scripts/dev/check_coverage.py | 11 | ||||
-rwxr-xr-x | scripts/dev/check_doc_changes.py | 20 | ||||
-rw-r--r-- | scripts/dev/ci/backtrace.sh (renamed from scripts/dev/ci/travis_backtrace.sh) | 8 | ||||
-rw-r--r-- | scripts/dev/ci/problemmatchers.py | 214 | ||||
-rw-r--r-- | scripts/dev/ci/travis_install.sh | 75 | ||||
-rw-r--r-- | scripts/dev/ci/travis_run.sh | 37 | ||||
-rw-r--r-- | scripts/dev/misc_checks.py | 29 | ||||
-rw-r--r-- | scripts/dev/recompile_requirements.py | 372 | ||||
-rw-r--r-- | scripts/dev/run_shellcheck.sh | 39 | ||||
-rw-r--r-- | scripts/dev/update_version.py | 6 | ||||
-rw-r--r-- | scripts/utils.py | 31 |
13 files changed, 690 insertions, 245 deletions
diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index e7e7f680b..5cb49c767 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -23,7 +23,6 @@ from typing import List, Optional import re import os -import os.path import sys import subprocess import shutil @@ -32,11 +31,12 @@ import argparse import io import pathlib -sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) +REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] +DOC_DIR = REPO_ROOT / 'qutebrowser' / 'html' / 'doc' -from scripts import utils +sys.path.insert(0, str(REPO_ROOT)) -DOC_DIR = pathlib.Path("qutebrowser/html/doc") +from scripts import utils class AsciiDoc: @@ -68,7 +68,7 @@ class AsciiDoc: def cleanup(self) -> None: """Clean up the temporary home directory for asciidoc.""" if self._homedir is not None and not self._failed: - shutil.rmtree(self._homedir) + shutil.rmtree(str(self._homedir)) def build(self) -> None: """Build either the website or the docs.""" @@ -80,9 +80,9 @@ class AsciiDoc: def _build_docs(self) -> None: """Render .asciidoc files to .html sites.""" - files = [(pathlib.Path('doc/{}.asciidoc'.format(f)), + files = [((REPO_ROOT / 'doc' / '{}.asciidoc'.format(f)), DOC_DIR / (f + ".html")) for f in self.FILES] - for src in pathlib.Path('doc/help/').glob('*.asciidoc'): + for src in (REPO_ROOT / 'doc' / 'help').glob('*.asciidoc'): dst = DOC_DIR / (src.stem + ".html") files.append((src, dst)) @@ -98,12 +98,12 @@ class AsciiDoc: for src, dst in files: assert self._tempdir is not None # for mypy modified_src = self._tempdir / src.name - with open(modified_src, 'w', encoding='utf-8') as modified_f, \ - open(src, 'r', encoding='utf-8') as f: + with modified_src.open('w', encoding='utf-8') as moded_f, \ + src.open('r', encoding='utf-8') as f: for line in f: for orig, repl in replacements: line = line.replace(orig, repl) - modified_f.write(line) + moded_f.write(line) self.call(modified_src, dst, *asciidoc_args) def _copy_images(self) -> None: @@ -112,29 +112,28 @@ class AsciiDoc: dst_path = DOC_DIR / 'img' dst_path.mkdir(exist_ok=True) for filename in ['cheatsheet-big.png', 'cheatsheet-small.png']: - src = pathlib.Path('doc') / 'img' / filename + src = REPO_ROOT / 'doc' / 'img' / filename dst = dst_path / filename - shutil.copy(src, dst) + shutil.copy(str(src), str(dst)) def _build_website_file(self, root: pathlib.Path, filename: str) -> None: """Build a single website file.""" src = root / filename assert self._website is not None # for mypy dst = pathlib.Path(self._website) - dst = dst / src.parent.relative_to('.') / (src.stem + ".html") + dst = dst / src.parent.relative_to(REPO_ROOT) / (src.stem + ".html") dst.parent.mkdir(exist_ok=True) assert self._tempdir is not None # for mypy modified_src = self._tempdir / src.name - shutil.copy('www/header.asciidoc', modified_src) + shutil.copy(str(REPO_ROOT / 'www' / 'header.asciidoc'), modified_src) outfp = io.StringIO() - with open(modified_src, 'r', encoding='utf-8') as header_file: - header = header_file.read() - header += "\n\n" + header = modified_src.read_text(encoding='utf-8') + header += "\n\n" - with open(src, 'r', encoding='utf-8') as infp: + with src.open('r', encoding='utf-8') as infp: outfp.write("\n\n") hidden = False found_title = False @@ -174,8 +173,8 @@ class AsciiDoc: current_lines = outfp.getvalue() outfp.close() - with open(modified_src, 'w+', encoding='utf-8') as final_version: - final_version.write(title + "\n\n" + header + current_lines) + modified_str = title + "\n\n" + header + current_lines + modified_src.write_text(modified_str, encoding='utf-8') asciidoc_args = ['--theme=qute', '-a toc', '-a toc-placement=manual', '-a', 'source-highlighter=pygments'] @@ -183,14 +182,14 @@ class AsciiDoc: def _build_website(self) -> None: """Prepare and build the website.""" - theme_file = (pathlib.Path('www') / 'qute.css').resolve() + theme_file = REPO_ROOT / 'www' / 'qute.css' assert self._themedir is not None # for mypy shutil.copy(theme_file, self._themedir) assert self._website is not None # for mypy outdir = pathlib.Path(self._website) - for item_path in pathlib.Path().rglob('*.asciidoc'): + for item_path in pathlib.Path(REPO_ROOT).rglob('*.asciidoc'): if item_path.stem in ['header', 'OpenSans-License']: continue self._build_website_file(item_path.parent, item_path.name) @@ -198,17 +197,18 @@ class AsciiDoc: copy = {'icons': 'icons', 'doc/img': 'doc/img', 'www/media': 'media/'} for src, dest in copy.items(): + full_src = REPO_ROOT / src full_dest = outdir / dest try: shutil.rmtree(full_dest) except FileNotFoundError: pass - shutil.copytree(src, full_dest) + shutil.copytree(full_src, full_dest) for dst, link_name in [ ('README.html', 'index.html'), - ((pathlib.Path('doc') / 'quickstart.html'), - 'quickstart.html')]: + ((pathlib.Path('doc') / 'quickstart.html'), 'quickstart.html'), + ]: assert isinstance(dst, (str, pathlib.Path)) # for mypy try: (outdir / link_name).symlink_to(dst) @@ -220,21 +220,16 @@ class AsciiDoc: if self._asciidoc is not None: return self._asciidoc - try: - subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) - except OSError: - pass - else: - return ['asciidoc'] - - try: - subprocess.run(['asciidoc.py'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) - except OSError: - pass - else: - return ['asciidoc.py'] + for executable in ['asciidoc', 'asciidoc.py']: + try: + subprocess.run([executable, '--version'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True) + except OSError: + pass + else: + return [executable] raise FileNotFoundError @@ -253,9 +248,14 @@ class AsciiDoc: cmdline += ['--out-file', str(dst)] cmdline += args cmdline.append(str(src)) + + # So the virtualenv's Pygments is found + bin_path = pathlib.Path(sys.executable).parent + try: env = os.environ.copy() env['HOME'] = str(self._homedir) + env['PATH'] = str(bin_path) + os.pathsep + env['PATH'] subprocess.run(cmdline, check=True, env=env) except (subprocess.CalledProcessError, OSError) as e: self._failed = True diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index 2f037ac68..ee0ac2c53 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -275,12 +275,12 @@ def build_windows(): utils.print_title("Running pyinstaller 32bit") _maybe_remove(out_32) - call_tox('pyinstaller32', '-r', python=python_x86) + call_tox('pyinstaller-32', '-r', python=python_x86) shutil.move(out_pyinstaller, out_32) utils.print_title("Running pyinstaller 64bit") _maybe_remove(out_64) - call_tox('pyinstaller', '-r', python=python_x64) + call_tox('pyinstaller-64', '-r', python=python_x64) shutil.move(out_pyinstaller, out_64) utils.print_title("Running 32bit smoke test") @@ -310,16 +310,17 @@ def build_windows(): ] utils.print_title("Zipping 32bit standalone...") - name = 'qutebrowser-{}-windows-standalone-win32'.format( - qutebrowser.__version__) + template = 'qutebrowser-{}-windows-standalone-{}' + name = os.path.join('dist', + template.format(qutebrowser.__version__, 'win32')) shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_32)) artifacts.append(('{}.zip'.format(name), 'application/zip', 'Windows 32bit standalone')) utils.print_title("Zipping 64bit standalone...") - name = 'qutebrowser-{}-windows-standalone-amd64'.format( - qutebrowser.__version__) + name = os.path.join('dist', + template.format(qutebrowser.__version__, 'amd64')) shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_64)) artifacts.append(('{}.zip'.format(name), 'application/zip', diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 7fa45dd90..12963de38 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -45,6 +45,13 @@ class Message: filename = attr.ib() text = attr.ib() + def show(self): + """Print this message.""" + if scriptutils.ON_CI: + scriptutils.gha_error(self.text) + else: + print(self.text) + MsgType = enum.Enum('MsgType', 'insufficient_coverage, perfect_file') @@ -159,6 +166,8 @@ PERFECT_FILES = [ 'config/configtypes.py'), ('tests/unit/config/test_configinit.py', 'config/configinit.py'), + ('tests/unit/config/test_qtargs.py', + 'config/qtargs.py'), ('tests/unit/config/test_configcommands.py', 'config/configcommands.py'), ('tests/unit/config/test_configutils.py', @@ -309,7 +318,7 @@ def main_check(): print() scriptutils.print_title("Coverage check failed") for msg in messages: - print(msg.text) + msg.show() print() filters = ','.join('qutebrowser/' + msg.filename for msg in messages) subprocess.run([sys.executable, '-m', 'coverage', 'report', diff --git a/scripts/dev/check_doc_changes.py b/scripts/dev/check_doc_changes.py index f673ad4ea..edc613f47 100755 --- a/scripts/dev/check_doc_changes.py +++ b/scripts/dev/check_doc_changes.py @@ -23,11 +23,17 @@ import sys import subprocess import os +import os.path -code = subprocess.run(['git', '--no-pager', 'diff', - '--exit-code', '--stat'], check=False).returncode +sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, + os.pardir)) -if os.environ.get('TRAVIS_PULL_REQUEST', 'false') != 'false': +from scripts import utils + +code = subprocess.run(['git', '--no-pager', 'diff', '--exit-code', '--stat', + '--', 'doc'], check=False).returncode + +if os.environ.get('GITHUB_REF', 'refs/heads/master') != 'refs/heads/master': if code != 0: print("Docs changed but ignoring change as we're building a PR") sys.exit(0) @@ -40,9 +46,9 @@ if code != 0: print() print('(Or you have uncommitted changes, in which case you can ignore ' 'this.)') - if 'TRAVIS' in os.environ: + if utils.ON_CI: + utils.gha_error('The autogenerated docs changed') print() - print("travis_fold:start:gitdiff") - subprocess.run(['git', '--no-pager', 'diff'], check=True) - print("travis_fold:end:gitdiff") + with utils.gha_group('Diff'): + subprocess.run(['git', '--no-pager', 'diff'], check=True) sys.exit(code) diff --git a/scripts/dev/ci/travis_backtrace.sh b/scripts/dev/ci/backtrace.sh index 227dde8a8..f9b32f6d6 100644 --- a/scripts/dev/ci/travis_backtrace.sh +++ b/scripts/dev/ci/backtrace.sh @@ -4,13 +4,15 @@ # to determine exe using file(1) and dump stack trace with gdb. # -case $TESTENV in +testenv=$1 + +case $testenv in py3*-pyqt*) - exe=$(readlink -f ".tox/$TESTENV/bin/python") + exe=$(readlink -f ".tox/$testenv/bin/python") full= ;; *) - echo "Skipping coredump analysis in testenv $TESTENV!" + echo "Skipping coredump analysis in testenv $testenv!" exit 0 ;; esac diff --git a/scripts/dev/ci/problemmatchers.py b/scripts/dev/ci/problemmatchers.py new file mode 100644 index 000000000..320d0deeb --- /dev/null +++ b/scripts/dev/ci/problemmatchers.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +"""Register problem matchers for GitHub Actions. + +Relevant docs: +https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md +https://github.com/actions/toolkit/blob/master/docs/commands.md#problem-matchers +""" + +import sys +import pathlib +import json + + +MATCHERS = { + # scripts/dev/ci/run.sh:41:39: error: Double quote array expansions to + # avoid re-splitting elements. [SC2068] + "shellcheck": [ + { + "pattern": [ + { + "regexp": r"^(.+):(\d+):(\d+):\s(note|warning|error):\s(.*)\s\[(SC\d+)\]$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5, + "code": 6, + }, + ], + }, + ], + + "yamllint": [ + { + "pattern": [ + { + "regexp": r"^\033\[4m([^\033]+)\033\[0m$", + "file": 1, + }, + { + "regexp": r"^ \033\[2m(\d+):(\d+)\033\[0m \033\[3[13]m([^\033]+)\033\[0m +([^\033]*)\033\[2m\(([^)]+)\)\033\[0m$", + "line": 1, + "column": 2, + "severity": 3, + "message": 4, + "code": 5, + "loop": True, + }, + ], + }, + ], + + # filename.py:313: unused function 'i_am_never_used' (60% confidence) + "vulture": [ + { + "severity": "warning", + "pattern": [ + { + "regexp": r"^([^:]+):(\d+): ([^(]+ \(\d+% confidence\))$", + "file": 1, + "line": 2, + "message": 3, + } + ] + }, + ], + + # filename.py:1:1: D100 Missing docstring in public module + "flake8": [ + { + # "undefined name" is FXXX (i.e. not an error), but e.g. multiple + # spaces before an operator is EXXX (i.e. an error) - that makes little + # sense, so let's just treat everything as a warning instead. + "severity": "warning", + "pattern": [ + { + "regexp": r"^(\033\[0m)?([^:]+):(\d+):(\d+): ([A-Z]\d{3}) (.*)$", + "file": 2, + "line": 3, + "column": 4, + "code": 5, + "message": 6, + }, + ], + }, + ], + + # filename.py:80: error: Name 'foo' is not defined [name-defined] + "mypy": [ + { + "pattern": [ + { + "regexp": r"^(\033\[0m)?([^:]+):(\d+): ([^:]+): (.*) \[(.*)\]$", + "file": 2, + "line": 3, + "severity": 4, + "message": 5, + "code": 6, + }, + ], + }, + ], + + # For some reason, ANSI color escape codes end up as part of the message + # GitHub gets with colored pylint output - so we have those escape codes + # (e.g. "\033[35m...\033[0m") as part of the regex patterns... + "pylint": [ + { + # filename.py:80:10: E0602: Undefined variable 'foo' (undefined-variable) + "severity": "error", + "pattern": [ + { + "regexp": r"^([^:]+):(\d+):(\d+): (E\d+): \033\[[\d;]+m([^\033]+).*$", + "file": 1, + "line": 2, + "column": 3, + "code": 4, + "message": 5, + }, + ], + }, + { + # filename.py:78:14: W0613: Unused argument 'unused' (unused-argument) + "severity": "warning", + "pattern": [ + { + "regexp": r"^([^:]+):(\d+):(\d+): ([A-DF-Z]\d+): \033\[[\d;]+m([^\033]+).*$", + "file": 1, + "line": 2, + "column": 3, + "code": 4, + "message": 5, + }, + ], + }, + ], + + "tests": [ + { + # pytest test summary output + "severity": "error", + "pattern": [ + { + "regexp": r'^=+ short test summary info =+$', + }, + { + "regexp": r"^((ERROR|FAILED) .*)", + "message": 1, + "loop": True, + } + ], + }, + { + # pytest error lines + # E end2end.fixtures.testprocess.WaitForTimeout: Timed out + # after 15000ms waiting for [...] + "severity": "error", + "pattern": [ + { + "regexp": r'^\033\[1m\033\[31mE ([a-zA-Z0-9.]+: [^\033]*)\033\[0m$', + "message": 1, + }, + ], + }, + ], +} + + +def add_matcher(output_dir, owner, data): + data['owner'] = owner + out_data = {'problemMatcher': [data]} + output_file = output_dir / '{}.json'.format(owner) + with output_file.open('w', encoding='utf-8') as f: + json.dump(out_data, f) + + print("::add-matcher::{}".format(output_file)) + + +def main(testenv, tempdir): + testenv = sys.argv[1] + if testenv.startswith('py3'): + testenv = 'tests' + + if testenv not in MATCHERS: + return + + output_dir = pathlib.Path(tempdir) + + for idx, data in enumerate(MATCHERS[testenv]): + owner = '{}-{}'.format(testenv, idx) + add_matcher(output_dir=output_dir, owner=owner, data=data) + + +if __name__ == '__main__': + sys.exit(main(*sys.argv[1:])) diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh deleted file mode 100644 index 2975a52d7..000000000 --- a/scripts/dev/ci/travis_install.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash -# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2016-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org> - -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. - -# Stolen from https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh -# and adjusted to use ((...)) -travis_retry() { - local ANSI_RED='\033[31;1m' - local ANSI_RESET='\033[0m' - local result=0 - local count=1 - while (( count < 3 )); do - if (( result != 0 )); then - echo -e "\\n${ANSI_RED}The command \"$*\" failed. Retrying, $count of 3.${ANSI_RESET}\\n" >&2 - fi - "$@" - result=$? - (( result == 0 )) && break - count=$(( count + 1 )) - sleep 1 - done - - if (( count > 3 )); then - echo -e "\\n${ANSI_RED}The command \"$*\" failed 3 times.${ANSI_RESET}\\n" >&2 - fi - - return $result -} - -pip_install() { - travis_retry python3 -m pip install "$@" -} - -npm_install() { - # Make sure npm is up-to-date first - travis_retry npm install -g npm - travis_retry npm install -g "$@" -} - -set -e - -if [[ -n $DOCKER ]]; then - exit 0 -fi - -case $TESTENV in - eslint) - npm_install eslint - ;; - shellcheck) - ;; - *) - pip_install -U pip - pip_install -U -r misc/requirements/requirements-tox.txt - if [[ $TESTENV == *-cov ]]; then - pip_install -U -r misc/requirements/requirements-codecov.txt - fi - ;; -esac diff --git a/scripts/dev/ci/travis_run.sh b/scripts/dev/ci/travis_run.sh deleted file mode 100644 index 96af14553..000000000 --- a/scripts/dev/ci/travis_run.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -if [[ -n $DOCKER ]]; then - docker run \ - --privileged \ - -v "$PWD:/outside" \ - -e "QUTE_BDD_WEBENGINE=$QUTE_BDD_WEBENGINE" \ - -e "DOCKER=$DOCKER" \ - -e "CI=$CI" \ - -e "TRAVIS=$TRAVIS" \ - "qutebrowser/travis:$DOCKER" -elif [[ $TESTENV == eslint ]]; then - # Can't run this via tox as we can't easily install tox in the javascript - # travis env - cd qutebrowser/javascript || exit 1 - eslint --color --report-unused-disable-directives . -elif [[ $TESTENV == shellcheck ]]; then - SCRIPTS=$( mktemp ) - find scripts/dev/ -name '*.sh' >"$SCRIPTS" - find misc/userscripts/ -type f -exec grep -lE '[/ ][bd]ash$|[/ ]sh$|[/ ]ksh$' {} + >>"$SCRIPTS" - mapfile -t scripts <"$SCRIPTS" - rm -f "$SCRIPTS" - docker run \ - -v "$PWD:/outside" \ - -w /outside \ - koalaman/shellcheck:stable "${scripts[@]}" -else - args=() - # We only run unit tests on macOS because it's quite slow. - [[ $TRAVIS_OS_NAME == osx ]] && args+=('--qute-bdd-webengine' '--no-xvfb' 'tests/unit') - - # WORKAROUND for unknown crash inside swrast_dri.so - # See https://github.com/qutebrowser/qutebrowser/pull/4218#issuecomment-421931770 - [[ $TESTENV == py36-pyqt59 ]] && export QT_QUICK_BACKEND=software - - tox -e "$TESTENV" -- "${args[@]}" -fi diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 6bf411bba..366abc9ca 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -82,22 +82,20 @@ def check_git(): def check_spelling(): """Check commonly misspelled words.""" # Words which I often misspell - words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully', - '[Oo]ccur[^rs .!]', '[Ss]eperator', '[Ee]xplicitely', - '[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly', - '[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited', - '[Rr]eproducable', '[Aa]n [Uu]ser', '[Cc]onvienience', - '[Ww]ether', '[Pp]rogramatically', '[Ss]plitted', '[Ee]xitted', - '[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily', - '[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting', - 'existant', '[Rr]esetted', '[Ss]imilarily', '[Ii]nformations', - '[Aa]n [Uu][Rr][Ll]', '[Tt]reshold'} + words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully', + 'occur[^rs .!]', 'seperator', 'explicitely', 'auxillary', + 'accidentaly', 'ambigious', 'loosly', 'initialis', 'convienence', + 'similiar', 'uncommited', 'reproducable', 'an user', + 'convienience', 'wether', 'programatically', 'splitted', + 'exitted', 'mininum', 'resett?ed', 'recieved', 'regularily', + 'underlaying', 'inexistant', 'elipsis', 'commiting', 'existant', + 'resetted', 'similarily', 'informations', 'an url', 'treshold', + 'artefact'} # Words which look better when splitted, but might need some fine tuning. - words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence', - '[Nn]ormalmode', '[Ee]ventloops', '[Ss]izehint', - '[Ss]tatemachine', '[Mm]etaobject', '[Ll]ogrecord', - '[Ff]iletype'} + words |= {'webelements', 'mouseevent', 'keysequence', 'normalmode', + 'eventloops', 'sizehint', 'statemachine', 'metaobject', + 'logrecord', 'filetype'} # Files which should be ignored, e.g. because they come from another # package @@ -117,7 +115,8 @@ def check_spelling(): continue for line in f: for w in words: - if (re.search(w, line) and + pattern = '[{}{}]{}'.format(w[0], w[0].upper(), w[1:]) + if (re.search(pattern, line) and fn not in seen[w] and '# pragma: no spellcheck' not in line): print('Found "{}" in {}!'.format(w, fn)) diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index 3e7db0c9a..7474c56c9 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -26,6 +26,7 @@ import os.path import glob import subprocess import tempfile +import argparse sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) @@ -36,6 +37,84 @@ REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..') # /scripts/dev -> /scripts -> / REQ_DIR = os.path.join(REPO_DIR, 'misc', 'requirements') +CHANGELOG_URLS = { + 'pyparsing': 'https://github.com/pyparsing/pyparsing/blob/master/CHANGES', + 'cherrypy': 'https://github.com/cherrypy/cherrypy/blob/master/CHANGES.rst', + 'pylint': 'http://pylint.pycqa.org/en/latest/whatsnew/changelog.html', + 'setuptools': 'https://github.com/pypa/setuptools/blob/master/CHANGES.rst', + 'pytest-cov': 'https://github.com/pytest-dev/pytest-cov', + 'requests': 'https://github.com/psf/requests/blob/master/HISTORY.md', + 'requests-file': 'https://github.com/dashea/requests-file/blob/master/CHANGES.rst', + 'werkzeug': 'https://github.com/pallets/werkzeug/blob/master/CHANGES.rst', + 'hypothesis': 'https://hypothesis.readthedocs.io/en/latest/changes.html', + 'mypy': 'https://mypy-lang.blogspot.com/', + 'pytest': 'https://docs.pytest.org/en/latest/changelog.html', + 'iniconfig': 'https://github.com/RonnyPfannschmidt/iniconfig/blob/master/CHANGELOG', + 'tox': 'https://tox.readthedocs.io/en/latest/changelog.html', + 'pyyaml': 'https://github.com/yaml/pyyaml/blob/master/CHANGES', + 'pytest-bdd': 'https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst', + 'snowballstemmer': 'https://github.com/snowballstem/snowball/blob/master/NEWS', + 'virtualenv': 'https://virtualenv.pypa.io/en/latest/changelog.html', + 'pip': 'https://pip.pypa.io/en/stable/news/', + 'packaging': 'https://pypi.org/project/packaging/', + 'flake8-docstrings': 'https://pypi.org/project/flake8-docstrings/', + 'attrs': 'http://www.attrs.org/en/stable/changelog.html', + 'jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst', + 'flake8': 'https://gitlab.com/pycqa/flake8/tree/master/docs/source/release-notes', + 'cffi': 'https://cffi.readthedocs.io/en/latest/whatsnew.html', + 'flake8-debugger': 'https://github.com/JBKahn/flake8-debugger/', + 'astroid': 'https://github.com/PyCQA/astroid/blob/2.4/ChangeLog', + 'pytest-instafail': 'https://github.com/pytest-dev/pytest-instafail/blob/master/CHANGES.rst', + 'coverage': 'https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst', + 'colorama': 'https://github.com/tartley/colorama/blob/master/CHANGELOG.rst', + 'hunter': 'https://github.com/ionelmc/python-hunter/blob/master/CHANGELOG.rst', + 'uritemplate': 'https://pypi.org/project/uritemplate/', + 'flake8-builtins': 'https://github.com/gforcada/flake8-builtins/blob/master/CHANGES.rst', + 'flake8-bugbear': 'https://github.com/PyCQA/flake8-bugbear', + 'flake8-tidy-imports': 'https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst', + 'flake8-tuple': 'https://github.com/ar4s/flake8_tuple/blob/master/HISTORY.rst', + 'more-itertools': 'https://github.com/erikrose/more-itertools/blob/master/docs/versions.rst', + 'pydocstyle': 'http://www.pydocstyle.org/en/latest/release_notes.html', + 'sphinx': 'https://www.sphinx-doc.org/en/master/changes.html', + 'jaraco.functools': 'https://github.com/jaraco/jaraco.functools/blob/master/CHANGES.rst', + 'parse': 'https://github.com/r1chardj0n3s/parse#potential-gotchas', + 'py': 'https://py.readthedocs.io/en/latest/changelog.html#changelog', + 'pytest-mock': 'https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst', + 'pytest-qt': 'https://github.com/pytest-dev/pytest-qt/blob/master/CHANGELOG.rst', + 'wcwidth': 'https://github.com/jquast/wcwidth#history', + 'pyinstaller': 'https://pyinstaller.readthedocs.io/en/stable/CHANGES.html', + 'pyinstaller-hooks-contrib': 'https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/CHANGELOG.rst', + 'pytest-benchmark': 'https://pytest-benchmark.readthedocs.io/en/stable/changelog.html', + 'typed-ast': 'https://github.com/python/typed_ast/commits/master', + 'docutils': 'https://docutils.sourceforge.io/RELEASE-NOTES.html', + 'bump2version': 'https://github.com/c4urself/bump2version/blob/master/CHANGELOG.md', + 'six': 'https://github.com/benjaminp/six/blob/master/CHANGES', + 'flake8-comprehensions': 'https://github.com/adamchainz/flake8-comprehensions/blob/master/HISTORY.rst', + 'altgraph': 'https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst', + 'urllib3': 'https://github.com/urllib3/urllib3/blob/master/CHANGES.rst', + 'wheel': 'https://github.com/pypa/wheel/blob/master/docs/news.rst', + 'mako': 'https://docs.makotemplates.org/en/latest/changelog.html', + 'lxml': 'https://lxml.de/4.5/changes-4.5.0.html', + 'jwcrypto': 'https://github.com/latchset/jwcrypto/commits/master', + 'tox-pip-version': 'https://github.com/pglass/tox-pip-version/commits/master', + 'wrapt': 'https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst', + 'pep517': 'https://github.com/pypa/pep517/commits/master', + 'cryptography': 'https://cryptography.io/en/latest/changelog/', + 'toml': 'https://github.com/uiri/toml/releases', + 'pyqt': 'https://www.riverbankcomputing.com/', + 'vulture': 'https://github.com/jendrikseipp/vulture/blob/master/CHANGELOG.md', + 'distlib': 'https://bitbucket.org/pypa/distlib/src/master/CHANGES.rst', + 'py-cpuinfo': 'https://github.com/workhorsy/py-cpuinfo/blob/master/ChangeLog', + 'cheroot': 'https://cheroot.cherrypy.org/en/latest/history.html', + 'certifi': 'https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport', + 'chardet': 'https://github.com/chardet/chardet/releases', + 'idna': 'https://github.com/kjd/idna/blob/master/HISTORY.rst', + 'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md', +} + +# PyQt versions which need SIP v4 +OLD_PYQT = {'pyqt-5.7', 'pyqt-5.9', 'pyqt-5.10', 'pyqt-5.11'} + def convert_line(line, comments): """Convert the given requirement line to place into the output.""" @@ -121,74 +200,261 @@ def get_all_names(): yield basename[len('requirements-'):-len('.txt-raw')] +def filter_names(names, old_pyqt=False): + """Filter requirement names.""" + if old_pyqt: + return sorted(names) + else: + return sorted(set(names) - OLD_PYQT) + + +def run_pip(venv_dir, *args, **kwargs): + """Run pip inside the virtualenv.""" + arg_str = ' '.join(str(arg) for arg in args) + utils.print_col('venv$ pip {}'.format(arg_str), 'blue') + venv_python = os.path.join(venv_dir, 'bin', 'python') + return subprocess.run([venv_python, '-m', 'pip'] + list(args), + check=True, **kwargs) + + def init_venv(host_python, venv_dir, requirements, pre=False): """Initialize a new virtualenv and install the given packages.""" - subprocess.run([host_python, '-m', 'venv', venv_dir], check=True) + with utils.gha_group('Creating virtualenv'): + utils.print_col('$ python3 -m venv {}'.format(venv_dir), 'blue') + subprocess.run([host_python, '-m', 'venv', venv_dir], check=True) - venv_python = os.path.join(venv_dir, 'bin', 'python') - subprocess.run([venv_python, '-m', 'pip', - 'install', '-U', 'pip'], check=True) + run_pip(venv_dir, 'install', '-U', 'pip') + run_pip(venv_dir, 'install', '-U', 'setuptools', 'wheel') - install_command = [venv_python, '-m', 'pip', 'install', '-r', requirements] + install_command = ['install', '-r', requirements] if pre: install_command.append('--pre') - subprocess.run(install_command, check=True) - subprocess.run([venv_python, '-m', 'pip', 'check'], check=True) - return venv_python + with utils.gha_group('Installing requirements'): + run_pip(venv_dir, *install_command) + run_pip(venv_dir, 'check') -def main(): - """Re-compile the given (or all) requirement files.""" - names = sys.argv[1:] if len(sys.argv) > 1 else sorted(get_all_names()) - for name in names: - utils.print_title(name) - filename = os.path.join(REQ_DIR, - 'requirements-{}.txt-raw'.format(name)) - if name == 'qutebrowser': - outfile = os.path.join(REPO_DIR, 'requirements.txt') +def parse_args(): + """Parse commandline arguments via argparse.""" + parser = argparse.ArgumentParser() + parser.add_argument('--old-pyqt', + action='store_true', + help='Also include old PyQt requirements.') + parser.add_argument('names', nargs='*') + return parser.parse_args() + + +def git_diff(*args): + """Run a git diff command.""" + command = (['git', '--no-pager', 'diff'] + list(args) + [ + '--', 'requirements.txt', 'misc/requirements/requirements-*.txt']) + proc = subprocess.run(command, + stdout=subprocess.PIPE, + encoding='utf-8', + check=True) + return proc.stdout.splitlines() + + +class Change: + + """A single requirements change from a git diff output.""" + + def __init__(self, name): + self.name = name + self.old = None + self.new = None + if name.lower() in CHANGELOG_URLS: + self.url = CHANGELOG_URLS[name.lower()] + self.link = '[{}]({})'.format(self.name, self.url) + else: + self.url = '(no changelog)' + self.link = self.name + + def __str__(self): + if self.old is None: + return '- {} new: {} {}'.format(self.name, self.new, self.url) + elif self.new is None: + return '- {} removed: {} {}'.format(self.name, self.old, + self.url) else: - outfile = os.path.join(REQ_DIR, 'requirements-{}.txt'.format(name)) - - if name in [ - # Need sip v4 which doesn't work on Python 3.8 - 'pyqt-5.7', 'pyqt-5.9', 'pyqt-5.10', 'pyqt-5.11', 'pyqt-5.12', - # Installs typed_ast on < 3.8 only - 'pylint', - ]: - host_python = 'python3.7' + return '- {} {} -> {} {}'.format(self.name, self.old, self.new, + self.url) + + def table_str(self): + """Generate a markdown table.""" + if self.old is None: + return '| {} | -- | {} |'.format(self.link, self.new) + elif self.new is None: + return '| {} | {} | -- |'.format(self.link, self.old) else: - host_python = sys.executable + return '| {} | {} | {} |'.format(self.link, self.old, self.new) + + +def print_changed_files(): + """Output all changed files from this run.""" + changed_files = set() + filenames = git_diff('--name-only') + for filename in filenames: + filename = filename.strip() + filename = filename.replace('misc/requirements/requirements-', '') + filename = filename.replace('.txt', '') + changed_files.add(filename) + files_text = '\n'.join('- ' + line for line in sorted(changed_files)) + + changes_dict = {} + diff = git_diff() + for line in diff: + if not line.startswith('-') and not line.startswith('+'): + continue + if line.startswith('+++ ') or line.startswith('--- '): + continue + + if '==' in line: + name, version = line[1:].split('==') + else: + name = line[1:] + version = '?' + + if name not in changes_dict: + changes_dict[name] = Change(name) + + if line.startswith('-'): + changes_dict[name].old = version + elif line.startswith('+'): + changes_dict[name].new = version + + changes = [change for _name, change in sorted(changes_dict.items())] + diff_text = '\n'.join(str(change) for change in changes) + + utils.print_title('Changed') + utils.print_subtitle('Files') + print(files_text) + print() + utils.print_subtitle('Diff') + print(diff_text) + + if 'CI' in os.environ: + print() + print('::set-output name=changed::' + + files_text.replace('\n', '%0A')) + table_header = [ + '| Requirement | old | new |', + '|-------------|-----|-----|', + ] + diff_table = '%0A'.join(table_header + + [change.table_str() for change in changes]) + print('::set-output name=diff::' + diff_table) + + +def get_host_python(name): + """Get the Python to use for a given requirement name. + + Old PyQt versions need sip v4 which doesn't work on Python 3.8 + ylint installs typed_ast on < 3.8 only + """ + if name in OLD_PYQT or name == 'pylint': + return 'python3.7' + else: + return sys.executable + + +def build_requirements(name): + """Build a requirements file.""" + utils.print_subtitle("Building") + filename = os.path.join(REQ_DIR, 'requirements-{}.txt-raw'.format(name)) + host_python = get_host_python(name) + + with open(filename, 'r', encoding='utf-8') as f: + comments = read_comments(f) + + with tempfile.TemporaryDirectory() as tmpdir: + init_venv(host_python=host_python, + venv_dir=tmpdir, + requirements=filename, + pre=comments['pre']) + with utils.gha_group('Freezing requirements'): + proc = run_pip(tmpdir, 'freeze', stdout=subprocess.PIPE) + reqs = proc.stdout.decode('utf-8') + if utils.ON_CI: + print(reqs.strip()) + + if name == 'qutebrowser': + outfile = os.path.join(REPO_DIR, 'requirements.txt') + else: + outfile = os.path.join(REQ_DIR, 'requirements-{}.txt'.format(name)) + + with open(outfile, 'w', encoding='utf-8') as f: + f.write("# This file is automatically generated by " + "scripts/dev/recompile_requirements.py\n\n") + for line in reqs.splitlines(): + if line.startswith('qutebrowser=='): + continue + f.write(convert_line(line, comments) + '\n') + + for line in comments['add']: + f.write(line + '\n') + + return outfile + + +def test_tox(): + """Test requirements via tox.""" + utils.print_title('Testing via tox') + host_python = get_host_python('tox') + req_path = os.path.join(REQ_DIR, 'requirements-tox.txt') + + with tempfile.TemporaryDirectory() as tmpdir: + venv_dir = os.path.join(tmpdir, 'venv') + tox_workdir = os.path.join(tmpdir, 'tox-workdir') + venv_python = os.path.join(venv_dir, 'bin', 'python') + init_venv(host_python, venv_dir, req_path) + list_proc = subprocess.run([venv_python, '-m', 'tox', '--listenvs'], + check=True, + stdout=subprocess.PIPE, + universal_newlines=True) + environments = list_proc.stdout.strip().split('\n') + for env in environments: + with utils.gha_group('tox for {}'.format(env)): + utils.print_subtitle(env) + utils.print_col('venv$ tox -e {} --notest'.format(env), 'blue') + subprocess.run([venv_python, '-m', 'tox', + '--workdir', tox_workdir, + '-e', env, + '--notest'], + check=True) + + +def test_requirements(name, outfile): + """Test a resulting requirements file.""" + print() + utils.print_subtitle("Testing") + + host_python = get_host_python(name) + with tempfile.TemporaryDirectory() as tmpdir: + init_venv(host_python, tmpdir, outfile) - utils.print_subtitle("Building") - with open(filename, 'r', encoding='utf-8') as f: - comments = read_comments(f) +def main(): + """Re-compile the given (or all) requirement files.""" + args = parse_args() + if args.names: + names = args.names + else: + names = filter_names(get_all_names(), old_pyqt=args.old_pyqt) - with tempfile.TemporaryDirectory() as tmpdir: - venv_python = init_venv(host_python=host_python, - venv_dir=tmpdir, - requirements=filename, - pre=comments['pre']) - proc = subprocess.run([venv_python, '-m', 'pip', 'freeze'], - check=True, stdout=subprocess.PIPE) - reqs = proc.stdout.decode('utf-8') + utils.print_col('Rebuilding requirements: ' + ', '.join(names), 'green') + for name in names: + utils.print_title(name) + outfile = build_requirements(name) + test_requirements(name, outfile) + + if not args.names: + # If we selected a subset, let's not go through the trouble of testing + # via tox. + test_tox() - with open(outfile, 'w', encoding='utf-8') as f: - f.write("# This file is automatically generated by " - "scripts/dev/recompile_requirements.py\n\n") - for line in reqs.splitlines(): - if line.startswith('qutebrowser=='): - continue - f.write(convert_line(line, comments) + '\n') - - for line in comments['add']: - f.write(line + '\n') - - # Test resulting file - utils.print_subtitle("Testing") - with tempfile.TemporaryDirectory() as tmpdir: - init_venv(host_python, tmpdir, outfile) + print_changed_files() if __name__ == '__main__': diff --git a/scripts/dev/run_shellcheck.sh b/scripts/dev/run_shellcheck.sh new file mode 100644 index 000000000..885e68375 --- /dev/null +++ b/scripts/dev/run_shellcheck.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +set -e + +script_list=$(mktemp) +find scripts/dev/ -name '*.sh' > "$script_list" +find misc/userscripts/ -type f -exec grep -lE '[/ ][bd]ash$|[/ ]sh$|[/ ]ksh$' {} + >> "$script_list" +mapfile -t scripts < "$script_list" +rm -f "$script_list" + +if [[ $1 == --docker ]]; then + shift 1 + docker run \ + -v "$PWD:/outside" \ + -w /outside \ + -t \ + koalaman/shellcheck:stable "$@" "${scripts[@]}" +else + shellcheck --version + shellcheck "$@" "${scripts[@]}" +fi diff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py index 13bd3d776..e86ff257d 100644 --- a/scripts/dev/update_version.py +++ b/scripts/dev/update_version.py @@ -88,9 +88,3 @@ if __name__ == "__main__": print("* macOS: git fetch && git checkout v{v} && " "python3 scripts/dev/build_release.py --upload" .format(v=version)) - - print("* On server:") - print(" - bash download_release.sh {v}" - .format(v=version)) - print(" - git pull github master && sudo python3 " - "scripts/asciidoc2html.py --website /srv/http/qutebrowser") diff --git a/scripts/utils.py b/scripts/utils.py index bdf3f96fc..f46e6a4de 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -22,6 +22,7 @@ import os import os.path import sys +import contextlib # Import side-effects are an evil thing, but here it's okay so scripts using @@ -54,6 +55,9 @@ fg_colors = { bg_colors = {name: col + 10 for name, col in fg_colors.items()} +ON_CI = 'CI' in os.environ + + def _esc(code): """Get an ANSI color code based on a color number.""" return '\033[{}m'.format(code) @@ -64,9 +68,9 @@ def print_col(text, color, file=sys.stdout): if use_color: fg = _esc(fg_colors[color.lower()]) reset = _esc(fg_colors['reset']) - print(''.join([fg, text, reset]), file=file) + print(''.join([fg, text, reset]), file=file, flush=True) else: - print(text, file=file) + print(text, file=file, flush=True) def print_error(text): @@ -90,3 +94,26 @@ def change_cwd(): cwd = os.getcwd() if os.path.split(cwd)[1] == 'scripts': os.chdir(os.path.join(cwd, os.pardir)) + + +@contextlib.contextmanager +def gha_group(name): + """Print a GitHub Actions group. + + Gets ignored if not on CI. + """ + if ON_CI: + print('::group::' + name) + yield + print('::endgroup::') + else: + yield + + +def gha_error(message): + """Print a GitHub Actions error. + + Should only be called on CI. + """ + assert ON_CI + print('::error::' + message) |