Skip to content

Commit a7b4069

Browse files
authored
Add option to hide code cell in Jupyter notebooks (#183)
* Initial commit adding option to hide code cells when rendering Jupyter notebooks * add types-six to requirements.txt * update types * skip parameter type check * add types-click to requirements.txt * Add types-Flask to requirements.txt * rename noinput * add feature to hide input cells tagged with 'remove_input' * rename no_input to hide_all_input; rename no_tag_input to hide_tagged_input * Update changelog for hide Jupyter code cell API * update parameters to match click * fix hide tagged input not working for static mode * hide_all_input will take precedence if both hide_all_input and hide_tagged_input are selected * Revert "hide_all_input will take precedence if both hide_all_input and hide_tagged_input are selected" This reverts commit 8e7e687. * hide_all_input will take precedence when both hide_all_input and hide_tagged_input are selected * update manifest creation so hide input options don't overwrite one another * update defaults * add jupyter manifest handling without changing default manifest format * update defaults * add Python 2 compatibility for hide cell feature * Update README with Hide Jupyter Notebook Input Code Cells * move version checking * update readme wrt hide input cell Jupyter dependencies * update TagRemovePreprocessor command for Python 2 * update TagRemovePreprocessor for Python 2 * update TagRemovePreprocessor to remove quotes for jupyter cli parsing
1 parent 11d584b commit a7b4069

File tree

5 files changed

+128
-12
lines changed

5 files changed

+128
-12
lines changed

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
78
## [1.5.4] - TBD
89

910
### Added
@@ -15,6 +16,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1516
rsconnect-python does not inspect the file contents to identify the object name, which must be
1617
one of the default names that Connect expects (`app`, `application`, `create_app`, or `make_app`).
1718

19+
- Ability to hide code cells when rendering Jupyter notebooks.
20+
21+
After setting up Connect and rsconnect-python, the user can render a Jupyter notebook without its corresponding code cells by passing the ' hide-all-input' flag through the rsconnect cli:
22+
23+
```
24+
rsconnect deploy notebook \
25+
-n server \
26+
-k APIKey \
27+
--hide-all-input \
28+
hello_world.ipynb
29+
```
30+
31+
To selectively hide the input of cells, the user can add a tag call 'hide_input' to the cell, then pass the ' hide-tagged-input' flag through the rsconnect cli:
32+
33+
```
34+
rsconnect deploy notebook \
35+
-n server \
36+
-k APIKey \
37+
--hide-tagged-input \
38+
hello_world.ipynb
39+
```
40+
1841
## [1.5.3] - 2021-05-06
1942

2043
### Added

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,35 @@ directory specified above.
513513
<div style="display:none">
514514
Generated from <code>rsconnect-python {{ rsconnect_python.version }}</code>
515515
</div>
516+
517+
### Hide Jupyter Notebook Input Code Cells
518+
519+
The user can render a Jupyter notebook without its corresponding input code cells by passing the '--hide-all-input' flag through the cli:
520+
521+
```
522+
rsconnect deploy notebook \
523+
--server https://connect.example.org:3939 \
524+
--api-key my-api-key \
525+
--hide-all-input \
526+
my-notebook.ipynb
527+
```
528+
529+
To selectively hide input cells in a Jupyter notebook, the user needs to follow a two step process:
530+
1. tag cells with the 'hide_input' tag,
531+
2. then pass the ' --hide-tagged-input' flag through the cli:
532+
533+
```
534+
rsconnect deploy notebook \
535+
--server https://connect.example.org:3939 \
536+
--api-key my-api-key \
537+
--hide-tagged-input \
538+
my-notebook.ipynb
539+
```
540+
541+
By default, rsconnect-python does not install Jupyter notebook related depenencies. These dependencies are installed via rsconnect-jupyter. When the user is using the hide input features in rsconnect-python by itself without rsconnect-jupyter, he/she needs to install the following package depenecies:
542+
543+
```
544+
notebook
545+
nbformat
546+
nbconvert>=5.6.1
547+
```

rsconnect/actions.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,8 @@ def deploy_jupyter_notebook(
473473
conda_mode=False,
474474
force_generate=False,
475475
log_callback=None,
476+
hide_all_input=False,
477+
hide_tagged_input=False,
476478
):
477479
"""
478480
A function to deploy a Jupyter notebook to Connect. Depending on the files involved
@@ -496,6 +498,8 @@ def deploy_jupyter_notebook(
496498
(the default) the lines from the deployment log will be returned as a sequence.
497499
If a log callback is provided, then None will be returned for the log lines part
498500
of the return tuple.
501+
:param hide_all_input: if True, will hide all input cells when rendering output
502+
:param hide_tagged_input: If True, will hide input code cells with the 'hide_input' tag when rendering output
499503
:return: the ultimate URL where the deployed app may be accessed and the sequence
500504
of log lines. The log lines value will be None if a log callback was provided.
501505
"""
@@ -504,7 +508,9 @@ def deploy_jupyter_notebook(
504508
connect_server, app_store, file_name, new, app_id, title, static
505509
)
506510
python, environment = get_python_env_info(file_name, python, conda_mode=conda_mode, force_generate=force_generate,)
507-
bundle = create_notebook_deployment_bundle(file_name, extra_files, app_mode, python, environment)
511+
bundle = create_notebook_deployment_bundle(
512+
file_name, extra_files, app_mode, python, environment, hide_all_input, hide_tagged_input
513+
)
508514
return _finalize_deploy(
509515
connect_server,
510516
app_store,
@@ -1097,6 +1103,8 @@ def get_python_env_info(file_name, python, conda_mode=False, force_generate=Fals
10971103

10981104
def create_notebook_deployment_bundle(
10991105
file_name, extra_files, app_mode, python, environment, extra_files_need_validating=True,
1106+
hide_all_input=None,
1107+
hide_tagged_input=None,
11001108
):
11011109
"""
11021110
Create an in-memory bundle, ready to deploy.
@@ -1107,6 +1115,8 @@ def create_notebook_deployment_bundle(
11071115
:param python: information about the version of Python being used.
11081116
:param environment: environmental information.
11091117
:param extra_files_need_validating: a flag indicating whether the list of extra
1118+
:param hide_all_input: if True, will hide all input cells when rendering output
1119+
:param hide_tagged_input: If True, will hide input code cells with the 'hide_input' tag when rendering output
11101120
files should be validated or not. Part of validating includes qualifying each
11111121
with the parent directory of the notebook file. If you provide False here, make
11121122
sure the names are properly qualified first.
@@ -1119,13 +1129,13 @@ def create_notebook_deployment_bundle(
11191129

11201130
if app_mode == AppModes.STATIC:
11211131
try:
1122-
return make_notebook_html_bundle(file_name, python)
1132+
return make_notebook_html_bundle(file_name, python, hide_all_input, hide_tagged_input)
11231133
except subprocess.CalledProcessError as exc:
11241134
# Jupyter rendering failures are often due to
11251135
# user code failing, vs. an internal failure of rsconnect-python.
11261136
raise api.RSConnectException(str(exc))
11271137
else:
1128-
return make_notebook_source_bundle(file_name, environment, extra_files)
1138+
return make_notebook_source_bundle(file_name, environment, extra_files, hide_all_input, hide_tagged_input)
11291139

11301140

11311141
def create_api_deployment_bundle(
@@ -1190,7 +1200,13 @@ def spool_deployment_log(connect_server, app, log_callback):
11901200

11911201

11921202
def create_notebook_manifest_and_environment_file(
1193-
entry_point_file, environment, app_mode=None, extra_files=None, force=True
1203+
entry_point_file,
1204+
environment,
1205+
app_mode=None,
1206+
extra_files=None,
1207+
force=True,
1208+
hide_all_input=False,
1209+
hide_tagged_input=False,
11941210
):
11951211
"""
11961212
Creates and writes a manifest.json file for the given notebook entry point file.
@@ -1206,13 +1222,18 @@ def create_notebook_manifest_and_environment_file(
12061222
:param extra_files: any extra files that should be included in the manifest.
12071223
:param force: if True, forces the environment file to be written. even if it
12081224
already exists.
1225+
:param hide_all_input: if True, will hide all input cells when rendering output
1226+
:param hide_tagged_input: If True, will hide input code cells with the 'hide_input' tag when rendering output
12091227
:return:
12101228
"""
1211-
if not write_notebook_manifest_json(entry_point_file, environment, app_mode, extra_files) or force:
1229+
if (
1230+
not write_notebook_manifest_json(entry_point_file, environment, app_mode, extra_files, hide_all_input, hide_tagged_input)
1231+
or force
1232+
):
12121233
write_environment_file(environment, dirname(entry_point_file))
12131234

12141235

1215-
def write_notebook_manifest_json(entry_point_file, environment, app_mode=None, extra_files=None):
1236+
def write_notebook_manifest_json(entry_point_file, environment, app_mode, extra_files, hide_all_input, hide_tagged_input):
12161237
"""
12171238
Creates and writes a manifest.json file for the given entry point file. If
12181239
the application mode is not provided, an attempt will be made to resolve one
@@ -1225,6 +1246,8 @@ def write_notebook_manifest_json(entry_point_file, environment, app_mode=None, e
12251246
:param app_mode: the application mode to assume. If this is None, the extension
12261247
portion of the entry point file name will be used to derive one.
12271248
:param extra_files: any extra files that should be included in the manifest.
1249+
:param hide_all_input: if True, will hide all input cells when rendering output
1250+
:param hide_tagged_input: If True, will hide input code cells with the 'hide_input' tag when rendering output
12281251
:return: whether or not the environment file (requirements.txt, environment.yml,
12291252
etc.) that goes along with the manifest exists.
12301253
"""

rsconnect/bundle.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ def bundle_add_buffer(bundle, filename, contents):
137137
bundle.addfile(file_info, buf)
138138

139139

140-
def write_manifest(relative_dir, nb_name, environment, output_dir):
141-
# type: (str, str, Environment, str) -> typing.Tuple[list, list]
140+
def write_manifest(relative_dir, nb_name, environment, output_dir, hide_all_input=False, hide_tagged_input=False):
141+
# type: (...) -> typing.Tuple[list, list]
142142
"""Create a manifest for source publishing the specified notebook.
143143
144144
The manifest will be written to `manifest.json` in the output directory..
@@ -148,6 +148,12 @@ def write_manifest(relative_dir, nb_name, environment, output_dir):
148148
"""
149149
manifest_filename = "manifest.json"
150150
manifest = make_source_manifest(nb_name, environment, AppModes.JUPYTER_NOTEBOOK)
151+
if hide_all_input:
152+
if 'jupyter' not in manifest: manifest['jupyter']= {}
153+
manifest['jupyter'].update({'hide_all_input': hide_all_input})
154+
if hide_tagged_input:
155+
if 'jupyter' not in manifest: manifest['jupyter']= {}
156+
manifest['jupyter'].update({'hide_tagged_input': hide_tagged_input})
151157
manifest_file = join(output_dir, manifest_filename)
152158
created = []
153159
skipped = []
@@ -205,6 +211,8 @@ def make_notebook_source_bundle(
205211
file, # type: str
206212
environment, # type: Environment
207213
extra_files=None, # type: typing.Optional[typing.List[str]]
214+
hide_all_input=False,
215+
hide_tagged_input=False,
208216
):
209217
# type: (...) -> typing.IO[bytes]
210218
"""Create a bundle containing the specified notebook and python environment.
@@ -217,6 +225,12 @@ def make_notebook_source_bundle(
217225
nb_name = basename(file)
218226

219227
manifest = make_source_manifest(nb_name, environment, AppModes.JUPYTER_NOTEBOOK)
228+
if hide_all_input:
229+
if 'jupyter' not in manifest: manifest['jupyter']= {}
230+
manifest['jupyter'].update({'hide_all_input': hide_all_input})
231+
if hide_tagged_input:
232+
if 'jupyter' not in manifest: manifest['jupyter']= {}
233+
manifest['jupyter'].update({'hide_tagged_input': hide_tagged_input})
220234
manifest_add_file(manifest, nb_name, base_dir)
221235
manifest_add_buffer(manifest, environment.filename, environment.contents)
222236

@@ -259,6 +273,8 @@ def make_html_manifest(filename):
259273
def make_notebook_html_bundle(
260274
filename, # type: str
261275
python, # type: str
276+
hide_all_input=False,
277+
hide_tagged_input=False,
262278
check_output=subprocess.check_output, # type: typing.Callable
263279
):
264280
# type: (...) -> typing.IO[bytes]
@@ -274,6 +290,14 @@ def make_notebook_html_bundle(
274290
"--to=html",
275291
filename,
276292
]
293+
if hide_all_input and hide_tagged_input or hide_all_input:
294+
cmd.append('--no-input')
295+
elif hide_tagged_input:
296+
version = check_output([python, '--version']).decode("utf-8")
297+
if version >= 'Python 3':
298+
cmd.append('--TagRemovePreprocessor.remove_input_tags=hide_input')
299+
else:
300+
cmd.append("--TagRemovePreprocessor.remove_input_tags=['hide_input']")
277301
try:
278302
output = check_output(cmd)
279303
except subprocess.CalledProcessError:

rsconnect/main.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,8 @@ def _deploy_bundle(
522522
"--force-generate", "-g", is_flag=True, help='Force generating "requirements.txt", even if it already exists.',
523523
)
524524
@click.option("--verbose", "-v", is_flag=True, help="Print detailed messages.")
525+
@click.option("--hide-all-input", is_flag=True, default=False, help="Hide all input cells when rendering output")
526+
@click.option("--hide-tagged-input", is_flag=True, default=False, help="Hide input code cells with the 'hide_input' tag")
525527
@click.argument("file", type=click.Path(exists=True, dir_okay=False, file_okay=True))
526528
@click.argument(
527529
"extra_files", nargs=-1, type=click.Path(exists=True, dir_okay=False, file_okay=True),
@@ -542,6 +544,8 @@ def deploy_notebook(
542544
verbose,
543545
file,
544546
extra_files,
547+
hide_all_input,
548+
hide_tagged_input,
545549
):
546550
set_verbosity(verbose)
547551

@@ -570,8 +574,9 @@ def deploy_notebook(
570574
_warn_on_ignored_requirements(dirname(file), environment.filename)
571575

572576
with cli_feedback("Creating deployment bundle"):
573-
bundle = create_notebook_deployment_bundle(file, extra_files, app_mode, python, environment, False)
574-
577+
bundle = create_notebook_deployment_bundle(
578+
file, extra_files, app_mode, python, environment, False, hide_all_input, hide_tagged_input
579+
)
575580
_deploy_bundle(
576581
connect_server, app_store, file, app_id, app_mode, deployment_name, title, default_title, bundle,
577582
)
@@ -936,12 +941,16 @@ def write_manifest():
936941
@click.option(
937942
"--force-generate", "-g", is_flag=True, help='Force generating "requirements.txt", even if it already exists.',
938943
)
944+
@click.option("--hide-all-input", help="Hide all input cells when rendering output")
945+
@click.option("--hide-tagged-input", is_flag=True, default=None, help="Hide input code cells with the 'hide_input' tag")
939946
@click.option("--verbose", "-v", "verbose", is_flag=True, help="Print detailed messages")
940947
@click.argument("file", type=click.Path(exists=True, dir_okay=False, file_okay=True))
941948
@click.argument(
942949
"extra_files", nargs=-1, type=click.Path(exists=True, dir_okay=False, file_okay=True),
943950
)
944-
def write_manifest_notebook(overwrite, python, conda, force_generate, verbose, file, extra_files):
951+
def write_manifest_notebook(
952+
overwrite, python, conda, force_generate, verbose, file, extra_files, hide_all_input=None, hide_tagged_input=None
953+
):
945954
set_verbosity(verbose)
946955
with cli_feedback("Checking arguments"):
947956
validate_file_is_notebook(file)
@@ -960,7 +969,12 @@ def write_manifest_notebook(overwrite, python, conda, force_generate, verbose, f
960969

961970
with cli_feedback("Creating manifest.json"):
962971
environment_file_exists = write_notebook_manifest_json(
963-
file, environment, AppModes.JUPYTER_NOTEBOOK, extra_files
972+
file,
973+
environment,
974+
AppModes.JUPYTER_NOTEBOOK,
975+
extra_files,
976+
hide_all_input,
977+
hide_tagged_input,
964978
)
965979

966980
if environment_file_exists and not force_generate:

0 commit comments

Comments
 (0)