Skip to content

Commit 58e5667

Browse files
committed
Entrypoints can be traitlets Configurable
This means a custom proxy can be installed, and configured using traitlets in a standard jupyter_server_config file
1 parent c01de61 commit 58e5667

File tree

8 files changed

+99
-5
lines changed

8 files changed

+99
-5
lines changed

.github/workflows/test.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ jobs:
167167
jupyter lab extension list
168168
jupyter lab extension list 2>&1 | grep -iE 'jupyter_server_proxy.*OK.*'
169169
170+
- name: Install a dummy entrypoint so we can test its loaded correctly
171+
run: |
172+
pip install ./tests/resources/dummyentrypoint/
173+
170174
# we have installed a pre-built wheel and configured code coverage to
171175
# inspect "jupyter_server_proxy", by re-locating to another directory,
172176
# there is no confusion about "jupyter_server_proxy" referring to our

jupyter_server_proxy/__init__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ._version import __version__ # noqa
44
from .api import IconHandler, ServersInfoHandler
5+
from .config import ServerProcess, ServerProcessEntryPoint
56
from .config import ServerProxy as ServerProxyConfig
67
from .config import get_entrypoint_server_processes, make_handlers, make_server_process
78
from .handlers import setup_handlers
@@ -45,7 +46,9 @@ def _load_jupyter_server_extension(nbapp):
4546
make_server_process(name, server_process_config, serverproxy_config)
4647
for name, server_process_config in serverproxy_config.servers.items()
4748
]
48-
server_processes += get_entrypoint_server_processes(serverproxy_config)
49+
server_processes += get_entrypoint_server_processes(
50+
serverproxy_config, parent=nbapp
51+
)
4952
server_handlers = make_handlers(base_url, server_processes)
5053
nbapp.web_app.add_handlers(".*", server_handlers)
5154

@@ -81,3 +84,5 @@ def _load_jupyter_server_extension(nbapp):
8184
# For backward compatibility
8285
load_jupyter_server_extension = _load_jupyter_server_extension
8386
_jupyter_server_extension_paths = _jupyter_server_extension_points
87+
88+
__all__ = ["ServerProcess", "ServerProcessEntryPoint"]

jupyter_server_proxy/config.py

+27-4
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def _default_path_info(self):
7676
)
7777

7878

79-
class ServerProcess(Configurable):
79+
class _ServerProcess(Configurable):
8080
name = Unicode(help="Name of the server").tag(config=True)
8181

8282
command = List(
@@ -264,6 +264,22 @@ def cats_only(response, path):
264264
).tag(config=True)
265265

266266

267+
class ServerProcess(_ServerProcess):
268+
"""
269+
A configurable server process for single standalone servers.
270+
This is separate from ServerProcessEntryPoint so that we can configure it
271+
independently of ServerProcessEntryPoint
272+
"""
273+
274+
275+
class ServerProcessEntryPoint(_ServerProcess):
276+
"""
277+
A ServeProcess entrypoint that is a Configurable.
278+
This is separate from ServerProcess so that we can configure it
279+
independently of ServerProcess
280+
"""
281+
282+
267283
def _make_proxy_handler(sp: ServerProcess):
268284
"""
269285
Create an appropriate handler with given parameters
@@ -319,16 +335,23 @@ def get_timeout(self):
319335
return _Proxy
320336

321337

322-
def get_entrypoint_server_processes(serverproxy_config):
338+
def get_entrypoint_server_processes(serverproxy_config, parent):
323339
sps = []
324340
for entry_point in entry_points(group="jupyter_serverproxy_servers"):
325341
name = entry_point.name
326342
try:
327-
server_process_config = entry_point.load()()
343+
server_process_callable = entry_point.load()
344+
if issubclass(server_process_callable, ServerProcessEntryPoint):
345+
server_process = server_process_callable(name=name, parent=parent)
346+
sps.append(server_process)
347+
else:
348+
server_process_config = server_process_callable()
349+
sps.append(
350+
make_server_process(name, server_process_config, serverproxy_config)
351+
)
328352
except Exception as e:
329353
warn(f"entry_point {name} was unable to be loaded: {str(e)}")
330354
continue
331-
sps.append(make_server_process(name, server_process_config, serverproxy_config))
332355
return sps
333356

334357

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[project]
2+
name = "test-jsp-dummyentrypoint"
3+
version = "0.0.0"
4+
5+
6+
[project.entry-points.jupyter_serverproxy_servers]
7+
test-serverprocessentrypoint = "test_jsp_dummyentrypoint:CustomServerProcessEntryPoint"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
Test whether ServerProcessEntryPoint can be configured using traitlets
3+
"""
4+
5+
import sys
6+
from pathlib import Path
7+
8+
from traitlets.config import default
9+
10+
from jupyter_server_proxy import ServerProcessEntryPoint
11+
12+
13+
class CustomServerProcessEntryPoint(ServerProcessEntryPoint):
14+
@default("command")
15+
def _default_command(self):
16+
parent = Path(__file__).parent.resolve()
17+
return [sys.executable, str(parent / "httpinfo.py"), "--port={port}"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
Simple webserver to respond with an echo of the sent request.
3+
"""
4+
5+
import argparse
6+
from http.server import BaseHTTPRequestHandler, HTTPServer
7+
8+
9+
class EchoRequestInfo(BaseHTTPRequestHandler):
10+
def do_GET(self):
11+
self.send_response(200)
12+
self.send_header("Content-type", "text/plain")
13+
self.end_headers()
14+
self.wfile.write(f"{self.requestline}\n".encode())
15+
self.wfile.write(f"{self.headers}\n".encode())
16+
17+
18+
if __name__ == "__main__":
19+
ap = argparse.ArgumentParser()
20+
ap.add_argument("--port", type=int)
21+
args = ap.parse_args()
22+
23+
httpd = HTTPServer(("127.0.0.1", args.port), EchoRequestInfo)
24+
httpd.serve_forever()

tests/resources/jupyter_server_config.py

+6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ def my_env():
5151
return {"MYVAR": "String with escaped {{var}}"}
5252

5353

54+
# Traitlets configuration for test-serverprocessentrypoint in the dummyentrypoint package
55+
c.CustomServerProcessEntryPoint.request_headers_override = {
56+
"X-Custom-Header": "custom-configurable"
57+
}
58+
59+
5460
c.ServerProxy.servers = {
5561
"python-http": {
5662
"command": [sys.executable, _get_path("httpinfo.py"), "--port={port}"],

tests/test_proxies.py

+8
Original file line numberDiff line numberDiff line change
@@ -528,3 +528,11 @@ async def test_server_proxy_rawsocket(
528528
await conn.write_message(msg)
529529
res = await conn.read_message()
530530
assert res == msg.swapcase()
531+
532+
533+
def test_server_configurable_class(a_server_port_and_token: Tuple[int, str]) -> None:
534+
PORT, TOKEN = a_server_port_and_token
535+
r = request_get(PORT, "/test-serverprocessentrypoint/", TOKEN, host="127.0.0.1")
536+
assert r.code == 200
537+
s = r.read().decode("ascii")
538+
assert "X-Custom-Header: custom-configurable\n" in s

0 commit comments

Comments
 (0)