""" A script for launching a Jupyter Notebook server so that .ipynb files can be opened via the browser. The server will be launched in the background so that it keeps running even if Thonny stops or runs something else. If it can't find the `notebook` module, it will first attempt to use pip to upgrade `pip` and `wheel` and then install `notebook`, which should install everything necessary to run a Jupyter Notebook server. This may not work if some of the necessary wheels are not available for your architecture via PyPI, in which case you may see instructions for installing a compiler on your machine, which you can follow to get things working (although this step is not trivial...). Please reach out to an instructor if you have any issues during the installation process. This script looks for currently-running servers on its preferred port first, and if there is one, by default it will simply open up that server's file browser in a web page. Otherwise it will start a new server and open up the file browser for that server. You can adjust this behavior by editing the `CONNECT_ONLY_ON_PORT` variable. The server it launches will remain running indefinitely. AUTHOR: Peter Mawhorter, Lyn Turbak CHANGELOG: ## Version 3.0 * Instead of always attempting to connect instead of launching a new server, this version will always launch a new server, as long as no server exists at a particular port, but will launch a new server even if servers exists with different ports than the one specified. It always launches a server on that specified port. So you can distribute multiple versions of the file, with different port settings (see `CONNECT_ONLY_ON_PORT`). ## Version 2.1 * Attempts to use custom cs111 package index as an extra package index when installing Jupyter. ## Version 2.0 * Attempts to run indefinitely in the background, and if it sees a running server already, just tries to connect to that. This is a design change to try to reduce dead kernels + allow operating multiple notebooks at once using the same server more easily when run via Thonny. ## Version 1.0 * Minor reformatting from Lyn's 0.10.2 * Changes to debugging output. * Removed quote-escaping which was causing problems after the move to list-of-strings based command-line construction. * An optimistic version bump in preparation for sending to students. # Version 0.10.2 (made by Lyn to Peter's 0.10.1) * Update server/kernel timeouts in documentation at top of file * Print version number * targetNames needs to include .exe extension for Windows, no extension for Macs. e.g., target "jupyter-notebook" on Mac is "jupyter-notebook.exe" in Windows. * print path using os.environ["PATH"], not subprocess.run("echo $PATH", ...), which doesn't work in Windows * change jupyter_exec and opt to be *lists of strings* rather than *strings". + subprocess.run excepts either format, but for single strings, Windows is confused by executables with spaces like C:\\Program Files (x86)\\Thonny\\python.exe (because it thinks the exectuable is "C:\\Program" and that "Files" and "(x86)\\Thonny\\python.exe" are the first two arguments). No such confusion occurs when the format is lists of strings. + IMPORTANT: when commands are a *list of strings*, shell keyword arg *must* be False! (the default) """ import os import sys import subprocess import urllib.parse import sysconfig import webbrowser __version__ = "3.0" """ Current version number. """ CONNECT_ONLY_ON_PORT = "9123" """ Set this to the specific port that we should use. If a server is running on that port, we connect to it without launching a new one, but if a server is running on a different port, we ignore that and run a new one on this port. Set to None to not match any port, meaning that a new server will always be launched. """ USE_TIMEOUTS = False """ Whether to use timeouts for the server, or leave it running indefinitely. """ print(f"Running launchNotebook version {__version__}") try: import notebook # noqa F401 except Exception: # Jupyter isn't installed, so we'll try to install it print( "We couldn't find Jupyter Notebook on your system, so we'll try" " to install it first." ) subprocess.check_call( [ sys.executable, '-m', 'pip', 'install', '--user', '--upgrade', 'pip', 'wheel' ] ) subprocess.check_call( [ sys.executable, '-m', 'pip', 'install', '--user', 'notebook', '--extra-index-url', 'https://cs111.wellesley.edu/content/pypackages' ] ) # Find ALL scripts directories with 'jupyter' OR 'jupyter-notebook' in # them, and add them to our PATH, since even running via python module # import bounces to the shell PATH at some point T_T. hits = set() # Looking for one of these targets in packages targetNames = set( [ name + ext for name in ['jupyter', 'jupyter-notebook'] for ext in [ '', # Mac has no extension '.exe' # Windows has .exe extension ] ] ) # Look for jupyter scripts in every possible script install dir print("Looking for Jupyter script...") look_in = set() schemes_found = set() for scheme in sysconfig.get_scheme_names(): bindir = sysconfig.get_path('scripts', scheme) if bindir is not None and os.path.exists(bindir): look_in.add(bindir) schemes_found.add(scheme) print( f"Found {len(look_in)} directories to search, from schemes:" f" {schemes_found}" ) for bindir in look_in: print(f"Looking for Jupyter in '{bindir}':") matches = set(os.listdir(bindir)).intersection(targetNames) if len(matches) > 0: hits.add(bindir) print(" Found: " + ', '.join(matches)) else: print(" No Jupyter scripts here.") if len(hits) == 0: # If we can't find jupyter, try anyways because maybe it's elsewhere # on their PATH? But print a warning message... print( "Even after installing Jupyter Notebook, we can't figure out" " how to launch it. We're going to try anyways, but it will" " probably crash." ) else: # Add entries at the end of the PATH for all the Python script dirs # we found with 'jupyter' in them. path = os.environ["PATH"] print("Adding entries to PATH:") for bindir in hits: print(' ' + bindir) path += ":" + bindir os.environ["PATH"] = path # Don't use subprocess.run with "echo $PATH" because in Windows 10 # this returns the string "$PATH" and not a string with path directories. finalPath = os.environ["PATH"] print("Your current PATH entries are:") for entry in finalPath.split(':'): print(' ' + entry) # This is how we'll launch jupyter... # Put command, args, opts etc in a *list of strings* # rather than *single string* # so that spaces in program names like # C:\\Program Files (x86)\\Thonny\\python.exe # don't cause an error in Windows in subprocess.run jupyter_exec = [sys.executable, '-m', 'jupyter_core.command'] print(f"Running {jupyter_exec + ['notebook', 'list']}") # Check for already-running servers: result = subprocess.run( jupyter_exec + ['notebook', 'list'], # shell=True, # shell must be False (default) when command is a list capture_output=True, text=True ) if result.stdout: print(result.stdout) # Abort if jupyter is not installed if result.returncode != 0: print( f"Jupyter Notebook server isn't working on your system (return" f" code {result.returncode}). You should ask an instructor for" f" help with this, or if you can, install Anaconda and use that" f" to open the notebook." ) sys.exit(1) if result.stdout and len(result.stdout.splitlines()) > 1: lines = result.stdout.splitlines()[1:] URLs = [line.split('::')[0].strip() for line in lines] print("One or more notebook servers are already active:") for url in URLs: print(' ' + url) try: secondColon = url.index(':', url.index(':') + 1) nextSlash = url.index('/', secondColon + 1) port = url[secondColon + 1:nextSlash] except ValueError: port = None if CONNECT_ONLY_ON_PORT and port == CONNECT_ONLY_ON_PORT: print("Attempting to connect to server on our preferred port...") webbrowser.open_new_tab(url) exit() print( "No server found on our preferred port; " "launching a new notebook server." ) root_dir = os.getcwd() default_url = "/tree" # Note: escaping quotes would be bad since we're using the # list-of-strings command-line approach # For the default URL, we want to fully encode it as a URL default_url = urllib.parse.quote(default_url) opts = [ # disable mathjax since it's potentially slow... "--no-mathjax", # prevent async io errors on Mac "--NotebookApp.kernel_manager_class=" + "notebook.services.kernels.kernelmanager.AsyncMappingKernelManager", ] if CONNECT_ONLY_ON_PORT: opts += ["--port=" + CONNECT_ONLY_ON_PORT] if USE_TIMEOUTS: opts += [ "--NotebookApp.shutdown_no_activity_timeout=14400", # 240 minutes "--MappingKernelManager.cull_idle_timeout=9000", # 150 minutes ] # Only try fancy root directory stuff if there are no spaces in the # folder name! if ( (not root_dir or ' ' not in root_dir) and ' ' not in root_dir and ' ' not in default_url ): opts += [ # Set root directory f'--notebook-dir="{root_dir}"', # Set default URL to current folder f'--NotebookApp.default_url="{default_url}"' ] # run without a target to launch into the Jupyter file browser args = jupyter_exec + ['notebook'] # Using Popen here instead of "run" so that it launches in the # background and we don't wait for it to complete. print(f'Running:\n{args + opts}') if hasattr(subprocess, "DETACHED_PROCESS"): flags = subprocess.DETACHED_PROCESS else: flags = 0 subprocess.Popen( args + opts, # shell=True, # shell must be False (default) when command is a list close_fds=True, # some places suggest this helps detach things? creationflags=flags # may also help detach? )