-
Notifications
You must be signed in to change notification settings - Fork 384
/
magics.py
203 lines (172 loc) · 6.96 KB
/
magics.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""Escape Jupyter magics when converting to other formats"""
import re
from .languages import _COMMENT, _SCRIPT_EXTENSIONS, usual_language_name
from .stringparser import StringParser
def get_comment(ext):
return re.escape(_SCRIPT_EXTENSIONS[ext]["comment"])
# A magic expression is a line or cell or metakernel magic (#94, #61) escaped zero, or multiple times
_MAGIC_RE = {
_SCRIPT_EXTENSIONS[ext]["language"]: re.compile(
r"^\s*({0} |{0})*(%|%%|%%%)[a-zA-Z]".format(get_comment(ext))
)
for ext in _SCRIPT_EXTENSIONS
}
_MAGIC_FORCE_ESC_RE = {
_SCRIPT_EXTENSIONS[ext]["language"]: re.compile(
r"^\s*({0} |{0})*(%|%%|%%%)[a-zA-Z](.*){0}\s*escape".format(get_comment(ext))
)
for ext in _SCRIPT_EXTENSIONS
}
_MAGIC_NOT_ESC_RE = {
_SCRIPT_EXTENSIONS[ext]["language"]: re.compile(
r"^\s*({0} |{0})*(%|%%|%%%)[a-zA-Z](.*){0}\s*noescape".format(get_comment(ext))
)
for ext in _SCRIPT_EXTENSIONS
}
_LINE_CONTINUATION_RE = re.compile(r".*\\\s*$")
# Rust magics start with single ':' #351
_MAGIC_RE["rust"] = re.compile(r"^(// |//)*:[a-zA-Z]")
_MAGIC_FORCE_ESC_RE["rust"] = re.compile(r"^(// |//)*:[a-zA-Z](.*)//\s*escape")
_MAGIC_FORCE_ESC_RE["rust"] = re.compile(r"^(// |//)*:[a-zA-Z](.*)//\s*noescape")
# C# magics start with '#!'
_MAGIC_RE["csharp"] = re.compile(r"^(// |//)*#![a-zA-Z]")
_MAGIC_FORCE_ESC_RE["csharp"] = re.compile(r"^(// |//)*#![a-zA-Z](.*)//\s*escape")
_MAGIC_FORCE_ESC_RE["csharp"] = re.compile(r"^(// |//)*#![a-zA-Z](.*)//\s*noescape")
# Commands starting with a question or exclamation mark have to be escaped
_PYTHON_HELP_OR_BASH_CMD = re.compile(r"^\s*(# |#)*\s*(\?|!)\s*[A-Za-z\.\~\$\\\/]")
# A bash command not followed by an equal sign or a parenthesis is a magic command
_PYTHON_MAGIC_CMD = re.compile(
r"^(# |#)*({})($|\s$|\s[^=,])".format(
"|".join(
# posix
["cat", "cd", "cp", "mv", "rm", "rmdir", "mkdir"]
+ # noqa: W504
# windows
["copy", "ddir", "echo", "ls", "ldir", "mkdir", "ren", "rmdir"]
)
)
)
# Python help commands end with ?
_IPYTHON_MAGIC_HELP = re.compile(r"^\s*(# )*[^\s]*\?\s*$")
_PYTHON_MAGIC_ASSIGN = re.compile(
r"^(# |#)*\s*([a-zA-Z_][a-zA-Z_$0-9]*)\s*=\s*(%|%%|%%%|!)[a-zA-Z](.*)"
)
_SCRIPT_LANGUAGES = [_SCRIPT_EXTENSIONS[ext]["language"] for ext in _SCRIPT_EXTENSIONS]
def is_magic(line, language, global_escape_flag=True, explicitly_code=False):
"""Is the current line a (possibly escaped) Jupyter magic, and should it be commented?"""
language = usual_language_name(language)
if language in ["octave", "matlab"] or language not in _SCRIPT_LANGUAGES:
return False
if _MAGIC_FORCE_ESC_RE[language].match(line):
return True
if not global_escape_flag or _MAGIC_NOT_ESC_RE[language].match(line):
return False
if _MAGIC_RE[language].match(line):
return True
if language != "python":
return False
if _PYTHON_HELP_OR_BASH_CMD.match(line):
return True
if _PYTHON_MAGIC_ASSIGN.match(line):
return True
if explicitly_code and _IPYTHON_MAGIC_HELP.match(line):
return True
return _PYTHON_MAGIC_CMD.match(line)
def need_explicit_marker(
source, language="python", global_escape_flag=True, explicitly_code=True
):
"""Does this code needs an explicit cell marker?"""
if language != "python" or not global_escape_flag or not explicitly_code:
return False
parser = StringParser(language)
for line in source:
if not parser.is_quoted() and is_magic(
line, language, global_escape_flag, explicitly_code
):
if not is_magic(line, language, global_escape_flag, False):
return True
parser.read_line(line)
return False
def comment_magic(
source, language="python", global_escape_flag=True, explicitly_code=True
):
"""Escape Jupyter magics with '# '"""
parser = StringParser(language)
next_is_magic = False
for pos, line in enumerate(source):
if not parser.is_quoted() and (
next_is_magic
or is_magic(line, language, global_escape_flag, explicitly_code)
):
if next_is_magic:
# this is the continuation line of a magic command on the previous line,
# so we don't want to indent the comment
unindented = line
indent = ""
else:
unindented = line.lstrip()
indent = line[: len(line) - len(unindented)]
source[pos] = indent + _COMMENT[language] + " " + unindented
next_is_magic = language == "python" and _LINE_CONTINUATION_RE.match(line)
parser.read_line(line)
return source
def unesc(line, language):
"""Uncomment once a commented line"""
comment = _COMMENT[language]
unindented = line.lstrip()
indent = line[: len(line) - len(unindented)]
if unindented.startswith(comment + " "):
return indent + unindented[len(comment) + 1 :]
if unindented.startswith(comment):
return indent + unindented[len(comment) :]
return line
def uncomment_magic(
source, language="python", global_escape_flag=True, explicitly_code=True
):
"""Unescape Jupyter magics"""
parser = StringParser(language)
next_is_magic = False
for pos, line in enumerate(source):
if not parser.is_quoted() and (
next_is_magic
or is_magic(line, language, global_escape_flag, explicitly_code)
):
source[pos] = unesc(line, language)
next_is_magic = language == "python" and _LINE_CONTINUATION_RE.match(line)
parser.read_line(line)
return source
_ESCAPED_CODE_START = {
".Rmd": re.compile(r"^(# |#)*```{.*}"),
".md": re.compile(r"^(# |#)*```"),
".markdown": re.compile(r"^(# |#)*```"),
}
_ESCAPED_CODE_START.update(
{
ext: re.compile(r"^({0} |{0})*({0}|{0} )\+".format(get_comment(ext)))
for ext in _SCRIPT_EXTENSIONS
}
)
def is_escaped_code_start(line, ext):
"""Is the current line a possibly commented code start marker?"""
return _ESCAPED_CODE_START[ext].match(line)
def escape_code_start(source, ext, language="python"):
"""Escape code start with '# '"""
parser = StringParser(language)
for pos, line in enumerate(source):
if not parser.is_quoted() and is_escaped_code_start(line, ext):
source[pos] = (
_SCRIPT_EXTENSIONS.get(ext, {}).get("comment", "#") + " " + line
)
parser.read_line(line)
return source
def unescape_code_start(source, ext, language="python"):
"""Unescape code start"""
parser = StringParser(language)
for pos, line in enumerate(source):
if not parser.is_quoted() and is_escaped_code_start(line, ext):
unescaped = unesc(line, language)
# don't remove comment char if we break the code start...
if is_escaped_code_start(unescaped, ext):
source[pos] = unescaped
parser.read_line(line)
return source