##// END OF EJS Templates
Merge pull request #9838 from takluyver/i9723...
Matthias Bussonnier -
r22763:345bf7d2 merge
parent child Browse files
Show More
@@ -1,374 +1,382 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 Class and program to colorize python source code for ANSI terminals.
3 Class and program to colorize python source code for ANSI terminals.
4
4
5 Based on an HTML code highlighter by Jurgen Hermann found at:
5 Based on an HTML code highlighter by Jurgen Hermann found at:
6 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52298
6 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52298
7
7
8 Modifications by Fernando Perez (fperez@colorado.edu).
8 Modifications by Fernando Perez (fperez@colorado.edu).
9
9
10 Information on the original HTML highlighter follows:
10 Information on the original HTML highlighter follows:
11
11
12 MoinMoin - Python Source Parser
12 MoinMoin - Python Source Parser
13
13
14 Title: Colorize Python source using the built-in tokenizer
14 Title: Colorize Python source using the built-in tokenizer
15
15
16 Submitter: Jurgen Hermann
16 Submitter: Jurgen Hermann
17 Last Updated:2001/04/06
17 Last Updated:2001/04/06
18
18
19 Version no:1.2
19 Version no:1.2
20
20
21 Description:
21 Description:
22
22
23 This code is part of MoinMoin (http://moin.sourceforge.net/) and converts
23 This code is part of MoinMoin (http://moin.sourceforge.net/) and converts
24 Python source code to HTML markup, rendering comments, keywords,
24 Python source code to HTML markup, rendering comments, keywords,
25 operators, numeric and string literals in different colors.
25 operators, numeric and string literals in different colors.
26
26
27 It shows how to use the built-in keyword, token and tokenize modules to
27 It shows how to use the built-in keyword, token and tokenize modules to
28 scan Python source code and re-emit it with no changes to its original
28 scan Python source code and re-emit it with no changes to its original
29 formatting (which is the hard part).
29 formatting (which is the hard part).
30 """
30 """
31 from __future__ import print_function
31 from __future__ import print_function
32 from __future__ import absolute_import
32 from __future__ import absolute_import
33 from __future__ import unicode_literals
33 from __future__ import unicode_literals
34
34
35 __all__ = ['ANSICodeColors','Parser']
35 __all__ = ['ANSICodeColors','Parser']
36
36
37 _scheme_default = 'Linux'
37 _scheme_default = 'Linux'
38
38
39
39
40 # Imports
40 # Imports
41 import keyword
41 import keyword
42 import os
42 import os
43 import sys
43 import sys
44 import token
44 import token
45 import tokenize
45 import tokenize
46
46
47 try:
47 try:
48 generate_tokens = tokenize.generate_tokens
48 generate_tokens = tokenize.generate_tokens
49 except AttributeError:
49 except AttributeError:
50 # Python 3. Note that we use the undocumented _tokenize because it expects
50 # Python 3. Note that we use the undocumented _tokenize because it expects
51 # strings, not bytes. See also Python issue #9969.
51 # strings, not bytes. See also Python issue #9969.
52 generate_tokens = tokenize._tokenize
52 generate_tokens = tokenize._tokenize
53
53
54 from IPython.utils.coloransi import TermColors, InputTermColors ,ColorScheme, ColorSchemeTable
54 from IPython.utils.coloransi import TermColors, InputTermColors ,ColorScheme, ColorSchemeTable
55 from IPython.utils.py3compat import PY3
55 from IPython.utils.py3compat import PY3
56
56
57 from .colorable import Colorable
57 from .colorable import Colorable
58
58
59 if PY3:
59 if PY3:
60 from io import StringIO
60 from io import StringIO
61 else:
61 else:
62 from StringIO import StringIO
62 from StringIO import StringIO
63
63
64 #############################################################################
64 #############################################################################
65 ### Python Source Parser (does Hilighting)
65 ### Python Source Parser (does Hilighting)
66 #############################################################################
66 #############################################################################
67
67
68 _KEYWORD = token.NT_OFFSET + 1
68 _KEYWORD = token.NT_OFFSET + 1
69 _TEXT = token.NT_OFFSET + 2
69 _TEXT = token.NT_OFFSET + 2
70
70
71 #****************************************************************************
71 #****************************************************************************
72 # Builtin color schemes
72 # Builtin color schemes
73
73
74 Colors = TermColors # just a shorthand
74 Colors = TermColors # just a shorthand
75
75
76 # Build a few color schemes
76 # Build a few color schemes
77 NoColor = ColorScheme(
77 NoColor = ColorScheme(
78 'NoColor',{
78 'NoColor',{
79 'header' : Colors.NoColor,
79 'header' : Colors.NoColor,
80 token.NUMBER : Colors.NoColor,
80 token.NUMBER : Colors.NoColor,
81 token.OP : Colors.NoColor,
81 token.OP : Colors.NoColor,
82 token.STRING : Colors.NoColor,
82 token.STRING : Colors.NoColor,
83 tokenize.COMMENT : Colors.NoColor,
83 tokenize.COMMENT : Colors.NoColor,
84 token.NAME : Colors.NoColor,
84 token.NAME : Colors.NoColor,
85 token.ERRORTOKEN : Colors.NoColor,
85 token.ERRORTOKEN : Colors.NoColor,
86
86
87 _KEYWORD : Colors.NoColor,
87 _KEYWORD : Colors.NoColor,
88 _TEXT : Colors.NoColor,
88 _TEXT : Colors.NoColor,
89
89
90 'in_prompt' : InputTermColors.NoColor, # Input prompt
90 'in_prompt' : InputTermColors.NoColor, # Input prompt
91 'in_number' : InputTermColors.NoColor, # Input prompt number
91 'in_number' : InputTermColors.NoColor, # Input prompt number
92 'in_prompt2' : InputTermColors.NoColor, # Continuation prompt
92 'in_prompt2' : InputTermColors.NoColor, # Continuation prompt
93 'in_normal' : InputTermColors.NoColor, # color off (usu. Colors.Normal)
93 'in_normal' : InputTermColors.NoColor, # color off (usu. Colors.Normal)
94
94
95 'out_prompt' : Colors.NoColor, # Output prompt
95 'out_prompt' : Colors.NoColor, # Output prompt
96 'out_number' : Colors.NoColor, # Output prompt number
96 'out_number' : Colors.NoColor, # Output prompt number
97
97
98 'normal' : Colors.NoColor # color off (usu. Colors.Normal)
98 'normal' : Colors.NoColor # color off (usu. Colors.Normal)
99 } )
99 } )
100
100
101 LinuxColors = ColorScheme(
101 LinuxColors = ColorScheme(
102 'Linux',{
102 'Linux',{
103 'header' : Colors.LightRed,
103 'header' : Colors.LightRed,
104 token.NUMBER : Colors.LightCyan,
104 token.NUMBER : Colors.LightCyan,
105 token.OP : Colors.Yellow,
105 token.OP : Colors.Yellow,
106 token.STRING : Colors.LightBlue,
106 token.STRING : Colors.LightBlue,
107 tokenize.COMMENT : Colors.LightRed,
107 tokenize.COMMENT : Colors.LightRed,
108 token.NAME : Colors.Normal,
108 token.NAME : Colors.Normal,
109 token.ERRORTOKEN : Colors.Red,
109 token.ERRORTOKEN : Colors.Red,
110
110
111 _KEYWORD : Colors.LightGreen,
111 _KEYWORD : Colors.LightGreen,
112 _TEXT : Colors.Yellow,
112 _TEXT : Colors.Yellow,
113
113
114 'in_prompt' : InputTermColors.Green,
114 'in_prompt' : InputTermColors.Green,
115 'in_number' : InputTermColors.LightGreen,
115 'in_number' : InputTermColors.LightGreen,
116 'in_prompt2' : InputTermColors.Green,
116 'in_prompt2' : InputTermColors.Green,
117 'in_normal' : InputTermColors.Normal, # color off (usu. Colors.Normal)
117 'in_normal' : InputTermColors.Normal, # color off (usu. Colors.Normal)
118
118
119 'out_prompt' : Colors.Red,
119 'out_prompt' : Colors.Red,
120 'out_number' : Colors.LightRed,
120 'out_number' : Colors.LightRed,
121
121
122 'normal' : Colors.Normal # color off (usu. Colors.Normal)
122 'normal' : Colors.Normal # color off (usu. Colors.Normal)
123 } )
123 } )
124
124
125 NeutralColors = ColorScheme(
125 NeutralColors = ColorScheme(
126 'Neutral',{
126 'Neutral',{
127 'header' : Colors.Red,
127 'header' : Colors.Red,
128 token.NUMBER : Colors.Cyan,
128 token.NUMBER : Colors.Cyan,
129 token.OP : Colors.Blue,
129 token.OP : Colors.Blue,
130 token.STRING : Colors.Blue,
130 token.STRING : Colors.Blue,
131 tokenize.COMMENT : Colors.Red,
131 tokenize.COMMENT : Colors.Red,
132 token.NAME : Colors.Normal,
132 token.NAME : Colors.Normal,
133 token.ERRORTOKEN : Colors.Red,
133 token.ERRORTOKEN : Colors.Red,
134
134
135 _KEYWORD : Colors.Green,
135 _KEYWORD : Colors.Green,
136 _TEXT : Colors.Blue,
136 _TEXT : Colors.Blue,
137
137
138 'in_prompt' : InputTermColors.Blue,
138 'in_prompt' : InputTermColors.Blue,
139 'in_number' : InputTermColors.LightBlue,
139 'in_number' : InputTermColors.LightBlue,
140 'in_prompt2' : InputTermColors.Blue,
140 'in_prompt2' : InputTermColors.Blue,
141 'in_normal' : InputTermColors.Normal, # color off (usu. Colors.Normal)
141 'in_normal' : InputTermColors.Normal, # color off (usu. Colors.Normal)
142
142
143 'out_prompt' : Colors.Red,
143 'out_prompt' : Colors.Red,
144 'out_number' : Colors.LightRed,
144 'out_number' : Colors.LightRed,
145
145
146 'normal' : Colors.Normal # color off (usu. Colors.Normal)
146 'normal' : Colors.Normal # color off (usu. Colors.Normal)
147 } )
147 } )
148
148
149
149 # Hack: the 'neutral' colours are not very visible on a dark background on
150 # Windows. Since Windows command prompts have a dark background by default, and
151 # relatively few users are likely to alter that, we will use the 'Linux' colours,
152 # designed for a dark background, as the default on Windows. Changing it here
153 # avoids affecting the prompt colours rendered by prompt_toolkit, where the
154 # neutral defaults do work OK.
155
156 if os.name == 'nt':
157 NeutralColors = LinuxColors.copy(name='Neutral')
150
158
151 LightBGColors = ColorScheme(
159 LightBGColors = ColorScheme(
152 'LightBG',{
160 'LightBG',{
153 'header' : Colors.Red,
161 'header' : Colors.Red,
154 token.NUMBER : Colors.Cyan,
162 token.NUMBER : Colors.Cyan,
155 token.OP : Colors.Blue,
163 token.OP : Colors.Blue,
156 token.STRING : Colors.Blue,
164 token.STRING : Colors.Blue,
157 tokenize.COMMENT : Colors.Red,
165 tokenize.COMMENT : Colors.Red,
158 token.NAME : Colors.Normal,
166 token.NAME : Colors.Normal,
159 token.ERRORTOKEN : Colors.Red,
167 token.ERRORTOKEN : Colors.Red,
160
168
161
169
162 _KEYWORD : Colors.Green,
170 _KEYWORD : Colors.Green,
163 _TEXT : Colors.Blue,
171 _TEXT : Colors.Blue,
164
172
165 'in_prompt' : InputTermColors.Blue,
173 'in_prompt' : InputTermColors.Blue,
166 'in_number' : InputTermColors.LightBlue,
174 'in_number' : InputTermColors.LightBlue,
167 'in_prompt2' : InputTermColors.Blue,
175 'in_prompt2' : InputTermColors.Blue,
168 'in_normal' : InputTermColors.Normal, # color off (usu. Colors.Normal)
176 'in_normal' : InputTermColors.Normal, # color off (usu. Colors.Normal)
169
177
170 'out_prompt' : Colors.Red,
178 'out_prompt' : Colors.Red,
171 'out_number' : Colors.LightRed,
179 'out_number' : Colors.LightRed,
172
180
173 'normal' : Colors.Normal # color off (usu. Colors.Normal)
181 'normal' : Colors.Normal # color off (usu. Colors.Normal)
174 } )
182 } )
175
183
176 # Build table of color schemes (needed by the parser)
184 # Build table of color schemes (needed by the parser)
177 ANSICodeColors = ColorSchemeTable([NoColor,LinuxColors,LightBGColors, NeutralColors],
185 ANSICodeColors = ColorSchemeTable([NoColor,LinuxColors,LightBGColors, NeutralColors],
178 _scheme_default)
186 _scheme_default)
179
187
180 class Parser(Colorable):
188 class Parser(Colorable):
181 """ Format colored Python source.
189 """ Format colored Python source.
182 """
190 """
183
191
184 def __init__(self, color_table=None, out = sys.stdout, parent=None, style=None):
192 def __init__(self, color_table=None, out = sys.stdout, parent=None, style=None):
185 """ Create a parser with a specified color table and output channel.
193 """ Create a parser with a specified color table and output channel.
186
194
187 Call format() to process code.
195 Call format() to process code.
188 """
196 """
189
197
190 super(Parser, self).__init__(parent=parent)
198 super(Parser, self).__init__(parent=parent)
191
199
192 self.color_table = color_table and color_table or ANSICodeColors
200 self.color_table = color_table and color_table or ANSICodeColors
193 self.out = out
201 self.out = out
194
202
195 def format(self, raw, out = None, scheme = ''):
203 def format(self, raw, out = None, scheme = ''):
196 return self.format2(raw, out, scheme)[0]
204 return self.format2(raw, out, scheme)[0]
197
205
198 def format2(self, raw, out = None, scheme = ''):
206 def format2(self, raw, out = None, scheme = ''):
199 """ Parse and send the colored source.
207 """ Parse and send the colored source.
200
208
201 If out and scheme are not specified, the defaults (given to
209 If out and scheme are not specified, the defaults (given to
202 constructor) are used.
210 constructor) are used.
203
211
204 out should be a file-type object. Optionally, out can be given as the
212 out should be a file-type object. Optionally, out can be given as the
205 string 'str' and the parser will automatically return the output in a
213 string 'str' and the parser will automatically return the output in a
206 string."""
214 string."""
207
215
208 string_output = 0
216 string_output = 0
209 if out == 'str' or self.out == 'str' or \
217 if out == 'str' or self.out == 'str' or \
210 isinstance(self.out,StringIO):
218 isinstance(self.out,StringIO):
211 # XXX - I don't really like this state handling logic, but at this
219 # XXX - I don't really like this state handling logic, but at this
212 # point I don't want to make major changes, so adding the
220 # point I don't want to make major changes, so adding the
213 # isinstance() check is the simplest I can do to ensure correct
221 # isinstance() check is the simplest I can do to ensure correct
214 # behavior.
222 # behavior.
215 out_old = self.out
223 out_old = self.out
216 self.out = StringIO()
224 self.out = StringIO()
217 string_output = 1
225 string_output = 1
218 elif out is not None:
226 elif out is not None:
219 self.out = out
227 self.out = out
220
228
221 # Fast return of the unmodified input for NoColor scheme
229 # Fast return of the unmodified input for NoColor scheme
222 if scheme == 'NoColor':
230 if scheme == 'NoColor':
223 error = False
231 error = False
224 self.out.write(raw)
232 self.out.write(raw)
225 if string_output:
233 if string_output:
226 return raw,error
234 return raw,error
227 else:
235 else:
228 return None,error
236 return None,error
229
237
230 # local shorthands
238 # local shorthands
231 colors = self.color_table[scheme].colors
239 colors = self.color_table[scheme].colors
232 self.colors = colors # put in object so __call__ sees it
240 self.colors = colors # put in object so __call__ sees it
233
241
234 # Remove trailing whitespace and normalize tabs
242 # Remove trailing whitespace and normalize tabs
235 self.raw = raw.expandtabs().rstrip()
243 self.raw = raw.expandtabs().rstrip()
236
244
237 # store line offsets in self.lines
245 # store line offsets in self.lines
238 self.lines = [0, 0]
246 self.lines = [0, 0]
239 pos = 0
247 pos = 0
240 raw_find = self.raw.find
248 raw_find = self.raw.find
241 lines_append = self.lines.append
249 lines_append = self.lines.append
242 while 1:
250 while 1:
243 pos = raw_find('\n', pos) + 1
251 pos = raw_find('\n', pos) + 1
244 if not pos: break
252 if not pos: break
245 lines_append(pos)
253 lines_append(pos)
246 lines_append(len(self.raw))
254 lines_append(len(self.raw))
247
255
248 # parse the source and write it
256 # parse the source and write it
249 self.pos = 0
257 self.pos = 0
250 text = StringIO(self.raw)
258 text = StringIO(self.raw)
251
259
252 error = False
260 error = False
253 try:
261 try:
254 for atoken in generate_tokens(text.readline):
262 for atoken in generate_tokens(text.readline):
255 self(*atoken)
263 self(*atoken)
256 except tokenize.TokenError as ex:
264 except tokenize.TokenError as ex:
257 msg = ex.args[0]
265 msg = ex.args[0]
258 line = ex.args[1][0]
266 line = ex.args[1][0]
259 self.out.write("%s\n\n*** ERROR: %s%s%s\n" %
267 self.out.write("%s\n\n*** ERROR: %s%s%s\n" %
260 (colors[token.ERRORTOKEN],
268 (colors[token.ERRORTOKEN],
261 msg, self.raw[self.lines[line]:],
269 msg, self.raw[self.lines[line]:],
262 colors.normal)
270 colors.normal)
263 )
271 )
264 error = True
272 error = True
265 self.out.write(colors.normal+'\n')
273 self.out.write(colors.normal+'\n')
266 if string_output:
274 if string_output:
267 output = self.out.getvalue()
275 output = self.out.getvalue()
268 self.out = out_old
276 self.out = out_old
269 return (output, error)
277 return (output, error)
270 return (None, error)
278 return (None, error)
271
279
272 def __call__(self, toktype, toktext, start_pos, end_pos, line):
280 def __call__(self, toktype, toktext, start_pos, end_pos, line):
273 """ Token handler, with syntax highlighting."""
281 """ Token handler, with syntax highlighting."""
274 (srow,scol) = start_pos
282 (srow,scol) = start_pos
275 (erow,ecol) = end_pos
283 (erow,ecol) = end_pos
276 colors = self.colors
284 colors = self.colors
277 owrite = self.out.write
285 owrite = self.out.write
278
286
279 # line separator, so this works across platforms
287 # line separator, so this works across platforms
280 linesep = os.linesep
288 linesep = os.linesep
281
289
282 # calculate new positions
290 # calculate new positions
283 oldpos = self.pos
291 oldpos = self.pos
284 newpos = self.lines[srow] + scol
292 newpos = self.lines[srow] + scol
285 self.pos = newpos + len(toktext)
293 self.pos = newpos + len(toktext)
286
294
287 # send the original whitespace, if needed
295 # send the original whitespace, if needed
288 if newpos > oldpos:
296 if newpos > oldpos:
289 owrite(self.raw[oldpos:newpos])
297 owrite(self.raw[oldpos:newpos])
290
298
291 # skip indenting tokens
299 # skip indenting tokens
292 if toktype in [token.INDENT, token.DEDENT]:
300 if toktype in [token.INDENT, token.DEDENT]:
293 self.pos = newpos
301 self.pos = newpos
294 return
302 return
295
303
296 # map token type to a color group
304 # map token type to a color group
297 if token.LPAR <= toktype <= token.OP:
305 if token.LPAR <= toktype <= token.OP:
298 toktype = token.OP
306 toktype = token.OP
299 elif toktype == token.NAME and keyword.iskeyword(toktext):
307 elif toktype == token.NAME and keyword.iskeyword(toktext):
300 toktype = _KEYWORD
308 toktype = _KEYWORD
301 color = colors.get(toktype, colors[_TEXT])
309 color = colors.get(toktype, colors[_TEXT])
302
310
303 #print '<%s>' % toktext, # dbg
311 #print '<%s>' % toktext, # dbg
304
312
305 # Triple quoted strings must be handled carefully so that backtracking
313 # Triple quoted strings must be handled carefully so that backtracking
306 # in pagers works correctly. We need color terminators on _each_ line.
314 # in pagers works correctly. We need color terminators on _each_ line.
307 if linesep in toktext:
315 if linesep in toktext:
308 toktext = toktext.replace(linesep, '%s%s%s' %
316 toktext = toktext.replace(linesep, '%s%s%s' %
309 (colors.normal,linesep,color))
317 (colors.normal,linesep,color))
310
318
311 # send text
319 # send text
312 owrite('%s%s%s' % (color,toktext,colors.normal))
320 owrite('%s%s%s' % (color,toktext,colors.normal))
313
321
314 def main(argv=None):
322 def main(argv=None):
315 """Run as a command-line script: colorize a python file or stdin using ANSI
323 """Run as a command-line script: colorize a python file or stdin using ANSI
316 color escapes and print to stdout.
324 color escapes and print to stdout.
317
325
318 Inputs:
326 Inputs:
319
327
320 - argv(None): a list of strings like sys.argv[1:] giving the command-line
328 - argv(None): a list of strings like sys.argv[1:] giving the command-line
321 arguments. If None, use sys.argv[1:].
329 arguments. If None, use sys.argv[1:].
322 """
330 """
323
331
324 usage_msg = """%prog [options] [filename]
332 usage_msg = """%prog [options] [filename]
325
333
326 Colorize a python file or stdin using ANSI color escapes and print to stdout.
334 Colorize a python file or stdin using ANSI color escapes and print to stdout.
327 If no filename is given, or if filename is -, read standard input."""
335 If no filename is given, or if filename is -, read standard input."""
328
336
329 import optparse
337 import optparse
330 parser = optparse.OptionParser(usage=usage_msg)
338 parser = optparse.OptionParser(usage=usage_msg)
331 newopt = parser.add_option
339 newopt = parser.add_option
332 newopt('-s','--scheme',metavar='NAME',dest='scheme_name',action='store',
340 newopt('-s','--scheme',metavar='NAME',dest='scheme_name',action='store',
333 choices=['Linux','LightBG','NoColor'],default=_scheme_default,
341 choices=['Linux','LightBG','NoColor'],default=_scheme_default,
334 help="give the color scheme to use. Currently only 'Linux'\
342 help="give the color scheme to use. Currently only 'Linux'\
335 (default) and 'LightBG' and 'NoColor' are implemented (give without\
343 (default) and 'LightBG' and 'NoColor' are implemented (give without\
336 quotes)")
344 quotes)")
337
345
338 opts,args = parser.parse_args(argv)
346 opts,args = parser.parse_args(argv)
339
347
340 if len(args) > 1:
348 if len(args) > 1:
341 parser.error("you must give at most one filename.")
349 parser.error("you must give at most one filename.")
342
350
343 if len(args) == 0:
351 if len(args) == 0:
344 fname = '-' # no filename given; setup to read from stdin
352 fname = '-' # no filename given; setup to read from stdin
345 else:
353 else:
346 fname = args[0]
354 fname = args[0]
347
355
348 if fname == '-':
356 if fname == '-':
349 stream = sys.stdin
357 stream = sys.stdin
350 else:
358 else:
351 try:
359 try:
352 stream = open(fname)
360 stream = open(fname)
353 except IOError as msg:
361 except IOError as msg:
354 print(msg, file=sys.stderr)
362 print(msg, file=sys.stderr)
355 sys.exit(1)
363 sys.exit(1)
356
364
357 parser = Parser()
365 parser = Parser()
358
366
359 # we need nested try blocks because pre-2.5 python doesn't support unified
367 # we need nested try blocks because pre-2.5 python doesn't support unified
360 # try-except-finally
368 # try-except-finally
361 try:
369 try:
362 try:
370 try:
363 # write colorized version to stdout
371 # write colorized version to stdout
364 parser.format(stream.read(),scheme=opts.scheme_name)
372 parser.format(stream.read(),scheme=opts.scheme_name)
365 except IOError as msg:
373 except IOError as msg:
366 # if user reads through a pager and quits, don't print traceback
374 # if user reads through a pager and quits, don't print traceback
367 if msg.args != (32,'Broken pipe'):
375 if msg.args != (32,'Broken pipe'):
368 raise
376 raise
369 finally:
377 finally:
370 if stream is not sys.stdin:
378 if stream is not sys.stdin:
371 stream.close() # in case a non-handled exception happened above
379 stream.close() # in case a non-handled exception happened above
372
380
373 if __name__ == "__main__":
381 if __name__ == "__main__":
374 main()
382 main()
General Comments 0
You need to be logged in to leave comments. Login now