You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			431 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			431 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
"""Notebook related utilities"""
 | 
						|
 | 
						|
# Copyright (c) Jupyter Development Team.
 | 
						|
# Distributed under the terms of the Modified BSD License.
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import errno
 | 
						|
import importlib.util
 | 
						|
import os
 | 
						|
import socket
 | 
						|
import sys
 | 
						|
import warnings
 | 
						|
from _frozen_importlib_external import _NamespacePath
 | 
						|
from contextlib import contextmanager
 | 
						|
from pathlib import Path
 | 
						|
from typing import TYPE_CHECKING, Any, NewType
 | 
						|
from urllib.parse import (
 | 
						|
    SplitResult,
 | 
						|
    quote,
 | 
						|
    unquote,
 | 
						|
    urlparse,
 | 
						|
    urlsplit,
 | 
						|
    urlunsplit,
 | 
						|
)
 | 
						|
from urllib.parse import (
 | 
						|
    urljoin as _urljoin,
 | 
						|
)
 | 
						|
from urllib.request import pathname2url as _pathname2url
 | 
						|
 | 
						|
from jupyter_core.utils import ensure_async as _ensure_async
 | 
						|
from packaging.version import Version
 | 
						|
from tornado.httpclient import AsyncHTTPClient, HTTPClient, HTTPRequest, HTTPResponse
 | 
						|
from tornado.netutil import Resolver
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from collections.abc import Generator, Sequence
 | 
						|
 | 
						|
ApiPath = NewType("ApiPath", str)
 | 
						|
 | 
						|
# Re-export
 | 
						|
urljoin = _urljoin
 | 
						|
pathname2url = _pathname2url
 | 
						|
ensure_async = _ensure_async
 | 
						|
 | 
						|
 | 
						|
def url_path_join(*pieces: str) -> str:
 | 
						|
    """Join components of url into a relative url
 | 
						|
 | 
						|
    Use to prevent double slash when joining subpath. This will leave the
 | 
						|
    initial and final / in place
 | 
						|
    """
 | 
						|
    initial = pieces[0].startswith("/")
 | 
						|
    final = pieces[-1].endswith("/")
 | 
						|
    stripped = [s.strip("/") for s in pieces]
 | 
						|
    result = "/".join(s for s in stripped if s)
 | 
						|
    if initial:
 | 
						|
        result = "/" + result
 | 
						|
    if final:
 | 
						|
        result = result + "/"
 | 
						|
    if result == "//":
 | 
						|
        result = "/"
 | 
						|
    return result
 | 
						|
 | 
						|
 | 
						|
def url_is_absolute(url: str) -> bool:
 | 
						|
    """Determine whether a given URL is absolute"""
 | 
						|
    return urlparse(url).path.startswith("/")
 | 
						|
 | 
						|
 | 
						|
def path2url(path: str) -> str:
 | 
						|
    """Convert a local file path to a URL"""
 | 
						|
    pieces = [quote(p) for p in path.split(os.sep)]
 | 
						|
    # preserve trailing /
 | 
						|
    if pieces[-1] == "":
 | 
						|
        pieces[-1] = "/"
 | 
						|
    url = url_path_join(*pieces)
 | 
						|
    return url
 | 
						|
 | 
						|
 | 
						|
def url2path(url: str) -> str:
 | 
						|
    """Convert a URL to a local file path"""
 | 
						|
    pieces = [unquote(p) for p in url.split("/")]
 | 
						|
    path = os.path.join(*pieces)
 | 
						|
    return path
 | 
						|
 | 
						|
 | 
						|
def url_escape(path: str) -> str:
 | 
						|
    """Escape special characters in a URL path
 | 
						|
 | 
						|
    Turns '/foo bar/' into '/foo%20bar/'
 | 
						|
    """
 | 
						|
    parts = path.split("/")
 | 
						|
    return "/".join([quote(p) for p in parts])
 | 
						|
 | 
						|
 | 
						|
