Skip to content

Commit

Permalink
Merge pull request psychopy#6742 from TEParsons/dev-enh-plugin-dlg-ch…
Browse files Browse the repository at this point in the history
…erry

ENH: Make install behaviour consistent between plugins and packages
  • Loading branch information
TEParsons committed Jul 26, 2024
2 parents cecbd49 + 07684e8 commit b908d5b
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 100 deletions.
5 changes: 4 additions & 1 deletion psychopy/app/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,10 @@ class Job:
"""
def __init__(self, parent, command='', terminateCallback=None,
inputCallback=None, errorCallback=None, extra=None):

# use the app instance if parent isn't given
if parent is None:
from psychopy.app import getAppInstance
parent = getAppInstance()
# command to be called, cannot be changed after spawning the process
self.parent = parent
self._command = command
Expand Down
84 changes: 11 additions & 73 deletions psychopy/app/plugin_manager/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,81 +227,19 @@ def installPackage(self, packageName, version=None, extra=None):
Dict of extra variables to be accessed by callback functions, use None
for a blank dict.
"""
# alert if busy
if self.isBusy:
msg = wx.MessageDialog(
self,
("Cannot install package. Wait for the installation already in "
"progress to complete first."),
"Installation Failed", wx.OK | wx.ICON_WARNING
)
msg.ShowModal()
return

# tab to output
self.output.open()

# interpreter path
pyExec = sys.executable
# environment
env = os.environ.copy()
# if given a pyproject.toml file, do editable install of parent folder
if str(packageName).endswith("pyproject.toml"):
if sys.platform != "darwin":
# on systems which allow it, do an editable install
packageName = f'-e "{os.path.dirname(packageName)}"'
else:
# on Mac, build a wheel
subprocess.call(
[pyExec, '-m', 'build'],
cwd=Path(packageName).parent,
env=env
)
# get wheel path
packageName = [
whl for whl in Path(packageName).parent.glob("**/*.whl")][0]

# On MacOS, we need to install to target instead of user since py2app
# doesn't support user installs correctly, this is a workaround for that
env = os.environ.copy()

# build the shell command to run the script
command = [pyExec, '-m', 'pip', 'install', str(packageName)]

# check if we are inside a venv, don't use --user if we are
if hasattr(sys, 'real_prefix') or (
hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
# we are in a venv
logging.warning(
"You are installing a package inside a virtual environment. "
"The package will be installed in the user site-packages directory."
)
else:
command.append('--user')

# add other options to the command
command += ['--prefer-binary', '--no-input', '--no-color']

# write command to output panel
self.output.writeCmd(" ".join(command))
# append own name to extra
if extra is None:
extra = {}
extra.update(
{'pipname': packageName}
)

# create a new job with the user script
self.pipProcess = jobs.Job(
self,
command=command,
# flags=execFlags,
inputCallback=self.output.writeStdOut, # both treated the same
errorCallback=self.output.writeStdErr,
# add version if given
if version is not None:
packageName += f"=={version}"
# use package tools to install
self.pipProcess = pkgtools.installPackage(
packageName,
upgrade=version is None,
forceReinstall=version is not None,
awaited=False,
outputCallback=self.output.writeStdOut,
terminateCallback=self.onInstallExit,
extra=extra
extra=extra,
)
self.pipProcess.start(env=env)

def installPlugin(self, pluginInfo, version=None):
"""Install a package.
Expand Down
100 changes: 74 additions & 26 deletions psychopy/tools/pkgtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,17 @@ def addDistribution(distPath):
sys.path.append(distPath)


def installPackage(package, target=None, upgrade=False, forceReinstall=False,
noDeps=False):
def installPackage(
package,
target=None,
upgrade=False,
forceReinstall=False,
noDeps=False,
awaited=True,
outputCallback=None,
terminateCallback=None,
extra=None,
):
"""Install a package using the default package management system.
This is intended to be used only by PsychoPy itself for installing plugins
Expand All @@ -209,19 +218,35 @@ def installPackage(package, target=None, upgrade=False, forceReinstall=False,
they are present in the current distribution.
noDeps : bool
Don't install dependencies if `True`.
awaited : bool
If False, then use an asynchronous install process - this function will return right away
and the plugin install will happen in a different thread.
outputCallback : function
Function to be called when any output text is received from the process performing the
install. Not used if awaited=True.
terminateCallback : function
Function to be called when installation is finished. Not used if awaited=True.
extra : dict
Extra information to be supplied to the install thread when installing asynchronously.
Not used if awaited=True.
Returns
-------
tuple
`True` if the package installed without errors. If `False`, check
'stderr' for more information. The package may still have installed
correctly, but it doesn't work. Second value contains standard output
and error from the subprocess.
tuple or psychopy.app.jobs.Job
If `awaited=True`:
`True` if the package installed without errors. If `False`, check
'stderr' for more information. The package may still have installed
correctly, but it doesn't work. Second value contains standard output
and error from the subprocess.
If `awaited=False`:
Returns the job (thread) which is running the install.
"""
if target is None:
target = prefs.paths['userPackages']


# convert extra to dict
if extra is None:
extra = {}
# check the directory exists before installing
if not os.path.exists(target):
raise NotADirectoryError(
Expand Down Expand Up @@ -264,25 +289,48 @@ def installPackage(package, target=None, upgrade=False, forceReinstall=False,
# get the environment for the subprocess
env = os.environ.copy()

# run command in subprocess
output = sp.Popen(
cmd,
stdout=sp.PIPE,
stderr=sp.PIPE,
shell=False,
universal_newlines=True,
env=env)
stdout, stderr = output.communicate() # blocks until process exits

sys.stdout.write(stdout)
sys.stderr.write(stderr)
# if unawaited, try to get jobs handler
if not awaited:
try:
from psychopy.app import jobs
except ModuleNotFoundError:
logging.warn(_translate(
"Could not install package {} asynchronously as psychopy.app.jobs is not found. "
"Defaulting to synchronous install."
).format(package))
awaited = True
if awaited:
# if synchronous, just use regular command line
proc = sp.Popen(
cmd,
stdout=sp.PIPE,
stderr=sp.PIPE,
shell=False,
universal_newlines=True,
env=env
)
# run
stdout, stderr = proc.communicate()
# print output
sys.stdout.write(stdout)
sys.stderr.write(stderr)
# refresh packages once done
refreshPackages()

refreshPackages()
return isInstalled(package), {'cmd': cmd, 'stdout': stdout, 'stderr': stderr}
else:
# otherwise, use a job (which can provide live feedback)
proc = jobs.Job(
parent=None,
command=cmd,
inputCallback=outputCallback,
errorCallback=outputCallback,
terminateCallback=terminateCallback,
extra=extra,
)
proc.start(env=env)

# Return True if installed, False if not
retcode = isInstalled(package)
# Return the return code and a dict of information from the console
return retcode, {"cmd": cmd, "stdout": stdout, "stderr": stderr}
return proc


def _getUserPackageTopLevels():
Expand Down

0 comments on commit b908d5b

Please sign in to comment.