From 32cf761e7b65391ad7a6f932442514fa12faf8d9 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 5 Oct 2021 09:41:26 +1000 Subject: [PATCH] expose pyqt6 packages - not yet used --- defs.bzl | 6 ++ pip/pyqt6/BUILD.bazel | 0 pip/pyqt6/defs.bzl | 47 +++++++++ pip/pyqt6/install_pyqt6.py | 196 +++++++++++++++++++++++++++++++++++ qt/.pylintrc | 2 +- qt/mypy.ini | 3 + scripts/copyright_headers.py | 1 + 7 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 pip/pyqt6/BUILD.bazel create mode 100644 pip/pyqt6/defs.bzl create mode 100644 pip/pyqt6/install_pyqt6.py diff --git a/defs.bzl b/defs.bzl index 4c75a192b..1042004ac 100644 --- a/defs.bzl +++ b/defs.bzl @@ -9,6 +9,7 @@ load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories") load("@com_github_ali5h_rules_pip//:defs.bzl", "pip_import") load("//pip/pyqt5:defs.bzl", "install_pyqt5") +load("//pip/pyqt6:defs.bzl", "install_pyqt6") anki_version = "2.1.49" @@ -43,6 +44,11 @@ def setup_deps(): python_runtime = "@python//:python", ) + install_pyqt6( + name = "pyqt6", + python_runtime = "@python//:python", + ) + node_repositories(package_json = ["@ankidesktop//:package.json"]) yarn_install( diff --git a/pip/pyqt6/BUILD.bazel b/pip/pyqt6/BUILD.bazel new file mode 100644 index 000000000..e69de29bb diff --git a/pip/pyqt6/defs.bzl b/pip/pyqt6/defs.bzl new file mode 100644 index 000000000..ca4519cff --- /dev/null +++ b/pip/pyqt6/defs.bzl @@ -0,0 +1,47 @@ +# based off https://github.com/ali5h/rules_pip/blob/master/defs.bzl + +pip_vendor_label = Label("@com_github_ali5h_rules_pip//:third_party/py/easy_install.py") + +def _execute(repository_ctx, arguments, quiet = False): + pip_vendor = str(repository_ctx.path(pip_vendor_label).dirname) + return repository_ctx.execute(arguments, environment = { + "PYTHONPATH": pip_vendor, + }, quiet = quiet) + +def _install_pyqt6_impl(repository_ctx): + python_interpreter = repository_ctx.attr.python_interpreter + if repository_ctx.attr.python_runtime: + python_interpreter = repository_ctx.path(repository_ctx.attr.python_runtime) + + args = [ + python_interpreter, + repository_ctx.path(repository_ctx.attr._script), + repository_ctx.path("."), + ] + + result = _execute(repository_ctx, args, quiet = repository_ctx.attr.quiet) + if result.return_code: + fail("failed: %s (%s)" % (result.stdout, result.stderr)) + +install_pyqt6 = repository_rule( + attrs = { + "python_interpreter": attr.string(default = "python", doc = """ +The command to run the Python interpreter used to invoke pip and unpack the +wheels. +"""), + "python_runtime": attr.label(doc = """ +The label to the Python run-time interpreted used to invoke pip and unpack the wheels. +If the label is specified it will overwrite the python_interpreter attribute. +"""), + "_script": attr.label( + executable = True, + default = Label("//pip/pyqt6:install_pyqt6.py"), + cfg = "host", + ), + "quiet": attr.bool( + default = True, + doc = "If stdout and stderr should be printed to the terminal.", + ), + }, + implementation = _install_pyqt6_impl, +) diff --git a/pip/pyqt6/install_pyqt6.py b/pip/pyqt6/install_pyqt6.py new file mode 100644 index 000000000..a04193c11 --- /dev/null +++ b/pip/pyqt6/install_pyqt6.py @@ -0,0 +1,196 @@ +# based on https://github.com/ali5h/rules_pip/blob/master/src/whl.py +# MIT + +"""downloads and parses info of a pkg and generates a BUILD file for it""" +import argparse +import glob +import logging +import os +import re +import shutil +import subprocess +import sys + +import pkginfo + +from pip._internal.commands import create_command +from pip._vendor import pkg_resources + + +def _create_nspkg_init(dirpath): + """Creates an init file to enable namespacing""" + if not os.path.exists(dirpath): + # Handle missing namespace packages by ignoring them + return + nspkg_init = os.path.join(dirpath, "__init__.py") + with open(nspkg_init, "w") as nspkg: + nspkg.write("__path__ = __import__('pkgutil').extend_path(__path__, __name__)") + + +def install_package(pkg, directory, pip_args): + """Downloads wheel for a package. Assumes python binary provided has + pip and wheel package installed. + + Args: + pkg: package name + directory: destination directory to download the wheel file in + python: python binary path used to run pip command + pip_args: extra pip args sent to pip + Returns: + str: path to the wheel file + """ + pip_args = [ + "--isolated", + "--disable-pip-version-check", + "--target", + directory, + "--no-deps", + "--ignore-requires-python", + pkg, + ] + pip_args + cmd = create_command("install") + cmd.main(pip_args) + + # need dist-info directory for pkg_resources to be able to find the packages + dist_info = glob.glob(os.path.join(directory, "*.dist-info"))[0] + # fix namespace packages by adding proper __init__.py files + namespace_packages = os.path.join(dist_info, "namespace_packages.txt") + if os.path.exists(namespace_packages): + with open(namespace_packages) as nspkg: + for line in nspkg.readlines(): + namespace = line.strip().replace(".", os.sep) + if namespace: + _create_nspkg_init(os.path.join(directory, namespace)) + + # PEP 420 -- Implicit Namespace Packages + if (sys.version_info[0], sys.version_info[1]) >= (3, 3): + for dirpath, dirnames, filenames in os.walk(directory): + # we are only interested in dirs with no init file + if "__init__.py" in filenames: + dirnames[:] = [] + continue + # remove bin and dist-info dirs + for ignored in ("bin", os.path.basename(dist_info)): + if ignored in dirnames: + dirnames.remove(ignored) + _create_nspkg_init(dirpath) + + return pkginfo.Wheel(dist_info) + + +def _cleanup(directory, pattern): + for p in glob.glob(os.path.join(directory, pattern)): + shutil.rmtree(p) + + +fix_none = re.compile(r"(\s*None) =") + + +def copy_and_fix_pyi(source, dest): + "Fix broken PyQt types." + with open(source) as input_file: + with open(dest, "w") as output_file: + for line in input_file.readlines(): + # inheriting from the missing sip.sipwrapper definition + # causes missing attributes not to be detected, as it's treating + # the class as inheriting from Any + line = line.replace("PyQt6.sip.wrapper", "object") + line = line.replace("PyQt6.sip.wrapper", "object") + # # remove blanket getattr in QObject which also causes missing + # # attributes not to be detected + if "def __getattr__(self, name: str) -> typing.Any" in line: + continue + output_file.write(line) + + +def merge_files(root, source): + for dirpath, _dirnames, filenames in os.walk(source): + target_dir = os.path.join(root, os.path.relpath(dirpath, source)) + if not os.path.exists(target_dir): + os.mkdir(target_dir) + for fname in filenames: + source_path = os.path.join(dirpath, fname) + target_path = os.path.join(target_dir, fname) + if not os.path.exists(target_path): + if fname.endswith(".pyi"): + copy_and_fix_pyi(source_path, target_path) + else: + shutil.copy2(source_path, target_path) + + +def main(): + base = sys.argv[1] + + local_site_packages = os.environ.get("PYTHON_SITE_PACKAGES") + if local_site_packages: + subprocess.run( + [ + "rsync", + "-ai", + "--include=PyQt**", + "--exclude=*", + local_site_packages, + base + "/", + ], + check=True, + ) + with open(os.path.join(base, "__init__.py"), "w") as file: + pass + + else: + packages = [ + ("pyqt6", "pyqt6==6.2.0"), + ("pyqt6-qt6", "pyqt6-qt6==6.2.0"), + ("pyqt6-webengine", "pyqt6-webengine==6.2.0"), + ("pyqt6-webengine-qt6", "pyqt6-webengine-qt6==6.2.0"), + ("pyqt6-sip", "pyqt6_sip==13.1.0"), + ] + + for (name, with_version) in packages: + # install package in subfolder + folder = os.path.join(base, "temp") + _pkg = install_package(with_version, folder, []) + # merge into parent + merge_files(base, folder) + shutil.rmtree(folder) + + # add missing py.typed file + with open(os.path.join(base, "py.typed"), "w") as file: + pass + + result = """ +load("@rules_python//python:defs.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "pkg", + srcs = glob(["**/*.py"]), + data = glob(["**/*"], exclude = [ + "**/*.py", + "**/*.pyc", + "**/* *", + "BUILD", + "WORKSPACE", + "bin/*", + "__pycache__", + # these make building slower + "Qt/qml/**", + "**/*.sip", + "**/*.png", + ]), + # This makes this directory a top-level in the python import + # search path for anything that depends on this. + imports = ["."], +) +""" + + # clean up + _cleanup(base, "__pycache__") + + with open(os.path.join(base, "BUILD"), "w") as f: + f.write(result) + + +if __name__ == "__main__": + main() diff --git a/qt/.pylintrc b/qt/.pylintrc index 3cbdc97e9..510c08da4 100644 --- a/qt/.pylintrc +++ b/qt/.pylintrc @@ -1,6 +1,6 @@ [MASTER] persistent = no -extension-pkg-whitelist=PyQt5 +extension-pkg-whitelist=PyQt5,PyQt6 ignore = forms,hooks_gen.py [TYPECHECK] diff --git a/qt/mypy.ini b/qt/mypy.ini index 10a49657d..bb304e0f4 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -67,3 +67,6 @@ ignore_missing_imports = True disallow_untyped_defs=false [mypy-anki.*] disallow_untyped_defs=false + +[mypy-PyQt6.*] +ignore_errors = True \ No newline at end of file diff --git a/scripts/copyright_headers.py b/scripts/copyright_headers.py index 3aed02dba..4173c542d 100644 --- a/scripts/copyright_headers.py +++ b/scripts/copyright_headers.py @@ -7,6 +7,7 @@ from pathlib import Path nonstandard_header = { "pip/pyqt5/install_pyqt5.py", + "pip/pyqt6/install_pyqt6.py", "pylib/anki/importing/pauker.py", "pylib/anki/importing/supermemo_xml.py", "pylib/anki/statsbg.py",