diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..7e18dd84 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,177 @@ +# GH Actions script to build the Pyzo binaries. + +name: CD + +on: + workflow_dispatch: + push: + tags: [ 'v*' ] + branches: [ cd ] + +jobs: + + # The default Windows build serving the majority of users. + # Not needed because already installed: choco install innosetup --version=5.6.1 + win64: + name: Build Windows 64 + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U pyside6 pyinstaller + pip install -r freeze/frozen_libs.txt + - name: Freeze + run: python freeze/pyzo_freeze.py + - name: Package + run: python freeze/pyzo_package.py + - name: Test frozen + run: python freeze/pyzo_test_frozen.py + - name: Upload distributions + uses: actions/upload-artifact@v2 + with: + path: | + freeze/dist/*.zip + freeze/dist/*.exe + name: dist + + # A 32bit windows build for people on old machines. + # Win32 is on the brink of deprecation, so we tune down on Py and Qt versions. + win32: + name: Build Windows 32 + runs-on: windows-latest + env: + PYZO_QT_API: PyQt5 + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: '3.9' + architecture: x86 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U ${{ env.PYZO_QT_API }} pyinstaller + pip install -r freeze/frozen_libs.txt + - name: Freeze + run: python freeze/pyzo_freeze.py + - name: Package + run: python freeze/pyzo_package.py + - name: Test frozen + run: python freeze/pyzo_test_frozen.py + - name: Upload distributions + uses: actions/upload-artifact@v2 + with: + path: | + freeze/dist/*.zip + name: dist + + # A MacOS build for x86_64. Via Rosetta this should work on all modern Macs, + # but an arm64 (M1) build would be nice in the future. + macos_amd64: + name: Build MacOS amd64 + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U pyside6 pyinstaller + pip install -r freeze/frozen_libs.txt + - name: Freeze + run: python freeze/pyzo_freeze.py + - name: Package + run: python freeze/pyzo_package.py + - name: Test frozen + shell: bash + run: python freeze/pyzo_test_frozen.py + - name: Upload distributions + uses: actions/upload-artifact@v2 + with: + path: | + freeze/dist/*.zip + freeze/dist/*.dmg + name: dist + + # For Linux we make a build on a somewhat older ubuntu. Most Linux users prefer (or are fine with) + # running Pyzo from source anyway. + linux_amd64: + name: Build Linux amd64 + runs-on: ubuntu-18.04 + env: + PYZO_QT_API: PyQt5 + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U ${{ env.PYZO_QT_API }} pyinstaller + pip install -r freeze/frozen_libs.txt + sudo apt install -y libdbus-1-3 libxkbcommon-x11-0 libxcb-icccm4 \ + libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \ + libxcb-xinerama0 libxcb-xfixes0 libxcb-shape0 + - name: Freeze + run: python freeze/pyzo_freeze.py + - name: Package + run: python freeze/pyzo_package.py + - name: Test frozen + run: xvfb-run --auto-servernum python freeze/pyzo_test_frozen.py + - name: Upload distributions + uses: actions/upload-artifact@v2 + with: + path: | + freeze/dist/*.tar.gz + name: dist + + + publish: + name: Publish binaries to Github + runs-on: ubuntu-latest + needs: [ win64, win32, macos_amd64, linux_amd64 ] + if: success() && startsWith(github.ref, 'refs/tags/v') + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Download assets + uses: actions/download-artifact@v1.0.0 + with: + name: dist + - name: Get version from git ref + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + - name: Create GH release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.get_version.outputs.VERSION }} + release_name: Release ${{ steps.get_version.outputs.VERSION }} + body: | + Autogenerated binary wheels that include wgpu-native. + See [the changelog](https://github.com/pygfx/wgpu-py/blob/main/CHANGELOG.md) for details. + draft: false + prerelease: false + - name: Upload release assets + # Move back to official action after fix https://github.com/actions/upload-release-asset/issues/4 + uses: AButler/upload-release-assets@v2.0 + with: + release-tag: ${{ steps.get_version.outputs.VERSION }} + files: 'dist/*.zip;dist/*.tar.gz;dist/*.dmg;dist/*.exe;dist/*.msi' + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d535c7c8..051b84cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,17 @@ +# GH Actions script to test Pyzo. + name: CI on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: lint-build: - name: Linting + name: Test linting runs-on: ubuntu-latest strategy: fail-fast: false @@ -51,6 +53,10 @@ jobs: os: ubuntu-latest pyversion: '3.9' qtlib: pyside6 + - name: Test Linux py310 + os: ubuntu-latest + pyversion: '3.10' + qtlib: pyside6 # OS's - name: Test Windows py310 os: windows-latest @@ -61,28 +67,24 @@ jobs: pyversion: '3.10' qtlib: pyside6 # Qt libs - - name: Test Linux py310 PyQt5 + - name: Test Linux py39 PyQt5 os: ubuntu-latest - pyversion: '3.10' + pyversion: '3.9' qtlib: pyqt5 - - name: Test Linux py310 PyQt6 + - name: Test Linux py39 PyQt6 os: ubuntu-latest - pyversion: '3.10' + pyversion: '3.9' qtlib: pyqt6 - - name: Test Linux py310 PySide2 + - name: Test Linux py39 PySide2 os: ubuntu-latest - pyversion: '3.10' + pyversion: '3.9' qtlib: pyside2 - - name: Test Linux py310 PySide6 - os: ubuntu-latest - pyversion: '3.10' - qtlib: pyside6 steps: - uses: actions/checkout@v2 - name: Setup os if: matrix.os == 'ubuntu-latest' run: | - sudo apt-get install libegl1-mesa + sudo apt install libegl1-mesa - name: Set up Python ${{ matrix.pyversion }} uses: actions/setup-python@v2 with: @@ -98,4 +100,14 @@ jobs: - name: Test on repo run: | pytest -v tests - # todo: add a dry-run or something so we actually boot Pyzo and close it + - name: Run Pyzo + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt install -y libdbus-1-3 libxkbcommon-x11-0 libxcb-icccm4 \ + libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \ + libxcb-xinerama0 libxcb-xfixes0 libxcb-shape0 + xvfb-run --auto-servernum python pyzo --test + - name: Run Pyzo + if: matrix.os != 'ubuntu-latest' + run: | + python pyzo --test diff --git a/.gitignore b/.gitignore index df0fe446..32093668 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,7 @@ _feedstock/ build/ dist/ frozen/ +freeze/build/ +freeze/dist/ *.pyc pyzo.egg-info/* diff --git a/README.md b/README.md index 6e32d88d..b5b9d253 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Pyzo - The Interactive editor for scientific Python [![PyPI Version](https://img.shields.io/pypi/v/pyzo.svg)](https://pypi.python.org/pypi/pyzo/) -[![Build Status](https://dev.azure.com/pyzo/pyzo/_apis/build/status/pyzo.pyzo?branchName=master)](https://dev.azure.com/pyzo/pyzo/_build/latest?definitionId=1&branchName=master) +[![CI](https://github.com/pyzo/pyzo/actions/workflows/ci.yml/badge.svg)](https://github.com/pyzo/pyzo/actions/workflows/ci.yml) Website: [pyzo.org](http://pyzo.org) diff --git a/TRANSLATIONS.md b/TRANSLATIONS.md index 15af97e8..abbb5ef6 100644 --- a/TRANSLATIONS.md +++ b/TRANSLATIONS.md @@ -5,7 +5,7 @@ Simplified Chinese. For the translations we make use of Qt's translation system. To update a translation, run Qt linguist on any of the `.tr` files -[here](https://github.com/pyzo/pyzo/tree/master/pyzo/resources/translations). +[here](https://github.com/pyzo/pyzo/tree/main/pyzo/resources/translations). Then submit the result, preferably via a Github pull request (but emailing it to me is fine too). diff --git a/freeze/download_count.py b/download_count.py similarity index 100% rename from freeze/download_count.py rename to download_count.py diff --git a/freeze/boot.py b/freeze/boot.py new file mode 100644 index 00000000..bb4078da --- /dev/null +++ b/freeze/boot.py @@ -0,0 +1,102 @@ +import os +import sys +import platform +import traceback +import importlib + +import dialite + + +TEST = "--test" in sys.argv + + +# %% Utils + + +def write(*msg): + print(*msg) + if TEST and os.getenv("PYZO_LOG", ""): + with open(os.getenv("PYZO_LOG"), "at") as f: + f.write(" ".join(msg) + "\n") + + +class SourceImporter: + def __init__(self, dir): + self.module_names = set() + for name in os.listdir(dir): + fullname = os.path.join(dir, name) + if name.endswith(".py"): + self.module_names.add(name) + elif os.path.isdir(fullname): + if os.path.isfile(os.path.join(fullname, "__init__.py")): + self.module_names.add(name) + + def find_spec(self, fullname, path, target=None): + if fullname.split(".")[0] in self.module_names: + return sys.meta_path[1].find_spec(fullname, path, target) + else: + return None + + +def error_handler(cls, err, tb, action=""): + title = "Application aborted" + if action: + title += f" while {action}" + msg = f"{cls.__name__}: {err}" + # Try writing traceback to stderr + try: + tb_info = "".join(traceback.format_list(traceback.extract_tb(tb))) + write(f"{title}\n{msg}\n{tb_info}") + except Exception: + pass + # Use dialite to show error in modal window + if not TEST: + dialite.fail(title, msg) + + +class BootAction: + def __init__(self, action): + self._action = action + try: + write(action) + except Exception: + pass + + def __enter__(self): + return self + + def __exit__(self, cls, err, tb): + if err: + error_handler(cls, err, tb, self._action) + sys.exit(1) + + +# %% Boot + +if TEST: + write("Checking Pyzo container") + write(platform.platform()) + write(sys.version) + + +with BootAction("Setting up source importer"): + source_dir = os.path.join(sys._MEIPASS, "source") + sys.path.insert(0, source_dir) + sys.meta_path.insert(0, SourceImporter(source_dir)) + + +with BootAction("Applying pre-import Qt tweaks"): + importlib.import_module("pyzo.pre_qt_import") + + +with BootAction("Importing Qt"): + QtCore = importlib.import_module("pyzo.qt." + "QtCore") + QtGui = importlib.import_module("pyzo.qt." + "QtGui") + QtWidgets = importlib.import_module("pyzo.qt." + "QtWidgets") + + +with BootAction("Running Pyzo"): + pyzo = importlib.import_module("pyzo") + write(f"Pyzo {pyzo.__version__}") + pyzo.start() + write("Stopped") diff --git a/freeze/dllutils.py b/freeze/dllutils.py deleted file mode 100644 index b9f04e42..00000000 --- a/freeze/dllutils.py +++ /dev/null @@ -1,248 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2012 Almar Klein -# This module is distributed under the terms of the (new) BSD License. - -""" Various utilities to modify Dynamic Link libraries. - -Needed to build the Pyzo distro, and it's possible that this -functionality is needed to fix extension modules after installation in -a Pyzo distro. - -This is a mix of utilities for Windows, Mac and Linux. - -""" - -import os -import stat -import sys -import subprocess -import time -import re - - -_COMMAND_TO_SEARCH_PATH = [] - - -def get_command_to_set_search_path(): - """Get the command to change the RPATH of executables and dynamic - libraries. Returns None if there is no such command or if it - cannot be found. - """ - - # Check if already computed - if _COMMAND_TO_SEARCH_PATH: - return _COMMAND_TO_SEARCH_PATH[0] - - # Get name of the utility - # In Pyzo it should be present in 'shared'. - utilCommand = None - if sys.platform.startswith("win"): - return - if sys.platform.startswith("linux"): - utilname = "patchelf" - if sys.platform.startswith("darwin"): - utilname = "install_name_tool" - if True: - # Try old Pyzo - utilCommand = os.path.join(sys.prefix, "shared", utilname) - if not os.path.isfile(utilCommand): - utilCommand = utilname - # Try new Pyzo / anaconda - utilCommand = os.path.join(sys.prefix, "bin", utilname) - if not os.path.isfile(utilCommand): - utilCommand = utilname - # Test whether it exists - try: - subprocess.check_output(["which", utilCommand]) - except Exception: - raise RuntimeError( - "Could not get command (%s) to set search path." % utilCommand - ) - - # Store and return - _COMMAND_TO_SEARCH_PATH.append(utilCommand) - return utilCommand - - -def set_search_path(fname, *args): - """set_search_path(fname, *args) - For the given library/executable, set the search path to the - relative paths specified in args. - - For Linux: The RPATH is the path to search for its dependencies. - http://enchildfone.wordpress.com/2010/03/23/a-description-of-rpath-origin-ld_library_path-and-portable-linux-binaries/ - - For Mac: We use the @rpath identifier to get similar behavior to - Linux. But each dependency must be specified. To realize this, we - need to check for each dependency whether it is on one of te given - search paths. - - For Windows: not supported in any way. Windows searches next to the - library and then in system paths and PATH. - - """ - - # Prepare - args = [arg.lstrip("/") for arg in args if arg] - args = [arg for arg in args if arg != "."] # Because we add empty dir anyway - args.append("") # make libs search next to themselves - command = get_command_to_set_search_path() - - if sys.platform.startswith("linux"): - # Create search path value - rpath = ":".join(["$ORIGIN/" + arg for arg in args]) - # Modify rpath using a call to patchelf utility - cmd = [command, "--set-rpath", rpath, fname] - subprocess.check_call(cmd) - print("Set RPATH for %r" % os.path.basename(fname)) - # print('Set RPATH for %r: %r' % (os.path.basename(fname), rpath)) - - elif sys.platform.startswith("darwin"): - # ensure write permissions - mode = os.stat(fname).st_mode - if not (mode & stat.S_IWUSR): - os.chmod(fname, mode | stat.S_IWUSR) - # let the file itself know its place (simpyl on rpath) - name = os.path.basename(fname) - subprocess.call(("install_name_tool", "-id", "@rpath/" + name, fname)) - # find the references: call otool -L on the file - otool = subprocess.Popen(("otool", "-L", fname), stdout=subprocess.PIPE) - references = otool.stdout.readlines()[1:] - - # Replace each reference - rereferencedlibs = [] - for reference in references: - # find the actual referenced file name - referencedFile = reference.decode().strip().split()[0] - if referencedFile.startswith("@"): - continue # the referencedFile is already a relative path - # Get lib name - _, name = os.path.split(referencedFile) - if name.lower() == "python": - name = "libpython" # Rename Python lib on Mac - # see if we provided the referenced file - potentiallibs = [ - os.path.join(os.path.dirname(fname), arg, name) for arg in args - ] - # if so, change the reference and rpath - if any([os.path.isfile(p) for p in potentiallibs]): - subprocess.call( - ( - "install_name_tool", - "-change", - referencedFile, - "@rpath/" + name, - fname, - ) - ) - for arg in args: - mac_add_rpath(fname, "@loader_path/" + arg) - mac_add_rpath(fname, "@executable_path/") # use libpython next to exe - rereferencedlibs.append(name) - if rereferencedlibs: - print( - 'Replaced refs for "%s": %s' - % (os.path.basename(fname), ", ".join(rereferencedlibs)) - ) - - elif sys.platform.startswith("win"): - raise RuntimeError( - "Windows has no way of setting the search path on a library or exe." - ) - else: - raise RuntimeError( - "Do not know how to set search path of library or exe on %s" % sys.platform - ) - - -def mac_add_rpath(fname, rpath): - """mac_add_rpath(fname, rpath) - Set the rpath for a Mac library or executble. If the rpath is already - registered, it is ignored. - """ - cmd = ["install_name_tool", "-add_rpath", rpath, fname] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - while p.poll() is None: - time.sleep(0.01) - if p.returncode: - msg = p.stdout.read().decode("utf-8") - if "would duplicate path" in msg: - pass # Ignore t - else: - raise RuntimeError("Could not set rpath: " + msg) - - -def remove_CRT_dependencies(dirname, recurse=True): - """remove_CRT_dependencies(path, recurse=True) - Check all .dll and .pyd files in the given directory (and its - subdirectories if recurse is True), removing the dependency on the - Windows C runtime from the embedded manifest. - """ - dllExt = [".dll", ".pyd"] - for entry in os.listdir(dirname): - p = os.path.join(dirname, entry) - if recurse and os.path.isdir(p): - remove_CRT_dependencies(p, recurse) - elif os.path.isfile(p) and os.path.splitext(p)[1].lower() in dllExt: - remove_CRT_dependency(p) - - -def remove_CRT_dependency(filename): - """remove_CRT_dependency(filename) - Modify the embedded manifest of a Windows dll (or pyd file), - such that it no longer depends on the Windows C runtime. - In effect, the dll will fall back to using the C runtime that - the executable depends on (and has loaded in memory). - - This function is not necessary for dll's and pyd's that come with - Python, because these are build without the CRT dependencies for a - while. However, some third party packages (e.g. PySide) do have - these dependencies, and they need to be removed in order to work - on a system that does not have the C-runtime installed. - - Based on this diff by C. Gohlke: - http://bugs.python.org/file15113/msvc9compiler_stripruntimes_regexp2.diff - See discussion at: http://bugs.python.org/issue4120 - - """ - if "QtCore" in filename: - 1 / 0 - - # Read the whole file - with open(filename, "rb") as f: - try: - bb = f.read() - except IOError: - # raise IOError('Could not read %s'%filename) - print("Warning: could not read %s" % filename) - return - - # Remove assemblyIdentity tag - # This code is different from that in python's distutils/msvc9compiler.py - # by removing re.DOTALL and replaceing the second DOT with "(.|\n|\r)", - # which means that the first DOT cannot contain newlines. Would we not do - # this, the match is too greedy (and causes tk85.dll to break). - pattern = ( - r"""|)""" - ) - pattern = re.compile(pattern.encode("ascii")) - bb, hasMatch = _replacePatternWithSpaces(pattern, bb) - if hasMatch: - # Remove dependentAssembly tag if it's empty - pattern = "\s*".encode("ascii") - bb, hasMatch = _replacePatternWithSpaces(pattern, bb) - # Write back - with open(filename, "wb") as f: - f.write(bb) - print("Removed embedded MSVCR dependency for: %s" % filename) - - -def _replacePatternWithSpaces(pattern, bb): - match = re.search(pattern, bb) - if match is not None: - L = match.end() - match.start() - bb = re.sub(pattern, b" " * L, bb) - return bb, True - else: - return bb, False diff --git a/freeze/freezeScript.py b/freeze/freezeScript.py deleted file mode 100644 index 6f5e28ab..00000000 --- a/freeze/freezeScript.py +++ /dev/null @@ -1,327 +0,0 @@ -#!/usr/bin/env python3 - -""" FREEZING Pyzo WITH PYINSTALLER - -Pyzo is frozen in such a way that it still uses the plain source code. -This is achieved by putting the Pyzo package in a subdirectory called -"source". This source directory is added to sys.path by __main__.py. - -In case we need better support for older MacOS: -https://gist.github.com/phfaist/a5b8a895b003822df5397731f4673042 - -""" - -import os -import re -import sys -import shutil -import zipfile -import tarfile -import subprocess - -import PyInstaller.__main__ - - -# Define app name and such -name = "pyzo" -thisDir = os.path.abspath(os.path.dirname(__file__)) -baseDir = os.path.abspath(os.path.join(thisDir, "..")) + "/" -srcDir = baseDir + "pyzo/" -distDir = baseDir + "frozen/" -iconFile = srcDir + "resources/appicons/pyzologo.ico" - -sys.path.insert(0, baseDir) - - -## Includes and excludes - -# The Qt toolkit that we use -QT_API = "PyQt5" - -# All known Qt toolkits, mainly to exclude them -qt_kits = {"PySide", "PySide2", "PyQt4", "PyQt5"} - -# Imports that PyInstaller may have missed, or that are simply common/useful -# and may be used by some tools. -includes = ["code", "shutil"] - -# Exclude stuff that somehow gets, or may get, selected by PyInstaller -excludes = ["numpy", "scipy", "win32com", "conda", "pip", "IPython"] - -# Excludes for tk -tk_excludes = [ - "pywin", - "pywin.debugger", - "pywin.debugger.dbgcon", - "pywin.dialogs", - "pywin.dialogs.list", - "Tkconstants", - "Tkinter", - "tcl", -] -excludes.extend(tk_excludes) - -# Excludes for Qt -qt_excludes = [ - "QtNetwork", - "QtOpenGL", - "QtXml", - "QtTest", - "QtSql", - "QtSvg", - "QtBluetooth", - "QtDBus", - "QtDesigner", - "QtLocation", - "QtPositioning", - "QtMultimedia", - "QtMultimediaWidgets", - "QtQml", - "QtQuick", - "QtSql", - "QtSvg", - "QtTest", - "QtWebKit", - "QtXml", - "QtXmlPatterns", - "QtDeclarative", - "QtScript", - "QtScriptTools", - "QtUiTools", - "QtQuickWidgets", - "QtSensors", - "QtSerialPort", - "QtWebChannel", - "QtWebKitWidgets", - "QtWebSockets", -] - -for qt_ver in qt_kits: - for excl in qt_excludes: - excludes.append(qt_ver + "." + excl) - -excludes.extend(qt_kits.difference([QT_API])) - - -## Freeze - -# Clear first -if os.path.isdir(distDir): - shutil.rmtree(distDir) -os.makedirs(distDir) - - -cmd = ["--clean", "--onedir", "--name", name, "--distpath", distDir] - -for m in includes: - cmd.extend(["--hidden-import", m]) -for m in excludes: - cmd.extend(["--exclude-module", m]) - -if sys.platform.startswith("win"): - cmd.append("--windowed") # not a console app - cmd.extend(["--icon", iconFile]) -elif sys.platform.startswith("darwin"): - cmd.append("--windowed") # makes a .app bundle - cmd.extend(["--icon", iconFile[:-3] + "icns"]) - cmd.extend(["--osx-bundle-identifier", "org.pyzo.pyzo4"]) - -cmd.append(srcDir + "__main__.py") - -PyInstaller.__main__.run(cmd) - -try: - os.remove(os.path.join(thisDir, "pyzo.spec")) -except Exception: - pass - - -## Process source code and other resources - -with open(srcDir + "__init__.py") as fh: - __version__ = re.search(r"__version__ = \"(.*?)\"", fh.read()).group(1) - -bitness = "32" if sys.maxsize <= 2 ** 32 else "64" - - -def copydir_smart(path1, path2): - """like shutil.copytree, but ... - * ignores __pycache__directories - * ignores hg, svn and git directories - """ - # Ensure destination directory does exist - if not os.path.isdir(path2): - os.makedirs(path2) - # Itereate over elements - count = 0 - for sub in os.listdir(path1): - fullsub1 = os.path.join(path1, sub) - fullsub2 = os.path.join(path2, sub) - if sub in ["__pycache__", ".hg", ".svn", ".git"]: - continue - elif sub.endswith(".pyc") and os.path.isfile(fullsub1[:-1]): - continue - elif os.path.isdir(fullsub1): - count += copydir_smart(fullsub1, fullsub2) - elif os.path.isfile(fullsub1): - shutil.copy(fullsub1, fullsub2) - count += 1 - # Return number of copies files - return count - - -SETTINGS_TEXT = """ -Portable settings folder ------------------------- -This folder can be used to let the application and the libaries that -it uses to store configuration files local to the executable. One use -case is having this app on a USB drive that you use on different -computers. - -This functionality is enabled if the folder is named "settings" and is -writable by the application (i.e. should not be in "c:\program files\..." -or "/usr/..."). This functionality can be deactivated by renaming -it (e.g. prepending an underscore). To reset config files, clear the -contents of the "pyzo" sub-folder (but do not remove the folder itself). - -Note that some libraries may ignore this functionality and use the -normal system configuration directory instead. - -This "standard" was discussed between the authors of WinPython, -PortablePython and Pyzo. Developers can use the appdata_dir() function -from https://bitbucket.org/pyzo/pyzolib/src/tip/paths.py to -use this standard. For more info, contact either of us. - -""".lstrip() - -# Post process the frozen dir (and the frozen app-dir on OS X) -frozenDirs = [os.path.join(distDir, "pyzo")] -if sys.platform.startswith("darwin"): - frozenDirs.append(os.path.join(distDir, "pyzo.app", "Contents", "MacOS")) - -for frozenDir in frozenDirs: - - # Copy the whole Pyzo package - copydir_smart(os.path.join(srcDir), os.path.join(frozenDir, "source", "pyzo")) - - # Create settings folder and put in a file - os.mkdir(os.path.join(frozenDir, "_settings")) - os.mkdir(os.path.join(frozenDir, "_settings", "pyzo")) - with open(os.path.join(frozenDir, "_settings", "README.txt"), "wb") as file: - file.write(SETTINGS_TEXT.encode("utf-8")) - - -# Patch info.plist -if sys.platform.startswith("darwin"): - extra_plist_info = """ - CFBundleShortVersionString - X.Y.Z - NSHighResolutionCapable - - """.strip() - extra_plist_info = "\n\t".join( - line.strip() for line in extra_plist_info.splitlines() - ) - extra_plist_info = extra_plist_info.replace("X.Y.Z", __version__) - plist_filename = os.path.join(distDir, "pyzo.app", "Contents", "Info.plist") - text = open(plist_filename, "rb").read().decode() - i1 = text.index("CFBundleShortVersionString", i1) + len("") - text = text[:i1] + extra_plist_info + text[i2:] - with open(plist_filename, "wb") as f: - f.write(text.encode()) - - -## Package things up - -# Linux: .tar.gz -# Windows: zip and exe installer -# MacOS: DMG - - -if sys.platform.startswith("linux"): - print("Packing up into tar.gz ...") - - oridir = os.getcwd() - os.chdir(distDir) - try: - tarfilename = "pyzo-" + __version__ + "-linux" + bitness + ".tar.gz" - tf = tarfile.open(tarfilename, "w|gz") - with tf: - tf.add("pyzo", arcname="pyzo-" + __version__) - finally: - os.chdir(oridir) - - -if sys.platform.startswith("win"): - print("Packing up into zip ...") - - zipfilename = "pyzo-" + __version__ + "-win" + bitness + ".zip" - zf = zipfile.ZipFile( - os.path.join(distDir, zipfilename), "w", compression=zipfile.ZIP_DEFLATED - ) - with zf: - for root, dirs, files in os.walk(os.path.join(distDir, "pyzo")): - for fname in files: - filename1 = os.path.join(root, fname) - filename2 = os.path.relpath(filename1, os.path.join(distDir, "pyzo")) - filename2 = os.path.join("pyzo-" + __version__, filename2) - zf.write(filename1, filename2) - - -if sys.platform.startswith("win") and bitness == "64": - # Note: for some reason the 32bit installer is broken. Ah well, the zip works. - print("Packing up into exe installer (via Inno Setup) ...") - - exes = [ - r"c:\Program Files (x86)\Inno Setup 5\ISCC.exe", - r"c:\Program Files (x86)\Inno Setup 6\ISCC.exe", - ] - for exe in exes: - if os.path.isfile(exe): - break - else: - raise RuntimeError("Could not find Inno Setup exe") - - # Set inno file - innoFile1 = os.path.join(thisDir, "installerBuilderScript.iss") - innoFile2 = os.path.join(thisDir, "installerBuilderScript2.iss") - text = open(innoFile1, "rb").read().decode() - text = text.replace("X.Y.Z", __version__).replace("64", bitness) - if bitness == "32": - text = text.replace("ArchitecturesInstallIn64BitMode = x64", "") - with open(innoFile2, "wb") as f: - f.write(text.encode()) - try: - subprocess.check_call([exe, "/Qp", innoFile2]) - finally: - os.remove(innoFile2) - - -if sys.platform.startswith("darwin"): - print("Packing up into DMG ...") - - appDir = distDir + "pyzo.app" - dmgFile = distDir + "pyzo-" + __version__ + "-macos.dmg" - - if ( - os.spawnlp( - os.P_WAIT, - "hdiutil", - "hdiutil", - "create", - "-fs", - "HFSX", - "-format", - "UDZO", - dmgFile, - "-imagekey", - "zlib-level=9", - "-srcfolder", - appDir, - "-volname", - "pyzo", - ) - != 0 - ): - raise OSError("creation of the dmg failed") diff --git a/freeze/frozen_libs.txt b/freeze/frozen_libs.txt new file mode 100644 index 00000000..e39bdaae --- /dev/null +++ b/freeze/frozen_libs.txt @@ -0,0 +1,2 @@ +packaging +dialite diff --git a/freeze/installerBuilderScript.iss b/freeze/installerBuilderScript.iss index 06a90472..b37094fb 100644 --- a/freeze/installerBuilderScript.iss +++ b/freeze/installerBuilderScript.iss @@ -11,7 +11,7 @@ ArchitecturesInstallIn64BitMode = x64 DefaultDirName = {pf}\pyzo DefaultGroupName = pyzo -SourceDir = ../frozen/pyzo +SourceDir = dist/pyzo OutputDir = .. OutputBaseFilename = pyzo-X.Y.Z-win64 diff --git a/freeze/pyzo_freeze.py b/freeze/pyzo_freeze.py new file mode 100644 index 00000000..cc41f9d2 --- /dev/null +++ b/freeze/pyzo_freeze.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 + +""" PyInstaller script +""" + +import os +import sys +import shutil +from distutils.sysconfig import get_python_lib + + +# Definitions +name = "pyzo" +qt_api = os.getenv("PYZO_QT_API", "PySide6") +this_dir = os.path.abspath(os.path.dirname(__file__)) + "/" +exe_script = this_dir + "boot.py" +dist_dir = this_dir + "dist/" +icon_file = os.path.abspath( + os.path.join(this_dir, "..", "pyzo", "resources", "appicons", "pyzologo.ico") +) + +# Run the script from the freeze dir +os.chdir(this_dir) + + +## Utils + + +def _find_modules(root, extensions, skip, parent=""): + """Yield all modules and packages and their submodules and subpackages found at `root`. + Nested folders that do _not_ contain an __init__.py file are assumed to also be on sys.path. + `extensions` should be a set of allowed file extensions (without the .). `skip` should be + a set of file or folder names to skip. The `parent` argument is for internal use only. + """ + for filename in os.listdir(root): + if filename.startswith("_"): + continue + if filename in skip: + continue + path = os.path.join(root, filename) + if os.path.isdir(path): + if filename.isidentifier() and os.path.exists( + os.path.join(path, "__init__.py") + ): + if parent: + packageName = parent + "." + filename + else: + packageName = filename + for module in _find_modules(path, extensions, skip, packageName): + yield module + elif not parent: + for module in _find_modules(path, extensions, skip, ""): + yield module + elif "." in filename: + moduleName, ext = filename.split(".", 1) + if ext in extensions and moduleName.isidentifier(): + if parent and moduleName == "__init__": + yield parent + elif parent: + yield parent + "." + moduleName + else: + yield moduleName + + +def get_stdlib_modules(): + """Return a list of all module names that are part of the Python Standard Library.""" + stdlib_path = get_python_lib(standard_lib=True) + extensions = {"py", "so", "dll", "pyd"} + skip = { + "site-packages", # not stdlib + "idlelib", # irrelevant for us + "lib2to3", # irrelevant for us + "test", # irrelevant for us + "turtledemo", # irrelevant for us + "tkinter", # not needed + "tk", + "tcl", + "unittest", # not needed + "distutils", # not needed - also must be avoided* + } + # On distutils: one distutils submodule causes IPython to be included, + # and with that, a sloth of libs like matplotlib and multiple Qt libs. + return list(_find_modules(stdlib_path, extensions, skip)) + + +def copydir_smart(path1, path2): + """like shutil.copytree, but ... + * ignores __pycache__directories + * ignores hg, svn and git directories + """ + # Ensure destination directory does exist + if not os.path.isdir(path2): + os.makedirs(path2) + # Itereate over elements + count = 0 + for sub in os.listdir(path1): + fullsub1 = os.path.join(path1, sub) + fullsub2 = os.path.join(path2, sub) + if sub in ["__pycache__", ".hg", ".svn", ".git"]: + continue + elif sub.endswith(".pyc") and os.path.isfile(fullsub1[:-1]): + continue + elif os.path.isdir(fullsub1): + count += copydir_smart(fullsub1, fullsub2) + elif os.path.isfile(fullsub1): + shutil.copy(fullsub1, fullsub2) + count += 1 + # Return number of copies files + return count + + +# All known Qt toolkits, excluded the one we will use +other_qt_kits = {"PySide", "PySide2", "PySide6", "PyQt4", "PyQt5", "PyQt6"} +other_qt_kits.remove(qt_api) + + +## Includes and excludes + +# We don't really make use of PySides detection mechanism, but instead specify +# explicitly what our binaries need. We include almost the whole stdlib, and +# a small subset of Qt. This way, future versions of Pyzo can work in the same +# container, and we still have a relatively small footprint. + +includes = [] +excludes = [] + + +# Include almost all stdlib modules +includes += get_stdlib_modules() + +# Include a few 3d party packages, e.g. deps of qtpy +includes += open(os.path.join(this_dir, "frozen_libs.txt"), "rt").read().split() + +# Include a subset of Qt modules +qt_includes = [ + "QtCore", # Standard + "QtGui", # Standard + "QtWidgets", # Standard + "QtHelp", # For docs + "QtOpenGLWidgets", # Because qtpy imports QOpenGLQWidget into QtWidgets +] +includes += [f"{qt_api}.{sub}" for sub in qt_includes] + + +# There is a tendency to include tk modules +excludes += ["tkinter", "tk", "tcl"] + +# Also exclude other Qt toolkits just to be sure +excludes += list(other_qt_kits) + +# PySide tends to include *all* qt modules, resulting in a 300MB or so folder, +# so we mark them as unwanted, getting us at around 120MB. +qt_excludes = [ + "QtNetwork", + "QtOpenGL", + "QtXml", + "QtTest", + "QtSql", + "QtSvg", + "QtBluetooth", + "QtDBus", + "QtDesigner", + "QtLocation", + "QtPositioning", + "QtMultimedia", + "QtMultimediaWidgets", + "QtQml", + "QtQuick", + "QtSql", + "QtSvg", + "QtTest", + "QtWebKit", + "QtXml", + "QtXmlPatterns", + "QtDeclarative", + "QtScript", + "QtScriptTools", + "QtUiTools", + "QtQuickWidgets", + "QtSensors", + "QtSerialPort", + "QtWebChannel", + "QtWebKitWidgets", + "QtWebSockets", +] +excludes += [f"{qt_api}.{sub}" for sub in qt_excludes] + + +## Freeze + +import PyInstaller.__main__ + + +# Clear first +if os.path.isdir(dist_dir): + shutil.rmtree(dist_dir) +os.makedirs(dist_dir) + + +cmd = ["--clean", "--onedir", "--name", name, "--distpath", dist_dir] + +for m in includes: + cmd.extend(["--hidden-import", m]) +for m in excludes: + cmd.extend(["--exclude-module", m]) + +if sys.platform.startswith("win"): + cmd.append("--windowed") # not a console app + cmd.extend(["--icon", icon_file]) +elif sys.platform.startswith("darwin"): + cmd.append("--windowed") # makes a .app bundle + cmd.extend(["--icon", icon_file[:-3] + "icns"]) + cmd.extend(["--osx-bundle-identifier", "org.pyzo.app"]) + +cmd.append(exe_script) + +PyInstaller.__main__.run(cmd) + +try: + os.remove(os.path.join(this_dir, f"{name}.spec")) +except Exception: + pass + + +## Add Pyzo source + +if sys.platform.startswith("darwin"): + target_dir = os.path.join(dist_dir, "pyzo.app", "Contents", "MacOS") +else: + target_dir = os.path.join(dist_dir, name) + +copydir_smart( + os.path.join(this_dir, "..", "pyzo"), os.path.join(target_dir, "source", "pyzo") +) + + +## Add portable settings dir + +SETTINGS_TEXT = """ +Portable settings folder +------------------------ +This folder can be used to let the application and the libaries that +it uses to store configuration files local to the executable. One use +case is having this app on a USB drive that you use on different +computers. + +This functionality is enabled if the folder is named "settings" and is +writable by the application (i.e. should not be in "c:\program files\..." +or "/usr/..."). This functionality can be deactivated by renaming +it (e.g. prepending an underscore). To reset config files, clear the +contents of the "pyzo" sub-folder (but do not remove the folder itself). + +Note that some libraries may ignore this functionality and use the +normal system configuration directory instead. +""".lstrip() + +# Create settings folder and put in a file +os.mkdir(os.path.join(target_dir, "_settings")) +os.mkdir(os.path.join(target_dir, "_settings", "pyzo")) +with open(os.path.join(target_dir, "_settings", "README.txt"), "wb") as file: + file.write(SETTINGS_TEXT.encode("utf-8")) diff --git a/freeze/pyzo_package.py b/freeze/pyzo_package.py new file mode 100644 index 00000000..265aec5c --- /dev/null +++ b/freeze/pyzo_package.py @@ -0,0 +1,128 @@ +import os +import re +import sys +import zipfile +import tarfile +import platform +import subprocess + + +this_dir = os.path.abspath(os.path.dirname(__file__)) + "/" +dist_dir = this_dir + "dist/" + + +with open(os.path.join(this_dir, "..", "pyzo", "__init__.py")) as fh: + __version__ = re.search(r"__version__ = \"(.*?)\"", fh.read()).group(1) + +bitness = "32" if sys.maxsize <= 2 ** 32 else "64" + +osname = os.getenv("PYZO_OSNAME", "") +if osname: + pass +elif sys.platform.startswith("linux"): + osname = "linux_" + platform.machine() +elif sys.platform.startswith("win"): + osname = f"win{bitness}" +elif sys.platform.startswith("darwin"): + osname = "macos_" + platform.machine() +else: + raise RuntimeError("Unknown platform") + +basename = f"pyzo-{__version__}-{osname}" + + +## Utils + + +def package_tar_gz(): + print("Packing up into tar.gz ...") + + oridir = os.getcwd() + os.chdir(dist_dir) + try: + tf = tarfile.open(basename + ".tar.gz", "w|gz") + with tf: + tf.add("pyzo", arcname="pyzo-" + __version__) + finally: + os.chdir(oridir) + + +def package_zip(): + print("Packing up into zip ...") + + zf = zipfile.ZipFile( + os.path.join(dist_dir, basename + ".zip"), "w", compression=zipfile.ZIP_DEFLATED + ) + with zf: + for root, dirs, files in os.walk(os.path.join(dist_dir, "pyzo")): + for fname in files: + filename1 = os.path.join(root, fname) + filename2 = os.path.relpath(filename1, os.path.join(dist_dir, "pyzo")) + filename2 = os.path.join("pyzo-" + __version__, filename2) + zf.write(filename1, filename2) + + +def package_inno_installer(): + print("Packing up into exe installer (via Inno Setup) ...") + + exes = [ + r"c:\Program Files (x86)\Inno Setup 5\ISCC.exe", + r"c:\Program Files (x86)\Inno Setup 6\ISCC.exe", + ] + for exe in exes: + if os.path.isfile(exe): + break + else: + raise RuntimeError("Could not find Inno Setup exe") + + # Set inno file + innoFile1 = os.path.join(this_dir, "installerBuilderScript.iss") + innoFile2 = os.path.join(this_dir, "installerBuilderScript2.iss") + text = open(innoFile1, "rb").read().decode() + text = text.replace("X.Y.Z", __version__).replace("64", bitness) + if bitness == "32": + text = text.replace("ArchitecturesInstallIn64BitMode = x64", "") + with open(innoFile2, "wb") as f: + f.write(text.encode()) + try: + subprocess.check_call([exe, "/Qp", innoFile2], cwd=dist_dir) + finally: + os.remove(innoFile2) + + +def package_dmg(): + print("Packing up into DMG ...") + + app_dir = "pyzo.app" + dmg_file = basename + ".dmg" + + cmd = ["hdiutil", "create"] + cmd.extend(["-srcfolder", app_dir]) + cmd.extend(["-volname", "pyzo"]) + cmd.extend(["-format", "UDZO"]) + cmd.extend(["-fs", "HFSX"]) + # cmd.extend(["-uid", "99"]) # who ever is mounting + # cmd.extend(["-gid", "99"]) # who ever is mounting + cmd.extend(["-mode", "555"]) # readonly + cmd.append("-noscrub") + cmd.append(dmg_file) + + subprocess.check_call(cmd, cwd=dist_dir) + + +## Build + + +if sys.platform.startswith("linux"): + package_zip() + package_tar_gz() + +if sys.platform.startswith("win"): + package_zip() + if bitness == "64": + # Note: for some reason the 32bit installer is broken. Ah well, the zip works. + package_inno_installer() + +if sys.platform.startswith("darwin"): + package_zip() + package_dmg() diff --git a/freeze/pyzo_test_frozen.py b/freeze/pyzo_test_frozen.py new file mode 100644 index 00000000..c7cdf8d3 --- /dev/null +++ b/freeze/pyzo_test_frozen.py @@ -0,0 +1,34 @@ +import os +import sys +import subprocess + +this_dir = os.path.abspath(os.path.dirname(__file__)) + "/" +dist_dir = os.path.join(this_dir, "dist") + +# Get what executable to run +if sys.platform.startswith("win"): + exe = os.path.join(dist_dir, "pyzo", "pyzo.exe") +elif sys.platform.startswith("darwin"): + exe = os.path.join(dist_dir, "pyzo.app", "Contents", "MacOS", "pyzo") +else: + exe = os.path.join(dist_dir, "pyzo", "pyzo") + +# Prepare log file +logfile = os.path.join(this_dir, "log.txt") +with open(logfile, "wt") as f: + f.write("") + +# Run Pyzo +os.environ["PYZO_LOG"] = logfile +subprocess.run([exe, "--test"]) + +# Process log +print("=" * 80) +with open(logfile, "rt") as f: + log = f.read() +os.remove(logfile) +print(log) +if log.strip().endswith("Stopped"): + sys.exit(0) +else: + sys.exit("Unsuccessful Pyzo test run") diff --git a/freeze/receive_file_from_vm.py b/freeze/receive_file_from_vm.py deleted file mode 100644 index fc722e27..00000000 --- a/freeze/receive_file_from_vm.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Getting files from/to the OS X VM can be a pain. This is a simple webserver -to allow posting a file. -""" - -import os -import asgish - - -@asgish.to_asgi -async def handler(request): - assert request.method == "POST" - filename2 = request.path.strip("/") - filename1 = filename2 + ".part" - assert "/" not in filename1 - with open(filename1, "wb") as f: - async for chunk in request.iter_body(): - f.write(chunk) - if os.path.isfile(filename2): - os.remove(filename2) - os.rename(filename1, filename2) - print("received", filename2) - return "Success!" - - -if __name__ == "__main__": - asgish.run(handler, "uvicorn", "0.0.0.0:80") diff --git a/pyzo/__init__.py b/pyzo/__init__.py index 4af089c0..e7260e18 100644 --- a/pyzo/__init__.py +++ b/pyzo/__init__.py @@ -43,319 +43,15 @@ # Set version number __version__ = "4.11.7" -import os import sys -import ctypes -import locale -import traceback # Check Python version -if sys.version < "3": - raise RuntimeError("Pyzo requires Python 3.x to run.") - -# Make each OS find platform plugins etc. - or let PyInstaller do its thing? -if getattr(sys, "frozen", False): - app_dir = os.path.dirname(sys.executable) - # if sys.platform.startswith('win'): - # os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = app_dir - # if sys.platform.startswith("linux"): - # # os.environ['QT_XKB_CONFIG_ROOT'] = '.' - # os.environ["FONTCONFIG_FILE"] = os.path.join( - # app_dir, "source/pyzo/resources", "fonts/linux_fonts.conf" - # ) - -# Automatically scale along on HDPI displays. See issue #531 and e.g. -# https://wiki.archlinux.org/index.php/HiDPI#Qt_5 -if "QT_AUTO_SCREEN_SCALE_FACTOR" not in os.environ: - os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" - -# Fix Qt now showing a window on MacOS Big Sur -os.environ["QT_MAC_WANTS_LAYER"] = "1" - -# Import yoton as an absolute package -from pyzo import yotonloader # noqa -from pyzo.util import paths - -# If there already is an instance of Pyzo, and the user is trying an -# Pyzo command, we should send the command to the other process and quit. -# We do this here, were we have not yet loaded Qt, so we are very light. -from pyzo.core import commandline - -if commandline.is_our_server_running(): - print("Started our command server") -else: - # Handle command line args now - res = commandline.handle_cmd_args() - if res: - print(res) - sys.exit() - else: - # No args, proceed with starting up - print("Our command server is *not* running") - - -from pyzo.util import zon as ssdf # zon is ssdf-light -from pyzo.util.qt import QtCore, QtGui, QtWidgets - -# Enable high-res displays -try: - ctypes.windll.shcore.SetProcessDpiAwareness(1) - ctypes.windll.shcore.SetProcessDpiAwareness(2) -except Exception: - pass # fail on non-windows -try: - QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) - QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) -except Exception: - pass # fail on older Qt's - -# Import language/translation tools -from pyzo.util._locale import translate, setLanguage # noqa - -# Set environ to let kernel know some stats about us -os.environ["PYZO_PREFIX"] = sys.prefix -_is_pyqt4 = hasattr(QtCore, "PYQT_VERSION_STR") -os.environ["PYZO_QTLIB"] = "PyQt4" if _is_pyqt4 else "PySide" - - -class MyApp(QtWidgets.QApplication): - """So we an open .py files on OSX. - OSX is smart enough to call this on the existing process. - """ - - def event(self, event): - if isinstance(event, QtGui.QFileOpenEvent): - fname = str(event.file()) - if fname and fname != "pyzo": - sys.argv[1:] = [] - sys.argv.append(fname) - res = commandline.handle_cmd_args() - if not commandline.is_our_server_running(): - print(res) - sys.exit() - return QtWidgets.QApplication.event(self, event) - - -if not sys.platform.startswith("darwin"): - MyApp = QtWidgets.QApplication # noqa - - -## Install excepthook -# In PyQt5 exceptions in Python will cuase an abort -# http://pyqt.sourceforge.net/Docs/PyQt5/incompatibilities.html - - -def pyzo_excepthook(type, value, tb): - out = "Uncaught Python exception: " + str(value) + "\n" - out += "".join(traceback.format_list(traceback.extract_tb(tb))) - out += "\n" - sys.stderr.write(out) - - -sys.excepthook = pyzo_excepthook - - -## Define some functions - - -# todo: move some stuff out of this module ... - - -def getResourceDirs(): - """getResourceDirs() - Get the directories to the resources: (pyzoDir, appDataDir, appConfigDir). - Also makes sure that the appDataDir has a "tools" directory and - a style file. - """ - - # # Get root of the Pyzo code. If frozen its in a subdir of the app dir - # pyzoDir = paths.application_dir() - # if paths.is_frozen(): - # pyzoDir = os.path.join(pyzoDir, 'source') - pyzoDir = os.path.abspath(os.path.dirname(__file__)) - if ".zip" in pyzoDir: - raise RuntimeError("The Pyzo package cannot be run from a zipfile.") - - # Get where the application data is stored (use old behavior on Mac) - appDataDir, appConfigDir = paths.appdata_dir("pyzo", roaming=True, macAsLinux=True) - - # Create tooldir if necessary - toolDir = os.path.join(appDataDir, "tools") - os.makedirs(toolDir, exist_ok=True) - - return pyzoDir, appDataDir, appConfigDir - - -def resetConfig(preserveState=True): - """resetConfig() - Deletes the config file to revert to default and prevent Pyzo from storing - its config on the next shutdown. - """ - # Get filenames - configFileName2 = os.path.join(appConfigDir, "config.ssdf") - os.remove(configFileName2) - global _saveConfigFile - _saveConfigFile = False - print("Deleted user config file. Restart Pyzo to revert to the default config.") - - -def loadThemes(): - """ - Load default and user themes (if exist) - """ - - def loadThemesFromDir(dname, isBuiltin=False): - if not os.path.isdir(dname): - return - for fname in [fname for fname in os.listdir(dname) if fname.endswith(".theme")]: - try: - theme = ssdf.load(os.path.join(dname, fname)) - assert ( - theme.name.lower() == fname.lower().split(".")[0] - ), "Theme name does not match filename" - theme.data = { - key.replace("_", "."): val for key, val in theme.data.items() - } - theme["builtin"] = isBuiltin - themes[theme.name.lower()] = theme - print("Loaded theme %r" % theme.name) - except Exception as ex: - print("Warning ! Error while reading %s: %s" % (fname, ex)) - - loadThemesFromDir(os.path.join(pyzoDir, "resources", "themes"), True) - loadThemesFromDir(os.path.join(appDataDir, "themes")) - - -def loadConfig(defaultsOnly=False): - """loadConfig(defaultsOnly=False) - Load default and site-wide configuration file(s) and that of the user (if it exists). - Any missing fields in the user config are set to the defaults. - """ - - # Function to insert names from one config in another - def replaceFields(base, new): - for key in new: - if key in base and isinstance(base[key], ssdf.Struct): - replaceFields(base[key], new[key]) - else: - base[key] = new[key] - - # Reset our pyzo.config structure - ssdf.clear(config) - - # Load default and inject in the pyzo.config - fname = os.path.join(pyzoDir, "resources", "defaultConfig.ssdf") - defaultConfig = ssdf.load(fname) - replaceFields(config, defaultConfig) - - # Platform specific keybinding: on Mac, Ctrl+Tab (actually Cmd+Tab) is a system shortcut - if sys.platform == "darwin": - config.shortcuts2.view__select_previous_file = "Alt+Tab," - - # Load site-wide config if it exists and inject in pyzo.config - fname = os.path.join(pyzoDir, "resources", "siteConfig.ssdf") - if os.path.isfile(fname): - try: - siteConfig = ssdf.load(fname) - replaceFields(config, siteConfig) - except Exception: - t = "Error while reading config file %r, maybe its corrupt?" - print(t % fname) - raise - - # Load user config and inject in pyzo.config - fname = os.path.join(appConfigDir, "config.ssdf") - if os.path.isfile(fname): - try: - userConfig = ssdf.load(fname) - replaceFields(config, userConfig) - except Exception: - t = "Error while reading config file %r, maybe its corrupt?" - print(t % fname) - raise - - -def saveConfig(): - """saveConfig() - Save all configureations to file. - """ - - # Let the editorStack save its state - if editors: - editors.saveEditorState() - - # Let the main window save its state - if main: - main.saveWindowState() - - # Store config - if _saveConfigFile: - ssdf.save(os.path.join(appConfigDir, "config.ssdf"), config) +if sys.version_info < (3, 6): + raise RuntimeError("Pyzo requires Python 3.6+ to run.") def start(): - """Run Pyzo.""" - - # Do some imports - from pyzo.core import pyzoLogging # noqa - to start logging asap - from pyzo.core.main import MainWindow - - # Apply users' preferences w.r.t. date representation etc - # this is required for e.g. strftime("%c") - # Just using '' does not seem to work on OSX. Thus - # this odd loop. - # locale.setlocale(locale.LC_ALL, "") - for x in ("", "C", "en_US", "en_US.utf8", "en_US.UTF-8"): - try: - locale.setlocale(locale.LC_ALL, x) - break - except Exception: - pass - - # Set to be aware of the systems native colors, fonts, etc. - QtWidgets.QApplication.setDesktopSettingsAware(True) - - # Instantiate the application - QtWidgets.qApp = MyApp(sys.argv) # QtWidgets.QApplication([]) - - # Choose language, get locale - appLocale = setLanguage(config.settings.language) - - # Create main window, using the selected locale - MainWindow(None, appLocale) - - # Enter the main loop - QtWidgets.qApp.exec_() - - -## Init - -# List of names that are later overriden (in main.py) -editors = None # The editor stack instance -shells = None # The shell stack instance -main = None # The mainwindow -icon = None # The icon -parser = None # The source parser -status = None # The statusbar (or None) - -# Get directories of interest -pyzoDir, appDataDir, appConfigDir = getResourceDirs() - -# Whether the config file should be saved -_saveConfigFile = True - -# Create ssdf in module namespace, and fill it -config = ssdf.new() -loadConfig() - -try: - # uses the fact that float("") raises ValueError to be NOP when qtscalefactor setting is not set - os.environ["QT_SCREEN_SCALE_FACTORS"] = str(float(config.settings.qtscalefactor)) -except Exception: - pass + """Start Pyzo.""" + from ._start import start -# Create style dict and fill it -themes = {} -loadThemes() -# Init default style name (set in main.restorePyzoState()) -defaultQtStyleName = "" + start() diff --git a/pyzo/__main__.py b/pyzo/__main__.py index 4fc7b98e..8e19fe73 100755 --- a/pyzo/__main__.py +++ b/pyzo/__main__.py @@ -1,63 +1,25 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright (C) 2016, the Pyzo development team -# -# Pyzo is distributed under the terms of the 2-Clause BSD License. -# The full license can be found in 'license.txt'. - -""" Pyzo __main__ module +""" +Pyzo __main__ module -This module takes enables starting Pyzo via either "python3 -m pyzo" or +This module enables starting Pyzo via either "python3 -m pyzo" or "python3 path/to/pyzo". - -In the first case it simply imports pyzo. In the latter case, that import -will generally fail, in which case the parent directory is added to sys.path -and the import is tried again. Then "pyzo.start()" is called. - """ import os import sys -class SourceImporter: - def __init__(self, dir): - self.module_names = {"pyzo", "yoton"} - for name in os.listdir(dir): - self.module_names.add(name) - - def find_spec(self, fullname, path, target=None): - if fullname.split(".")[0] in self.module_names: - return sys.meta_path[-1].find_spec(fullname, path, target) - else: - return None - - -if getattr(sys, "frozen", False): - # Allow importing from the source dir, and install spec finder to overload - # PyInstaller's finder when appropriate. - source_dir = os.path.join(sys._MEIPASS, "source") - sys.path.insert(0, source_dir) - sys.meta_path.insert(0, SourceImporter(source_dir)) - # Import - import pyzo +# Very probably run as a script, either the package or the __main__ +# directly. Add parent directory to sys.path and try again. +this_dir = os.path.abspath(os.path.dirname(__file__)) +sys.path.insert(0, os.path.split(this_dir)[0]) -else: - # Try importing - try: - import pyzo - except ImportError: - # Very probably run as a script, either the package or the __main__ - # directly. Add parent directory to sys.path and try again. - thisDir = os.path.abspath(os.path.dirname(__file__)) - sys.path.insert(0, os.path.split(thisDir)[0]) - try: - import pyzo - except ImportError: - raise ImportError("Could not import Pyzo in either way.") +import pyzo def main(): + # Must have a function main here, as the entry-point to this module pyzo.start() diff --git a/pyzo/_start.py b/pyzo/_start.py new file mode 100644 index 00000000..c9f88344 --- /dev/null +++ b/pyzo/_start.py @@ -0,0 +1,325 @@ +import os +import sys +import ctypes +import locale +import traceback + +import pyzo + +# Import this module that applies some tweaks that need to be applied +# before import qt. This is a separate module, so that the frozen app +# can import before checking the qt import. +from . import pre_qt_import # noqa: F401 + +# Import yoton as an absolute package +from pyzo import yotonloader # noqa +from pyzo.util import paths + +# If there already is an instance of Pyzo, and the user is trying an +# Pyzo command, we should send the command to the other process and quit. +# We do this here, were we have not yet loaded Qt, so we are very light. +from pyzo.core import commandline + +if commandline.is_our_server_running(): + print("Started our command server") +else: + # Handle command line args now + res = commandline.handle_cmd_args() + if res: + print(res) + sys.exit() + else: + # No args, proceed with starting up + print("Our command server is *not* running") + + +from pyzo.util import zon as ssdf # zon is ssdf-light +from pyzo.qt import QtCore, QtGui, QtWidgets + +# Enable high-res displays +try: + ctypes.windll.shcore.SetProcessDpiAwareness(1) + ctypes.windll.shcore.SetProcessDpiAwareness(2) +except Exception: + pass # fail on non-windows +try: + QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) + QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) +except Exception: + pass # fail on older Qt's + +# Import language/translation tools +from pyzo.util._locale import translate, setLanguage # noqa + +pyzo.translate = translate +pyzo.setLanguage = setLanguage + +# Set environ to let kernel know some stats about us +os.environ["PYZO_PREFIX"] = sys.prefix +_is_pyqt4 = hasattr(QtCore, "PYQT_VERSION_STR") +os.environ["PYZO_QTLIB"] = "PyQt4" if _is_pyqt4 else "PySide" + + +class MyApp(QtWidgets.QApplication): + """So we an open .py files on OSX. + OSX is smart enough to call this on the existing process. + """ + + def event(self, event): + if isinstance(event, QtGui.QFileOpenEvent): + fname = str(event.file()) + if fname and fname != "pyzo": + sys.argv[1:] = [] + sys.argv.append(fname) + res = commandline.handle_cmd_args() + if not commandline.is_our_server_running(): + print(res) + sys.exit() + return QtWidgets.QApplication.event(self, event) + + +if not sys.platform.startswith("darwin"): + MyApp = QtWidgets.QApplication # noqa + + +## Install excepthook +# In PyQt5 exceptions in Python will cause an abort +# http://pyqt.sourceforge.net/Docs/PyQt5/incompatibilities.html + + +def pyzo_excepthook(type, value, tb): + out = "Uncaught Python exception: " + str(value) + "\n" + out += "".join(traceback.format_list(traceback.extract_tb(tb))) + out += "\n" + sys.stderr.write(out) + + +sys.excepthook = pyzo_excepthook + + +## Define some functions + + +# todo: move some stuff out of this module ... + + +def getResourceDirs(): + """getResourceDirs() + Get the directories to the resources: (pyzoDir, appDataDir, appConfigDir). + Also makes sure that the appDataDir has a "tools" directory and + a style file. + """ + + pyzoDir = os.path.abspath(os.path.dirname(__file__)) + if ".zip" in pyzoDir: + raise RuntimeError("The Pyzo package cannot be run from a zipfile.") + + # Get where the application data is stored (use old behavior on Mac) + appDataDir, appConfigDir = paths.appdata_dir("pyzo", roaming=True, macAsLinux=True) + + # Create tooldir if necessary + toolDir = os.path.join(appDataDir, "tools") + os.makedirs(toolDir, exist_ok=True) + + return pyzoDir, appDataDir, appConfigDir + + +def resetConfig(preserveState=True): + """resetConfig() + Deletes the config file to revert to default and prevent Pyzo from storing + its config on the next shutdown. + """ + # Get filenames + configFileName2 = os.path.join(pyzo.appConfigDir, "config.ssdf") + os.remove(configFileName2) + pyzo._saveConfigFile = False + print("Deleted user config file. Restart Pyzo to revert to the default config.") + + +def loadThemes(): + """ + Load default and user themes (if exist) + """ + + def loadThemesFromDir(dname, isBuiltin=False): + if not os.path.isdir(dname): + return + for fname in [fname for fname in os.listdir(dname) if fname.endswith(".theme")]: + try: + theme = ssdf.load(os.path.join(dname, fname)) + assert ( + theme.name.lower() == fname.lower().split(".")[0] + ), "Theme name does not match filename" + theme.data = { + key.replace("_", "."): val for key, val in theme.data.items() + } + theme["builtin"] = isBuiltin + pyzo.themes[theme.name.lower()] = theme + print("Loaded theme %r" % theme.name) + except Exception as ex: + print("Warning ! Error while reading %s: %s" % (fname, ex)) + + loadThemesFromDir(os.path.join(pyzo.pyzoDir, "resources", "themes"), True) + loadThemesFromDir(os.path.join(pyzo.appDataDir, "themes")) + + +def loadConfig(defaultsOnly=False): + """loadConfig(defaultsOnly=False) + Load default and site-wide configuration file(s) and that of the user (if it exists). + Any missing fields in the user config are set to the defaults. + """ + + # Function to insert names from one config in another + def replaceFields(base, new): + for key in new: + if key in base and isinstance(base[key], ssdf.Struct): + replaceFields(base[key], new[key]) + else: + base[key] = new[key] + + config = pyzo.config + + # Reset our pyzo.config structure + ssdf.clear(config) + + # Load default and inject in the pyzo.config + fname = os.path.join(pyzo.pyzoDir, "resources", "defaultConfig.ssdf") + defaultConfig = ssdf.load(fname) + replaceFields(config, defaultConfig) + + # Platform specific keybinding: on Mac, Ctrl+Tab (actually Cmd+Tab) is a system shortcut + if sys.platform == "darwin": + config.shortcuts2.view__select_previous_file = "Alt+Tab," + + # Load site-wide config if it exists and inject in pyzo.config + fname = os.path.join(pyzo.pyzoDir, "resources", "siteConfig.ssdf") + if os.path.isfile(fname): + try: + siteConfig = ssdf.load(fname) + replaceFields(config, siteConfig) + except Exception: + t = "Error while reading config file %r, maybe its corrupt?" + print(t % fname) + raise + + # Load user config and inject in pyzo.config + fname = os.path.join(pyzo.appConfigDir, "config.ssdf") + if os.path.isfile(fname): + try: + userConfig = ssdf.load(fname) + replaceFields(config, userConfig) + except Exception: + t = "Error while reading config file %r, maybe its corrupt?" + print(t % fname) + raise + + +def saveConfig(): + """saveConfig() + Save all configureations to file. + """ + + # Let the editorStack save its state + if pyzo.editors: + pyzo.editors.saveEditorState() + + # Let the main window save its state + if pyzo.main: + pyzo.main.saveWindowState() + + # Store config + if pyzo._saveConfigFile: + ssdf.save(os.path.join(pyzo.appConfigDir, "config.ssdf"), pyzo.config) + + +pyzo.getResourceDirs = getResourceDirs +pyzo.resetConfig = resetConfig +pyzo.loadThemes = loadThemes +pyzo.saveConfig = saveConfig + + +def start(): + """Run Pyzo.""" + + # Do some imports + import pyzo + from pyzo.core import pyzoLogging # noqa - to start logging asap + from pyzo.core.main import MainWindow + + # Apply users' preferences w.r.t. date representation etc + # this is required for e.g. strftime("%c") + # Just using '' does not seem to work on OSX. Thus + # this odd loop. + # locale.setlocale(locale.LC_ALL, "") + for x in ("", "C", "en_US", "en_US.utf8", "en_US.UTF-8"): + try: + locale.setlocale(locale.LC_ALL, x) + break + except Exception: + pass + + # # Set to be aware of the systems native colors, fonts, etc. + # QtWidgets.QApplication.setDesktopSettingsAware(True) + + # Instantiate the application + QtWidgets.qApp = MyApp(sys.argv) # QtWidgets.QApplication([]) + + # Choose language, get locale + appLocale = setLanguage(pyzo.config.settings.language) + + # Create main window, using the selected locale + MainWindow(None, appLocale) + + # In test mode, we close after 5 seconds + # We also write "Closed" to the log (if a filename is provided) which we use + # in our tests to determine that Pyzo did a successful run. + if "--test" in sys.argv: + close_signal = lambda: print("Stopped") + if os.getenv("PYZO_LOG", ""): + close_signal = lambda: open(os.getenv("PYZO_LOG"), "at").write("Stopped") + pyzo.test_close_timer = t = QtCore.QTimer() + t.setInterval(5000) + t.setSingleShot(True) + t.timeout.connect(lambda: [close_signal(), pyzo.main.close()]) + t.start() + + # Enter the main loop + if hasattr(QtWidgets.qApp, "exec"): + QtWidgets.qApp.exec() + else: + QtWidgets.qApp.exec_() + + +## Init + +# List of names that are later overriden (in main.py) +pyzo.editors = None # The editor stack instance +pyzo.shells = None # The shell stack instance +pyzo.main = None # The mainwindow +pyzo.icon = None # The icon +pyzo.parser = None # The source parser +pyzo.status = None # The statusbar (or None) + +# Get directories of interest +pyzo.pyzoDir, pyzo.appDataDir, pyzo.appConfigDir = getResourceDirs() + +# Whether the config file should be saved +pyzo._saveConfigFile = True + +# Create ssdf in module namespace, and fill it +pyzo.config = ssdf.new() +loadConfig() + +try: + # uses the fact that float("") raises ValueError to be NOP when qtscalefactor setting is not set + os.environ["QT_SCREEN_SCALE_FACTORS"] = str( + float(pyzo.config.settings.qtscalefactor) + ) +except Exception: + pass + +# Create style dict and fill it +pyzo.themes = {} +loadThemes() +# Init default style name (set in main.restorePyzoState()) +pyzo.defaultQtStyleName = "" diff --git a/pyzo/codeeditor/qt.py b/pyzo/codeeditor/qt.py index d59119e8..f806cd27 100644 --- a/pyzo/codeeditor/qt.py +++ b/pyzo/codeeditor/qt.py @@ -1,2 +1,2 @@ # This is the one place where codeeditor depends on Pyzo itself -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa diff --git a/pyzo/core/about.py b/pyzo/core/about.py index cd24f0d7..1b8ac259 100644 --- a/pyzo/core/about.py +++ b/pyzo/core/about.py @@ -1,8 +1,8 @@ import os import sys -from pyzo.util.qt import QtCore, QtGui, QtWidgets -from pyzo.util import qt +from pyzo.qt import QtCore, QtGui, QtWidgets +from pyzo import qt import pyzo from pyzo.util import paths diff --git a/pyzo/core/assistant.py b/pyzo/core/assistant.py index bd1fad03..b4382c38 100644 --- a/pyzo/core/assistant.py +++ b/pyzo/core/assistant.py @@ -15,7 +15,7 @@ """ -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa from pyzo import getResourceDirs import os @@ -135,7 +135,7 @@ def __init__(self, parent=None, collection_filename=None): When collection_file is none, it is determined from the appDataDir. """ - from pyzo.util.qt import QtHelp + from pyzo.qt import QtHelp super().__init__(parent) self.setWindowTitle("Help") @@ -264,7 +264,7 @@ def onSearchFinish(self, hits): self._helpBrowser.setSource(QtCore.QUrl(url)) def showHelpForTerm(self, name): - from pyzo.util.qt import QtHelp + from pyzo.qt import QtHelp # Cache for later use: self._search_term = name diff --git a/pyzo/core/baseTextCtrl.py b/pyzo/core/baseTextCtrl.py index fe5301e2..dea890aa 100644 --- a/pyzo/core/baseTextCtrl.py +++ b/pyzo/core/baseTextCtrl.py @@ -17,7 +17,7 @@ from pyzo.core.pyzoLogging import print import pyzo.codeeditor.parsers.tokens as Tokens -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets qt = QtGui diff --git a/pyzo/core/commandline.py b/pyzo/core/commandline.py index c42090cc..f10a5fd0 100644 --- a/pyzo/core/commandline.py +++ b/pyzo/core/commandline.py @@ -108,7 +108,7 @@ def handle_cmd_args(): otherwise. """ args = sys.argv[1:] - request = " ".join(args) + request = " ".join(arg for arg in args if not arg.startswith("--")) if "psn_" in request and not os.path.isfile(request): request = " ".join(args[1:]) # An OSX thing when clicking app icon request = request.strip() diff --git a/pyzo/core/compactTabWidget.py b/pyzo/core/compactTabWidget.py index a18c6c1e..e6f79dd7 100644 --- a/pyzo/core/compactTabWidget.py +++ b/pyzo/core/compactTabWidget.py @@ -10,7 +10,7 @@ """ -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa import sys if sys.version_info[0] < 3: diff --git a/pyzo/core/editor.py b/pyzo/core/editor.py index c9235bc3..91a70a99 100644 --- a/pyzo/core/editor.py +++ b/pyzo/core/editor.py @@ -15,7 +15,7 @@ import os, sys import re, codecs -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets qt = QtGui diff --git a/pyzo/core/editorTabs.py b/pyzo/core/editorTabs.py index b46a9600..1873562e 100644 --- a/pyzo/core/editorTabs.py +++ b/pyzo/core/editorTabs.py @@ -15,7 +15,7 @@ """ import os, time, gc -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets import pyzo from pyzo.core.compactTabWidget import CompactTabWidget diff --git a/pyzo/core/history.py b/pyzo/core/history.py index 2d44f82d..e054e119 100644 --- a/pyzo/core/history.py +++ b/pyzo/core/history.py @@ -2,7 +2,7 @@ import datetime import pyzo -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa class CommandHistory(QtCore.QObject): diff --git a/pyzo/core/icons.py b/pyzo/core/icons.py index ce7f31e0..653032f7 100644 --- a/pyzo/core/icons.py +++ b/pyzo/core/icons.py @@ -12,7 +12,7 @@ """ -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets import pyzo diff --git a/pyzo/core/main.py b/pyzo/core/main.py index 17b5f263..f5483b1b 100644 --- a/pyzo/core/main.py +++ b/pyzo/core/main.py @@ -20,8 +20,8 @@ from pyzo.core.icons import IconArtist from pyzo.core import commandline from pyzo.core.statusbar import StatusBar -from pyzo.util import qt -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo import qt +from pyzo.qt import QtCore, QtGui, QtWidgets from pyzo.core.splash import SplashWidget from pyzo.util import paths from pyzo.util import zon as ssdf # zon is ssdf-light @@ -71,8 +71,8 @@ def __init__(self, parent=None, locale=None): # Init dockwidget settings self.setTabPosition(QtCore.Qt.AllDockWidgetAreas, QtWidgets.QTabWidget.South) self.setDockOptions( - QtWidgets.QMainWindow.AllowNestedDocks - | QtWidgets.QMainWindow.AllowTabbedDocks + QtWidgets.QMainWindow.AllowTabbedDocks + | QtWidgets.QMainWindow.AllowNestedDocks # | QtWidgets.QMainWindow.AnimatedDocks ) @@ -524,7 +524,7 @@ def loadFonts(): fontDir = os.path.join(pyzo.pyzoDir, "resources", "fonts") # Get database object - db = QtGui.QFontDatabase() + db = QtGui.QFontDatabase # static class # Set default font pyzo.codeeditor.Manager.setDefaultFontFamily("DejaVu Sans Mono") diff --git a/pyzo/core/menu.py b/pyzo/core/menu.py index 5fb9016f..ccf1fe09 100644 --- a/pyzo/core/menu.py +++ b/pyzo/core/menu.py @@ -19,7 +19,7 @@ import ast import json -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets import pyzo from pyzo.core.compactTabWidget import CompactTabWidget diff --git a/pyzo/core/pdfExport.py b/pyzo/core/pdfExport.py index 28d43804..0da061f8 100644 --- a/pyzo/core/pdfExport.py +++ b/pyzo/core/pdfExport.py @@ -1,4 +1,4 @@ -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets from pyzo import translate import pyzo import os @@ -15,7 +15,7 @@ class PdfExport(QtWidgets.QDialog): def __init__(self): super().__init__() - from pyzo.util.qt import QtPrintSupport + from pyzo.qt import QtPrintSupport self.printer = QtPrintSupport.QPrinter( QtPrintSupport.QPrinter.HighResolution, diff --git a/pyzo/core/shell.py b/pyzo/core/shell.py index ef358010..46744771 100644 --- a/pyzo/core/shell.py +++ b/pyzo/core/shell.py @@ -24,7 +24,7 @@ import pyzo from pyzo.util import zon as ssdf # zon is ssdf-light -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets Qt = QtCore.Qt diff --git a/pyzo/core/shellInfoDialog.py b/pyzo/core/shellInfoDialog.py index 56c44a05..dc1ac862 100644 --- a/pyzo/core/shellInfoDialog.py +++ b/pyzo/core/shellInfoDialog.py @@ -12,7 +12,7 @@ """ import os, sys -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa import pyzo from pyzo.core.pyzoLogging import print diff --git a/pyzo/core/shellStack.py b/pyzo/core/shellStack.py index e0a123ce..0dcfbfef 100644 --- a/pyzo/core/shellStack.py +++ b/pyzo/core/shellStack.py @@ -14,7 +14,7 @@ import time import webbrowser -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa import pyzo from pyzo import translate diff --git a/pyzo/core/splash.py b/pyzo/core/splash.py index e6dcfe8d..9e82aaed 100644 --- a/pyzo/core/splash.py +++ b/pyzo/core/splash.py @@ -13,7 +13,7 @@ import os import pyzo -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa from pyzo import translate STYLESHEET = """ diff --git a/pyzo/core/statusbar.py b/pyzo/core/statusbar.py index 37d34617..5b05822c 100644 --- a/pyzo/core/statusbar.py +++ b/pyzo/core/statusbar.py @@ -3,7 +3,7 @@ Functionality for status bar in pyzo. """ -from pyzo.util.qt import QtWidgets +from pyzo.qt import QtWidgets class StatusBar(QtWidgets.QStatusBar): diff --git a/pyzo/core/themeEdit.py b/pyzo/core/themeEdit.py index b5cdb188..2624a7d9 100644 --- a/pyzo/core/themeEdit.py +++ b/pyzo/core/themeEdit.py @@ -1,6 +1,6 @@ import os -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets import pyzo from pyzo.util import zon as ssdf diff --git a/pyzo/pre_qt_import.py b/pyzo/pre_qt_import.py new file mode 100644 index 00000000..a0a42461 --- /dev/null +++ b/pyzo/pre_qt_import.py @@ -0,0 +1,10 @@ +import os + + +# Automatically scale along on HDPI displays. See issue #531 and e.g. +# https://wiki.archlinux.org/index.php/HiDPI#Qt_5 +if "QT_AUTO_SCREEN_SCALE_FACTOR" not in os.environ: + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" + +# Fix Qt now showing a window on MacOS Big Sur +os.environ["QT_MAC_WANTS_LAYER"] = "1" diff --git a/pyzo/util/qt/QtCore.py b/pyzo/qt/QtCore.py similarity index 100% rename from pyzo/util/qt/QtCore.py rename to pyzo/qt/QtCore.py diff --git a/pyzo/util/qt/QtGui.py b/pyzo/qt/QtGui.py similarity index 100% rename from pyzo/util/qt/QtGui.py rename to pyzo/qt/QtGui.py diff --git a/pyzo/util/qt/QtHelp.py b/pyzo/qt/QtHelp.py similarity index 100% rename from pyzo/util/qt/QtHelp.py rename to pyzo/qt/QtHelp.py diff --git a/pyzo/util/qt/QtPrintSupport.py b/pyzo/qt/QtPrintSupport.py similarity index 100% rename from pyzo/util/qt/QtPrintSupport.py rename to pyzo/qt/QtPrintSupport.py diff --git a/pyzo/util/qt/QtWidgets.py b/pyzo/qt/QtWidgets.py similarity index 95% rename from pyzo/util/qt/QtWidgets.py rename to pyzo/qt/QtWidgets.py index 4ae634fd..6e7c5546 100644 --- a/pyzo/util/qt/QtWidgets.py +++ b/pyzo/qt/QtWidgets.py @@ -14,7 +14,7 @@ if PYQT6: from PyQt6 import QtWidgets from PyQt6.QtWidgets import * - from PyQt6.QtGui import QAction, QActionGroup, QShortcut + from PyQt6.QtGui import QAction, QActionGroup, QShortcut, QFileSystemModel from PyQt6.QtOpenGLWidgets import QOpenGLWidget # Map missing/renamed methods diff --git a/pyzo/util/qt/__init__.py b/pyzo/qt/__init__.py similarity index 100% rename from pyzo/util/qt/__init__.py rename to pyzo/qt/__init__.py diff --git a/pyzo/util/qt/_patch/__init__.py b/pyzo/qt/_patch/__init__.py similarity index 100% rename from pyzo/util/qt/_patch/__init__.py rename to pyzo/qt/_patch/__init__.py diff --git a/pyzo/util/qt/_patch/qheaderview.py b/pyzo/qt/_patch/qheaderview.py similarity index 100% rename from pyzo/util/qt/_patch/qheaderview.py rename to pyzo/qt/_patch/qheaderview.py diff --git a/pyzo/util/qt/compat.py b/pyzo/qt/compat.py similarity index 100% rename from pyzo/util/qt/compat.py rename to pyzo/qt/compat.py diff --git a/pyzo/util/qt/enums_compat.py b/pyzo/qt/enums_compat.py similarity index 100% rename from pyzo/util/qt/enums_compat.py rename to pyzo/qt/enums_compat.py diff --git a/pyzo/util/qt/sip.py b/pyzo/qt/sip.py similarity index 100% rename from pyzo/util/qt/sip.py rename to pyzo/qt/sip.py diff --git a/pyzo/util/qt/uic.py b/pyzo/qt/uic.py similarity index 100% rename from pyzo/util/qt/uic.py rename to pyzo/qt/uic.py diff --git a/pyzo/tools/__init__.py b/pyzo/tools/__init__.py index 344cb1d8..bf02f17b 100644 --- a/pyzo/tools/__init__.py +++ b/pyzo/tools/__init__.py @@ -36,7 +36,7 @@ import os, sys, imp import pyzo -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa from pyzo.util import zon as ssdf from pyzo import translate # noqa (we have an eval down here) diff --git a/pyzo/tools/pyzoFileBrowser/__init__.py b/pyzo/tools/pyzoFileBrowser/__init__.py index b5d75671..0967a981 100644 --- a/pyzo/tools/pyzoFileBrowser/__init__.py +++ b/pyzo/tools/pyzoFileBrowser/__init__.py @@ -44,7 +44,7 @@ import pyzo from pyzo.util import zon as ssdf -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa from .browser import Browser from .utils import cleanpath, isdir diff --git a/pyzo/tools/pyzoHistoryViewer.py b/pyzo/tools/pyzoHistoryViewer.py index b8a709c5..f51e622b 100644 --- a/pyzo/tools/pyzoHistoryViewer.py +++ b/pyzo/tools/pyzoHistoryViewer.py @@ -13,7 +13,7 @@ """ import pyzo -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa from pyzo import translate from pyzo.core.menu import Menu diff --git a/pyzo/tools/pyzoInteractiveHelp.py b/pyzo/tools/pyzoInteractiveHelp.py index a63c8b1b..e263f7a7 100644 --- a/pyzo/tools/pyzoInteractiveHelp.py +++ b/pyzo/tools/pyzoInteractiveHelp.py @@ -8,7 +8,7 @@ import sys, re from functools import partial -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa import pyzo tool_name = pyzo.translate("pyzoInteractiveHelp", "Interactive help") diff --git a/pyzo/tools/pyzoLogger.py b/pyzo/tools/pyzoLogger.py index 804a37a9..6f383a22 100644 --- a/pyzo/tools/pyzoLogger.py +++ b/pyzo/tools/pyzoLogger.py @@ -7,7 +7,7 @@ import sys, os, code import pyzo -from pyzo.util.qt import QtCore, QtGui, QtWidgets # noqa +from pyzo.qt import QtCore, QtGui, QtWidgets # noqa from pyzo.core.shell import BaseShell from pyzo.core.pyzoLogging import splitConsole diff --git a/pyzo/tools/pyzoSourceStructure.py b/pyzo/tools/pyzoSourceStructure.py index 33f35cbd..5824e9c5 100644 --- a/pyzo/tools/pyzoSourceStructure.py +++ b/pyzo/tools/pyzoSourceStructure.py @@ -5,7 +5,7 @@ # The full license can be found in 'license.txt'. import pyzo -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets from pyzo import translate tool_name = translate("pyzoSourceStructure", "Source structure") diff --git a/pyzo/tools/pyzoWebBrowser.py b/pyzo/tools/pyzoWebBrowser.py index f5aeaaea..28c565b3 100644 --- a/pyzo/tools/pyzoWebBrowser.py +++ b/pyzo/tools/pyzoWebBrowser.py @@ -7,11 +7,11 @@ import urllib.request, urllib.parse -from pyzo.util.qt import QtCore, QtWidgets +from pyzo.qt import QtCore, QtWidgets imported_qtwebkit = True try: - from pyzo.util.qt import QtWebKit + from pyzo.qt import QtWebKit except ImportError: imported_qtwebkit = False diff --git a/pyzo/tools/pyzoWorkspace.py b/pyzo/tools/pyzoWorkspace.py index afa0b3f4..70430b98 100644 --- a/pyzo/tools/pyzoWorkspace.py +++ b/pyzo/tools/pyzoWorkspace.py @@ -6,7 +6,7 @@ import pyzo -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets tool_name = pyzo.translate("pyzoWorkspace", "Workspace") tool_summary = pyzo.translate( diff --git a/pyzo/util/_locale.py b/pyzo/util/_locale.py index 3da9ee0e..66b4219d 100644 --- a/pyzo/util/_locale.py +++ b/pyzo/util/_locale.py @@ -11,7 +11,7 @@ import os, sys, time import pyzo -from pyzo.util.qt import QtCore, QtWidgets +from pyzo.qt import QtCore, QtWidgets QLocale = QtCore.QLocale diff --git a/pyzo/util/bootstrapconda.py b/pyzo/util/bootstrapconda.py index 471ca61e..aefe8fb0 100644 --- a/pyzo/util/bootstrapconda.py +++ b/pyzo/util/bootstrapconda.py @@ -14,7 +14,7 @@ import urllib.request import pyzo -from pyzo.util.qt import QtCore, QtWidgets +from pyzo.qt import QtCore, QtWidgets from pyzo import translate base_url = "http://repo.continuum.io/miniconda/" diff --git a/pyzo/util/paths.py b/pyzo/util/paths.py index c72a64d8..c268ca60 100644 --- a/pyzo/util/paths.py +++ b/pyzo/util/paths.py @@ -20,7 +20,7 @@ # * See docstring: that's why the functions tend to not re-use each-other import sys -from pyzo.util.qt import QtCore +from pyzo.qt import QtCore ISWIN = sys.platform.startswith("win") ISMAC = sys.platform.startswith("darwin") diff --git a/pyzo/util/pyzowizard.py b/pyzo/util/pyzowizard.py index 23b51d2f..f65eb05e 100644 --- a/pyzo/util/pyzowizard.py +++ b/pyzo/util/pyzowizard.py @@ -14,7 +14,7 @@ import re import pyzo -from pyzo.util.qt import QtCore, QtGui, QtWidgets +from pyzo.qt import QtCore, QtGui, QtWidgets from pyzo import translate from pyzo.util._locale import LANGUAGES, LANGUAGE_SYNONYMS, setLanguage diff --git a/setup.cfg b/setup.cfg index 8667450d..330bedc3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,8 +5,8 @@ max_line_length = 88 -exclude: test_*.py,docs/*,build/*,dist/*,frozen/*,_feedstock/*,pyzo/util/qt/, - pyzo/util/paths.py,pyzo/resources/tutorial.py +exclude: test_*.py,docs/*,build/*,dist/*,frozen/*,freeze/dist*,_feedstock/*,pyzo/util/qt/, + pyzo/qt/,pyzo/util/paths.py,pyzo/resources/tutorial.py extend-ignore = F821, E203, E501, E231, E401, E402, E262, E265, E266, E302, E731, W293, W605, D, N, B diff --git a/tests/test_api.py b/tests/test_api.py index f9b62625..a875c4b5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,5 +1,54 @@ +import sys import pyzo +import subprocess def test_api(): assert pyzo.__version__ + + +qt_libs = ["PySide", "PySide2", "PySide6", "PyQt4", "PyQt5", "PyQt6"] + +code1 = """ +import sys +import pyzo +print(list(sys.modules.keys())) +""" + + +def test_import1(): + x = subprocess.check_output([sys.executable, "-c", code1]) + modules = eval(x.decode()) + assert isinstance(modules, list) + assert "sys" in modules + assert "pyzo" in modules + + assert "pyzo.core" not in modules + assert not any(qt_lib in modules for qt_lib in qt_libs) + + assert "pyzo.qt" not in modules + + +code2 = """ +import sys +import pyzo +import pyzo.qt +print(list(sys.modules.keys())) +""" + + +def test_import2(): + x = subprocess.check_output([sys.executable, "-c", code2]) + modules = eval(x.decode()) + assert isinstance(modules, list) + assert "sys" in modules + assert "pyzo" in modules + + assert "pyzo.qt" in modules + assert any(qt_lib in modules for qt_lib in qt_libs) + + assert "pyzo.core" not in modules + + +test_import1() +test_import2()