Skip to content

Commit 39907fa

Browse files
author
Release Manager
committed
gh-37878: Run sage doc server for jupyterlab <!-- ^ Please provide a concise and informative title. --> <!-- ^ Don't put issue numbers in the title, do this in the PR description below. --> <!-- ^ For example, instead of "Fixes #12345" use "Introduce new method to calculate 1 + 2". --> <!-- v Describe your changes below in detail. --> <!-- v Why is this change required? What problem does it solve? --> <!-- v If this PR resolves an open issue, please link to it here. For example, "Fixes #12345". --> Reopen #35139 because of the renewed interest: https://groups.google.com/g/sage-devel/c/kzSWB8ps7VA New environment variables added to `sage.env`: ``` SAGE_DOC_SERVER_URL = var("SAGE_DOC_SERVER_URL") SAGE_DOC_LOCAL_PORT = var("SAGE_DOC_LOCAL_PORT", "0") ``` When Jupyter notebook launches, - If `SAGE_DOC_SERVER_URL` is set, the url is used; - else if local sage documentation is installed, a doc http server with port `SAGE_DOC_LOCAL_PORT` starts. If the port is 0 (the default), then a random port is selected by the system and stored to the environment variable; - else the online official documentation website https://doc.sagemath.org is used for the help menu of the Jupyter notebook. Fixes #34794. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [ ] I have linked a relevant issue or discussion. - [ ] I have created tests covering the changes. - [ ] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - #12345: short description why this is a dependency --> <!-- - #34567: ... --> URL: #37878 Reported by: Kwankyu Lee Reviewer(s): Kwankyu Lee, Marc Culler
2 parents 58c4079 + 1617269 commit 39907fa

File tree

3 files changed

+83
-34
lines changed

3 files changed

+83
-34
lines changed

src/bin/sage-notebook

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import ast
77
import argparse
88
import logging
99
import textwrap
10+
from contextlib import contextmanager
11+
1012
logging.basicConfig()
1113
logger = logging.getLogger()
1214

@@ -19,7 +21,6 @@ _system_jupyter_url = "https://doc.sagemath.org/html/en/installation/launching.h
1921
class NotebookJupyter():
2022

2123
def print_banner(self):
22-
banner()
2324
print('Please wait while the Sage Jupyter Notebook server starts...')
2425

2526
@classmethod
@@ -50,7 +51,6 @@ class NotebookJupyter():
5051

5152
class NotebookJupyterlab():
5253
def print_banner(self):
53-
banner()
5454
print('Please wait while the Jupyterlab server starts...')
5555

5656
@classmethod
@@ -77,7 +77,6 @@ class NotebookJupyterlab():
7777
class SageNBExport(NotebookJupyter):
7878

7979
def print_banner(self):
80-
banner()
8180
print('Please wait while the SageNB export server starts...')
8281

