Bladeren bron

Integrate include option pypi_wheels, add PyQt5 example

Thomas Kluyver 9 jaren geleden
bovenliggende
commit
f73c9a8bcb

+ 1 - 1
examples/pyqt/installer.cfg

@@ -10,4 +10,4 @@ version=3.3.5
 packages=listapp
     PyQt4
     sip
-files = README
+files = README.md

+ 14 - 0
examples/pyqt5/installer.cfg

@@ -0,0 +1,14 @@
+[Application]
+name=List App (PyQt5)
+version=1.0
+entry_point=listapp:main
+
+[Python]
+version=3.5.1
+bitness=64
+format=bundled
+
+[Include]
+packages=listapp
+pypi_wheels= PyQt5==5.6
+    sip==4.18

+ 33 - 0
examples/pyqt5/listapp/__init__.py

@@ -0,0 +1,33 @@
+import sys
+from PyQt5 import QtWidgets
+
+from .main import Ui_MainWindow
+
+class Main(QtWidgets.QMainWindow):
+    def __init__(self):
+        super().__init__()
+        self.ui = Ui_MainWindow()
+        self.ui.setupUi(self)
+        
+        self.ui.add_button.clicked.connect(self.add_item)
+
+    def get_radio_option(self):
+        if self.ui.radio_1.isChecked():
+            return 'Thing 1'
+        elif self.ui.radio_2.isChecked():
+            return 'Thing 2'
+        elif self.ui.radio_3.isChecked():
+            return 'Thing 3'
+        elif self.ui.radio_4.isChecked():
+            return 'Last thing'
+        return 'No thing'
+        
+    def add_item(self):
+        text = self.get_radio_option()
+        QtWidgets.QListWidgetItem(text, self.ui.listWidget)        
+
+def main():
+    app = QtWidgets.QApplication(sys.argv)
+    window = Main()
+    window.show()
+    sys.exit(app.exec_())

+ 81 - 0
examples/pyqt5/listapp/main.py

@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'main.ui'
+#
+# Created by: PyQt5 UI code generator 5.5.1
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+class Ui_MainWindow(object):
+    def setupUi(self, MainWindow):
+        MainWindow.setObjectName("MainWindow")
+        MainWindow.resize(393, 606)
+        self.centralwidget = QtWidgets.QWidget(MainWindow)
+        self.centralwidget.setObjectName("centralwidget")
+        self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
+        self.horizontalLayout.setObjectName("horizontalLayout")
+        self.groupBox = QtWidgets.QGroupBox(self.centralwidget)
+        self.groupBox.setObjectName("groupBox")
+        self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.radio_1 = QtWidgets.QRadioButton(self.groupBox)
+        self.radio_1.setObjectName("radio_1")
+        self.verticalLayout.addWidget(self.radio_1)
+        self.radio_2 = QtWidgets.QRadioButton(self.groupBox)
+        self.radio_2.setObjectName("radio_2")
+        self.verticalLayout.addWidget(self.radio_2)
+        self.radio_3 = QtWidgets.QRadioButton(self.groupBox)
+        self.radio_3.setObjectName("radio_3")
+        self.verticalLayout.addWidget(self.radio_3)
+        self.radio_4 = QtWidgets.QRadioButton(self.groupBox)
+        self.radio_4.setObjectName("radio_4")
+        self.verticalLayout.addWidget(self.radio_4)
+        self.add_button = QtWidgets.QPushButton(self.groupBox)
+        self.add_button.setObjectName("add_button")
+        self.verticalLayout.addWidget(self.add_button)
+        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+        self.verticalLayout.addItem(spacerItem)
+        self.horizontalLayout.addWidget(self.groupBox)
+        self.listWidget = QtWidgets.QListWidget(self.centralwidget)
+        self.listWidget.setObjectName("listWidget")
+        self.horizontalLayout.addWidget(self.listWidget)
+        MainWindow.setCentralWidget(self.centralwidget)
+        self.menubar = QtWidgets.QMenuBar(MainWindow)
+        self.menubar.setGeometry(QtCore.QRect(0, 0, 393, 24))
+        self.menubar.setObjectName("menubar")
+        self.menuFile = QtWidgets.QMenu(self.menubar)
+        self.menuFile.setObjectName("menuFile")
+        MainWindow.setMenuBar(self.menubar)
+        self.statusbar = QtWidgets.QStatusBar(MainWindow)
+        self.statusbar.setObjectName("statusbar")
+        MainWindow.setStatusBar(self.statusbar)
+        self.actionNew = QtWidgets.QAction(MainWindow)
+        self.actionNew.setObjectName("actionNew")
+        self.actionOpen = QtWidgets.QAction(MainWindow)
+        self.actionOpen.setObjectName("actionOpen")
+        self.actionSave = QtWidgets.QAction(MainWindow)
+        self.actionSave.setObjectName("actionSave")
+        self.menuFile.addAction(self.actionNew)
+        self.menuFile.addAction(self.actionOpen)
+        self.menuFile.addAction(self.actionSave)
+        self.menubar.addAction(self.menuFile.menuAction())
+
+        self.retranslateUi(MainWindow)
+        QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+    def retranslateUi(self, MainWindow):
+        _translate = QtCore.QCoreApplication.translate
+        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
+        self.groupBox.setTitle(_translate("MainWindow", "Click things"))
+        self.radio_1.setText(_translate("MainWindow", "Thing 1"))
+        self.radio_2.setText(_translate("MainWindow", "Thing 2"))
+        self.radio_3.setText(_translate("MainWindow", "Thing 3"))
+        self.radio_4.setText(_translate("MainWindow", "Last thing"))
+        self.add_button.setText(_translate("MainWindow", "Add to list"))
+        self.menuFile.setTitle(_translate("MainWindow", "File"))
+        self.actionNew.setText(_translate("MainWindow", "New"))
+        self.actionOpen.setText(_translate("MainWindow", "Open"))
+        self.actionSave.setText(_translate("MainWindow", "Save"))
+

