Skip to content

Commit 584cdf8

Browse files
authored
gh-123614: Add save function to turtle.py (#123617)
1 parent 1f9d163 commit 584cdf8

File tree

4 files changed

+122
-1
lines changed

4 files changed

+122
-1
lines changed

Doc/library/turtle.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ Input methods
427427
Methods specific to Screen
428428
| :func:`bye`
429429
| :func:`exitonclick`
430+
| :func:`save`
430431
| :func:`setup`
431432
| :func:`title`
432433
@@ -2269,6 +2270,24 @@ Methods specific to Screen, not inherited from TurtleScreen
22692270
client script.
22702271

22712272

2273+
.. function:: save(filename, overwrite=False)
2274+
2275+
Save the current turtle drawing (and turtles) as a PostScript file.
2276+
2277+
:param filename: the path of the saved PostScript file
2278+
:param overwrite: if ``False`` and there already exists a file with the given
2279+
filename, then the function will raise a
2280+
``FileExistsError``. If it is ``True``, the file will be
2281+
overwritten.
2282+
2283+
.. doctest::
2284+
:skipif: _tkinter is None
2285+
2286+
>>> screen.save("my_drawing.ps")
2287+
>>> screen.save("my_drawing.ps", overwrite=True)
2288+
2289+
.. versionadded:: 3.14
2290+
22722291
.. function:: setup(width=_CFG["width"], height=_CFG["height"], startx=_CFG["leftright"], starty=_CFG["topbottom"])
22732292

22742293
Set the size and position of the main window. Default values of arguments

Lib/test/test_turtle.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import os
12
import pickle
3+
import re
24
import unittest
5+
import unittest.mock
6+
import tempfile
37
from test import support
48
from test.support import import_helper
59
from test.support import os_helper
@@ -130,6 +134,7 @@ def assertVectorsAlmostEqual(self, vec1, vec2):
130134
self.assertAlmostEqual(
131135
i, j, msg='values at index {} do not match'.format(idx))
132136

137+
133138
class Multiplier:
134139

135140
def __mul__(self, other):
@@ -461,6 +466,67 @@ def test_teleport(self):
461466
self.assertTrue(tpen.isdown())
462467

463468

469+
class TestTurtleScreen(unittest.TestCase):
470+
def test_save_raises_if_wrong_extension(self) -> None:
471+
screen = unittest.mock.Mock()
472+
473+
msg = "Unknown file extension: '.png', must be one of {'.ps', '.eps'}"
474+
with (
475+
tempfile.TemporaryDirectory() as tmpdir,
476+
self.assertRaisesRegex(ValueError, re.escape(msg))
477+
):
478+
turtle.TurtleScreen.save(screen, os.path.join(tmpdir, "file.png"))
479+
480+
def test_save_raises_if_parent_not_found(self) -> None:
481+
screen = unittest.mock.Mock()
482+
483+
with tempfile.TemporaryDirectory() as tmpdir:
484+
parent = os.path.join(tmpdir, "unknown_parent")
485+
msg = f"The directory '{parent}' does not exist. Cannot save to it"
486+
487+
with self.assertRaisesRegex(FileNotFoundError, re.escape(msg)):
488+
turtle.TurtleScreen.save(screen, os.path.join(parent, "a.ps"))
489+
490+
def test_save_raises_if_file_found(self) -> None:
491+
screen = unittest.mock.Mock()
492+
493+
with tempfile.TemporaryDirectory() as tmpdir:
494+
file_path = os.path.join(tmpdir, "some_file.ps")
495+
with open(file_path, "w") as f:
496+
f.write("some text")
497+
498+
msg = (
499+
f"The file '{file_path}' already exists. To overwrite it use"
500+
" the 'overwrite=True' argument of the save function."
501+
)
502+
with self.assertRaisesRegex(FileExistsError, re.escape(msg)):
503+
turtle.TurtleScreen.save(screen, file_path)
504+
505+
def test_save_overwrites_if_specified(self) -> None:
506+
screen = unittest.mock.Mock()
507+
screen.cv.postscript.return_value = "postscript"
508+
509+
with tempfile.TemporaryDirectory() as tmpdir:
510+
file_path = os.path.join(tmpdir, "some_file.ps")
511+
with open(file_path, "w") as f:
512+
f.write("some text")
513+
514+
turtle.TurtleScreen.save(screen, file_path, overwrite=True)
515+
with open(file_path) as f:
516+
assert f.read() == "postscript"
517+
518+
def test_save(self) -> None:
519+
screen = unittest.mock.Mock()
520+
screen.cv.postscript.return_value = "postscript"
521+
522+
with tempfile.TemporaryDirectory() as tmpdir:
523+
file_path = os.path.join(tmpdir, "some_file.ps")
524+
525+
turtle.TurtleScreen.save(screen, file_path)
526+
with open(file_path) as f:
527+
assert f.read() == "postscript"
528+
529+
464530
class TestModuleLevel(unittest.TestCase):
465531
def test_all_signatures(self):
466532
import inspect

Lib/turtle.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
import sys
107107

108108
from os.path import isfile, split, join
109+
from pathlib import Path
109110
from copy import deepcopy
110111
from tkinter import simpledialog
111112

@@ -115,7 +116,7 @@
115116
'clearscreen', 'colormode', 'delay', 'exitonclick', 'getcanvas',
116117
'getshapes', 'listen', 'mainloop', 'mode', 'numinput',
117118
'onkey', 'onkeypress', 'onkeyrelease', 'onscreenclick', 'ontimer',
118-
'register_shape', 'resetscreen', 'screensize', 'setup',
119+
'register_shape', 'resetscreen', 'screensize', 'save', 'setup',
119120
'setworldcoordinates', 'textinput', 'title', 'tracer', 'turtles', 'update',
120121
'window_height', 'window_width']
121122
_tg_turtle_functions = ['back', 'backward', 'begin_fill', 'begin_poly', 'bk',
@@ -1492,6 +1493,39 @@ def screensize(self, canvwidth=None, canvheight=None, bg=None):
14921493
"""
14931494
return self._resize(canvwidth, canvheight, bg)
14941495

1496+
def save(self, filename, *, overwrite=False):
1497+
"""Save the drawing as a PostScript file
1498+
1499+
Arguments:
1500+
filename -- a string, the path of the created file.
1501+
Must end with '.ps' or '.eps'.
1502+
1503+
Optional arguments:
1504+
overwrite -- boolean, if true, then existing files will be overwritten
1505+
1506+
Example (for a TurtleScreen instance named screen):
1507+
>>> screen.save('my_drawing.eps')
1508+
"""
1509+
filename = Path(filename)
1510+
if not filename.parent.exists():
1511+
raise FileNotFoundError(
1512+
f"The directory '{filename.parent}' does not exist."
1513+
" Cannot save to it."
1514+
)
1515+
if not overwrite and filename.exists():
1516+
raise FileExistsError(
1517+
f"The file '{filename}' already exists. To overwrite it use"
1518+
" the 'overwrite=True' argument of the save function."
1519+
)
1520+
if (ext := filename.suffix) not in {".ps", ".eps"}:
1521+
raise ValueError(
1522+
f"Unknown file extension: '{ext}',"
1523+
" must be one of {'.ps', '.eps'}"
1524+
)
1525+
1526+
postscript = self.cv.postscript()
1527+
filename.write_text(postscript)
1528+
14951529
onscreenclick = onclick
14961530
resetscreen = reset
14971531
clearscreen = clear
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`turtle.save` to easily save Turtle drawings as PostScript files.
2+
Patch by Marie Roald and Yngve Mardal Moe.

0 commit comments

Comments
 (0)