Coverage for src/turtlesc/__init__.py: 99%
343 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-01 13:58 -0400
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-01 13:58 -0400
1import turtle, time, re
3# SC TODO - some kind of live replay sort of thing?
4# SC TODO - some kind of chart maker that records the screen after every movement?
7ALL_SHORTCUTS = '# f b l r h c g tele x y st u pd pu ps pc fc bc sh cir undo bf ef sleep n s e w nw ne sw se u t cs css spd eoc' + \
8 'forward backward left right home clear goto setx sety stamp update pendown penup pensize pencolor fillcolor bgcolor setheading' + \
9 'circle undo begin_fill end_fill north south east west northwest northeast southwest southeast reset bye done exitonclick update' + \
10 'tracer hide show dot clearstamp clearstamps degrees radians speed'
12CARDINAL_TO_DEGREES = {'n': '90', 's': '270', 'e': '0', 'w': '180', 'nw': '135', 'ne': '45', 'sw': '225', 'se': '315'}
14_MAP_FULL_TO_SHORT_NAMES = {'forward': 'f', 'backward': 'b', 'right': 'r', 'left': 'l', 'home': 'h', 'clear': 'c',
15 'goto': 'g', 'teleport': 'tele', 'setx': 'x', 'sety': 'y', 'stamp': 'st', 'update': 'u', 'pendown': 'pd', 'penup': 'pu',
16 'pensize': 'ps', 'pencolor': 'pc', 'fillcolor': 'fc', 'bgcolor': 'bc', 'setheading': 'sh', 'circle': 'cir',
17 'begin_fill': 'bf', 'end_fill': 'ef', 'north': 'n', 'south': 's', 'east': 'e', 'west': 'w',
18 'northwest': 'nw', 'northeast': 'ne', 'southwest': 'sw', 'southeast': 'se', 'update': 'u', 'tracer': 't',
19 'clearstamp': 'cs', 'clearstamps': 'css', 'speed': 'spd', 'exitonclick': 'eoc'}
21RECORDED_SHORTCUTS = []
22_NOW_RECORDING = False
24class TurtleShortcutException(Exception):
25 pass
27def sc(*args, turtle_obj=None, _return_turtle_code=False, skip=False): # type: () -> int
28 """TODO
29 """
31 """Supported commands:
33 f N - forward(N)
34 b N - backward(N)
35 l N - left(N)
36 r N - right(N)
37 h - home()
38 c - clear()
39 g X Y - goto(X, Y)
40 tele X Y - teleport(X, Y)
41 x X - setx(X)
42 y Y - sety(Y)
43 st - stamp()
44 pd - pendown()
45 pu - penup()
46 ps N - pensize(N)
47 pc RGB - pencolor(RGB) (RGB value can either be a single string like `red` or three dec/hex numbers `1.0 0.0 0.5` or `FF FF 00`
48 fc RGB - fillcolor(RGB)
49 bc RGB - bgcolor(RGB)
50 sh N - setheading(N)
51 cir N - circle(N)
52 undo - undo()
53 bf - begin_fill()
54 ef - end_fill()
55 reset - reset()
57 sleep N - time.sleep(N)
59 n N - setheading(90);forward(N)
60 s N - setheading(270);forward(N)
61 w N - setheading(180);forward(N)
62 e N - setheading(0);forward(N)
63 nw N - setheading(135);forward(N)
64 ne N - setheading(45);forward(N)
65 sw N - setheading(225);forward(N)
66 se N - setheading(315);forward(N)
67 north N - setheading(90);forward(N)
68 south N - setheading(270);forward(N)
69 west N - setheading(180);forward(N)
70 east N - setheading(0);forward(N)
71 northwest N - setheading(135);forward(N)
72 northeast N - setheading(45);forward(N)
73 southwest N - setheading(225);forward(N)
74 southeast N - setheading(315);forward(N)
76 done - done()
77 bye - bye()
78 exitonclick - exitonclick()
80 t N1 N2 - tracer(N1, N2)
81 u - update()
83 hide - hide()
84 show - show()
86 dot N - dot(N)
87 cs N - clearstamp(N)
88 css N - clearstamps(N)
89 degrees - degrees()
90 radians - radians()
92 spd N - speed(N) but N can also be 'fastest', 'fast', 'normal', 'slow', 'slowest'
93 !!shape N - shape(N) where N can be “arrow”, “turtle”, “circle”, “square”, “triangle”, “classic”
94 !!resizemode N - resizemode(N) where N can be “auto”, “user”, or "noresize"
95 !!bgpic N - bgpic(N) where the N filename cannot have a comma in it.
97 !!shapesize N1 N2 N3 - shapesize(N1, N2, N3)
98 !!settiltangle N - settiltangle(N)
99 !!tilt N - tilt(N)
100 !!tiltangle N - tiltangle(N)
105 Note:
108 Furthermore, you can also use the full names: forward N translates to forward(N).
109 Note: None of these functions can take string args that have spaces in them, since spaces are the arg delimiter here.
110 Note: You also can't use variables here, only static values. But you can use f-strings.
112 Return value is the number of commands executed.
113 Whitespace is insignificant. ' f 100 ' is the same as 'f 100'
114 """
116 if skip:
117 if _return_turtle_code:
118 return ()
119 else:
120 return 0
122 # Join multiple arg strings into one, separated by commas:
123 shortcuts = ','.join(args)
125 # Newlines become commas as well:
126 shortcuts = shortcuts.replace('\n', ',')
128 if shortcuts == '' or len(shortcuts.split(',')) == 0:
129 return 0
131 count_of_shortcuts_run = 0
133 # Go through and check that all shortcuts are syntactically correct:
134 for shortcut in shortcuts.split(','):
135 count_of_shortcuts_run += _run_shortcut(shortcut, turtle_obj=turtle_obj, dry_run=True)
137 # Go through and actually run all the shortcuts:
138 count_of_shortcuts_run = 0
139 turtle_code = tuple()
140 for shortcut in shortcuts.split(','):
141 if _return_turtle_code:
142 turtle_code += _run_shortcut(shortcut, turtle_obj=turtle_obj, _return_turtle_code=True)
143 else:
144 count_of_shortcuts_run += _run_shortcut(shortcut, turtle_obj=turtle_obj)
146 if _return_turtle_code:
147 # Return a multi-line string of Python code calling turtle functions:
148 return '\n'.join(turtle_code)
149 else:
150 return count_of_shortcuts_run
153def _run_shortcut(shortcut, turtle_obj=None, dry_run=False, _return_turtle_code=False):
154 '''Runs a single shortcut'''
156 if turtle_obj is None:
157 turtle_obj = turtle # Use the main turtle given by the module.
159 # Clean up shortcut name from " FOrWARD " to "f", for example.
160 shortcut_parts = shortcut.strip().split()
161 if len(shortcut_parts) == 0:
162 if _return_turtle_code:
163 return ('',)
164 else:
165 return 0 # Return 0 because blank strings have zero shortcuts.
166 _sc = shortcut_parts[0].lower()
167 _sc = _MAP_FULL_TO_SHORT_NAMES.get(_sc, _sc)
169 # Check that the shortcut's syntax is valid:
171 if _sc not in ALL_SHORTCUTS:
172 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[0] + '` is not a turtle shortcut.')
174 raise_exception = False
175 count_of_shortcuts_run = 0
178 # SHORTCUTS THAT TAKE A VARIABLE NUMBER OF ARGUMENTS:
179 if _sc in ('#',):
180 if _sc == '#':
181 if _return_turtle_code:
182 return(shortcut) # Return the comment as is.
183 pass # Comments do nothing.
184 else: # pragma: no cover
185 assert False, 'Unhandled shortcut: ' + _sc
188 # SHORTCUTS THAT TAKE A SINGLE NUMERIC ARGUMENT:
189 elif _sc in ('f', 'b', 'r', 'l', 'x', 'y', 'ps', 'sh', 'cir', 'sleep', 'n', 's', 'e', 'w', 'nw', 'ne', 'sw', 'se', 'dot', 'cs', 'spd'):
190 if len(shortcut_parts) < 2:
191 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Missing the required numeric argument.')
192 if len(shortcut_parts) > 2:
193 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Too many arguments.')
195 # Convert the string arguments for the `speed` shortcut to their numeric equivalents.
196 if _sc == 'spd':
197 shortcut_parts[1] = {'fastest': 0, 'fast': 10, 'normal': 6, 'slow': 3, 'slowest': 1}.get(shortcut_parts[1].lower(), shortcut_parts[1].lower())
199 try:
200 float(shortcut_parts[1])
201 except ValueError:
202 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
203 if raise_exception:
204 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[1] + '` is not a number.')
206 # `dot` shortcut doesn't allow negative values:
207 if _sc == 'dot' and float(shortcut_parts[1]) < 0:
208 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `dot` argument cannot be a negative number.')
210 if not dry_run:
211 # Run the shortcut that has exactly one numeric argument:
212 if _sc == 'f':
213 if _return_turtle_code:
214 return ('forward(' + shortcut_parts[1] + ')',)
215 turtle_obj.forward(float(shortcut_parts[1]))
216 elif _sc == 'b':
217 if _return_turtle_code:
218 return ('backward(' + shortcut_parts[1] + ')',)
219 turtle_obj.backward(float(shortcut_parts[1]))
220 elif _sc == 'r':
221 if _return_turtle_code:
222 return ('right(' + shortcut_parts[1] + ')',)
223 turtle_obj.right(float(shortcut_parts[1]))
224 elif _sc == 'l':
225 if _return_turtle_code:
226 return ('left(' + shortcut_parts[1] + ')',)
227 turtle_obj.left(float(shortcut_parts[1]))
228 elif _sc == 'x':
229 if _return_turtle_code:
230 return ('setx(' + shortcut_parts[1] + ')',)
231 turtle_obj.setx(float(shortcut_parts[1]))
232 elif _sc == 'y':
233 if _return_turtle_code:
234 return ('sety(' + shortcut_parts[1] + ')',)
235 turtle_obj.sety(float(shortcut_parts[1]))
236 elif _sc == 'ps':
237 if _return_turtle_code:
238 return ('pensize(' + shortcut_parts[1] + ')',)
239 turtle_obj.pensize(float(shortcut_parts[1]))
240 elif _sc == 'sh':
241 if _return_turtle_code:
242 return ('setheading(' + shortcut_parts[1] + ')',)
243 turtle_obj.setheading(float(shortcut_parts[1]))
244 elif _sc == 'cir':
245 if _return_turtle_code:
246 return ('circle(' + shortcut_parts[1] + ')',)
247 turtle_obj.circle(float(shortcut_parts[1]))
248 elif _sc == 'sleep':
249 if _return_turtle_code:
250 return ('sleep(' + shortcut_parts[1] + ')', )
251 time.sleep(float(shortcut_parts[1]))
252 elif _sc in ('n', 's', 'e', 'w', 'nw', 'ne', 'sw', 'se'):
253 originally_in_radians_mode = in_radians_mode()
255 if _return_turtle_code:
256 if originally_in_radians_mode:
257 return ('degrees()', 'setheading(' + CARDINAL_TO_DEGREES[_sc] + ')', 'forward(' + shortcut_parts[1] + ')', 'radians()')
258 else:
259 return ('setheading(' + CARDINAL_TO_DEGREES[_sc] + ')', 'forward(' + shortcut_parts[1] + ')')
260 turtle.degrees()
261 if _sc == 'n':
262 turtle.setheading(90)
263 elif _sc == 's':
264 turtle.setheading(270)
265 elif _sc == 'e':
266 turtle.setheading(0)
267 elif _sc == 'w':
268 turtle.setheading(180)
269 elif _sc == 'nw':
270 turtle.setheading(135)
271 elif _sc == 'ne':
272 turtle.setheading(45)
273 elif _sc == 'sw':
274 turtle.setheading(225)
275 elif _sc == 'se':
276 turtle.setheading(315)
277 else: # pragma: no cover
278 assert False, 'Unhandled shortcut: ' + _sc
279 turtle_obj.forward(float(shortcut_parts[1]))
280 if originally_in_radians_mode:
281 turtle.radians()
282 elif _sc == 'dot':
283 if _return_turtle_code:
284 return ('dot(' + shortcut_parts[1] + ')',)
285 turtle_obj.dot(float(shortcut_parts[1]))
286 elif _sc == 'cs':
287 if _return_turtle_code:
288 return ('clearstamp(' + shortcut_parts[1] + ')',)
289 turtle_obj.clearstamp(float(shortcut_parts[1]))
290 elif _sc == 'spd':
291 if _return_turtle_code:
292 return ('speed(' + str(shortcut_parts[1]) + ')',)
293 turtle_obj.speed(float(shortcut_parts[1]))
294 else: # pragma: no cover
295 assert False, 'Unhandled shortcut: ' + _sc
296 count_of_shortcuts_run += 1
302 # SHORTCUTS THAT TAKE A SINGLE INTEGER ARGUMENT OR NONE ARGUMENT:
303 elif _sc in ('css',):
304 if len(shortcut_parts) > 2:
305 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Too many arguments.')
307 # Technically, the css shortcut can take a float argument, but it gets passed to int() silently. Not ideal, but not a big deal either.
309 if len(shortcut_parts) == 2:
310 try:
311 int(shortcut_parts[1])
312 except ValueError:
313 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
314 if raise_exception:
315 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[1] + '` is not a number.')
317 if not dry_run:
318 # Run the shortcut:
319 if _sc == 'css':
320 if len(shortcut_parts) == 1:
321 if _return_turtle_code:
322 return ('clearstamps()',)
323 turtle_obj.clearstamps()
324 elif len(shortcut_parts) == 2:
325 if _return_turtle_code:
326 return ('clearstamps(' + shortcut_parts[1] + ')',)
327 turtle_obj.clearstamps(int(shortcut_parts[1]))
328 else: # pragma: no cover
329 assert False, 'Unhandled shortcut: ' + _sc
330 else: # pragma: no cover
331 assert False, 'Unhandled shortcut: ' + _sc
332 count_of_shortcuts_run += 1
339 # SHORTCUTS THAT TAKE EXACTLY TWO NUMERIC ARGUMENTS:
340 elif _sc in ('g', 't', 'tele'):
341 if len(shortcut_parts) < 3:
342 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Missing two required numeric argument.')
343 elif len(shortcut_parts) > 3:
344 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Too many arguments.')
346 try:
347 float(shortcut_parts[1])
348 except ValueError:
349 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
350 if raise_exception:
351 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[1] + '` is not a number.')
352 try:
353 float(shortcut_parts[2])
354 except ValueError:
355 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
356 if raise_exception:
357 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[2] + '` is not a number.')
359 if not dry_run:
360 # Run the shortcut that has exactly two numeric arguments:
361 x = float(shortcut_parts[1])
362 y = float(shortcut_parts[2])
364 # Run the shortcut:
365 if _sc == 'g':
366 if _return_turtle_code:
367 return ('goto(' + shortcut_parts[1] + ', ' + shortcut_parts[2] + ')',)
368 turtle_obj.goto(x, y)
369 elif _sc == 't':
370 if _return_turtle_code:
371 return ('tracer(' + shortcut_parts[1] + ', ' + shortcut_parts[2] + ')',)
372 turtle.tracer(x, y) # Note: tracer() is not a Turtle method, there's only the global tracer() function.
373 elif _sc == 'tele':
374 if _return_turtle_code:
375 return ('teleport(' + shortcut_parts[1] + ', ' + shortcut_parts[2] + ')',)
376 turtle_obj.teleport(x, y)
377 else: # pragma: no cover
378 assert False, 'Unhandled shortcut: ' + _sc
379 count_of_shortcuts_run += 1
385 # SHORTCUTS THAT TAKE EXACTLY ZERO ARGUMENTS:
386 elif _sc in ('h', 'c', 'st', 'pd', 'pu', 'undo', 'bf', 'ef', 'reset', 'bye', 'done', 'eoc', 'u', 'show', 'hide'):
387 if len(shortcut_parts) > 1:
388 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: This shortcut does not have arguments.')
390 if not dry_run:
391 # Run the shortcut that has exactly zero arguments:
392 if _sc == 'h':
393 if _return_turtle_code:
394 return ('home()',)
395 turtle_obj.home()
396 elif _sc == 'c':
397 if _return_turtle_code:
398 return ('clear()',)
399 turtle_obj.clear()
400 elif _sc == 'st':
401 if _return_turtle_code:
402 return ('stamp()',)
403 turtle_obj.stamp()
404 elif _sc == 'pd':
405 if _return_turtle_code:
406 return ('pendown()',)
407 turtle_obj.pendown()
408 elif _sc == 'pu':
409 if _return_turtle_code:
410 return ('penup()',)
411 turtle_obj.penup()
412 elif _sc == 'undo':
413 if _return_turtle_code:
414 return ('undo()',)
415 turtle_obj.undo()
416 elif _sc == 'bf':
417 if _return_turtle_code:
418 return ('begin_fill()',)
419 turtle_obj.begin_fill()
420 elif _sc == 'ef':
421 if _return_turtle_code:
422 return ('end_fill()',)
423 turtle_obj.end_fill()
424 elif _sc == 'reset':
425 if _return_turtle_code:
426 return ('reset()',)
427 turtle_obj.reset()
428 elif _sc == 'bye': # pragma: no cover
429 if _return_turtle_code:
430 return ('bye()',)
431 turtle_obj.bye()
432 elif _sc == 'done': # pragma: no cover
433 if _return_turtle_code:
434 return ('done()',)
435 turtle_obj.done()
436 elif _sc == 'eoc': # pragma: no cover
437 if _return_turtle_code:
438 return ('exitonclick()',)
439 turtle_obj.exitonclick()
440 elif _sc == 'u':
441 if _return_turtle_code:
442 return ('update()',)
443 turtle_obj.update()
444 elif _sc == 'show':
445 if _return_turtle_code:
446 return ('showturtle()',)
447 turtle_obj.showturtle()
448 elif _sc == 'hide':
449 if _return_turtle_code:
450 return ('hideturtle()',)
451 turtle_obj.hideturtle()
452 else: # pragma: no cover
453 assert False, 'Unhandled shortcut: ' + _sc
454 count_of_shortcuts_run += 1
458 # SHORTCUTS THAT TAKE AN RGB OR COLOR ARGUMENT:
459 elif _sc in ('pc', 'fc', 'bc'):
460 color_arg_is_color_name = False # Start as False. If it's a color name, we'll set this to True.
462 if len(shortcut_parts) < 2:
463 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Missing required RGB argument.')
464 elif len(shortcut_parts) not in (2, 4):
465 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Invalid RGB argument. It must either be a color name like `red` or three numbers like `1.0 0.5 0.0` or `255 0 255` or `FF 00 FF`.')
467 if len(shortcut_parts) == 4:
468 # We expect the color arg to either be something like (255, 0, 0) or (1.0, 0.0, 0.0):
469 raise_exception = False
471 try:
472 float(shortcut_parts[1])
473 except ValueError:
474 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
475 if raise_exception:
476 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[1] + '` is not a number.')
478 try:
479 float(shortcut_parts[2])
480 except ValueError:
481 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
482 if raise_exception:
483 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[2] + '` is not a number.')
485 try:
486 float(shortcut_parts[3])
487 except ValueError:
488 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
489 if raise_exception:
490 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[3] + '` is not a number.')
492 if turtle_obj.colormode() == 1.0:
493 color_arg = (float(shortcut_parts[1]), float(shortcut_parts[2]), float(shortcut_parts[3]))
494 elif turtle_obj.colormode() == 255:
495 # Convert strings like '1.0' to floats first, then to int. (Calling int('1.0') would raise a ValueError.)
496 color_arg = (int(float(shortcut_parts[1])), int(float(shortcut_parts[2])), int(float(shortcut_parts[3])))
497 else: # pragma: no cover
498 assert False, 'Unhandled colormode: ' + str(turtle_obj.colormode())
500 if turtle_obj.colormode() == 1.0 and (color_arg[0] > 1.0 or color_arg[1] > 1.0 or color_arg[2] > 1.0):
501 raise TurtleShortcutException(shortcut + ' is invalid because colormode is 1.0 and one or more RGB color values are greater than 1.0.')
503 elif len(shortcut_parts) == 2:
504 # We expect the color arg to be a string like 'blue' or '#FF0000':
505 raise_exception = False
507 if re.match(r'^#[0-9A-Fa-f]{6}$', shortcut_parts[1]):
508 # Color arg is a hex code like '#FF0000', and not a name like 'blue'.
509 color_arg_is_color_name = False # It's already False, but I put this here to be explicit.
510 else:
511 # shortcut_parts[1] must be a color name like 'blue'
512 color_arg_is_color_name = True
513 color_arg = shortcut_parts[1]
515 # Test the color name by actually calling pencolor():
516 original_pen_color = turtle_obj.pencolor()
517 try:
518 turtle_obj.pencolor(color_arg)
519 except turtle.TurtleGraphicsError:
520 raise_exception = True # We don't raise here so we can hide the original TurtleGraphicsError and make the stack trace a bit neater.
521 if raise_exception:
522 if re.match(r'^[0-9A-Fa-f]{6}$', shortcut_parts[1]):
523 raise TurtleShortcutException('Syntax error in `' + shortcut + "`: '" + shortcut_parts[1] + "' is not a valid color. Did you mean '# " + shortcut_parts[1] + "'?")
524 else:
525 raise TurtleShortcutException('Syntax error in `' + shortcut + "`: '" + shortcut_parts[1] + "' is not a valid color.")
527 # NOTE: This code here is to handle an unfixed bug in turtle.py. If the color mode is 1.0 and you set
528 # the color to (1.0, 0.0, 0.0) and then change the color mode to 255, the color will be (255.0, 0.0, 0.0)
529 # but these float values are not a valid setting for a color while in mode 255. So we have to convert them
530 # to integers here.
531 if isinstance(original_pen_color, tuple) and turtle_obj.colormode() == 255:
532 turtle_obj.pencolor(int(original_pen_color[0]), int(original_pen_color[1]), int(original_pen_color[2]))
533 else:
534 turtle_obj.pencolor(original_pen_color)
536 if not dry_run:
537 # Return the turtle code, if that was asked:
538 if _return_turtle_code:
539 if _sc == 'pc':
540 func_name_prefix = 'pen'
541 elif _sc == 'fc':
542 func_name_prefix = 'fill'
543 elif _sc == 'bc':
544 func_name_prefix = 'bg'
546 if color_arg_is_color_name:
547 return (func_name_prefix + "color('" + str(color_arg) + "')",)
548 else:
549 return (func_name_prefix + 'color(' + str(color_arg) + ')',)
551 # Run the shortcut that has an RGB color argument:
552 if _sc == 'pc':
553 turtle_obj.pencolor(color_arg)
554 elif _sc == 'fc':
555 turtle_obj.fillcolor(color_arg)
556 elif _sc == 'bc':
557 turtle_obj.bgcolor(color_arg)
558 else: # pragma: no cover
559 assert False, 'Unhandled shortcut: ' + _sc
560 count_of_shortcuts_run += 1
562 # If begin_recording() has been called, log the shortcut.
563 if _NOW_RECORDING and not dry_run:
564 RECORDED_SHORTCUTS.append(shortcut)
566 return count_of_shortcuts_run
569def in_radians_mode():
570 """Returns True if turtle is in radians mode, False if in degrees mode."""
571 original_heading = turtle.heading()
572 turtle.left(1)
573 turtle.radians() # Switch to radians mode.
574 turtle.right(1)
575 if turtle.heading() == original_heading:
576 return True
577 else:
578 turtle.degrees() # Switch back to degrees mode.
579 return False
582def in_degrees_mode():
583 """Returns True if turtle is in degrees mode, False if in radians mode."""
584 return not in_radians_mode()
587def scs(*args):
588 """Returns the shortcut string of Python code that would be executed by the sc() function, suitable for printing to the screen."""
589 return sc(*args, _return_turtle_code=True)
591def psc(*args):
592 """Prints the Python code that would be executed by the sc() function."""
593 print(sc(*args, _return_turtle_code=True))
596def begin_recording(shortcut_list=None):
597 global RECORDED_SHORTCUTS, _NOW_RECORDING
598 RECORDED_SHORTCUTS = []
600 _NOW_RECORDING = True
602def end_recording():
603 global RECORDED_SHORTCUTS, _NOW_RECORDING
605 _NOW_RECORDING = False
606 return RECORDED_SHORTCUTS