def url_unescape(path: str) -> str:
 | 
						|
    """Unescape special characters in a URL path
 | 
						|
 | 
						|
    Turns '/foo%20bar/' into '/foo bar/'
 | 
						|
    """
 | 
						|
    return "/".join([unquote(p) for p in path.split("/")])
 | 
						|
 | 
						|
 | 
						|
def samefile_simple(path: str, other_path: str) -> bool:
 | 
						|
    """
 | 
						|
    Fill in for os.path.samefile when it is unavailable (Windows+py2).
 | 
						|
 | 
						|
    Do a case-insensitive string comparison in this case
 | 
						|
    plus comparing the full stat result (including times)
 | 
						|
    because Windows + py2 doesn't support the stat fields
 | 
						|
    needed for identifying if it's the same file (st_ino, st_dev).
 | 
						|
 | 
						|
    Only to be used if os.path.samefile is not available.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    path : str
 | 
						|
        representing a path to a file
 | 
						|
    other_path : str
 | 
						|
        representing a path to another file
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    same:   Boolean that is True if both path and other path are the same
 | 
						|
    """
 | 
						|
    path_stat = os.stat(path)
 | 
						|
    other_path_stat = os.stat(other_path)
 | 
						|
    return path.lower() == other_path.lower() and path_stat == other_path_stat
 | 
						|
 | 
						|
 | 
						|
def to_os_path(path: ApiPath, root: str = "") -> str:
 | 
						|
    """Convert an API path to a filesystem path
 | 
						|
 | 
						|
    If given, root will be prepended to the path.
 | 
						|
    root must be a filesystem path already.
 | 
						|
    """
 | 
						|
    parts = str(path).strip("/").split("/")
 | 
						|
    parts = [p for p in parts if p != ""]  #  remove duplicate splits
 | 
						|
    path_ = os.path.join(root, *parts)
 | 
						|
    return os.path.normpath(path_)
 | 
						|
 | 
						|
 | 
						|
def to_api_path(os_path: str, root: str = "") -> ApiPath:
 | 
						|
    """Convert a filesystem path to an API path
 | 
						|
 | 
						|
    If given, root will be removed from the path.
 | 
						|
    root must be a filesystem path already.
 | 
						|
    """
 | 
						|
    if os_path.startswith(root):
 | 
						|
        os_path = os_path[len(root) :]
 | 
						|
    parts = os_path.strip(os.path.sep).split(os.path.sep)
 | 
						|
    parts = [p for p in parts if p != ""]  # remove duplicate splits
 | 
						|
    path = "/".join(parts)
 | 
						|
    return ApiPath(path)
 | 
						|
 | 
						|
 | 
						|
