Skip to content

Commit 83809d1

Browse files
authored
Explicitly use utf-8 encoding for everything (#61)
* Explicitly use utf-8 encoding for everything * Remove nbconvert<6 requirement * Add release note
1 parent 69ac51d commit 83809d1

14 files changed

+60
-47
lines changed

HISTORY.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
# History
22

3+
## 0.3.0 (Unreleased)
4+
5+
- Explicitly set all input and output file encodings to UTF-8, which fixes a bug with HTML exports on Windows with `nbconvert` v6.0.
6+
- This is not expected to cause any backwards compatibility issues. However, in the _very_ unlikely instance that your `jupyter_notebook_config.py` file or your `nbautoexport.json` config file is Windows-1252-encoded _and_ contains non-ASCII characters, you will need to convert them to UTF-8. ([#57](https://github.com/drivendataorg/nbautoexport/issues/57), [#61](https://github.com/drivendataorg/nbautoexport/pull/61))
7+
38
## 0.2.1 (2020-09-18)
49

5-
- `nbconvert` released verion 6, which breaks some functionality. Pinning to `nbconvert<6` until we can address [#57](https://github.com/drivendataorg/nbautoexport/issues/57).
10+
- `nbconvert` released version 6, which may break HTML exports on Windows. Pinning to `nbconvert<6` until we can address [#57](https://github.com/drivendataorg/nbautoexport/issues/57).
611

712
## 0.2.0 (2020-09-04)
813

nbautoexport/export.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ def postprocess(self, input: str):
3737
return
3838

3939
# Rewrite converted file to new path, removing cell numbers
40-
with input.open("r") as f:
40+
with input.open("r", encoding="utf-8") as f:
4141
text = f.read()
42-
with new_path.open("w") as f:
42+
with new_path.open("w", encoding="utf-8") as f:
4343
f.write(re.sub(r"\n#\sIn\[(([0-9]+)|(\s))\]:\n{2}", "", text))
4444

4545
# For some formats, we also need to move the assets directory, for stuff like images
@@ -80,7 +80,7 @@ def post_save(model: dict, os_path: str, contents_manager: FileContentsManager):
8080

8181
if should_convert:
8282
config = NbAutoexportConfig.parse_file(
83-
path=save_progress_indicator, content_type="application/json"
83+
path=save_progress_indicator, content_type="application/json", encoding="utf-8"
8484
)
8585
export_notebook(os_path, config=config)
8686

nbautoexport/jupyter_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ def install_post_save_hook(config_path: Optional[Path] = None):
5454
if not config_path.exists():
5555
logger.debug(f"No existing Jupyter configuration detected at {config_path}. Creating...")
5656
config_path.parent.mkdir(exist_ok=True, parents=True)
57-
with config_path.open("w") as fp:
57+
with config_path.open("w", encoding="utf-8") as fp:
5858
fp.write(post_save_hook_initialize_block)
5959
logger.info("nbautoexport post-save hook installed.")
6060
return
6161

6262
# If config exists, check for existing nbautoexport initialize block and install as appropriate
6363
logger.debug(f"Detected existing Jupyter configuration at {config_path}")
6464

65-
with config_path.open("r") as fp:
65+
with config_path.open("r", encoding="utf-8") as fp:
6666
config = fp.read()
6767

6868
if block_regex.search(config):
@@ -78,7 +78,7 @@ def install_post_save_hook(config_path: Optional[Path] = None):
7878

7979
if parse_version(existing_version) < parse_version(__version__):
8080
logger.info(f"Updating nbautoexport post-save hook with version {__version__}...")
81-
with config_path.open("w") as fp:
81+
with config_path.open("w", encoding="utf-8") as fp:
8282
# Open as w replaces existing file. We're replacing entire config.
8383
fp.write(block_regex.sub(post_save_hook_initialize_block, config))
8484
else:

nbautoexport/nbautoexport.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ def clean(
9999
sentinel_path = directory / SAVE_PROGRESS_INDICATOR_FILE
100100
validate_sentinel_path(sentinel_path)
101101

102-
config = NbAutoexportConfig.parse_file(path=sentinel_path, content_type="application/json")
102+
config = NbAutoexportConfig.parse_file(
103+
path=sentinel_path, content_type="application/json", encoding="utf-8"
104+
)
103105

104106
# Combine exclude patterns from config and command-line
105107
config.clean.exclude.extend(exclude)
@@ -204,7 +206,9 @@ def export(
204206
# Configuration: input options override existing sentinel file
205207
if sentinel_path.exists():
206208
typer.echo(f"Reading existing configuration file from {sentinel_path} ...")
207-
config = NbAutoexportConfig.parse_file(path=sentinel_path, content_type="application/json")
209+
config = NbAutoexportConfig.parse_file(
210+
path=sentinel_path, content_type="application/json", encoding="utf-8"
211+
)
208212

209213
# Overrides
210214
if len(export_formats) > 0:
@@ -338,7 +342,7 @@ def configure(
338342
(Path(jupyter_config_dir()) / "jupyter_notebook_config.py").expanduser().resolve()
339343
)
340344
if jupyter_config_file.exists():
341-
with jupyter_config_file.open("r") as fp:
345+
with jupyter_config_file.open("r", encoding="utf-8") as fp:
342346
jupyter_config_text = fp.read()
343347
if block_regex.search(jupyter_config_text):
344348
installed = True

nbautoexport/sentinel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,5 @@ def install_sentinel(directory: Path, config: NbAutoexportConfig, overwrite: boo
6060
else:
6161
logger.info(f"Creating configuration file at {sentinel_path}")
6262
logger.info(f"\n{config.json(indent=2)}")
63-
with sentinel_path.open("w") as fp:
63+
with sentinel_path.open("w", encoding="utf-8") as fp:
6464
fp.write(config.json(indent=2))

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
jupyter-contrib-nbextensions>=0.5.1
2-
nbconvert>=5.6.1,<6
2+
nbconvert>=5.6.1
33
packaging
44
pydantic
55
typer>=0.3.0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
def load_requirements(path: Path):
1212
requirements = []
13-
with path.open("r") as fp:
13+
with path.open("r", encoding="utf-8") as fp:
1414
for line in fp.readlines():
1515
if line.startswith("-r"):
1616
requirements += load_requirements(line.split(" ")[1].strip())

tests/test_cli_clean.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def notebooks_dir(tmp_path, notebook_asset):
5858
def test_clean(notebooks_dir, need_confirmation, organize_by):
5959
sentinel_path = notebooks_dir / SAVE_PROGRESS_INDICATOR_FILE
6060
config = NbAutoexportConfig(export_formats=EXPECTED_FORMATS, organize_by=organize_by)
61-
with sentinel_path.open("w") as fp:
61+
with sentinel_path.open("w", encoding="utf-8") as fp:
6262
fp.write(config.json())
6363

6464
if need_confirmation:
@@ -87,7 +87,7 @@ def test_clean_relative(notebooks_dir, organize_by):
8787
with working_directory(notebooks_dir):
8888
sentinel_path = Path(SAVE_PROGRESS_INDICATOR_FILE)
8989
config = NbAutoexportConfig(export_formats=EXPECTED_FORMATS, organize_by=organize_by)
90-
with sentinel_path.open("w") as fp:
90+
with sentinel_path.open("w", encoding="utf-8") as fp:
9191
fp.write(config.json())
9292

9393
result = CliRunner().invoke(app, ["clean", "."], input="y")
@@ -119,7 +119,7 @@ def test_clean_relative_subdirectory(notebooks_dir, organize_by):
119119

120120
sentinel_path = subdir / SAVE_PROGRESS_INDICATOR_FILE
121121
config = NbAutoexportConfig(export_formats=EXPECTED_FORMATS, organize_by=organize_by)
122-
with sentinel_path.open("w") as fp:
122+
with sentinel_path.open("w", encoding="utf-8") as fp:
123123
fp.write(config.json())
124124

125125
result = CliRunner().invoke(app, ["clean", "subdir"], input="y")
@@ -168,7 +168,7 @@ def test_clean_exclude(notebooks_dir):
168168
organize_by="extension",
169169
clean=CleanConfig(exclude=["keep.txt", "images/*.jpg"]),
170170
)
171-
with sentinel_path.open("w") as fp:
171+
with sentinel_path.open("w", encoding="utf-8") as fp:
172172
fp.write(config.json())
173173

174174
command = ["clean", "subdir", "-e", "**/*.md"]
@@ -199,7 +199,7 @@ def test_clean_exclude(notebooks_dir):
199199

200200
def test_clean_abort(notebooks_dir):
201201
sentinel_path = notebooks_dir / SAVE_PROGRESS_INDICATOR_FILE
202-
with sentinel_path.open("w") as fp:
202+
with sentinel_path.open("w", encoding="utf-8") as fp:
203203
fp.write(NbAutoexportConfig(export_formats=EXPECTED_FORMATS).json())
204204

205205
starting_files = set(notebooks_dir.glob("**/*"))
@@ -216,7 +216,7 @@ def test_clean_abort(notebooks_dir):
216216

217217
def test_clean_dry_run(notebooks_dir):
218218
sentinel_path = notebooks_dir / SAVE_PROGRESS_INDICATOR_FILE
219-
with sentinel_path.open("w") as fp:
219+
with sentinel_path.open("w", encoding="utf-8") as fp:
220220
fp.write(NbAutoexportConfig(export_formats=EXPECTED_FORMATS).json())
221221

222222
starting_files = set(notebooks_dir.glob("**/*"))

tests/test_cli_configure.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ def test_configure_defaults(tmp_path):
2020
assert result.exit_code == 0
2121

2222
config = NbAutoexportConfig.parse_file(
23-
path=tmp_path / SAVE_PROGRESS_INDICATOR_FILE, content_type="application/json"
23+
path=tmp_path / SAVE_PROGRESS_INDICATOR_FILE,
24+
content_type="application/json",
25+
encoding="utf-8",
2426
)
2527

2628
expected_config = NbAutoexportConfig()
@@ -45,7 +47,9 @@ def test_configure_specified(tmp_path):
4547
assert result.exit_code == 0
4648

4749
config = NbAutoexportConfig.parse_file(
48-
path=tmp_path / SAVE_PROGRESS_INDICATOR_FILE, content_type="application/json"
50+
path=tmp_path / SAVE_PROGRESS_INDICATOR_FILE,
51+
content_type="application/json",
52+
encoding="utf-8",
4953
)
5054

5155
expected_config = NbAutoexportConfig(
@@ -91,7 +95,7 @@ def test_force_overwrite(tmp_path):
9195
app, ["configure", str(tmp_path), "-o", "-f", "script", "-f", "html", "-b", "notebook"]
9296
)
9397
assert result.exit_code == 0
94-
with (tmp_path / ".nbautoexport").open("r") as fp:
98+
with (tmp_path / ".nbautoexport").open("r", encoding="utf-8") as fp:
9599
config = json.load(fp)
96100

97101
expected_config = NbAutoexportConfig(export_formats=["script", "html"], organize_by="notebook")
@@ -120,7 +124,7 @@ def test_configure_oudated_initialize_warning(tmp_path, monkeypatch):
120124
monkeypatch.setenv("JUPYTER_CONFIG_DIR", str(tmp_path))
121125

122126
jupyter_config_path = tmp_path / "jupyter_notebook_config.py"
123-
with jupyter_config_path.open("w") as fp:
127+
with jupyter_config_path.open("w", encoding="utf-8") as fp:
124128
initialize_block = jupyter_config.version_regex.sub(
125129
"0", jupyter_config.post_save_hook_initialize_block
126130
)

tests/test_cli_export.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def test_export_with_config_no_cli_opts(notebooks_dir, input_type, organize_by):
9595

9696
sentinel_path = notebooks_dir / SAVE_PROGRESS_INDICATOR_FILE
9797
config = NbAutoexportConfig(export_formats=EXPECTED_FORMATS, organize_by=organize_by)
98-
with sentinel_path.open("w") as fp:
98+
with sentinel_path.open("w", encoding="utf-8") as fp:
9999
fp.write(config.json())
100100

101101
result = CliRunner().invoke(app, ["export", input_path])
@@ -125,7 +125,7 @@ def test_export_with_config_with_cli_opts(notebooks_dir, input_type, organize_by
125125

126126
sentinel_path = notebooks_dir / SAVE_PROGRESS_INDICATOR_FILE
127127
written_config = NbAutoexportConfig()
128-
with sentinel_path.open("w") as fp:
128+
with sentinel_path.open("w", encoding="utf-8") as fp:
129129
fp.write(written_config.json())
130130

131131
expected_config = NbAutoexportConfig(export_formats=EXPECTED_FORMATS, organize_by=organize_by)
@@ -156,7 +156,7 @@ def test_export_relative(notebooks_dir, input_type, organize_by):
156156

157157
sentinel_path = Path(SAVE_PROGRESS_INDICATOR_FILE)
158158
config = NbAutoexportConfig(export_formats=EXPECTED_FORMATS, organize_by=organize_by)
159-
with sentinel_path.open("w") as fp:
159+
with sentinel_path.open("w", encoding="utf-8") as fp:
160160
fp.write(config.json())
161161

162162
if input_type == "dir":
@@ -190,7 +190,7 @@ def test_clean_relative_subdirectory(notebooks_dir, input_type, organize_by):
190190

191191
sentinel_path = subdir / SAVE_PROGRESS_INDICATOR_FILE
192192
config = NbAutoexportConfig(export_formats=EXPECTED_FORMATS, organize_by=organize_by)
193-
with sentinel_path.open("w") as fp:
193+
with sentinel_path.open("w", encoding="utf-8") as fp:
194194
fp.write(config.json())
195195

196196
expected_notebooks = find_notebooks(subdir)

tests/test_cli_install.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def test_install_new_config(tmp_path, monkeypatch):
1313
assert result.exit_code == 0
1414
assert config_path.exists()
1515

16-
with config_path.open("r") as fp:
16+
with config_path.open("r", encoding="utf-8") as fp:
1717
config = fp.read()
1818
assert config == jupyter_config.post_save_hook_initialize_block
1919

@@ -23,15 +23,15 @@ def test_install_existing_config(tmp_path, monkeypatch):
2323

2424
config_path = tmp_path / "jupyter_notebook_config.py"
2525

26-
with config_path.open("w") as fp:
26+
with config_path.open("w", encoding="utf-8") as fp:
2727
fp.write("print('hello world!')")
2828
assert config_path.exists()
2929

3030
result = CliRunner().invoke(app, ["install"])
3131
assert result.exit_code == 0
3232
assert config_path.exists()
3333

34-
with config_path.open("r") as fp:
34+
with config_path.open("r", encoding="utf-8") as fp:
3535
config = fp.read()
3636
assert config == (
3737
"print('hello world!')" + "\n" + jupyter_config.post_save_hook_initialize_block
@@ -45,23 +45,23 @@ def test_install_new_config_with_path(tmp_path):
4545
assert result.exit_code == 0
4646
assert config_path.exists()
4747

48-
with config_path.open("r") as fp:
48+
with config_path.open("r", encoding="utf-8") as fp:
4949
config = fp.read()
5050
assert config == jupyter_config.post_save_hook_initialize_block
5151

5252

5353
def test_install_existing_config_with_path(tmp_path):
5454
config_path = tmp_path / "nonstandard_config.py"
5555

56-
with config_path.open("w") as fp:
56+
with config_path.open("w", encoding="utf-8") as fp:
5757
fp.write("print('hello world!')")
5858
assert config_path.exists()
5959

6060
result = CliRunner().invoke(app, ["install", "--jupyter-config", str(config_path)])
6161
assert result.exit_code == 0
6262
assert config_path.exists()
6363

64-
with config_path.open("r") as fp:
64+
with config_path.open("r", encoding="utf-8") as fp:
6565
config = fp.read()
6666
assert config == (
6767
"print('hello world!')" + "\n" + jupyter_config.post_save_hook_initialize_block

tests/test_export.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def test_post_save_no_sentinel(notebooks_dir):
6262
def test_post_save_organize_by_notebook(notebooks_dir):
6363
notebook_path = notebooks_dir / "the_notebook.ipynb"
6464
sentinel_path = notebooks_dir / SAVE_PROGRESS_INDICATOR_FILE
65-
with sentinel_path.open("w") as fp:
65+
with sentinel_path.open("w", encoding="utf-8") as fp:
6666
json.dump(
6767
NbAutoexportConfig(export_formats=["script", "html"], organize_by="notebook").dict(),
6868
fp,
@@ -84,7 +84,7 @@ def test_post_save_organize_by_notebook(notebooks_dir):
8484
def test_post_save_organize_by_extension(notebooks_dir):
8585
notebook_path = notebooks_dir / "the_notebook.ipynb"
8686
sentinel_path = notebooks_dir / SAVE_PROGRESS_INDICATOR_FILE
87-
with sentinel_path.open("w") as fp:
87+
with sentinel_path.open("w", encoding="utf-8") as fp:
8888
json.dump(
8989
NbAutoexportConfig(export_formats=["script", "html"], organize_by="extension").dict(),
9090
fp,
@@ -109,7 +109,7 @@ def test_post_save_type_file(notebooks_dir):
109109
"""Test that post_save should do nothing if model type is 'file'."""
110110
notebook_path = notebooks_dir / "the_notebook.ipynb"
111111
sentinel_path = notebooks_dir / SAVE_PROGRESS_INDICATOR_FILE
112-
with sentinel_path.open("w") as fp:
112+
with sentinel_path.open("w", encoding="utf-8") as fp:
113113
json.dump(NbAutoexportConfig().dict(), fp)
114114

115115
assert notebook_path.exists()
@@ -125,7 +125,7 @@ def test_post_save_type_directory(notebooks_dir):
125125
"""Test that post_save should do nothing if model type is 'directory'."""
126126
notebook_path = notebooks_dir / "the_notebook.ipynb"
127127
sentinel_path = notebooks_dir / SAVE_PROGRESS_INDICATOR_FILE
128-
with sentinel_path.open("w") as fp:
128+
with sentinel_path.open("w", encoding="utf-8") as fp:
129129
json.dump(NbAutoexportConfig().dict(), fp)
130130

131131
assert notebook_path.exists()

0 commit comments

Comments
 (0)