diff --git a/CHANGELOG.md b/CHANGELOG.md index 6109e5728..c42f0a1f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1586,6 +1586,7 @@ raw template {%- endblock in_prompt -%} """ + exporter_attr = AttrExporter() output_attr, _ = exporter_attr.from_notebook_node(nb) assert "raw template" in output_attr diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index cd305afbf..cee32410f 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -17,7 +17,16 @@ from textwrap import dedent, fill from jupyter_core.application import JupyterApp, base_aliases, base_flags -from traitlets import Bool, DottedObjectName, Instance, List, Type, Unicode, default, observe +from traitlets import ( + Bool, + DottedObjectName, + Instance, + List, + Type, + Unicode, + default, + observe, +) from traitlets.config import Configurable, catch_config_error from traitlets.utils.importstring import import_item @@ -60,6 +69,7 @@ def validate(self, obj, value): "writer": "NbConvertApp.writer_class", "post": "NbConvertApp.postprocessor_class", "output": "NbConvertApp.output_base", + "newline": "NbConvertApp.newline", "output-dir": "FilesWriter.build_directory", "reveal-prefix": "SlidesExporter.reveal_url_prefix", "nbformat": "NotebookExporter.nbformat_version", @@ -120,7 +130,10 @@ def validate(self, obj, value): ), "coalesce-streams": ( { - "NbConvertApp": {"use_output_suffix": False, "export_format": "notebook"}, + "NbConvertApp": { + "use_output_suffix": False, + "export_format": "notebook", + }, "FilesWriter": {"build_directory": ""}, "CoalesceStreamsPreprocessor": {"enabled": True}, }, @@ -186,6 +199,11 @@ def validate(self, obj, value): }, """Whether the HTML in Markdown cells and cell outputs should be sanitized..""", ), + "use-crlf-newline": ( + {"NbConvertApp": {"newline": "\r\n"}}, + "Implies CLRF newline.", + ), + "use-lf-newline": ({"NbConvertApp": {"newline": "\n"}}, "Implies LF newline."), } ) @@ -311,6 +329,12 @@ def _writer_class_changed(self, change): new = self.writer_aliases[new.lower()] self.writer_factory = import_item(new) + newline = Unicode( + None, + help="""The line ending to use when writing text, defaults to builtin:`open`""", + allow_none=True, + ).tag(config=True) + # Post-processor specific variables postprocessor = Instance( "nbconvert.postprocessors.base.PostProcessorBase", @@ -523,7 +547,9 @@ def write_single_notebook(self, output, resources): if not self.writer: msg = "No writer object defined!" raise ValueError(msg) - return self.writer.write(output, resources, notebook_name=notebook_name) + return self.writer.write( + output, resources, notebook_name=notebook_name, newline=self.newline + ) def postprocess_single_notebook(self, write_results): """Step 4: Post-process the written file @@ -628,7 +654,13 @@ def document_config_options(self): """ categories = { category: [c for c in self._classes_inc_parents() if category in c.__name__.lower()] - for category in ["app", "exporter", "writer", "preprocessor", "postprocessor"] + for category in [ + "app", + "exporter", + "writer", + "preprocessor", + "postprocessor", + ] } accounted_for = {c for category in categories.values() for c in category} categories["other"] = [c for c in self._classes_inc_parents() if c not in accounted_for] diff --git a/nbconvert/writers/files.py b/nbconvert/writers/files.py index 8349360a4..dc7b2ca7c 100644 --- a/nbconvert/writers/files.py +++ b/nbconvert/writers/files.py @@ -77,7 +77,7 @@ def _write_items(self, items, build_dir): with open(dest, "wb") as f: f.write(data) - def write(self, output, resources, notebook_name=None, **kw): + def write(self, output, resources, notebook_name=None, newline=None, **kw): """ Consume and write Jinja output to the file system. Output directory is set via the 'build_directory' variable of this instance (a @@ -147,11 +147,25 @@ def write(self, output, resources, notebook_name=None, **kw): dest_path = Path(build_directory) / dest # Write conversion results. - self.log.info("Writing %i bytes to %s", len(output), dest_path) + if "\\\\" in repr(newline): + self.log.warning( + "Argument 'newline' contains escaped escape characters: %s", + format(repr(newline)), + ) + if isinstance(output, str): - with open(dest_path, "w", encoding="utf-8") as f: + self.log.info( + "Writing %i len string to %s, (repr(newline): %s)", + len(output), + dest_path, + repr(newline), + ) + with open(dest_path, "w", encoding="utf-8", newline=newline) as f: f.write(output) + else: + # newline only applies in text mode. + self.log.info("Writing %i bytes to %s", len(output), dest_path) with open(dest_path, "wb") as f: f.write(output)