+ 118 - 0
examples/pyqt5/listapp/main.ui

@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>393</width>
+    <height>606</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>MainWindow</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QHBoxLayout" name="horizontalLayout">
+    <item>
+     <widget class="QGroupBox" name="groupBox">
+      <property name="title">
+       <string>Click things</string>
+      </property>
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <item>
+        <widget class="QRadioButton" name="radio_1">
+         <property name="text">
+          <string>Thing 1</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QRadioButton" name="radio_2">
+         <property name="text">
+          <string>Thing 2</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QRadioButton" name="radio_3">
+         <property name="text">
+          <string>Thing 3</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QRadioButton" name="radio_4">
+         <property name="text">
+          <string>Last thing</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="add_button">
+         <property name="text">
+          <string>Add to list</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </item>
+    <item>
+     <widget class="QListWidget" name="listWidget"/>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>393</width>
+     <height>24</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuFile">
+    <property name="title">
+     <string>File</string>
+    </property>
+    <addaction name="actionNew"/>
+    <addaction name="actionOpen"/>
+    <addaction name="actionSave"/>
+   </widget>
+   <addaction name="menuFile"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+  <action name="actionNew">
+   <property name="text">
+    <string>New</string>
+   </property>
+  </action>
+  <action name="actionOpen">
+   <property name="text">
+    <string>Open</string>
+   </property>
+  </action>
+  <action name="actionSave">
+   <property name="text">
+    <string>Save</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 14 - 1
nsist/__init__.py

@@ -25,6 +25,7 @@ else:
 
 from .copymodules import copy_modules
 from .nsiswriter import NSISFileWriter
+from .pypi import fetch_pypi_wheels
 from .util import download, text_types, get_cache_dir
 
 __version__ = '1.6'
@@ -73,7 +74,9 @@ class InstallerBuilder(object):
             in the config file
     :param str icon: Path to an icon for the application
     :param list packages: List of strings for importable packages to include
+    :param list pypi_reqs: Package specifications to fetch from PyPI as wheels
     :param list extra_files: List of 2-tuples (file, destination) of files to include
+    :param list exclude: Paths of files to exclude that would otherwise be included
     :param str py_version: Full version of Python to bundle
     :param int py_bitness: Bitness of bundled Python (32 or 64)
     :param str build_dir: Directory to run the build in
