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

1import turtle, time, re 

2 

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? 

5 

6 

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' 

11 

12CARDINAL_TO_DEGREES = {'n': '90', 's': '270', 'e': '0', 'w': '180', 'nw': '135', 'ne': '45', 'sw': '225', 'se': '315'} 

13 

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'} 

20 

21RECORDED_SHORTCUTS = [] 

22_NOW_RECORDING = False 

23 

24class TurtleShortcutException(Exception): 

25 pass 

26 

27def sc(*args, turtle_obj=None, _return_turtle_code=False, skip=False): # type: () -> int 

28 """TODO 

29 """ 

30 

31 """Supported commands: 

32 

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() 

56 

57 sleep N - time.sleep(N) 

58 

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) 

75 

76 done - done() 

77 bye - bye() 

78 exitonclick - exitonclick() 

79 

80 t N1 N2 - tracer(N1, N2) 

81 u - update() 

82 

83 hide - hide() 

84 show - show() 

85 

86 dot N - dot(N) 

87 cs N - clearstamp(N) 

88 css N - clearstamps(N) 

89 degrees - degrees() 

90 radians - radians() 

91  

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. 

96 

97 !!shapesize N1 N2 N3 - shapesize(N1, N2, N3) 

98 !!settiltangle N - settiltangle(N) 

99 !!tilt N - tilt(N) 

100 !!tiltangle N - tiltangle(N) 

101  

102 

103 

104 

105 Note:  

106 

107 

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. 

111 

112 Return value is the number of commands executed. 

113 Whitespace is insignificant. ' f 100 ' is the same as 'f 100' 

114 """ 

115 

116 if skip: 

117 if _return_turtle_code: 

118 return () 

119 else: 

120 return 0 

121 

122 # Join multiple arg strings into one, separated by commas: 

123 shortcuts = ','.join(args) 

124 

125 # Newlines become commas as well: 

126 shortcuts = shortcuts.replace('\n', ',') 

127 

128 if shortcuts == '' or len(shortcuts.split(',')) == 0: 

129 return 0 

130 

131 count_of_shortcuts_run = 0 

132 

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) 

136 

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) 

145 

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 

151 

152 

153def _run_shortcut(shortcut, turtle_obj=None, dry_run=False, _return_turtle_code=False): 

154 '''Runs a single shortcut''' 

155 

156 if turtle_obj is None: 

157 turtle_obj = turtle # Use the main turtle given by the module. 

158 

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) 

168 

169 # Check that the shortcut's syntax is valid: 

170 

171 if _sc not in ALL_SHORTCUTS: 

172 raise TurtleShortcutException('Syntax error in `' + shortcut + '`: `' + shortcut_parts[0] + '` is not a turtle shortcut.') 

173 

174 raise_exception = False 

175 count_of_shortcuts_run = 0 

176 

177 

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 

186 

187 

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.') 

194 

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()) 

198 

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.') 

205 

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.') 

209 

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() 

254 

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 

297 

298 

299 

300 

301 

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.') 

306 

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. 

308 

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.') 

316 

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 

333 

334 

335 

336 

337 

338 

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.') 

345 

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.') 

358 

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]) 

363 

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 

380 

381 

382 

383 

384 

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.') 

389 

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 

455 

456 

457 

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. 

461 

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`.') 

466 

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 

470 

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.') 

477 

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.') 

484 

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.') 

491 

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()) 

499 

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.') 

502 

503 elif len(shortcut_parts) == 2: 

504 # We expect the color arg to be a string like 'blue' or '#FF0000': 

505 raise_exception = False 

506 

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] 

514 

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.") 

526 

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) 

535 

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' 

545 

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) + ')',) 

550 

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 

561 

562 # If begin_recording() has been called, log the shortcut. 

563 if _NOW_RECORDING and not dry_run: 

564 RECORDED_SHORTCUTS.append(shortcut) 

565 

566 return count_of_shortcuts_run 

567 

568 

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 

580 

581 

582def in_degrees_mode(): 

583 """Returns True if turtle is in degrees mode, False if in radians mode.""" 

584 return not in_radians_mode() 

585 

586 

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) 

590 

591def psc(*args): 

592 """Prints the Python code that would be executed by the sc() function.""" 

593 print(sc(*args, _return_turtle_code=True)) 

594 

595 

596def begin_recording(shortcut_list=None): 

597 global RECORDED_SHORTCUTS, _NOW_RECORDING 

598 RECORDED_SHORTCUTS = [] 

599 

600 _NOW_RECORDING = True 

601 

602def end_recording(): 

603 global RECORDED_SHORTCUTS, _NOW_RECORDING 

604 

605 _NOW_RECORDING = False 

606 return RECORDED_SHORTCUTS 

607