PageRenderTime 55ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/Orange/canvas/application/addons.py

https://gitlab.com/zaverichintan/orange3
Python | 793 lines | 757 code | 32 blank | 4 comment | 29 complexity | 0b732d4e933766b796a759d90d31409b MD5 | raw file
  1. import sys
  2. import sysconfig
  3. import os
  4. import re
  5. import errno
  6. import shlex
  7. import shutil
  8. import subprocess
  9. import itertools
  10. import concurrent.futures
  11. from site import USER_SITE
  12. from glob import iglob
  13. from collections import namedtuple, deque
  14. from xml.sax.saxutils import escape
  15. from distutils import version
  16. import pkg_resources
  17. try:
  18. import docutils.core
  19. except ImportError:
  20. docutils = None
  21. from PyQt4.QtGui import (
  22. QWidget, QDialog, QLabel, QLineEdit, QTreeView, QHeaderView,
  23. QTextBrowser, QTextOption, QDialogButtonBox, QProgressDialog,
  24. QVBoxLayout, QPalette, QStandardItemModel, QStandardItem,
  25. QSortFilterProxyModel, QItemSelectionModel, QStyle, QStyledItemDelegate,
  26. QStyleOptionViewItemV4, QApplication, QHBoxLayout
  27. )
  28. from PyQt4.QtCore import (
  29. Qt, QObject, QMetaObject, QEvent, QSize, QTimer, QThread, Q_ARG
  30. )
  31. from PyQt4.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
  32. from ..gui.utils import message_warning, message_information, \
  33. message_critical as message_error
  34. from ..help.manager import get_dist_meta, trim
  35. OFFICIAL_ADDONS = [
  36. "Orange-Bioinformatics",
  37. "Orange3-DataFusion",
  38. "Orange3-Prototypes",
  39. "Orange3-Text",
  40. "Orange3-Network",
  41. "Orange3-Associate",
  42. ]
  43. Installable = namedtuple(
  44. "Installable",
  45. ["name",
  46. "version",
  47. "summary",
  48. "description",
  49. "package_url",
  50. "release_urls"]
  51. )
  52. ReleaseUrl = namedtuple(
  53. "ReleaseUrl",
  54. ["filename",
  55. "url",
  56. "size",
  57. "python_version",
  58. "package_type"
  59. ]
  60. )
  61. Available = namedtuple(
  62. "Available",
  63. ["installable"]
  64. )
  65. Installed = namedtuple(
  66. "Installed",
  67. ["installable",
  68. "local"]
  69. )
  70. def is_updatable(item):
  71. if isinstance(item, Available):
  72. return False
  73. elif item.installable is None:
  74. return False
  75. else:
  76. inst, dist = item
  77. try:
  78. v1 = version.StrictVersion(dist.version)
  79. v2 = version.StrictVersion(inst.version)
  80. except ValueError:
  81. pass
  82. else:
  83. return v1 < v2
  84. return (version.LooseVersion(dist.version) <
  85. version.LooseVersion(inst.version))
  86. class TristateCheckItemDelegate(QStyledItemDelegate):
  87. """
  88. A QStyledItemDelegate which properly toggles Qt.ItemIsTristate check
  89. state transitions on user interaction.
  90. """
  91. def editorEvent(self, event, model, option, index):
  92. flags = model.flags(index)
  93. if not flags & Qt.ItemIsUserCheckable or \
  94. not option.state & QStyle.State_Enabled or \
  95. not flags & Qt.ItemIsEnabled:
  96. return False
  97. checkstate = model.data(index, Qt.CheckStateRole)
  98. if checkstate is None:
  99. return False
  100. widget = option.widget
  101. style = widget.style() if widget else QApplication.style()
  102. if event.type() in {QEvent.MouseButtonPress, QEvent.MouseButtonRelease,
  103. QEvent.MouseButtonDblClick}:
  104. pos = event.pos()
  105. opt = QStyleOptionViewItemV4(option)
  106. self.initStyleOption(opt, index)
  107. rect = style.subElementRect(
  108. QStyle.SE_ItemViewItemCheckIndicator, opt, widget)
  109. if event.button() != Qt.LeftButton or not rect.contains(pos):
  110. return False
  111. if event.type() in {QEvent.MouseButtonPress,
  112. QEvent.MouseButtonDblClick}:
  113. return True
  114. elif event.type() == QEvent.KeyPress:
  115. if event.key() != Qt.Key_Space and event.key() != Qt.Key_Select:
  116. return False
  117. else:
  118. return False
  119. if model.flags(index) & Qt.ItemIsTristate:
  120. checkstate = (checkstate + 1) % 3
  121. else:
  122. checkstate = \
  123. Qt.Unchecked if checkstate == Qt.Checked else Qt.Checked
  124. return model.setData(index, checkstate, Qt.CheckStateRole)
  125. class AddonManagerWidget(QWidget):
  126. statechanged = Signal()
  127. def __init__(self, parent=None, **kwargs):
  128. super(AddonManagerWidget, self).__init__(parent, **kwargs)
  129. self.setLayout(QVBoxLayout())
  130. self.__header = QLabel(
  131. wordWrap=True,
  132. textFormat=Qt.RichText
  133. )
  134. self.__search = QLineEdit(
  135. placeholderText=self.tr("Filter")
  136. )
  137. self.layout().addWidget(self.__search)
  138. self.__view = view = QTreeView(
  139. rootIsDecorated=False,
  140. editTriggers=QTreeView.NoEditTriggers,
  141. selectionMode=QTreeView.SingleSelection,
  142. alternatingRowColors=True
  143. )
  144. self.__view.setItemDelegateForColumn(0, TristateCheckItemDelegate())
  145. self.layout().addWidget(view)
  146. self.__model = model = QStandardItemModel()
  147. model.setHorizontalHeaderLabels(["", "Name", "Version", "Action"])
  148. model.dataChanged.connect(self.__data_changed)
  149. proxy = QSortFilterProxyModel(
  150. filterKeyColumn=1,
  151. filterCaseSensitivity=Qt.CaseInsensitive
  152. )
  153. proxy.setSourceModel(model)
  154. self.__search.textChanged.connect(proxy.setFilterFixedString)
  155. view.setModel(proxy)
  156. view.selectionModel().selectionChanged.connect(
  157. self.__update_details
  158. )
  159. header = self.__view.header()
  160. header.setResizeMode(0, QHeaderView.Fixed)
  161. header.setResizeMode(2, QHeaderView.ResizeToContents)
  162. self.__details = QTextBrowser(
  163. frameShape=QTextBrowser.NoFrame,
  164. readOnly=True,
  165. lineWrapMode=QTextBrowser.WidgetWidth,
  166. openExternalLinks=True,
  167. )
  168. self.__details.setWordWrapMode(QTextOption.WordWrap)
  169. palette = QPalette(self.palette())
  170. palette.setColor(QPalette.Base, Qt.transparent)
  171. self.__details.setPalette(palette)
  172. self.layout().addWidget(self.__details)
  173. def set_items(self, items):
  174. self.__items = items
  175. model = self.__model
  176. model.clear()
  177. model.setHorizontalHeaderLabels(["", "Name", "Version", "Action"])
  178. for item in items:
  179. if isinstance(item, Installed):
  180. installed = True
  181. ins, dist = item
  182. name = dist.project_name
  183. summary = get_dist_meta(dist).get("Summary", "")
  184. version = ins.version if ins is not None else dist.version
  185. else:
  186. installed = False
  187. (ins,) = item
  188. dist = None
  189. name = ins.name
  190. summary = ins.summary
  191. version = ins.version
  192. updatable = is_updatable(item)
  193. item1 = QStandardItem()
  194. item1.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable |
  195. Qt.ItemIsUserCheckable |
  196. (Qt.ItemIsTristate if updatable else 0))
  197. if installed and updatable:
  198. item1.setCheckState(Qt.PartiallyChecked)
  199. elif installed:
  200. item1.setCheckState(Qt.Checked)
  201. else:
  202. item1.setCheckState(Qt.Unchecked)
  203. item2 = QStandardItem(name)
  204. item2.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
  205. item2.setToolTip(summary)
  206. item2.setData(item, Qt.UserRole)
  207. if updatable:
  208. version = "{} < {}".format(dist.version, ins.version)
  209. item3 = QStandardItem(version)
  210. item3.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
  211. item4 = QStandardItem()
  212. item4.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
  213. model.appendRow([item1, item2, item3, item4])
  214. self.__view.resizeColumnToContents(0)
  215. self.__view.setColumnWidth(
  216. 1, max(150, self.__view.sizeHintForColumn(1)))
  217. self.__view.setColumnWidth(
  218. 2, max(150, self.__view.sizeHintForColumn(2)))
  219. if self.__items:
  220. self.__view.selectionModel().select(
  221. self.__view.model().index(0, 0),
  222. QItemSelectionModel.Select | QItemSelectionModel.Rows
  223. )
  224. def item_state(self):
  225. steps = []
  226. for i, item in enumerate(self.__items):
  227. modelitem = self.__model.item(i, 0)
  228. state = modelitem.checkState()
  229. if modelitem.flags() & Qt.ItemIsTristate and state == Qt.Checked:
  230. steps.append((Upgrade, item))
  231. elif isinstance(item, Available) and state == Qt.Checked:
  232. steps.append((Install, item))
  233. elif isinstance(item, Installed) and state == Qt.Unchecked:
  234. steps.append((Uninstall, item))
  235. return steps
  236. def __selected_row(self):
  237. indices = self.__view.selectedIndexes()
  238. if indices:
  239. proxy = self.__view.model()
  240. indices = [proxy.mapToSource(index) for index in indices]
  241. return indices[0].row()
  242. else:
  243. return -1
  244. def __data_changed(self, topleft, bottomright):
  245. rows = range(topleft.row(), bottomright.row() + 1)
  246. proxy = self.__view.model()
  247. map_to_source = proxy.mapToSource
  248. for i in rows:
  249. sourceind = map_to_source(proxy.index(i, 0))
  250. modelitem = self.__model.itemFromIndex(sourceind)
  251. actionitem = self.__model.item(modelitem.row(), 3)
  252. item = self.__items[modelitem.row()]
  253. state = modelitem.checkState()
  254. flags = modelitem.flags()
  255. if flags & Qt.ItemIsTristate and state == Qt.Checked:
  256. actionitem.setText("Update")
  257. elif isinstance(item, Available) and state == Qt.Checked:
  258. actionitem.setText("Install")
  259. elif isinstance(item, Installed) and state == Qt.Unchecked:
  260. actionitem.setText("Uninstall")
  261. else:
  262. actionitem.setText("")
  263. self.statechanged.emit()
  264. def __update_details(self):
  265. index = self.__selected_row()
  266. if index == -1:
  267. self.__details.setText("")
  268. else:
  269. item = self.__model.item(index, 1)
  270. item = item.data(Qt.UserRole)
  271. assert isinstance(item, (Installed, Available))
  272. # if isinstance(item, Available):
  273. # self.__installed_label.setText("")
  274. # self.__available_label.setText(str(item.available.version))
  275. # elif item.installable is not None:
  276. # self.__installed_label.setText(str(item.local.version))
  277. # self.__available_label.setText(str(item.available.version))
  278. # else:
  279. # self.__installed_label.setText(str(item.local.version))
  280. # self.__available_label.setText("")
  281. text = self._detailed_text(item)
  282. self.__details.setText(text)
  283. def _detailed_text(self, item):
  284. if isinstance(item, Installed):
  285. remote, dist = item
  286. if remote is None:
  287. description = get_dist_meta(dist).get("Description")
  288. description = description
  289. else:
  290. description = remote.description
  291. else:
  292. description = item[0].description
  293. if docutils is not None:
  294. try:
  295. html = docutils.core.publish_string(
  296. trim(description),
  297. writer_name="html",
  298. settings_overrides={
  299. "output-encoding": "utf-8",
  300. # "embed-stylesheet": False,
  301. # "stylesheet": [],
  302. # "stylesheet_path": []
  303. }
  304. ).decode("utf-8")
  305. except docutils.utils.SystemMessage:
  306. html = "<pre>{}<pre>".format(escape(description))
  307. except Exception:
  308. html = "<pre>{}<pre>".format(escape(description))
  309. else:
  310. html = "<pre>{}<pre>".format(escape(description))
  311. return html
  312. def sizeHint(self):
  313. return QSize(480, 420)
  314. def method_queued(method, sig, conntype=Qt.QueuedConnection):
  315. name = method.__name__
  316. obj = method.__self__
  317. assert isinstance(obj, QObject)
  318. def call(*args):
  319. args = [Q_ARG(atype, arg) for atype, arg in zip(sig, args)]
  320. return QMetaObject.invokeMethod(obj, name, conntype, *args)
  321. return call
  322. class AddonManagerDialog(QDialog):
  323. _packages = None
  324. def __init__(self, parent=None, **kwargs):
  325. super().__init__(parent, **kwargs)
  326. self.setLayout(QVBoxLayout())
  327. self.layout().setContentsMargins(0, 0, 0, 0)
  328. self.addonwidget = AddonManagerWidget()
  329. self.layout().addWidget(self.addonwidget)
  330. info_bar = QWidget()
  331. info_layout = QHBoxLayout()
  332. info_bar.setLayout(info_layout)
  333. self.layout().addWidget(info_bar)
  334. buttons = QDialogButtonBox(
  335. orientation=Qt.Horizontal,
  336. standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel
  337. )
  338. buttons.accepted.connect(self.__accepted)
  339. buttons.rejected.connect(self.reject)
  340. self.layout().addWidget(buttons)
  341. # No system access => install into user site-packages
  342. self.user_install = not os.access(sysconfig.get_path("purelib"),
  343. os.W_OK)
  344. self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
  345. if AddonManagerDialog._packages is None:
  346. self._f_pypi_addons = self._executor.submit(list_pypi_addons)
  347. else:
  348. self._f_pypi_addons = concurrent.futures.Future()
  349. self._f_pypi_addons.set_result(AddonManagerDialog._packages)
  350. self._f_pypi_addons.add_done_callback(
  351. method_queued(self._set_packages, (object,))
  352. )
  353. self.__progress = QProgressDialog(
  354. self, Qt.Sheet,
  355. minimum=0, maximum=0,
  356. labelText=self.tr("Retrieving package list"),
  357. sizeGripEnabled=False,
  358. windowTitle="Progress"
  359. )
  360. self.__progress.rejected.connect(self.reject)
  361. self.__thread = None
  362. self.__installer = None
  363. @Slot(object)
  364. def _set_packages(self, f):
  365. if self.__progress.isVisible():
  366. self.__progress.close()
  367. try:
  368. packages = f.result()
  369. except (IOError, OSError) as err:
  370. message_warning(
  371. "Could not retrieve package list",
  372. title="Error",
  373. informative_text=str(err),
  374. parent=self
  375. )
  376. packages = []
  377. except Exception:
  378. raise
  379. else:
  380. AddonManagerDialog._packages = packages
  381. installed = list_installed_addons()
  382. dists = {dist.project_name: dist for dist in installed}
  383. packages = {pkg.name: pkg for pkg in packages}
  384. # For every pypi available distribution not listed by
  385. # list_installed_addons, check if it is actually already
  386. # installed.
  387. ws = pkg_resources.WorkingSet()
  388. for pkg_name in set(packages.keys()).difference(set(dists.keys())):
  389. try:
  390. d = ws.find(pkg_resources.Requirement.parse(pkg_name))
  391. except pkg_resources.VersionConflict:
  392. pass
  393. except ValueError:
  394. # Requirements.parse error ?
  395. pass
  396. else:
  397. if d is not None:
  398. dists[d.project_name] = d
  399. project_names = unique(
  400. itertools.chain(packages.keys(), dists.keys())
  401. )
  402. items = []
  403. for name in project_names:
  404. if name in dists and name in packages:
  405. item = Installed(packages[name], dists[name])
  406. elif name in dists:
  407. item = Installed(None, dists[name])
  408. elif name in packages:
  409. item = Available(packages[name])
  410. else:
  411. assert False
  412. items.append(item)
  413. self.addonwidget.set_items(items)
  414. def showEvent(self, event):
  415. super().showEvent(event)
  416. if not self._f_pypi_addons.done():
  417. QTimer.singleShot(0, self.__progress.show)
  418. def done(self, retcode):
  419. super().done(retcode)
  420. self._f_pypi_addons.cancel()
  421. self._executor.shutdown(wait=False)
  422. if self.__thread is not None:
  423. self.__thread.quit()
  424. self.__thread.wait(1000)
  425. def closeEvent(self, event):
  426. super().closeEvent(event)
  427. self._f_pypi_addons.cancel()
  428. self._executor.shutdown(wait=False)
  429. if self.__thread is not None:
  430. self.__thread.quit()
  431. self.__thread.wait(1000)
  432. def __accepted(self):
  433. steps = self.addonwidget.item_state()
  434. if steps:
  435. # Move all uninstall steps to the front
  436. steps = sorted(
  437. steps, key=lambda step: 0 if step[0] == Uninstall else 1
  438. )
  439. self.__installer = Installer(steps=steps,
  440. user_install=self.user_install)
  441. self.__thread = QThread(self)
  442. self.__thread.start()
  443. self.__installer.moveToThread(self.__thread)
  444. self.__installer.finished.connect(self.__on_installer_finished)
  445. self.__installer.error.connect(self.__on_installer_error)
  446. self.__installer.installStatusChanged.connect(
  447. self.__progress.setLabelText)
  448. self.__progress.show()
  449. self.__progress.setLabelText("Installing")
  450. self.__installer.start()
  451. else:
  452. self.accept()
  453. def __on_installer_error(self, command, pkg, retcode, output):
  454. message_error(
  455. "An error occurred while running a subprocess", title="Error",
  456. informative_text="{} exited with non zero status.".format(command),
  457. details="".join(output),
  458. parent=self
  459. )
  460. self.reject()
  461. def __on_installer_finished(self):
  462. message = (
  463. ("Changes successfully applied in <i>{}</i>.<br>".format(
  464. USER_SITE) if self.user_install else '') +
  465. "Please restart Orange for changes to take effect.")
  466. message_information(message, parent=self)
  467. self.accept()
  468. def list_pypi_addons():
  469. """
  470. List add-ons available on pypi.
  471. """
  472. from ..config import ADDON_PYPI_SEARCH_SPEC
  473. import xmlrpc.client
  474. pypi = xmlrpc.client.ServerProxy("http://pypi.python.org/pypi")
  475. addons = pypi.search(ADDON_PYPI_SEARCH_SPEC)
  476. for addon in OFFICIAL_ADDONS:
  477. if not any(a for a in addons if a['name'] == addon):
  478. versions = pypi.package_releases(addon)
  479. if versions:
  480. addons.append({"name": addon, "version": max(versions)})
  481. multicall = xmlrpc.client.MultiCall(pypi)
  482. for addon in addons:
  483. name, version = addon["name"], addon["version"]
  484. multicall.release_data(name, version)
  485. multicall.release_urls(name, version)
  486. results = list(multicall())
  487. release_data = results[::2]
  488. release_urls = results[1::2]
  489. packages = []
  490. for release, urls in zip(release_data, release_urls):
  491. if release and urls:
  492. # ignore releases without actual source/wheel/egg files,
  493. # or with empty metadata (deleted from PyPi?).
  494. urls = [ReleaseUrl(url["filename"], url["url"],
  495. url["size"], url["python_version"],
  496. url["packagetype"])
  497. for url in urls]
  498. packages.append(
  499. Installable(release["name"], release["version"],
  500. release["summary"], release["description"],
  501. release["package_url"],
  502. urls)
  503. )
  504. return packages
  505. def list_installed_addons():
  506. from ..config import ADDON_ENTRY
  507. workingset = pkg_resources.WorkingSet(sys.path)
  508. return [ep.dist for ep in
  509. workingset.iter_entry_points(ADDON_ENTRY)]
  510. def unique(iterable):
  511. seen = set()
  512. def observed(el):
  513. observed = el in seen
  514. seen.add(el)
  515. return observed
  516. return (el for el in iterable if not observed(el))
  517. Install, Upgrade, Uninstall = 1, 2, 3
  518. class Installer(QObject):
  519. installStatusChanged = Signal(str)
  520. started = Signal()
  521. finished = Signal()
  522. error = Signal(str, object, int, list)
  523. def __init__(self, parent=None, steps=[], user_install=False):
  524. QObject.__init__(self, parent)
  525. self.__interupt = False
  526. self.__queue = deque(steps)
  527. self.__user_install = user_install
  528. def start(self):
  529. QTimer.singleShot(0, self._next)
  530. def interupt(self):
  531. self.__interupt = True
  532. def setStatusMessage(self, message):
  533. self.__statusMessage = message
  534. self.installStatusChanged.emit(message)
  535. @Slot()
  536. def _next(self):
  537. def fmt_cmd(cmd):
  538. return "Command failed: python " + " ".join(map(shlex.quote, cmd))
  539. command, pkg = self.__queue.popleft()
  540. if command == Install:
  541. inst = pkg.installable
  542. self.setStatusMessage("Installing {}".format(inst.name))
  543. cmd = (["-m", "pip", "install"] +
  544. (["--user"] if self.__user_install else []) +
  545. [inst.name])
  546. process = python_process(cmd, bufsize=-1, universal_newlines=True)
  547. retcode, output = self.__subprocessrun(process)
  548. if retcode != 0:
  549. self.error.emit(fmt_cmd(cmd), pkg, retcode, output)
  550. return
  551. elif command == Upgrade:
  552. inst = pkg.installable
  553. self.setStatusMessage("Upgrading {}".format(inst.name))
  554. cmd = (["-m", "pip", "install", "--upgrade", "--no-deps"] +
  555. (["--user"] if self.__user_install else []) +
  556. [inst.name])
  557. process = python_process(cmd, bufsize=-1, universal_newlines=True)
  558. retcode, output = self.__subprocessrun(process)
  559. if retcode != 0:
  560. self.error.emit(fmt_cmd(cmd), pkg, retcode, output)
  561. return
  562. # Why is this here twice??
  563. cmd = (["-m", "pip", "install"] +
  564. (["--user"] if self.__user_install else []) +
  565. [inst.name])
  566. process = python_process(cmd, bufsize=-1, universal_newlines=True)
  567. retcode, output = self.__subprocessrun(process)
  568. if retcode != 0:
  569. self.error.emit(fmt_cmd(cmd), pkg, retcode, output)
  570. return
  571. elif command == Uninstall:
  572. dist = pkg.local
  573. self.setStatusMessage("Uninstalling {}".format(dist.project_name))
  574. cmd = ["-m", "pip", "uninstall", "--yes", dist.project_name]
  575. process = python_process(cmd, bufsize=-1, universal_newlines=True)
  576. retcode, output = self.__subprocessrun(process)
  577. if self.__user_install:
  578. # Remove the package forcefully; pip doesn't (yet) uninstall
  579. # --user packages (or any package outside sys.prefix?)
  580. # google: pip "Not uninstalling ?" "outside environment"
  581. install_path = os.path.join(
  582. USER_SITE, re.sub('[^\w]', '_', dist.project_name))
  583. pip_record = next(iglob(install_path + '*.dist-info/RECORD'),
  584. None)
  585. if pip_record:
  586. with open(pip_record) as f:
  587. files = [line.rsplit(',', 2)[0] for line in f]
  588. else:
  589. files = [os.path.join(
  590. USER_SITE, 'orangecontrib',
  591. dist.project_name.split('-')[-1].lower()),]
  592. for match in itertools.chain(files, iglob(install_path + '*')):
  593. print('rm -rf', match)
  594. if os.path.isdir(match):
  595. shutil.rmtree(match)
  596. elif os.path.exists(match):
  597. os.unlink(match)
  598. if retcode != 0:
  599. self.error.emit(fmt_cmd(cmd), pkg, retcode, output)
  600. return
  601. if self.__queue:
  602. QTimer.singleShot(0, self._next)
  603. else:
  604. self.finished.emit()
  605. def __subprocessrun(self, process):
  606. output = []
  607. while process.poll() is None:
  608. try:
  609. line = process.stdout.readline()
  610. except IOError as ex:
  611. if ex.errno != errno.EINTR:
  612. raise
  613. else:
  614. output.append(line)
  615. print(line, end="")
  616. # Read remaining output if any
  617. line = process.stdout.read()
  618. if line:
  619. output.append(line)
  620. print(line, end="")
  621. return process.returncode, output
  622. def python_process(args, script_name=None, cwd=None, env=None, **kwargs):
  623. """
  624. Run a `sys.executable` in a subprocess with `args`.
  625. """
  626. executable = sys.executable
  627. if os.name == "nt" and os.path.basename(executable) == "pythonw.exe":
  628. # Don't run the script with a 'gui' (detached) process.
  629. dirname = os.path.dirname(executable)
  630. executable = os.path.join(dirname, "python.exe")
  631. # by default a new console window would show up when executing the
  632. # script
  633. startupinfo = subprocess.STARTUPINFO()
  634. if hasattr(subprocess, "STARTF_USESHOWWINDOW"):
  635. startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  636. else:
  637. # This flag was missing in inital releases of 2.7
  638. startupinfo.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW
  639. kwargs["startupinfo"] = startupinfo
  640. if script_name is not None:
  641. script = script_name
  642. else:
  643. script = executable
  644. process = subprocess.Popen(
  645. [script] + args,
  646. executable=executable,
  647. cwd=cwd,
  648. env=env,
  649. stderr=subprocess.STDOUT,
  650. stdout=subprocess.PIPE,
  651. **kwargs
  652. )
  653. return process