1"""Read and write notebooks as regular .py files.
2
3Authors:
4
5* Brian Granger
6"""
7
8# -----------------------------------------------------------------------------
9# Copyright (C) 2008-2011 The IPython Development Team
10#
11# Distributed under the terms of the BSD License. The full license is in
12# the file LICENSE, distributed as part of this software.
13# -----------------------------------------------------------------------------
14
15# -----------------------------------------------------------------------------
16# Imports
17# -----------------------------------------------------------------------------
18from __future__ import annotations
19
20import ast
21import re
22
23from .nbbase import (
24 nbformat,
25 nbformat_minor,
26 new_code_cell,
27 new_heading_cell,
28 new_notebook,
29 new_text_cell,
30 new_worksheet,
31)
32from .rwbase import NotebookReader, NotebookWriter
33
34# -----------------------------------------------------------------------------
35# Code
36# -----------------------------------------------------------------------------
37
38_encoding_declaration_re = re.compile(r"^#.*coding[:=]\s*([-\w.]+)")
39
40
41class PyReaderError(Exception):
42 """An error raised for a pyreader error."""
43
44
45class PyReader(NotebookReader):
46 """A python notebook reader."""
47
48 def reads(self, s, **kwargs):
49 """Convert a string to a notebook"""
50 return self.to_notebook(s, **kwargs)
51
52 def to_notebook(self, s, **kwargs):
53 """Convert a string to a notebook"""
54 lines = s.splitlines()
55 cells = []
56 cell_lines: list[str] = []
57 kwargs = {}
58 state = "codecell"
59 for line in lines:
60 if line.startswith("# <nbformat>") or _encoding_declaration_re.match(line):
61 pass
62 elif line.startswith("# <codecell>"):
63 cell = self.new_cell(state, cell_lines, **kwargs)
64 if cell is not None:
65 cells.append(cell)
66 state = "codecell"
67 cell_lines = []
68 kwargs = {}
69 elif line.startswith("# <htmlcell>"):
70 cell = self.new_cell(state, cell_lines, **kwargs)
71 if cell is not None:
72 cells.append(cell)
73 state = "htmlcell"
74 cell_lines = []
75 kwargs = {}
76 elif line.startswith("# <markdowncell>"):
77 cell = self.new_cell(state, cell_lines, **kwargs)
78 if cell is not None:
79 cells.append(cell)
80 state = "markdowncell"
81 cell_lines = []
82 kwargs = {}
83 # VERSIONHACK: plaintext -> raw
84 elif line.startswith(("# <rawcell>", "# <plaintextcell>")):
85 cell = self.new_cell(state, cell_lines, **kwargs)
86 if cell is not None:
87 cells.append(cell)
88 state = "rawcell"
89 cell_lines = []
90 kwargs = {}
91 elif line.startswith("# <headingcell"):
92 cell = self.new_cell(state, cell_lines, **kwargs)
93 if cell is not None:
94 cells.append(cell)
95 cell_lines = []
96 m = re.match(r"# <headingcell level=(?P<level>\d)>", line)
97 if m is not None:
98 state = "headingcell"
99 kwargs = {}
100 kwargs["level"] = int(m.group("level"))
101 else:
102 state = "codecell"
103 kwargs = {}
104 cell_lines = []
105 else:
106 cell_lines.append(line)
107 if cell_lines and state == "codecell":
108 cell = self.new_cell(state, cell_lines)
109 if cell is not None:
110 cells.append(cell)
111 ws = new_worksheet(cells=cells)
112 return new_notebook(worksheets=[ws])
113
114 def new_cell(self, state, lines, **kwargs):
115 """Create a new cell."""
116 if state == "codecell":
117 input_ = "\n".join(lines)
118 input_ = input_.strip("\n")
119 if input_:
120 return new_code_cell(input=input_)
121 elif state == "htmlcell":
122 text = self._remove_comments(lines)
123 if text:
124 return new_text_cell("html", source=text)
125 elif state == "markdowncell":
126 text = self._remove_comments(lines)
127 if text:
128 return new_text_cell("markdown", source=text)
129 elif state == "rawcell":
130 text = self._remove_comments(lines)
131 if text:
132 return new_text_cell("raw", source=text)
133 elif state == "headingcell":
134 text = self._remove_comments(lines)
135 level = kwargs.get("level", 1)
136 if text:
137 return new_heading_cell(source=text, level=level)
138
139 def _remove_comments(self, lines):
140 new_lines = []
141 for line in lines:
142 if line.startswith("#"):
143 new_lines.append(line[2:])
144 else:
145 new_lines.append(line)
146 text = "\n".join(new_lines)
147 text = text.strip("\n")
148 return text # noqa: RET504
149
150 def split_lines_into_blocks(self, lines):
151 """Split lines into code blocks."""
152 if len(lines) == 1:
153 yield lines[0]
154 raise StopIteration()
155
156 source = "\n".join(lines)
157 code = ast.parse(source)
158 starts = [x.lineno - 1 for x in code.body]
159 for i in range(len(starts) - 1):
160 yield "\n".join(lines[starts[i] : starts[i + 1]]).strip("\n")
161 yield "\n".join(lines[starts[-1] :]).strip("\n")
162
163
164class PyWriter(NotebookWriter):
165 """A Python notebook writer."""
166
167 def writes(self, nb, **kwargs):
168 """Convert a notebook to a string."""
169 lines = ["# -*- coding: utf-8 -*-"]
170 lines.extend(
171 [
172 "# <nbformat>%i.%i</nbformat>" % (nbformat, nbformat_minor),
173 "",
174 ]
175 )
176 for ws in nb.worksheets:
177 for cell in ws.cells:
178 if cell.cell_type == "code":
179 input_ = cell.get("input")
180 if input_ is not None:
181 lines.extend(["# <codecell>", ""])
182 lines.extend(input_.splitlines())
183 lines.append("")
184 elif cell.cell_type == "html":
185 input_ = cell.get("source")
186 if input_ is not None:
187 lines.extend(["# <htmlcell>", ""])
188 lines.extend(["# " + line for line in input_.splitlines()])
189 lines.append("")
190 elif cell.cell_type == "markdown":
191 input_ = cell.get("source")
192 if input_ is not None:
193 lines.extend(["# <markdowncell>", ""])
194 lines.extend(["# " + line for line in input_.splitlines()])
195 lines.append("")
196 elif cell.cell_type == "raw":
197 input_ = cell.get("source")
198 if input_ is not None:
199 lines.extend(["# <rawcell>", ""])
200 lines.extend(["# " + line for line in input_.splitlines()])
201 lines.append("")
202 elif cell.cell_type == "heading":
203 input_ = cell.get("source")
204 level = cell.get("level", 1)
205 if input_ is not None:
206 lines.extend(["# <headingcell level=%s>" % level, ""])
207 lines.extend(["# " + line for line in input_.splitlines()])
208 lines.append("")
209 lines.append("")
210 return "\n".join(lines)
211
212
213_reader = PyReader()
214_writer = PyWriter()
215
216reads = _reader.reads
217read = _reader.read
218to_notebook = _reader.to_notebook
219write = _writer.write
220writes = _writer.writes