8382
@classmethod
@@ -131,7 +130,6 @@ EXAMPLES:
131130
132131
"""
133132

134-
135133
notebook_launcher = {
136134
'default': NotebookJupyter, # change this to change the default
137135
'ipython': NotebookJupyter,
@@ -140,7 +138,6 @@ notebook_launcher = {
140138
'export': SageNBExport,
141139
}
142140

143-
144141
notebook_names = ', '.join(notebook_launcher.keys())
145142

146143

@@ -187,6 +184,32 @@ def trac_23428_browser_workaround():
187184
os.environ['BROWSER'] = 'open'
188185

189186

187+
@contextmanager
188+
def sage_doc_server():
189+
from functools import partial
190+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
191+
from threading import Thread
192+
from sage.env import SAGE_DOC, SAGE_DOC_LOCAL_PORT as port
193+
194+
server = ThreadingHTTPServer(('127.0.0.1', int(port)),
195+
partial(SimpleHTTPRequestHandler, directory=SAGE_DOC))
196+
197+
if port == '0':
198+
port = str(server.server_address[1])
199+
os.environ['SAGE_DOC_LOCAL_PORT'] = port
200+
201+
server_thread = Thread(target=server.serve_forever, name="sage_doc_server")
202+
server_thread.start()
203+
print(f'Sage doc server started running at http://127.0.0.1:{port}')
204+
205+
try:
206+
yield
207+
finally:
208+
server.shutdown()
209+
server_thread.join()
210+
print(f'Sage doc server stopped runnning at http://127.0.0.1:{port}')
211+
212+
190213
if __name__ == '__main__':
191214
parser = make_parser()
192215
args, unknown = parser.parse_known_args(sys.argv[1:])
@@ -227,4 +250,19 @@ if __name__ == '__main__':
227250
launcher.print_help()
228251
sys.exit(0)
229252

230-
launcher(unknown)
253+
banner()
254+
255+
# Start a Sage doc server if the Sage documentation is available locally.
256+
# See the corresponding code in src/sage/repl/ipython_kernel/kernel.py.
257+
258+
from sage.env import SAGE_DOC_SERVER_URL
259+
from sage.features.sagemath import sagemath_doc_html
260+
261+
if SAGE_DOC_SERVER_URL:
262+
print(f'Sage doc server is running at {SAGE_DOC_SERVER_URL}')
263+
launcher(unknown)
264+
elif sagemath_doc_html().is_present():
265+
with sage_doc_server():
266+
launcher(unknown)
267+
else:
268+
launcher(unknown)

src/sage/env.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ def var(key: str, *fallbacks: Optional[str], force: bool = False) -> Optional[st
187187
SAGE_PKGS = var("SAGE_PKGS", join(SAGE_ROOT, "build", "pkgs"))
188188
SAGE_ROOT_GIT = var("SAGE_ROOT_GIT", join(SAGE_ROOT, ".git"))
189189

190+
# Sage doc server (local server with PORT if URL is not given)
191+
SAGE_DOC_SERVER_URL = var("SAGE_DOC_SERVER_URL")
192+
# The default port is 0 so that the system will assign a random unused port > 1024
193+
SAGE_DOC_LOCAL_PORT = var("SAGE_DOC_LOCAL_PORT", "0")
194+
190195
# ~/.sage
191196
DOT_SAGE = var("DOT_SAGE", join(os.environ.get("HOME"), ".sage"))
192197
SAGE_STARTUP_FILE = var("SAGE_STARTUP_FILE", join(DOT_SAGE, "init.sage"))

src/sage/repl/ipython_kernel/kernel.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -100,62 +100,68 @@ def help_links(self):
100100
sage: sk = SageKernel.__new__(SageKernel)
101101
sage: sk.help_links
102102
[{'text': 'Sage Documentation',
103-
'url': 'https://doc.sagemath.org/html/en/index.html'},
103+
'url': '.../html/en/index.html'},
104104
...]
105105
"""
106-
# DEPRECATED: The URLs in the form 'kernelspecs/...' were used for
107-
# classical Jupyter notebooks. For instance,
108-
#
109-
# 'kernelspecs/sagemath/doc/html/en/index.html'
110-
#
111-
# is constructed by kernel_url('doc/html/en/index.html'), but these
112-
# URLs of local files don't work for JupyterLab. Hence all URLs here
113-
# have been replaced with URLs of online documents.
114-
115-
from sage.repl.ipython_kernel.install import SageKernelSpec
116-
identifier = SageKernelSpec.identifier()
117-
118-
def kernel_url(x):
119-
# URLs starting with 'kernelspecs' are prepended by the
120-
# browser with the appropriate path
121-
return 'kernelspecs/{0}/{1}'.format(identifier, x)
106+
# A Sage doc server starts when Jupyter notebook launches if the Sage
107+
# documentation is available locally. See the corresponding code in
108+
# src/bin/sage-notebook.
109+
110+
from sage.env import SAGE_DOC_SERVER_URL
111+
from sage.features.sagemath import sagemath_doc_html
112+
113+
if SAGE_DOC_SERVER_URL:
114+
def doc_url(path):
115+
return f'{SAGE_DOC_SERVER_URL}/{path}'
116+
elif sagemath_doc_html().is_present():
117+
from sage.env import SAGE_DOC_LOCAL_PORT as port
118+
119+
def doc_url(path):
120+
return f'http://127.0.0.1:{port}/{path}'
121+
else:
122+
def doc_url(path):
123+
return f'https://doc.sagemath.org/{path}'
122124

123125
return [
124126
{
125127
'text': 'Sage Documentation',
126-
'url': "https://doc.sagemath.org/html/en/index.html",
128+
'url': doc_url('html/en/index.html'),
127129
},
128130
{
129131
'text': 'A Tour of Sage',
130-
'url': "https://doc.sagemath.org/html/en/a_tour_of_sage/index.html",
132+
'url': doc_url('html/en/a_tour_of_sage/index.html'),
131133
},
132134
{
133135
'text': 'Tutorial',
134-
'url': "https://doc.sagemath.org/html/en/tutorial/index.html",
136+
'url': doc_url('html/en/tutorial/index.html'),
135137
},
136138
{
137139
'text': 'Thematic Tutorials',
138-
'url': "https://doc.sagemath.org/html/en/thematic_tutorials/index.html",
140+
'url': doc_url('html/en/thematic_tutorials/index.html'),
139141
},
140142
{
141143
'text': 'PREP Tutorials',
142-
'url': "https://doc.sagemath.org/html/en/prep/index.html",
144+
'url': doc_url('html/en/prep/index.html'),
143145
},
144146
{
145147
'text': 'Constructions',
146-
'url': "https://doc.sagemath.org/html/en/constructions/index.html",
148+
'url': doc_url('html/en/constructions/index.html'),
147149
},
148150
{
149151
'text': 'FAQ',
150-
'url': "https://doc.sagemath.org/html/en/faq/index.html",
152+
'url': doc_url('html/en/faq/index.html'),
153+
},
154+
{
155+
'text': 'Reference Manual',
156+
'url': doc_url('html/en/reference/index.html'),
151157
},
152158
{
153-
'text': 'Reference',
154-
'url': "https://doc.sagemath.org/html/en/reference/index.html",
159+
'text': "Installation Guide",
160+
'url': doc_url('html/en/installation/index.html'),
155161
},
156162
{
157-
'text': "Developer's Guide",
158-
'url': "https://doc.sagemath.org/html/en/developer/index.html",
163+
'text': "Developer Guide",
164+
'url': doc_url('html/en/developer/index.html'),
159165
},
160166
{
161167
'text': "Python",

0 commit comments

Comments
 (0)