@@ -85,7 +88,7 @@ class InstallerBuilder(object):
                 py_bitness=DEFAULT_BITNESS, py_format='installer',
                 build_dir=DEFAULT_BUILD_DIR,
                 installer_name=None, nsi_template=None,
-                exclude=None):
+                exclude=None, pypi_wheel_reqs=None):
         self.appname = appname
         self.version = version
         self.shortcuts = shortcuts
@@ -93,6 +96,7 @@ class InstallerBuilder(object):
         self.packages = packages or []
         self.exclude = [os.path.normpath(p) for p in (exclude or [])]
         self.extra_files = extra_files or []
+        self.pypi_wheel_reqs = pypi_wheel_reqs or []
 
         # Python options
         self.py_version = py_version
@@ -321,10 +325,18 @@ if __name__ == '__main__':
         build_pkg_dir = pjoin(self.build_dir, 'pkgs')
         if os.path.isdir(build_pkg_dir):
             shutil.rmtree(build_pkg_dir)
+
+        # 1. Manually prepared packages
         if os.path.isdir('pynsist_pkgs'):
             shutil.copytree('pynsist_pkgs', build_pkg_dir)
         else:
             os.mkdir(build_pkg_dir)
+
+        # 2. Wheels from PyPI
+        fetch_pypi_wheels(self.pypi_wheel_reqs, build_pkg_dir,
+                          py_version=self.py_version, bitness=self.py_bitness)
+
+        # 3. Copy importable modules
         copy_modules(self.packages, build_pkg_dir,
                      py_version=self.py_version, exclude=self.exclude)
 
@@ -479,6 +491,7 @@ def main(argv=None):
             icon = appcfg.get('icon', DEFAULT_ICON),
             shortcuts = shortcuts,
             packages = cfg.get('Include', 'packages', fallback='').splitlines(),
+            pypi_wheel_reqs = cfg.get('Include', 'pypi_wheels', fallback='').splitlines(),
             extra_files = configreader.read_extra_files(cfg),
             py_version = cfg.get('Python', 'version', fallback=DEFAULT_PY_VERSION),
             py_bitness = cfg.getint('Python', 'bitness', fallback=DEFAULT_BITNESS),

+ 1 - 0
nsist/configreader.py

@@ -69,6 +69,7 @@ CONFIG_VALIDATORS = {
     ]),
     'Include': SectionValidator([
         ('packages', False),
+        ('pypi_wheels', False),
         ('files', False),
         ('exclude', False),
     ]),

+ 17 - 2
nsist/pypi.py

@@ -3,6 +3,7 @@ import errno
 import hashlib
 import logging
 import re
+import zipfile
 
 import yarg
 from requests_download import download, HashTracker
@@ -20,7 +21,7 @@ def find_pypi_release(requirement):
 
 class NoWheelError(Exception): pass
 
-class WheelFinder(object):
+class WheelDownloader(object):
     def __init__(self, requirement, py_version, bitness):
         self.requirement = requirement
         self.py_version = py_version
@@ -98,7 +99,7 @@ class WheelFinder(object):
             return None
         return release_dir / rel.filename
 
-    def find(self):
+    def fetch(self):
         p = self.check_cache()
         if p is not None:
             return p
@@ -139,3 +140,17 @@ class CachedRelease(object):
     def __init__(self, filename):
         self.filename = filename
         self.package_type = 'wheel' if filename.endswith('.whl') else ''
+
+def extract_wheel(whl_file, target_dir):
+    with zipfile.ZipFile(str(whl_file), mode='r') as zf:
+        names = zf.namelist()
+        # TODO: Do anything with data and dist-info folders?
+        pkg_files = [n for n in names \
+                     if not n.split('/')[0].endswith(('.data', '.dist-info'))]
+        zf.extractall(target_dir, members=pkg_files)
+
+def fetch_pypi_wheels(requirements, target_dir, py_version, bitness):
+    for req in requirements:
+        wd = WheelDownloader(req, py_version, bitness)
+        whl_file = wd.fetch()
+        extract_wheel(whl_file, target_dir)