Coverage for src/turtlesc/__init__.py: 100%
320 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-19 14:21 -0400
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-19 14:21 -0400
1import turtle, time, re
3ALL_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' + \
4 'forward backward left right home clear goto setx sety stamp update pendown penup pensize pencolor fillcolor bgcolor setheading' + \
5 'circle undo begin_fill end_fill north south east west northwest northeast southwest southeast reset bye done exitonclick update' + \
6 'tracer hide show dot clearstamp clearstamps degrees radians speed'
8CARDINAL_TO_DEGREES = {'n': '90', 's': '270', 'e': '0', 'w': '180', 'nw': '135', 'ne': '45', 'sw': '225', 'se': '315'}
10_MAP_FULL_TO_SHORT_NAMES = {'forward': 'f', 'backward': 'b', 'right': 'r', 'left': 'l', 'home': 'h', 'clear': 'c',
11 'goto': 'g', 'teleport': 'tele', 'setx': 'x', 'sety': 'y', 'stamp': 'st', 'update': 'u', 'pendown': 'pd', 'penup': 'pu',
12 'pensize': 'ps', 'pencolor': 'pc', 'fillcolor': 'fc', 'bgcolor': 'bc', 'setheading': 'sh', 'circle': 'cir',
13 'begin_fill': 'bf', 'end_fill': 'ef', 'north': 'n', 'south': 's', 'east': 'e', 'west': 'w',
14 'northwest': 'nw', 'northeast': 'ne', 'southwest': 'sw', 'southeast': 'se', 'update': 'u', 'tracer': 't',
15 'clearstamp': 'cs', 'clearstamps': 'css', 'speed': 'spd'}
17class TurtleShortcutException(Exception):
18 pass
20def sc(*args, turtle_obj=None, _return_turtle_code=False): # type: () -> int
21 """TODO
22 """
24 """Supported commands:
26 f N - forward(N)
27 b N - backward(N)
28 l N - left(N)
29 r N - right(N)
30 h - home()
31 c - clear()
32 g X Y - goto(X, Y)
33 tele X Y - teleport(X, Y)
34 x X - setx(X)
35 y Y - sety(Y)
36 st - stamp()
37 pd - pendown()
38 pu - penup()
39 ps N - pensize(N)
40 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`
41 fc RGB - fillcolor(RGB)
42 bc RGB - bgcolor(RGB)
43 sh N - setheading(N)
44 cir N - circle(N)
45 undo - undo()
46 bf - begin_fill()
47 ef - end_fill()
48 reset - reset()
50 sleep N - time.sleep(N)
52 n N - setheading(90);forward(N)
53 s N - setheading(270);forward(N)
54 w N - setheading(180);forward(N)
55 e N - setheading(0);forward(N)
56 nw N - setheading(135);forward(N)
57 ne N - setheading(45);forward(N)
58 sw N - setheading(225);forward(N)
59 se N - setheading(315);forward(N)
60 north N - setheading(90);forward(N)
61 south N - setheading(270);forward(N)
62 west N - setheading(180);forward(N)
63 east N - setheading(0);forward(N)
64 northwest N - setheading(135);forward(N)
65 northeast N - setheading(45);forward(N)
66 southwest N - setheading(225);forward(N)
67 southeast N - setheading(315);forward(N)
69 done - done()
70 bye - bye()
71 exitonclick - exitonclick()
73 t N1 N2 - tracer(N1, N2)
74 u - update()
76 hide - hide()
77 show - show()
79 dot N - dot(N)
80 cs N - clearstamp(N)
81 css N - clearstamps(N)
82 degrees - degrees()
83 radians - radians()
85 spd N - speed(N) but N can also be 'fastest', 'fast', 'normal', 'slow', 'slowest'
86 !!shape N - shape(N) where N can be “arrow”, “turtle”, “circle”, “square”, “triangle”, “classic”
87 !!resizemode N - resizemode(N) where N can be “auto”, “user”, or "noresize"
88 !!bgpic N - bgpic(N) where the N filename cannot have a comma in it.
90 !!shapesize N1 N2 N3 - shapesize(N1, N2, N3)
91 !!settiltangle N - settiltangle(N)
92 !!tilt N - tilt(N)
93 !!tiltangle N - tiltangle(N)
98 Note:
101 Furthermore, you can also use the full names: forward N translates to forward(N).
102 Note: None of these functions can take string args that have spaces in them, since spaces are the arg delimiter here.
103 Note: You also can't use variables here, only static values. But you can use f-strings.
105 Return value is the number of commands executed.
106 Whitespace is insignificant. ' f 100 ' is the same as 'f 100'
107 """
109 """
112 """
113 # Join multiple arg strings into one, separated by commas:
114 shortcuts = ','.join(args)
116 # Newlines become commas as well:
117 shortcuts = shortcuts.replace('\n', ',')
119 if shortcuts == '' or len(shortcuts.split(',')) == 0:
120 return 0
122 count_of_shortcuts_run = 0
124 # Go through and check that all shortcuts are syntactically correct:
125 for shortcut in shortcuts.split(','):
126 count_of_shortcuts_run += _run_shortcut(shortcut, turtle_obj=turtle_obj, dry_run=True)
128 # Go through and actually run all the shortcuts:
129 count_of_shortcuts_run = 0
130 turtle_code = tuple()
131 for shortcut in shortcuts.split(','):
132 if _return_turtle_code:
133 turtle_code += _run_shortcut(shortcut, turtle_obj=turtle_obj, _return_turtle_code=True)
134 else:
135 count_of_shortcuts_run += _run_shortcut(shortcut, turtle_obj=turtle_obj)
137 if _return_turtle_code:
138 return turtle_code
139 else:
140 return count_of_shortcuts_run
143def _run_shortcut(shortcut, turtle_obj=None, dry_run=False, _return_turtle_code=False):
144 if turtle_obj is None:
145 turtle_obj = turtle # Use the main turtle given by the module.
147 # Clean up shortcut name from " FOrWARD " to "f", for example.
148 shortcut_parts = shortcut.strip().split()
149 if len(shortcut_parts) == 0:
150 return 0
151 _sc = shortcut_parts[0].lower()
152 _sc = _MAP_FULL_TO_SHORT_NAMES.get(_sc, _sc)
154 # Check that the shortcut's syntax is valid:
156 if _sc not in ALL_SHORTCUTS:
157 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[0] + '` is not a turtle shortcut.')
159 raise_exception = False
160 count_of_shortcuts_run = 0
164 # SHORTCUTS THAT TAKE A SINGLE NUMERIC ARGUMENT:
165 if _sc in ('f', 'b', 'r', 'l', 'x', 'y', 'ps', 'sh', 'cir', 'sleep', 'n', 's', 'e', 'w', 'nw', 'ne', 'sw', 'se', 'dot', 'cs', 'spd'):
166 if len(shortcut_parts) < 2:
167 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Missing the required numeric argument.')
168 if len(shortcut_parts) > 2:
169 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Too many arguments.')
171 # Convert the string arguments for the `speed` shortcut to their numeric equivalents.
172 if _sc == 'spd':
173 shortcut_parts[1] = {'fastest': 0, 'fast': 10, 'normal': 6, 'slow': 3, 'slowest': 1}.get(shortcut_parts[1].lower(), shortcut_parts[1].lower())
175 try:
176 float(shortcut_parts[1])
177 except ValueError:
178 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
179 if raise_exception:
180 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[1] + '` is not a number.')
182 # `dot` shortcut doesn't allow negative values:
183 if _sc == 'dot' and float(shortcut_parts[1]) < 0:
184 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `dot` argument cannot be a negative number.')
186 if not dry_run:
187 # Run the shortcut that has exactly one numeric argument:
188 if _sc == 'f':
189 if _return_turtle_code:
190 return ('forward(' + shortcut_parts[1] + ')',)
191 turtle_obj.forward(float(shortcut_parts[1]))
192 elif _sc == 'b':
193 if _return_turtle_code:
194 return ('backward(' + shortcut_parts[1] + ')',)
195 turtle_obj.backward(float(shortcut_parts[1]))
196 elif _sc == 'r':
197 if _return_turtle_code:
198 return ('right(' + shortcut_parts[1] + ')',)
199 turtle_obj.right(float(shortcut_parts[1]))
200 elif _sc == 'l':
201 if _return_turtle_code:
202 return ('left(' + shortcut_parts[1] + ')',)
203 turtle_obj.left(float(shortcut_parts[1]))
204 elif _sc == 'x':
205 if _return_turtle_code:
206 return ('setx(' + shortcut_parts[1] + ')',)
207 turtle_obj.setx(float(shortcut_parts[1]))
208 elif _sc == 'y':
209 if _return_turtle_code:
210 return ('sety(' + shortcut_parts[1] + ')',)
211 turtle_obj.sety(float(shortcut_parts[1]))
212 elif _sc == 'ps':
213 if _return_turtle_code:
214 return ('pensize(' + shortcut_parts[1] + ')',)
215 turtle_obj.pensize(float(shortcut_parts[1]))
216 elif _sc == 'sh':
217 if _return_turtle_code:
218 return ('setheading(' + shortcut_parts[1] + ')',)
219 turtle_obj.setheading(float(shortcut_parts[1]))
220 elif _sc == 'cir':
221 if _return_turtle_code:
222 return ('circle(' + shortcut_parts[1] + ')',)
223 turtle_obj.circle(float(shortcut_parts[1]))
224 elif _sc == 'sleep':
225 if _return_turtle_code:
226 return ('sleep(' + shortcut_parts[1] + ')', )
227 time.sleep(float(shortcut_parts[1]))
228 elif _sc in ('n', 's', 'e', 'w', 'nw', 'ne', 'sw', 'se'):
229 originally_in_radians_mode = in_radians_mode()
231 if _return_turtle_code:
232 if originally_in_radians_mode:
233 return ('degrees()', 'setheading(' + CARDINAL_TO_DEGREES[_sc] + ')', 'forward(' + shortcut_parts[1] + ')', 'radians()')
234 else:
235 return ('setheading(' + CARDINAL_TO_DEGREES[_sc] + ')', 'forward(' + shortcut_parts[1] + ')')
236 turtle.degrees()
237 if _sc == 'n':
238 turtle.setheading(90)
239 elif _sc == 's':
240 turtle.setheading(270)
241 elif _sc == 'e':
242 turtle.setheading(0)
243 elif _sc == 'w':
244 turtle.setheading(180)
245 elif _sc == 'nw':
246 turtle.setheading(135)
247 elif _sc == 'ne':
248 turtle.setheading(45)
249 elif _sc == 'sw':
250 turtle.setheading(225)
251 elif _sc == 'se':
252 turtle.setheading(315)
253 else: # pragma: no cover
254 assert False, 'Unhandled shortcut: ' + _sc
255 turtle_obj.forward(float(shortcut_parts[1]))
256 if originally_in_radians_mode:
257 turtle.radians()
258 elif _sc == 'dot':
259 if _return_turtle_code:
260 return ('dot(' + shortcut_parts[1] + ')',)
261 turtle_obj.dot(float(shortcut_parts[1]))
262 elif _sc == 'cs':
263 if _return_turtle_code:
264 return ('clearstamp(' + shortcut_parts[1] + ')',)
265 turtle_obj.clearstamp(float(shortcut_parts[1]))
266 elif _sc == 'spd':
267 if _return_turtle_code:
268 return ('speed(' + str(shortcut_parts[1]) + ')',)
269 turtle_obj.speed(float(shortcut_parts[1]))
270 else: # pragma: no cover
271 assert False, 'Unhandled shortcut: ' + _sc
272 count_of_shortcuts_run += 1
278 # SHORTCUTS THAT TAKE A SINGLE INTEGER ARGUMENT OR NONE ARGUMENT:
279 elif _sc in ('css',):
280 if len(shortcut_parts) > 2:
281 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Too many arguments.')
283 # Technically, the css shortcut can take a float argument, but it gets passed to int() silently. Not ideal, but not a big deal either.
285 if len(shortcut_parts) == 2:
286 try:
287 int(shortcut_parts[1])
288 except ValueError:
289 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
290 if raise_exception:
291 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[1] + '` is not a number.')
293 if not dry_run:
294 # Run the shortcut:
295 if _sc == 'css':
296 if len(shortcut_parts) == 1:
297 if _return_turtle_code:
298 return ('clearstamps()',)
299 turtle_obj.clearstamps()
300 elif len(shortcut_parts) == 2:
301 if _return_turtle_code:
302 return ('clearstamps(' + shortcut_parts[1] + ')',)
303 turtle_obj.clearstamps(int(shortcut_parts[1]))
304 else: # pragma: no cover
305 assert False, 'Unhandled shortcut: ' + _sc
306 else: # pragma: no cover
307 assert False, 'Unhandled shortcut: ' + _sc
308 count_of_shortcuts_run += 1
315 # SHORTCUTS THAT TAKE EXACTLY TWO NUMERIC ARGUMENTS:
316 elif _sc in ('g', 't', 'tele'):
317 if len(shortcut_parts) < 3:
318 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Missing two required numeric argument.')
319 elif len(shortcut_parts) > 3:
320 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Too many arguments.')
322 try:
323 float(shortcut_parts[1])
324 except ValueError:
325 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
326 if raise_exception:
327 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[1] + '` is not a number.')
328 try:
329 float(shortcut_parts[2])
330 except ValueError:
331 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
332 if raise_exception:
333 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[2] + '` is not a number.')
335 if not dry_run:
336 # Run the shortcut that has exactly two numeric arguments:
337 x = float(shortcut_parts[1])
338 y = float(shortcut_parts[2])
340 # Run the shortcut:
341 if _sc == 'g':
342 if _return_turtle_code:
343 return ('goto(' + shortcut_parts[1] + ', ' + shortcut_parts[2] + ')',)
344 turtle_obj.goto(x, y)
345 elif _sc == 't':
346 if _return_turtle_code:
347 return ('tracer(' + shortcut_parts[1] + ', ' + shortcut_parts[2] + ')',)
348 turtle.tracer(x, y) # Note: tracer() is not a Turtle method, there's only the global tracer() function.
349 elif _sc == 'tele':
350 if _return_turtle_code:
351 return ('teleport(' + shortcut_parts[1] + ', ' + shortcut_parts[2] + ')',)
352 turtle_obj.teleport(x, y)
353 else: # pragma: no cover
354 assert False, 'Unhandled shortcut: ' + _sc
355 count_of_shortcuts_run += 1
361 # SHORTCUTS THAT TAKE EXACTLY ZERO ARGUMENTS:
362 elif _sc in ('h', 'c', 'st', 'pd', 'pu', 'undo', 'bf', 'ef', 'reset', 'bye', 'done', 'exitonclick', 'u', 'show', 'hide'):
363 if len(shortcut_parts) > 1:
364 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: This shortcut does not have arguments.')
366 if not dry_run:
367 # Run the shortcut that has exactly zero arguments:
368 if _sc == 'h':
369 if _return_turtle_code:
370 return ('home()',)
371 turtle_obj.home()
372 elif _sc == 'c':
373 if _return_turtle_code:
374 return ('clear()',)
375 turtle_obj.clear()
376 elif _sc == 'st':
377 if _return_turtle_code:
378 return ('stamp()',)
379 turtle_obj.stamp()
380 elif _sc == 'pd':
381 if _return_turtle_code:
382 return ('pendown()',)
383 turtle_obj.pendown()
384 elif _sc == 'pu':
385 if _return_turtle_code:
386 return ('penup()',)
387 turtle_obj.penup()
388 elif _sc == 'undo':
389 if _return_turtle_code:
390 return ('undo()',)
391 turtle_obj.undo()
392 elif _sc == 'bf':
393 if _return_turtle_code:
394 return ('begin_fill()',)
395 turtle_obj.begin_fill()
396 elif _sc == 'ef':
397 if _return_turtle_code:
398 return ('end_fill()',)
399 turtle_obj.end_fill()
400 elif _sc == 'reset':
401 if _return_turtle_code:
402 return ('reset()',)
403 turtle_obj.reset()
404 elif _sc == 'bye': # pragma: no cover
405 if _return_turtle_code:
406 return ('bye()',)
407 turtle_obj.bye()
408 elif _sc == 'done': # pragma: no cover
409 if _return_turtle_code:
410 return ('done()',)
411 turtle_obj.done()
412 elif _sc == 'exitonclick': # pragma: no cover
413 if _return_turtle_code:
414 return ('exitonclick()',)
415 turtle_obj.exitonclick()
416 elif _sc == 'u':
417 if _return_turtle_code:
418 return ('update()',)
419 turtle_obj.update()
420 elif _sc == 'show':
421 if _return_turtle_code:
422 return ('showturtle()',)
423 turtle_obj.showturtle()
424 elif _sc == 'hide':
425 if _return_turtle_code:
426 return ('hideturtle()',)
427 turtle_obj.hideturtle()
428 else: # pragma: no cover
429 assert False, 'Unhandled shortcut: ' + _sc
430 count_of_shortcuts_run += 1
434 # SHORTCUTS THAT TAKE AN RGB OR COLOR ARGUMENT:
435 elif _sc in ('pc', 'fc', 'bc'):
436 color_arg_is_color_name = False # Start as False. If it's a color name, we'll set this to True.
438 if len(shortcut_parts) < 2:
439 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: Missing required RGB argument.')
440 elif len(shortcut_parts) not in (2, 4):
441 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`.')
443 if len(shortcut_parts) == 4:
444 # We expect the color arg to either be something like (255, 0, 0) or (1.0, 0.0, 0.0):
445 raise_exception = False
447 try:
448 float(shortcut_parts[1])
449 except ValueError:
450 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
451 if raise_exception:
452 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[1] + '` is not a number.')
454 try:
455 float(shortcut_parts[2])
456 except ValueError:
457 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
458 if raise_exception:
459 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[2] + '` is not a number.')
461 try:
462 float(shortcut_parts[3])
463 except ValueError:
464 raise_exception = True # We don't raise here so we can hide the original ValueError and make the stack trace a bit neater.
465 if raise_exception:
466 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[3] + '` is not a number.')
468 if turtle_obj.colormode() == 1.0:
469 color_arg = (float(shortcut_parts[1]), float(shortcut_parts[2]), float(shortcut_parts[3]))
470 elif turtle_obj.colormode() == 255:
471 # Convert strings like '1.0' to floats first, then to int. (Calling int('1.0') would raise a ValueError.)
472 color_arg = (int(float(shortcut_parts[1])), int(float(shortcut_parts[2])), int(float(shortcut_parts[3])))
473 else: # pragma: no cover
474 assert False, 'Unhandled colormode: ' + str(turtle_obj.colormode())
476 if turtle_obj.colormode() == 1.0 and (color_arg[0] > 1.0 or color_arg[1] > 1.0 or color_arg[2] > 1.0):
477 raise TurtleShortcutException(shortcut + ' is invalid because colormode is 1.0 and one or more RGB color values are greater than 1.0.')
479 elif len(shortcut_parts) == 2:
480 # We expect the color arg to be a string like 'blue' or '#FF0000':
481 raise_exception = False
483 if re.match(r'^#[0-9A-Fa-f]{6}$', shortcut_parts[1]):
484 # Color arg is a hex code like '#FF0000', and not a name like 'blue'.
485 color_arg_is_color_name = False # It's already False, but I put this here to be explicit.
486 else:
487 # shortcut_parts[1] must be a color name like 'blue'
488 color_arg_is_color_name = True
489 color_arg = shortcut_parts[1]
491 # Test the color name by actually calling pencolor():
492 original_pen_color = turtle_obj.pencolor()
493 try:
494 turtle_obj.pencolor(color_arg)
495 except turtle.TurtleGraphicsError:
496 raise_exception = True # We don't raise here so we can hide the original TurtleGraphicsError and make the stack trace a bit neater.
497 if raise_exception:
498 if re.match(r'^[0-9A-Fa-f]{6}$', shortcut_parts[1]):
499 raise TurtleShortcutException('Syntax error in `' + shortcut + "`: '" + shortcut_parts[1] + "' is not a valid color. Did you mean '# " + shortcut_parts[1] + "'?")
500 else:
501 raise TurtleShortcutException('Syntax error in `' + shortcut + "`: '" + shortcut_parts[1] + "' is not a valid color.")
503 # NOTE: This code here is to handle an unfixed bug in turtle.py. If the color mode is 1.0 and you set
504 # 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)
505 # but these float values are not a valid setting for a color while in mode 255. So we have to convert them
506 # to integers here.
507 if isinstance(original_pen_color, tuple) and turtle_obj.colormode() == 255:
508 turtle_obj.pencolor(int(original_pen_color[0]), int(original_pen_color[1]), int(original_pen_color[2]))
510 if not dry_run:
511 # Return the turtle code, if that was asked:
512 if _return_turtle_code:
513 if _sc == 'pc':
514 func_name_prefix = 'pen'
515 elif _sc == 'fc':
516 func_name_prefix = 'fill'
517 elif _sc == 'bc':
518 func_name_prefix = 'bg'
520 if color_arg_is_color_name:
521 return (func_name_prefix + "color('" + str(color_arg) + "')",)
522 else:
523 return (func_name_prefix + 'color(' + str(color_arg) + ')',)
525 # Run the shortcut that has an RGB color argument:
526 if _sc == 'pc':
527 turtle_obj.pencolor(color_arg)
528 elif _sc == 'fc':
529 turtle_obj.fillcolor(color_arg)
530 elif _sc == 'bc':
531 turtle_obj.bgcolor(color_arg)
532 else: # pragma: no cover
533 assert False, 'Unhandled shortcut: ' + _sc
534 count_of_shortcuts_run += 1
536 return count_of_shortcuts_run
539def in_radians_mode():
540 """Returns True if turtle is in radians mode, False if in degrees mode."""
541 original_heading = turtle.heading()
542 turtle.left(1)
543 turtle.radians() # Switch to radians mode.
544 turtle.right(1)
545 if turtle.heading() == original_heading:
546 return True
547 else:
548 turtle.degrees() # Switch back to degrees mode.
549 return False
552def in_degrees_mode():
553 """Returns True if turtle is in degrees mode, False if in radians mode."""
554 return not in_radians_mode()
557def get_turtle_code(*args):
558 """Returns the Python code that would be executed by the sc() function."""
559 return sc(*args, _return_turtle_code=True)