diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 57c187a9..fb3ade2f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -167,6 +167,10 @@ jobs: jupyter lab extension list jupyter lab extension list 2>&1 | grep -iE 'jupyter_server_proxy.*OK.*' + - name: Install a dummy entrypoint so we can test its loaded correctly + run: | + pip install ./tests/resources/dummyentrypoint/ + # we have installed a pre-built wheel and configured code coverage to # inspect "jupyter_server_proxy", by re-locating to another directory, # there is no confusion about "jupyter_server_proxy" referring to our diff --git a/jupyter_server_proxy/__init__.py b/jupyter_server_proxy/__init__.py index cbce0d5f..addae684 100644 --- a/jupyter_server_proxy/__init__.py +++ b/jupyter_server_proxy/__init__.py @@ -2,6 +2,7 @@ from ._version import __version__ # noqa from .api import IconHandler, ServersInfoHandler +from .config import ServerProcess, ServerProcessEntryPoint from .config import ServerProxy as ServerProxyConfig from .config import get_entrypoint_server_processes, make_handlers, make_server_process from .handlers import setup_handlers @@ -45,7 +46,9 @@ def _load_jupyter_server_extension(nbapp): make_server_process(name, server_process_config, serverproxy_config) for name, server_process_config in serverproxy_config.servers.items() ] - server_processes += get_entrypoint_server_processes(serverproxy_config) + server_processes += get_entrypoint_server_processes( + serverproxy_config, parent=nbapp + ) server_handlers = make_handlers(base_url, server_processes) nbapp.web_app.add_handlers(".*", server_handlers) @@ -81,3 +84,5 @@ def _load_jupyter_server_extension(nbapp): # For backward compatibility load_jupyter_server_extension = _load_jupyter_server_extension _jupyter_server_extension_paths = _jupyter_server_extension_points + +__all__ = ["ServerProcess", "ServerProcessEntryPoint"] diff --git a/jupyter_server_proxy/config.py b/jupyter_server_proxy/config.py index 4b21cf70..d157fbc0 100644 --- a/jupyter_server_proxy/config.py +++ b/jupyter_server_proxy/config.py @@ -76,7 +76,7 @@ def _default_path_info(self): ) -class ServerProcess(Configurable): +class _ServerProcess(Configurable): name = Unicode(help="Name of the server").tag(config=True) command = List( @@ -264,6 +264,22 @@ def cats_only(response, path): ).tag(config=True) +class ServerProcess(_ServerProcess): + """ + A configurable server process for single standalone servers. + This is separate from ServerProcessEntryPoint so that we can configure it + independently of ServerProcessEntryPoint + """ + + +class ServerProcessEntryPoint(_ServerProcess): + """ + A ServeProcess entrypoint that is a Configurable. + This is separate from ServerProcess so that we can configure it + independently of ServerProcess + """ + + def _make_proxy_handler(sp: ServerProcess): """ Create an appropriate handler with given parameters @@ -319,16 +335,23 @@ def get_timeout(self): return _Proxy -def get_entrypoint_server_processes(serverproxy_config): +def get_entrypoint_server_processes(serverproxy_config, parent): sps = [] for entry_point in entry_points(group="jupyter_serverproxy_servers"): name = entry_point.name try: - server_process_config = entry_point.load()() + server_process_callable = entry_point.load() + if issubclass(server_process_callable, ServerProcessEntryPoint): + server_process = server_process_callable(name=name, parent=parent) + sps.append(server_process) + else: + server_process_config = server_process_callable() + sps.append( + make_server_process(name, server_process_config, serverproxy_config) + ) except Exception as e: warn(f"entry_point {name} was unable to be loaded: {str(e)}") continue - sps.append(make_server_process(name, server_process_config, serverproxy_config)) return sps diff --git a/tests/resources/dummyentrypoint/pyproject.toml b/tests/resources/dummyentrypoint/pyproject.toml new file mode 100644 index 00000000..2d9e43c2 --- /dev/null +++ b/tests/resources/dummyentrypoint/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "test-jsp-dummyentrypoint" +version = "0.0.0" + + +[project.entry-points.jupyter_serverproxy_servers] +test-serverprocessentrypoint = "test_jsp_dummyentrypoint:CustomServerProcessEntryPoint" diff --git a/tests/resources/dummyentrypoint/test_jsp_dummyentrypoint/__init__.py b/tests/resources/dummyentrypoint/test_jsp_dummyentrypoint/__init__.py new file mode 100644 index 00000000..a920c553 --- /dev/null +++ b/tests/resources/dummyentrypoint/test_jsp_dummyentrypoint/__init__.py @@ -0,0 +1,17 @@ +""" +Test whether ServerProcessEntryPoint can be configured using traitlets +""" + +import sys +from pathlib import Path + +from traitlets.config import default + +from jupyter_server_proxy import ServerProcessEntryPoint + + +class CustomServerProcessEntryPoint(ServerProcessEntryPoint): + @default("command") + def _default_command(self): + parent = Path(__file__).parent.resolve() + return [sys.executable, str(parent / "httpinfo.py"), "--port={port}"] diff --git a/tests/resources/dummyentrypoint/test_jsp_dummyentrypoint/httpinfo.py b/tests/resources/dummyentrypoint/test_jsp_dummyentrypoint/httpinfo.py new file mode 100644 index 00000000..07d01ddb --- /dev/null +++ b/tests/resources/dummyentrypoint/test_jsp_dummyentrypoint/httpinfo.py @@ -0,0 +1,24 @@ +""" +Simple webserver to respond with an echo of the sent request. +""" + +import argparse +from http.server import BaseHTTPRequestHandler, HTTPServer + + +class EchoRequestInfo(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Content-type", "text/plain") + self.end_headers() + self.wfile.write(f"{self.requestline}\n".encode()) + self.wfile.write(f"{self.headers}\n".encode()) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int) + args = ap.parse_args() + + httpd = HTTPServer(("127.0.0.1", args.port), EchoRequestInfo) + httpd.serve_forever() diff --git a/tests/resources/jupyter_server_config.py b/tests/resources/jupyter_server_config.py index ac1e0dfe..06ed03c9 100644 --- a/tests/resources/jupyter_server_config.py +++ b/tests/resources/jupyter_server_config.py @@ -51,6 +51,12 @@ def my_env(): return {"MYVAR": "String with escaped {{var}}"} +# Traitlets configuration for test-serverprocessentrypoint in the dummyentrypoint package +c.CustomServerProcessEntryPoint.request_headers_override = { + "X-Custom-Header": "custom-configurable" +} + + c.ServerProxy.servers = { "python-http": { "command": [sys.executable, _get_path("httpinfo.py"), "--port={port}"], diff --git a/tests/test_proxies.py b/tests/test_proxies.py index 4573517c..3afdb3fd 100644 --- a/tests/test_proxies.py +++ b/tests/test_proxies.py @@ -528,3 +528,11 @@ async def test_server_proxy_rawsocket( await conn.write_message(msg) res = await conn.read_message() assert res == msg.swapcase() + + +def test_server_configurable_class(a_server_port_and_token: Tuple[int, str]) -> None: + PORT, TOKEN = a_server_port_and_token + r = request_get(PORT, "/test-serverprocessentrypoint/", TOKEN, host="127.0.0.1") + assert r.code == 200 + s = r.read().decode("ascii") + assert "X-Custom-Header: custom-configurable\n" in s