Skip to content

Commit 027dfba

Browse files
authored
Merge pull request #888 from krinsman/step7_8
Step7_8 (Add ability to write default config file)
2 parents 7124c89 + 2999deb commit 027dfba

File tree

3 files changed

+88
-5
lines changed

3 files changed

+88
-5
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,13 @@ publishing, and we'd love to hear from you.
193193

194194
#### Config file
195195

196-
Newer versions of NBViewer will be configurable using a config file, `nbviewer_config.py`. In the directory where you run the command `python -m nbviewer` to start NBViewer, also add a file `nbviewer_config.py` which uses [the standard configuration syntax for Jupyter projects](https://traitlets.readthedocs.io/en/stable/config.html).
196+
NBViewer is configurable using a config file, by default called `nbviewer_config.py`. You can modify the name and location of the config file that NBViewer looks for using the `--config-file` command line flag. (The location is always a relative path, i.e. relative to where the command `python -m nbviewer` is run, and never an absolute path.)
197197

198-
For example, to configure the value of a configurable `foo`, add the line `c.NBViewer.foo = 'bar'` to the `nbviewer_config.py` file located where you run `python -m nbviewer`. Again, currently very few features of NBViewer are configurable this way, but we hope to steadily increase the number of configurable characteristics of NBViewer in future releases.
198+
If you don't know which attributes of NBViewer you can configure using the config file, run `python -m nbviewer --generate-config` (or `python -m nbviewer --generate-config --config-file="my_custom_name.py"`) to write a default config file which has all of the configurable options commented out and set to their default values. To change a configurable option to a new value, uncomment the corresponding line and change the default value to the new value.
199+
200+
The config file uses [the standard configuration syntax for Jupyter projects](https://traitlets.readthedocs.io/en/stable/config.html).
201+
202+
One thing this allows you to do, for example, is to write your custom implementations of any of the standard page rendering [handlers](https://www.tornadoweb.org/en/stable/guide/structure.html#subclassing-requesthandler) included in NBViewer, e.g. by subclassing the original handlers to include custom logic along with custom output possibilities, and then have these custom handlers always loaded by default, by modifying the corresponding lines in the config file. This is effectively another way to extend NBViewer.
199203

200204
## Securing the Notebook Viewer
201205

nbviewer/app.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ class NBViewer(Application):
7777

7878
name = Unicode('nbviewer')
7979

80-
config_file = Unicode('nbviewer_config.py', help="The config file to load").tag(config=True)
81-
8280
# Use this to insert custom configuration of handlers for NBViewer extensions
8381
handler_settings = Dict().tag(config=True)
8482

@@ -335,9 +333,47 @@ def init_tornado_application(self):
335333
# create the app
336334
self.tornado_application = web.Application(handlers, debug=options.debug, **settings)
337335

336+
# Mostly copied from JupyterHub because if it isn't broken then don't fix it
337+
def write_config_file(self):
338+
"""Write our default config to a .py config file"""
339+
config_file_dir = os.path.dirname(os.path.abspath(options.config_file))
340+
if not os.path.isdir(config_file_dir):
341+
self.exit("{} does not exist. The destination directory must exist before generating config file.".format(config_file_dir))
342+
if os.path.exists(options.config_file) and not options.answer_yes:
343+
answer = ''
344+
345+
def ask():
346+
prompt = "Overwrite %s with default config? [y/N]" % options.config_file
347+
try:
348+
return input(prompt).lower() or 'n'
349+
except KeyboardInterrupt:
350+
print('') # empty line
351+
return 'n'
352+
353+
answer = ask()
354+
while not answer.startswith(('y', 'n')):
355+
print("Please answer 'yes' or 'no'")
356+
answer = ask()
357+
if answer.startswith('n'):
358+
self.exit("Not overwriting config file with default.")
359+
360+
# Inherited method from traitlets.Application
361+
config_text = self.generate_config_file()
362+
if isinstance(config_text, bytes):
363+
config_text = config_text.decode('utf8')
364+
print("Writing default config to: %s" % options.config_file)
365+
with open(options.config_file, mode='w') as f:
366+
f.write(config_text)
367+
self.exit("Wrote default config file.")
368+
338369
def __init__(self, *args, **kwargs):
339370
super().__init__(*args, **kwargs)
340-
self.load_config_file(self.config_file)
371+
372+
if options.generate_config:
373+
self.write_config_file()
374+
375+
# Inherited method from traitlets.Application
376+
self.load_config_file(options.config_file)
341377
self.init_tornado_application()
342378

343379
def init_options():
@@ -353,14 +389,17 @@ def init_options():
353389
else:
354390
default_host, default_port = '0.0.0.0', 5000
355391

392+
define("answer_yes", default=False, help="Answer yes to any questions (e.g. confirm overwrite).", type=bool)
356393
define("base_url", default='/', help='URL base for the server')
357394
define("binder_base_url", default="https://mybinder.org/v2", help="URL base for binder notebook execution service", type=str)
358395
define("cache_expiry_max", default=2*60*60, help="maximum cache expiry (seconds)", type=int)
359396
define("cache_expiry_min", default=10*60, help="minimum cache expiry (seconds)", type=int)
397+
define("config_file", default='nbviewer_config.py', help="The config file to load", type=str)
360398
define("content_security_policy", default="connect-src 'none';", help="Content-Security-Policy header setting", type=str)
361399
define("debug", default=False, help="run in debug mode", type=bool)
362400
define("default_format", default="html", help="format to use for legacy / URLs", type=str)
363401
define("frontpage", default=FRONTPAGE_JSON, help="path to json file containing frontpage content", type=str)
402+
define("generate_config", default=False, help="Generate default config file and then stop.", type=bool)
364403
define("host", default=default_host, help="run on the given interface", type=str)
365404
define("ipywidgets_base_url", default="https://unpkg.com/", help="URL base for ipywidgets JS package", type=str)
366405
define("jupyter_js_widgets_version", default="*", help="Version specifier for jupyter-js-widgets JS package", type=str)

nbviewer/tests/test_app.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import os
2+
import sys
3+
4+
from tempfile import NamedTemporaryFile
5+
from subprocess import PIPE
6+
from subprocess import Popen
7+
8+
# Also copied mostly from JupyterHub since again -- if not broken, don't fix.
9+
def test_generate_config():
10+
with NamedTemporaryFile(prefix='nbviewer_config', suffix='.py') as tf:
11+
cfg_file = tf.name
12+
with open(cfg_file, 'w') as f:
13+
f.write("c.A = 5")
14+
p = Popen(
15+
[sys.executable, '-m', 'nbviewer', '--generate-config', '--config-file={}'.format(cfg_file)],
16+
stdout=PIPE,
17+
stdin=PIPE,
18+
)
19+
out, _ = p.communicate(b'n')
20+
out = out.decode('utf8', 'replace')
21+
assert os.path.exists(cfg_file)
22+
with open(cfg_file) as f:
23+
cfg_text = f.read()
24+
assert cfg_text == 'c.A = 5'
25+
26+
p = Popen(
27+
[sys.executable, '-m', 'nbviewer', '--generate-config', '--config-file={}'.format(cfg_file)],
28+
stdout=PIPE,
29+
stdin=PIPE,
30+
)
31+
out, _ = p.communicate(b'x\ny')
32+
out = out.decode('utf8', 'replace')
33+
assert os.path.exists(cfg_file)
34+
with open(cfg_file) as f:
35+
cfg_text = f.read()
36+
os.remove(cfg_file)
37+
assert cfg_file in out
38+
assert 'NBViewer.name' not in cfg_text # This shouldn't be configurable
39+
assert 'NBViewer.local_handler' in cfg_text
40+
assert 'NBViewer.static_path' in cfg_text

0 commit comments

Comments
 (0)