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 new_code_cell, new_notebook, new_text_cell, new_worksheet
24from .rwbase import NotebookReader, NotebookWriter
25
26# -----------------------------------------------------------------------------
27# Code
28# -----------------------------------------------------------------------------
29
30_encoding_declaration_re = re.compile(r"^#.*coding[:=]\s*([-\w.]+)")
31
32
33class PyReaderError(Exception):
34 """An error raised by the PyReader."""
35
36
37class PyReader(NotebookReader):
38 """A Python notebook reader."""
39
40 def reads(self, s, **kwargs):
41 """Convert a string to a notebook."""
42 return self.to_notebook(s, **kwargs)
43
44 def to_notebook(self, s, **kwargs):
45 """Convert a string to a notebook."""
46 lines = s.splitlines()
47 cells = []
48 cell_lines: list[str] = []
49 state = "codecell"
50 for line in lines:
51 if line.startswith("# <nbformat>") or _encoding_declaration_re.match(line):
52 pass
53 elif line.startswith("# <codecell>"):
54 cell = self.new_cell(state, cell_lines)
55 if cell is not None:
56 cells.append(cell)
57 state = "codecell"
58 cell_lines = []
59 elif line.startswith("# <htmlcell>"):
60 cell = self.new_cell(state, cell_lines)
61 if cell is not None:
62 cells.append(cell)
63 state = "htmlcell"
64 cell_lines = []
65 elif line.startswith("# <markdowncell>"):
66 cell = self.new_cell(state, cell_lines)
67 if cell is not None:
68 cells.append(cell)
69 state = "markdowncell"
70 cell_lines = []
71 else:
72 cell_lines.append(line)
73 if cell_lines and state == "codecell":
74 cell = self.new_cell(state, cell_lines)
75 if cell is not None:
76 cells.append(cell)
77 ws = new_worksheet(cells=cells)
78 return new_notebook(worksheets=[ws])
79
80 def new_cell(self, state, lines):
81 """Create a new cell."""
82 if state == "codecell":
83 input_ = "\n".join(lines)
84 input_ = input_.strip("\n")
85 if input_:
86 return new_code_cell(input=input_)
87 elif state == "htmlcell":
88 text = self._remove_comments(lines)
89 if text:
90 return new_text_cell("html", source=text)
91 elif state == "markdowncell":
92 text = self._remove_comments(lines)
93 if text:
94 return new_text_cell("markdown", source=text)
95
96 def _remove_comments(self, lines):
97 new_lines = []
98 for line in lines:
99 if line.startswith("#"):
100 new_lines.append(line[2:])
101 else:
102 new_lines.append(line)
103 text = "\n".join(new_lines)
104 text = text.strip("\n")
105 return text # noqa: RET504
106
107 def split_lines_into_blocks(self, lines):
108 """Split lines into code blocks."""
109 if len(lines) == 1:
110 yield lines[0]
111 raise StopIteration()
112
113 source = "\n".join(lines)
114 code = ast.parse(source)
115 starts = [x.lineno - 1 for x in code.body]
116 for i in range(len(starts) - 1):
117 yield "\n".join(lines[starts[i] : starts[i + 1]]).strip("\n")
118 yield "\n".join(lines[starts[-1] :]).strip("\n")
119
120
121class PyWriter(NotebookWriter):
122 """A Python notebook writer."""
123
124 def writes(self, nb, **kwargs):
125 """Convert a notebook object to a string."""
126 lines = ["# -*- coding: utf-8 -*-"]
127 lines.extend(["# <nbformat>2</nbformat>", ""])
128 for ws in nb.worksheets:
129 for cell in ws.cells:
130 if cell.cell_type == "code":
131 input_ = cell.get("input")
132 if input_ is not None:
133 lines.extend(["# <codecell>", ""])
134 lines.extend(input_.splitlines())
135 lines.append("")
136 elif cell.cell_type == "html":
137 input_ = cell.get("source")
138 if input_ is not None:
139 lines.extend(["# <htmlcell>", ""])
140 lines.extend(["# " + line for line in input_.splitlines()])
141 lines.append("")
142 elif cell.cell_type == "markdown":
143 input_ = cell.get("source")
144 if input_ is not None:
145 lines.extend(["# <markdowncell>", ""])
146 lines.extend(["# " + line for line in input_.splitlines()])
147 lines.append("")
148 lines.append("")
149 return str("\n".join(lines))
150
151
152_reader = PyReader()
153_writer = PyWriter()
154
155reads = _reader.reads
156read = _reader.read
157to_notebook = _reader.to_notebook
158write = _writer.write
159writes = _writer.writes