Compare commits

...

4 Commits

Author SHA1 Message Date
Brennen Raimer
210fe834e8 Merge commit '7444564cb61d6725549e2e398eedc60ce822b26c' into dev 2024-05-16 15:45:07 -04:00
Brennen Raimer
7444564cb6 Windows is such a tremendous pain about everything... 2024-05-14 14:12:06 -04:00
Brennen Raimer
c9c5e3756f add a message to acknowledge that the script is running towards the start of the script 2024-05-14 10:16:18 -04:00
Brennen Raimer
68c7272752 initial dev work completed and tested 2024-05-13 23:15:45 -04:00
4 changed files with 69 additions and 35 deletions

View File

@@ -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`).

View File

@@ -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",
)

View File

@@ -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": {

View File

@@ -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