Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix jupyterlab downgrade issue on extension installation #15650

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
187 changes: 97 additions & 90 deletions jupyterlab/extensions/pypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import math
import re
import sys
import tempfile
import xmlrpc.client
from datetime import datetime, timedelta, timezone
from functools import partial
Expand All @@ -27,6 +28,7 @@
from async_lru import alru_cache
from traitlets import CFloat, CInt, Unicode, config, observe

from jupyterlab._version import __version__
from jupyterlab.extensions.manager import (
ActionResult,
ExtensionManager,
Expand Down Expand Up @@ -330,103 +332,108 @@ async def install(self, name: str, version: Optional[str] = None) -> ActionResul
The action result
"""
current_loop = tornado.ioloop.IOLoop.current()
with tempfile.NamedTemporaryFile(mode="w+", delete=True) as fconstraint:
fconstraint.write(f"jupyterlab=={__version__}")
fconstraint.flush()

cmdline = [
sys.executable,
"-m",
"pip",
"install",
"--no-input",
"--quiet",
"--progress-bar",
"off",
"--constraint",
fconstraint.name,
]
if version is not None:
cmdline.append(f"{name}=={version}")
else:
cmdline.append(name)

cmdline = [
sys.executable,
"-m",
"pip",
"install",
"--no-input",
"--quiet",
"--progress-bar",
"off",
]
if version is not None:
cmdline.append(f"{name}=={version}")
else:
cmdline.append(name)

pkg_action = {}
try:
tmp_cmd = cmdline.copy()
tmp_cmd.insert(-1, "--dry-run")
tmp_cmd.insert(-1, "--report")
tmp_cmd.insert(-1, "-")
result = await current_loop.run_in_executor(
None, partial(run, tmp_cmd, capture_output=True, check=True)
)

action_info = json.loads(result.stdout.decode("utf-8"))
pkg_action = next(
filter(
lambda p: p.get("metadata", {}).get("name") == name.replace("_", "-"),
action_info.get("install", []),
pkg_action = {}
try:
tmp_cmd = cmdline.copy()
tmp_cmd.insert(-1, "--dry-run")
tmp_cmd.insert(-1, "--report")
tmp_cmd.insert(-1, "-")
result = await current_loop.run_in_executor(
None, partial(run, tmp_cmd, capture_output=True, check=True)
)
)
except CalledProcessError as e:
self.log.debug(f"Fail to get installation report: {e.stderr}", exc_info=e)
except Exception as err:
self.log.debug("Fail to get installation report.", exc_info=err)
else:
self.log.debug(f"Actions to be executed by pip {json.dumps(action_info)}.")

self.log.debug(f"Executing '{' '.join(cmdline)}'")
action_info = json.loads(result.stdout.decode("utf-8"))
pkg_action = next(
filter(
lambda p: p.get("metadata", {}).get("name") == name.replace("_", "-"),
action_info.get("install", []),
)
)
except CalledProcessError as e:
self.log.debug(f"Fail to get installation report: {e.stderr}", exc_info=e)
except Exception as err:
self.log.debug("Fail to get installation report.", exc_info=err)
else:
self.log.debug(f"Actions to be executed by pip {json.dumps(action_info)}.")

result = await current_loop.run_in_executor(
None, partial(run, cmdline, capture_output=True)
)
self.log.debug(f"Executing '{' '.join(cmdline)}'")

self.log.debug(f"return code: {result.returncode}")
self.log.debug(f"stdout: {result.stdout.decode('utf-8')}")
error = result.stderr.decode("utf-8")
if result.returncode == 0:
self.log.debug(f"stderr: {error}")
# Figure out if the package has server or kernel parts
jlab_metadata = None
try:
download_url: str = pkg_action.get("download_info", {}).get("url")
if download_url is not None:
response = await self._httpx_client.get(download_url)
if response.status_code < 400: # noqa PLR2004
if download_url.endswith(".whl"):
with ZipFile(io.BytesIO(response.content)) as wheel:
for name in filter(
lambda f: Path(f).name == "package.json",
wheel.namelist(),
):
data = json.loads(wheel.read(name))
jlab_metadata = data.get("jupyterlab")
if jlab_metadata is not None:
break
elif download_url.endswith("tar.gz"):
with TarFile(io.BytesIO(response.content)) as sdist:
for name in filter(
lambda f: Path(f).name == "package.json",
sdist.getnames(),
):
data = json.load(sdist.extractfile(sdist.getmember(name)))
jlab_metadata = data.get("jupyterlab")
if jlab_metadata is not None:
break
else:
self.log.debug(f"Failed to get '{download_url}'; {response!s}")
except Exception as e:
self.log.debug("Fail to get package.json.", exc_info=e)
result = await current_loop.run_in_executor(
None, partial(run, cmdline, capture_output=True)
)

follow_ups = [
"frontend",
]
if jlab_metadata is not None:
discovery = jlab_metadata.get("discovery", {})
if "kernel" in discovery:
follow_ups.append("kernel")
if "server" in discovery:
follow_ups.append("server")
self.log.debug(f"return code: {result.returncode}")
self.log.debug(f"stdout: {result.stdout.decode('utf-8')}")
error = result.stderr.decode("utf-8")
if result.returncode == 0:
self.log.debug(f"stderr: {error}")
# Figure out if the package has server or kernel parts
jlab_metadata = None
try:
download_url: str = pkg_action.get("download_info", {}).get("url")
if download_url is not None:
response = await self._httpx_client.get(download_url)
if response.status_code < 400: # noqa PLR2004
if download_url.endswith(".whl"):
with ZipFile(io.BytesIO(response.content)) as wheel:
for name in filter(
lambda f: Path(f).name == "package.json",
wheel.namelist(),
):
data = json.loads(wheel.read(name))
jlab_metadata = data.get("jupyterlab")
if jlab_metadata is not None:
break
elif download_url.endswith("tar.gz"):
with TarFile(io.BytesIO(response.content)) as sdist:
for name in filter(
lambda f: Path(f).name == "package.json",
sdist.getnames(),
):
data = json.load(sdist.extractfile(sdist.getmember(name)))
jlab_metadata = data.get("jupyterlab")
if jlab_metadata is not None:
break
else:
self.log.debug(f"Failed to get '{download_url}'; {response!s}")
except Exception as e:
self.log.debug("Fail to get package.json.", exc_info=e)

follow_ups = [
"frontend",
]
if jlab_metadata is not None:
discovery = jlab_metadata.get("discovery", {})
if "kernel" in discovery:
follow_ups.append("kernel")
if "server" in discovery:
follow_ups.append("server")

return ActionResult(status="ok", needs_restart=follow_ups)
else:
self.log.error(f"Failed to installed {name}: code {result.returncode}\n{error}")
return ActionResult(status="error", message=error)
return ActionResult(status="ok", needs_restart=follow_ups)
else:
self.log.error(f"Failed to installed {name}: code {result.returncode}\n{error}")
return ActionResult(status="error", message=error)

async def uninstall(self, extension: str) -> ActionResult:
"""Uninstall the required extension.
Expand Down