def check_version(v: str, check: str) -> bool:
 | 
						|
    """check version string v >= check
 | 
						|
 | 
						|
    If dev/prerelease tags result in TypeError for string-number comparison,
 | 
						|
    it is assumed that the dependency is satisfied.
 | 
						|
    Users on dev branches are responsible for keeping their own packages up to date.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        return bool(Version(v) >= Version(check))
 | 
						|
    except TypeError:
 | 
						|
        return True
 | 
						|
 | 
						|
 | 
						|
# Copy of IPython.utils.process.check_pid:
 | 
						|
 | 
						|
 | 
						|
def _check_pid_win32(pid: int) -> bool:
 | 
						|
    import ctypes
 | 
						|
 | 
						|
    # OpenProcess returns 0 if no such process (of ours) exists
 | 
						|
    # positive int otherwise
 | 
						|
    return bool(ctypes.windll.kernel32.OpenProcess(1, 0, pid))  # type:ignore[attr-defined]
 | 
						|
 | 
						|
 | 
						|
def _check_pid_posix(pid: int) -> bool:
 | 
						|
    """Copy of IPython.utils.process.check_pid"""
 | 
						|
    try:
 | 
						|
        os.kill(pid, 0)
 | 
						|
    except OSError as err:
 | 
						|
        if err.errno == errno.ESRCH:
 | 
						|
            return False
 | 
						|
        elif err.errno == errno.EPERM:
 | 
						|
            # Don't have permission to signal the process - probably means it exists
 | 
						|
            return True
 | 
						|
        raise
 | 
						|
    else:
 | 
						|
        return True
 | 
						|
 | 
						|
 | 
						|
if sys.platform == "win32":
 | 
						|
    check_pid = _check_pid_win32
 | 
						|
else:
 | 
						|
    check_pid = _check_pid_posix
 | 
						|
 | 
						|
 | 
						|
async def run_sync_in_loop(maybe_async):
 | 
						|
    """**DEPRECATED**: Use ``ensure_async`` from jupyter_core instead."""
 | 
						|
    warnings.warn(
 | 
						|
        "run_sync_in_loop is deprecated since Jupyter Server 2.0, use 'ensure_async' from jupyter_core instead",
 | 
						|
        DeprecationWarning,
 | 
						|
        stacklevel=2,
 | 
						|
    )
 | 
						|
    return ensure_async(maybe_async)
 | 
						|
 | 
						|
 | 
						|
def urlencode_unix_socket_path(socket_path: str) -> str:
 | 
						|
    """Encodes a UNIX socket path string from a socket path for the `http+unix` URI form."""
 | 
						|
    return socket_path.replace("/", "%2F")
 | 
						|
 | 
						|
 | 
						|
def urldecode_unix_socket_path(socket_path: str) -> str:
 | 
						|
    """Decodes a UNIX sock path string from an encoded sock path for the `http+unix` URI form."""
 | 
						|
    return socket_path.replace("%2F", "/")
 | 
						|
 | 
						|
 | 
						|
def urlencode_unix_socket(socket_path: str) -> str:
 | 
						|
    """Encodes a UNIX socket URL from a socket path for the `http+unix` URI form."""
 | 
						|
    return "http+unix://%s" % urlencode_unix_socket_path(socket_path)
 | 
						|
 | 
						|
 | 
						|
def unix_socket_in_use(socket_path: str) -> bool:
 | 
						|
    """Checks whether a UNIX socket path on disk is in use by attempting to connect to it."""
 | 
						|
    if not os.path.exists(socket_path):
 | 
						|
        return False
 | 
						|
 | 
						|
    try:
 | 
						|
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 | 
						|
        sock.connect(socket_path)
 | 
						|
    except OSError:
 | 
						|
        return False
 | 
						|
    else:
 | 
						|
        return True
 | 
						|
    finally:
 | 
						|
        sock.close()
 | 
						|
 | 
						|
 | 
						|
@contextmanager
 | 
						|
def _request_for_tornado_client(
 | 
						|
    urlstring: str, method: str = "GET", body: Any = None, headers: Any = None
 | 
						|
) -> Generator[HTTPRequest, None, None]:
 | 
						|
    """A utility that provides a context that handles
 | 
						|
    HTTP, HTTPS, and HTTP+UNIX request.
 | 
						|
    Creates a tornado HTTPRequest object with a URL
 | 
						|
    that tornado's HTTPClients can accept.
 | 
						|
    If the request is made to a unix socket, temporarily
 | 
						|
    configure the AsyncHTTPClient to resolve the URL
 | 
						|
    and connect to the proper socket.
 | 
						|
    """
 | 
						|
    parts = urlsplit(urlstring)
 | 
						|
    if parts.scheme in ["http", "https"]:
 | 
						|
        pass
 | 
						|
    elif parts.scheme == "http+unix":
 | 
						|
        # If unix socket, mimic HTTP.
 | 
						|
        parts = SplitResult(
 | 
						|
            scheme="http",
 | 
						|
            netloc=parts.netloc,
 | 
						|
            path=parts.path,
 | 
						|
            query=parts.query,
 | 
						|
            fragment=parts.fragment,
 | 
						|
        )
 | 
						|
 | 
						|
        class UnixSocketResolver(Resolver):
 | 
						|
            """A resolver that routes HTTP requests to unix sockets
 | 
						|
            in tornado HTTP clients.
 | 
						|
            Due to constraints in Tornados' API, the scheme of the
 | 
						|
            must be `http` (not `http+unix`). Applications should replace
 | 
						|
            the scheme in URLS before making a request to the HTTP client.
 | 
						|
            """
 | 
						|
 | 
						|
            def initialize(self, resolver):
 | 
						|
                self.resolver = resolver
 | 
						|
 | 
						|
            def close(self):
 | 
						|
                self.resolver.close()
 | 
						|
 | 
						|
            async def resolve(self, host, port, *args, **kwargs):
 | 
						|
                return [(socket.AF_UNIX, urldecode_unix_socket_path(host))]
 | 
						|
 | 
						|
        resolver = UnixSocketResolver(resolver=Resolver())
 | 
						|
        AsyncHTTPClient.configure(None, resolver=resolver)
 | 
						|
    else:
 | 
						|
        msg = "Unknown URL scheme."
 | 
						|
        raise Exception(msg)
 | 
						|
 | 
						|
    # Yield the request for the given client.
 | 
						|
    url = urlunsplit(parts)
 | 
						|
    request = HTTPRequest(url, method=method, body=body, headers=headers, validate_cert=False)
 | 
						|
    yield request
 | 
						|
 | 
						|
 | 
						|
def fetch(
 | 
						|
    urlstring: str, method: str = "GET", body: Any = None, headers: Any = None
 | 
						|
) -> HTTPResponse:
 | 
						|
    """
 | 
						|
    Send a HTTP, HTTPS, or HTTP+UNIX request
 | 
						|
    to a Tornado Web Server. Returns a tornado HTTPResponse.
 | 
						|
    """
 | 
						|
    with _request_for_tornado_client(
 | 
						|
        urlstring, method=method, body=body, headers=headers
 | 
						|
    ) as request:
 | 
						|
        response = HTTPClient(AsyncHTTPClient).fetch(request)
 | 
						|
    return response
 | 
						|
 | 
						|
 | 
						|
async def async_fetch(
 | 
						|
    urlstring: str, method: str = "GET", body: Any = None, headers: Any = None, io_loop: Any = None
 | 
						|
) -> HTTPResponse:
 | 
						|
    """
 | 
						|
    Send an asynchronous HTTP, HTTPS, or HTTP+UNIX request
 | 
						|
    to a Tornado Web Server. Returns a tornado HTTPResponse.
 | 
						|
    """
 | 
						|
    with _request_for_tornado_client(
 | 
						|
        urlstring, method=method, body=body, headers=headers
 | 
						|
    ) as request:
 | 
						|
        response = await AsyncHTTPClient(io_loop).fetch(request)
 | 
						|
    return response
 | 
						|
 | 
						|
 | 
						|
def is_namespace_package(namespace: str) -> bool | None:
 | 
						|
    """Is the provided namespace a Python Namespace Package (PEP420).
 | 
						|
 | 
						|
    https://www.python.org/dev/peps/pep-0420/#specification
 | 
						|
 | 
						|
    Returns `None` if module is not importable.
 | 
						|
 | 
						|
    """
 | 
						|
    # NOTE: using submodule_search_locations because the loader can be None
 | 
						|
    try:
 | 
						|
        spec = importlib.util.find_spec(namespace)
 | 
						|
    except ValueError:  # spec is not set - see https://docs.python.org/3/library/importlib.html#importlib.util.find_spec
 | 
						|
        return None
 | 
						|
 | 
						|
    if not spec:
 | 
						|
        # e.g. module not installed
 | 
						|
        return None
 | 
						|
    return isinstance(spec.submodule_search_locations, _NamespacePath)
 | 
						|
 | 
						|
 | 
						|
def filefind(filename: str, path_dirs: Sequence[str]) -> str:
 | 
						|
    """Find a file by looking through a sequence of paths.
 | 
						|
 | 
						|
    For use in FileFindHandler.
 | 
						|
 | 
						|
    Iterates through a sequence of paths looking for a file and returns
 | 
						|
    the full, absolute path of the first occurrence of the file.
 | 
						|
 | 
						|
    Absolute paths are not accepted for inputs.
 | 
						|
 | 
						|
    This function does not automatically try any paths,
 | 
						|
    such as the cwd or the user's home directory.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    filename : str
 | 
						|
        The filename to look for. Must be a relative path.
 | 
						|
    path_dirs : sequence of str
 | 
						|
        The sequence of paths to look in for the file.
 | 
						|
        Walk through each element and join with ``filename``.
 | 
						|
        Only after ensuring the path resolves within the directory is it checked for existence.
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    Raises :exc:`OSError` or returns absolute path to file.
 | 
						|
    """
 | 
						|
    file_path = Path(filename)
 | 
						|
 | 
						|
    # If the input is an absolute path, reject it
 | 
						|
    if file_path.is_absolute():
 | 
						|
        msg = f"{filename} is absolute, filefind only accepts relative paths."
 | 
						|
        raise OSError(msg)
 | 
						|
 | 
						|
    for path_str in path_dirs:
 | 
						|
        path = Path(path_str).absolute()
 | 
						|
        test_path = path / file_path
 | 
						|
        # os.path.abspath resolves '..', but Path.absolute() doesn't
 | 
						|
        # Path.resolve() does, but traverses symlinks, which we don't want
 | 
						|
        test_path = Path(os.path.abspath(test_path))
 | 
						|
        if not test_path.is_relative_to(path):
 | 
						|
            # points outside root, e.g. via `filename='../foo'`
 | 
						|
            continue
 | 
						|
        # make sure we don't call is_file before we know it's a file within a prefix
 | 
						|
        # GHSA-hrw6-wg82-cm62 - can leak password hash on windows.
 | 
						|
        if test_path.is_file():
 | 
						|
            return os.path.abspath(test_path)
 | 
						|
 | 
						|
    msg = f"File {filename!r} does not exist in any of the search paths: {path_dirs!r}"
 | 
						|
    raise OSError(msg)
 | 
						|
 | 
						|
 | 
						|
def import_item(name: str) -> Any:
 | 
						|
    """Import and return ``bar`` given the string ``foo.bar``.
 | 
						|
    Calling ``bar = import_item("foo.bar")`` is the functional equivalent of
 | 
						|
    executing the code ``from foo import bar``.
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    name : str
 | 
						|
      The fully qualified name of the module/package being imported.
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    mod : module object
 | 
						|
       The module that was imported.
 | 
						|
    """
 | 
						|
 | 
						|
    parts = name.rsplit(".", 1)
 | 
						|
    if len(parts) == 2:
 | 
						|
        # called with 'foo.bar....'
 | 
						|
        package, obj = parts
 | 
						|
        module = __import__(package, fromlist=[obj])
 | 
						|
        try:
 | 
						|
            pak = getattr(module, obj)
 | 
						|
        except AttributeError as e:
 | 
						|
            raise ImportError("No module named %s" % obj) from e
 | 
						|
        return pak
 | 
						|
    else:
 | 
						|
        # called with un-dotted string
 | 
						|
        return __import__(parts[0])
 | 
						|
 | 
						|
 | 
						|
class JupyterServerAuthWarning(RuntimeWarning):
 | 
						|
    """Emitted when authentication configuration issue is detected.
 | 
						|
 | 
						|
    Intended for filtering out expected warnings in tests, including
 | 
						|
    downstream tests, rather than for users to silence this warning.
 | 
						|
    """
 |