Compare commits
4 Commits
9a3e661563
...
210fe834e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
210fe834e8 | ||
|
|
7444564cb6 | ||
|
|
c9c5e3756f | ||
|
|
68c7272752 |
14
README.md
14
README.md
@@ -1,20 +1,20 @@
|
||||
# App Mode Jupyter Environment and Shortcut
|
||||
|
||||
Adds a shortcut for running [JupyterLab](https://jupyter.org/) in a Chromium-based browser's "app" mode (i.e. The application takes up the full browser window and has no browser UI e.g. the address bar, as if Jupyter Lab were a native application) for an existing environment with Jupyter Lab installed or creating one for you.
|
||||
Adds a shortcut for running [JupyterLab](https://jupyter.org/) in a Chromium-based browser's "app" mode (i.e. The application takes up the full browser window and has no browser UI e.g. the address bar, as if Jupyter Lab were a native application) for an existing environment with Jupyter Lab installed or creating one for you.
|
||||
|
||||
## Requirements
|
||||
|
||||
* An conda-based installation of [Anaconda](https://anaconda.org/), [Miniconda](https://docs.conda.io/en/latest/miniconda.html) or [Miniforge](https://github.com/conda-forge/miniforge). Miniforge is recommended.
|
||||
* [`menuinst`](https://conda.github.io/menuinst/) >= 2.0.0 must be installed in the `base` environment alongside `conda` or in whichever environment contains conda
|
||||
* A Chromium-based browser
|
||||
* On Windows, [Google Chrome](https://www.google.com/chrome/), [Brave](https://brave.com/), and [Microsoft Edge](https://www.microsoft.com/en-us/edge) are supported. Preference will be given to the first one of these browsers found. Microsoft Edge is guaranteed to be installed on Windows 10+ and is a fall-back if no other supported browser is found.
|
||||
* On Windows, [Google Chrome](https://www.google.com/chrome/), [Brave](https://brave.com/), and [Microsoft Edge](https://www.microsoft.com/en-us/edge) are supported. Preference will be given to your default browser, if it appears to be a Chromium-based browser, or to the first one of these browsers found. Microsoft Edge is guaranteed to be installed on Windows 10+ and is a fall-back if no other supported browser is found.
|
||||
* On MacOS, [Google Chrome](https://www.google.com/chrome/), [Microsoft Edge](https://www.microsoft.com/en-us/edge), [Brave](https://brave.com/), and [Chromium](https://www.chromium.org/getting-involved/download-chromium/) are preferred in that order.
|
||||
* On Linux, [Google Chrome](https://flathub.org/apps/com.google.Chrome), [Microsoft Edge](https://flathub.org/apps/com.microsoft.Edge), [Brave](https://flathub.org/apps/com.brave.Browser), and [Chromium](https://flathub.org/apps/org.chromium.Chromium) are supported and preferred in that order. If [flatpak](https://flatpak.org/) is installed and one of these browsers is installed with Flatpak (recommended), it will be preferred over any snap-installed or package manager-installed versions of any of these browsers.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone or download this repository
|
||||
2. From a terminal, activate your `base` conda environment (or whichever environment conda is installed in)
|
||||
2. From a terminal, activate your `base` conda environment (or whichever environment conda is installed in)
|
||||
3. Run `python ./setup_jupyter.py your_jupyter_env_name_here`
|
||||
|
||||
## Remove Shortcut
|
||||
@@ -24,12 +24,12 @@ To remove the shortcut created by this script, run `python ./setup_jupyter.py --
|
||||
## FAQ
|
||||
|
||||
1. Why don't you support Mozilla Firefox or Safari?
|
||||
|
||||
|
||||
To my knowledge, neither have an "app-mode" like Chromium-based browsers do. When/if they ever do, they will be supported.
|
||||
|
||||
2. Why am I being prompted for admin permissions?
|
||||
|
||||
By default, `menuinst` tries to install/remove the shortcut from a system-level location. You can safely cancel/ignore that prompt(s) (it may prompt you multiple times) which will cause `menuinst` to fallback to a user-specific location instead. Unfortunately, there does not seem to be a way to have `menuinst` prefer to install/remove shortcuts for the current user rather than system-wide.
|
||||
By default, `menuinst` tries to install/remove the shortcut from a system-level location. You can safely cancel/ignore that prompt(s) (it may prompt you multiple times) which will cause `menuinst` to fallback to a user-specific location instead. Unfortunately, there does not seem to be a way to have `menuinst` prefer to install/remove shortcuts for the current user rather than system-wide.
|
||||
|
||||
3. What about other Chromium-based browsers?
|
||||
|
||||
@@ -49,8 +49,8 @@ To remove the shortcut created by this script, run `python ./setup_jupyter.py --
|
||||
|
||||
7. What is `nb_conda_kernels` and why does this script want to install it in my Jupyter environment.
|
||||
|
||||
`nb_conda_kernels` allows JupyterLab to run kernels in **any** installed conda environment which has a Jupyter kernel (e.g. ipykernel for Python) installed in it without having to install Jupyter and all its dependencies into that conda environment. It allows you to have a single version of JupyterLab installed in a single, purpose-made environment so you are not maintaining multiple JupyterLab versions or configurations.
|
||||
`nb_conda_kernels` allows JupyterLab to run kernels in **any** installed conda environment which has a Jupyter kernel (e.g. ipykernel for Python) installed in it without having to install Jupyter and all its dependencies into that conda environment. It allows you to have a single version of JupyterLab installed in a single, purpose-made environment so you are not maintaining multiple JupyterLab versions or configurations.
|
||||
|
||||
8. How should I update my Jupyter environment?
|
||||
|
||||
That's up to you. I will periodically run `conda update -n jupyter --all && conda update -n jupyter python` to update everything in my Jupyter environment (named `jupyter`).
|
||||
That's up to you. I will periodically run `conda update -n jupyter --all && conda update -n jupyter python` to update everything in my Jupyter environment (named `jupyter`).
|
||||
|
||||
@@ -3,8 +3,9 @@ import platform
|
||||
|
||||
from io import StringIO
|
||||
from itertools import product
|
||||
from os import environ
|
||||
from os import environ, pathsep
|
||||
from pathlib import Path
|
||||
from re import search
|
||||
from subprocess import CalledProcessError, run
|
||||
|
||||
logging.getLogger(__name__).propagate=True
|
||||
@@ -12,7 +13,7 @@ logging.getLogger(__name__).propagate=True
|
||||
def find_flatpak_browser():
|
||||
if platform.system() != "Linux":
|
||||
return
|
||||
|
||||
|
||||
for path_element in map(Path, environ["PATH"].split(":")):
|
||||
if (path_element/"flatpak").is_file():
|
||||
flatpak_bin = path_element/"flatpak"
|
||||
@@ -20,7 +21,7 @@ def find_flatpak_browser():
|
||||
else:
|
||||
# flatpak is not installed
|
||||
return
|
||||
|
||||
|
||||
flatpak_apps = ("com.google.Chrome", "com.microsoft.Edge", "com.brave.Browser", "org.chromium.Chromium")
|
||||
|
||||
try:
|
||||
@@ -39,15 +40,17 @@ def find_flatpak_browser():
|
||||
else:
|
||||
with StringIO(flatpak_process.stdout.decode()) as flatpak_output:
|
||||
installed_flatpaks = flatpak_output.readlines()[1:]
|
||||
|
||||
|
||||
for flatpak_app in flatpak_apps:
|
||||
if flatpak_app in [_.strip() for _ in installed_flatpaks]:
|
||||
return f"flatpak run --filesystem={Path.home()/'.local/share/jupyter/runtime'} {flatpak_app} --start-maximized --profile-directory=Default --app=%s"
|
||||
|
||||
|
||||
def find_browser():
|
||||
# find a chromium-based browser on the system to use.
|
||||
# find a chromium-based browser on the system to use.
|
||||
if platform.system() == "Windows":
|
||||
from winreg import HKEY_CLASSES_ROOT, OpenKey, QueryValue
|
||||
|
||||
cmds = (
|
||||
"Google/Chrome/Application/chrome.exe",
|
||||
"BraveSoftware/Brave-Browser/Application/brave.exe",
|
||||
@@ -55,6 +58,17 @@ def find_browser():
|
||||
)
|
||||
path_elements = map(Path, (environ["ProgramFiles"], environ["ProgramFiles(x86)"]))
|
||||
|
||||
# do a lookup of default browser command in windows registry
|
||||
try:
|
||||
with OpenKey(HKEY_CLASSES_ROOT, r"http\shell\open") as k:
|
||||
default_browser_cmd = QueryValue(k, "command").replace('"%1"', "").replace('"', "").strip()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
# if the command looks like one of the known chromium browsers or even vaguely resembles one, return immediately with a browser command
|
||||
if any(default_browser_cmd.endswith(cmd.replace("/", pathsep)) for cmd in cmds) or search(r"\\Application\\\w+\.exe$", default_browser_cmd):
|
||||
return f'"{default_browser_cmd}" --start-maximized --profile-directory=Default --app=%s'
|
||||
|
||||
elif platform.system() == "Linux":
|
||||
# prefers Google Chrome on Linux due to popularity of the browser
|
||||
cmds = ("google-chrome", "microsoft-edge", "brave", "chromium")
|
||||
@@ -63,8 +77,8 @@ def find_browser():
|
||||
elif platform.system() == "Darwin":
|
||||
# also prefers Google Chrome on MacOS due to popularity of the browser
|
||||
cmds = (
|
||||
"Contents/MacOS/Google Chrome",
|
||||
"Contents/MacOS/Microsoft Edge",
|
||||
"Contents/MacOS/Google Chrome",
|
||||
"Contents/MacOS/Microsoft Edge",
|
||||
"Contents/MacOS/Brave Browser",
|
||||
"Contents/MacOS/Chromium",
|
||||
)
|
||||
|
||||
@@ -15,9 +15,18 @@
|
||||
"icon": "{{ MENU_DIR }}/jupyterlab.{{ ICON_EXT }}",
|
||||
"platforms": {
|
||||
"win": {
|
||||
"file_extensions": [
|
||||
".ipynb"
|
||||
],
|
||||
"precreate": "{{ BASE_PREFIX }}\\condabin\\conda.bat init cmd.exe",
|
||||
"command": [
|
||||
"{{ BASE_PREFIX }}\\condabin\\conda.bat",
|
||||
"run",
|
||||
"--live-stream",
|
||||
"--prefix",
|
||||
"{{ PREFIX }}",
|
||||
"jupyter",
|
||||
"lab",
|
||||
"--config={{ MENU_DIR }}/jupyter_lab_config.py"
|
||||
],
|
||||
"activate": false,
|
||||
"quicklaunch": false
|
||||
},
|
||||
"osx": {
|
||||
|
||||
@@ -31,7 +31,7 @@ def get_current_prefix() -> Path:
|
||||
current_prefix = Path(environ["_CONDA_PREFIX"])
|
||||
except KeyError as e:
|
||||
raise RuntimeError("No active conda environment detected. Please activate your base conda environment") from e
|
||||
|
||||
|
||||
LOGGER.debug(f"Current prefix is {current_prefix}")
|
||||
|
||||
return current_prefix
|
||||
@@ -50,7 +50,7 @@ def get_base_prefix() -> Path:
|
||||
|
||||
# your base environment will be the grandparent of the conda executable
|
||||
return conda_exe.parents[1]
|
||||
|
||||
|
||||
|
||||
def get_menuinst_version() -> tuple[str, str, str]:
|
||||
conda_process = run(["conda", "list", "--prefix", str(get_base_prefix()), "--json"], capture_output=True, check=True)
|
||||
@@ -59,13 +59,13 @@ def get_menuinst_version() -> tuple[str, str, str]:
|
||||
conda_pkgs = loads(conda_process.stdout)
|
||||
except JSONDecodeError as decode_exception:
|
||||
raise JSONDecodeError(f"Error parsing conda output as JSON: {decode_exception}") from decode_exception
|
||||
|
||||
|
||||
for pkg in conda_pkgs:
|
||||
if pkg["name"] == "menuinst":
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("menuinst was not found in the base conda envirionment")
|
||||
|
||||
|
||||
LOGGER.debug(f"menuinst=={pkg['version']} found")
|
||||
|
||||
return pkg["version"].split(".")
|
||||
@@ -74,7 +74,7 @@ def get_menuinst_version() -> tuple[str, str, str]:
|
||||
def in_base_env() -> bool:
|
||||
return get_current_prefix().samefile(get_base_prefix())
|
||||
|
||||
|
||||
|
||||
def menuinst_gt_v2_present() -> bool:
|
||||
major, minor, patch = get_menuinst_version()
|
||||
|
||||
@@ -89,7 +89,7 @@ def stage_configs(destination_dir: Path) -> Path:
|
||||
repo_dir = Path(__file__).parent
|
||||
shortcut_json = repo_dir / "jupyterlab_shortcut.json"
|
||||
jupyterlab_config = repo_dir / "jupyter_lab_config.py"
|
||||
|
||||
|
||||
LOGGER.debug(f"Repository directory is {repo_dir}")
|
||||
LOGGER.debug(f"menuinst spec json file is {shortcut_json}")
|
||||
LOGGER.debug(f"JupyterLab config file is {jupyterlab_config}")
|
||||
@@ -153,14 +153,14 @@ def svg_to_icns(svg_file: PathLike) -> None:
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
iconset_dir = Path(temp_dir)/"jupyterlab.iconset"
|
||||
iconset_dir.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
for size in [2**n for n in range(4, 11)]:
|
||||
outfile = iconset_dir/f"icon_{size}x{size}.png"
|
||||
try:
|
||||
run(
|
||||
[
|
||||
"qlmanage",
|
||||
"-t",
|
||||
"-t",
|
||||
"-s",
|
||||
str(size),
|
||||
"-o",
|
||||
@@ -281,9 +281,9 @@ def ensure_env(env_name: str) -> Path:
|
||||
end=linesep,
|
||||
file=condarc
|
||||
)
|
||||
|
||||
|
||||
else:
|
||||
# add minimal new jupyter packages t existing environment
|
||||
# add minimal new jupyter packages t existing environment
|
||||
pkgs = {pkg["name"] for pkg in env_spec}
|
||||
|
||||
missing_pkgs = {"jupyterlab", "nb_conda_kernels", "ipykernel"} - pkgs
|
||||
@@ -306,15 +306,25 @@ def ensure_env(env_name: str) -> Path:
|
||||
|
||||
(env_prefix/"Menu").mkdir(exist_ok=True)
|
||||
|
||||
return env_prefix
|
||||
return env_prefix
|
||||
|
||||
|
||||
def main(target_env_name: str, remove_shortcut: bool=False) -> Union[int, None]:
|
||||
if not in_base_env():
|
||||
LOGGER.warning("Not in base environment. Re-running this script from the base environment")
|
||||
|
||||
# I don't know why, but Windows *really* does not want to resolve conda in PATH if you aren't in the base environment
|
||||
try:
|
||||
conda_exe = environ["CONDA_EXE"]
|
||||
except KeyError:
|
||||
try:
|
||||
conda_exe = environ["_CONDA_EXE"]
|
||||
except KeyError:
|
||||
conda_exe = "conda"
|
||||
|
||||
# call conda run to re-run this in the base prefix
|
||||
rerun_proces = run(
|
||||
["conda", "run", "--prefix", str(get_base_prefix()), "--no-capture-output", "python", *argv],
|
||||
[conda_exe, "run", "--prefix", str(get_base_prefix()), "--no-capture-output", "python", *argv],
|
||||
capture_output=False,
|
||||
check=False
|
||||
)
|
||||
@@ -331,10 +341,10 @@ def main(target_env_name: str, remove_shortcut: bool=False) -> Union[int, None]:
|
||||
run(["python", *argv], capture_output=False, check=True)
|
||||
elif meets_prerequisites() and remove_shortcut:
|
||||
from menuinst.api import remove
|
||||
|
||||
|
||||
target_prefix = get_base_prefix() / f"envs/{target_env_name}"
|
||||
menu_dir = target_prefix / "Menu"
|
||||
|
||||
|
||||
shortcut_json = menu_dir / "jupyterlab_shortcut.json"
|
||||
jupyterlab_config = menu_dir / "jupyter_lab_config.py"
|
||||
match platform.system():
|
||||
@@ -346,10 +356,10 @@ def main(target_env_name: str, remove_shortcut: bool=False) -> Union[int, None]:
|
||||
icon_file = menu_dir/"jupyterlab.ico"
|
||||
case _:
|
||||
raise RuntimeError(f"{platform.system()} is not a supported platform")
|
||||
|
||||
|
||||
if shortcut_json.is_file():
|
||||
LOGGER.debug("Removing JupyterLab shortcut")
|
||||
try:
|
||||
try:
|
||||
remove(shortcut_json, target_prefix=target_prefix)
|
||||
except:
|
||||
pass
|
||||
@@ -357,7 +367,7 @@ def main(target_env_name: str, remove_shortcut: bool=False) -> Union[int, None]:
|
||||
LOGGER.info("JupyterLab shortcut removed")
|
||||
else:
|
||||
LOGGER.error(f"Shortcut spec file '{shortcut_json}' does not exist, therefore the shotcut cannot be removed by this script. Please delete it manually")
|
||||
|
||||
|
||||
if menu_dir.exists() and menu_dir.is_dir():
|
||||
LOGGER.debug(f"Cleaning up {target_env_name} environment's Menu directory {menu_dir}")
|
||||
jupyterlab_config.unlink(missing_ok=True)
|
||||
@@ -424,7 +434,7 @@ if __name__ == "__main__":
|
||||
parser.add_argument(
|
||||
"name",
|
||||
help = "Name of the target conda environment containing JupyterLab. If it does not exist, one will be created"
|
||||
)
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -442,6 +452,7 @@ if __name__ == "__main__":
|
||||
logging.captureWarnings(True)
|
||||
|
||||
try:
|
||||
logging.info("Creating Jupyterlab shortcut. This may take a moment...")
|
||||
exit(main(args.name, args.remove))
|
||||
except CalledProcessError as e:
|
||||
# catch any exceptions raised if calling conda returns an unsuccessful return code
|
||||
|
||||
Reference in New Issue
Block a user