Skip to content

gh-123614: Add save function to turtle.py #123617

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 13, 2024
Merged
19 changes: 19 additions & 0 deletions Doc/library/turtle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ Input methods
Methods specific to Screen
| :func:`bye`
| :func:`exitonclick`
| :func:`save`
| :func:`setup`
| :func:`title`

Expand Down Expand Up @@ -2269,6 +2270,24 @@ Methods specific to Screen, not inherited from TurtleScreen
client script.


.. function:: save(filename, overwrite=False)

Save the current turtle drawing (and turtles) as a PostScript file.

:param filename: the path of the saved PostScript file
:param overwrite: if ``False`` and there already exists a file with the given
filename, then the function will raise a
``FileExistsError``. If it is ``True``, the file will be
overwritten.

.. doctest::
:skipif: _tkinter is None

>>> screen.save("my_drawing.ps")
>>> screen.save("my_drawing.ps", overwrite=True)

.. versionadded:: 3.14

.. function:: setup(width=_CFG["width"], height=_CFG["height"], startx=_CFG["leftright"], starty=_CFG["topbottom"])

Set the size and position of the main window. Default values of arguments
Expand Down
66 changes: 66 additions & 0 deletions Lib/test/test_turtle.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import os
import pickle
import re
import unittest
import unittest.mock
import tempfile
from test import support
from test.support import import_helper
from test.support import os_helper
Expand Down Expand Up @@ -130,6 +134,7 @@ def assertVectorsAlmostEqual(self, vec1, vec2):
self.assertAlmostEqual(
i, j, msg='values at index {} do not match'.format(idx))


class Multiplier:

def __mul__(self, other):
Expand Down Expand Up @@ -461,6 +466,67 @@ def test_teleport(self):
self.assertTrue(tpen.isdown())


class TestTurtleScreen(unittest.TestCase):
def test_save_raises_if_wrong_extension(self) -> None:
screen = unittest.mock.Mock()

msg = "Unknown file extension: '.png', must be one of {'.ps', '.eps'}"
with (
tempfile.TemporaryDirectory() as tmpdir,
self.assertRaisesRegex(ValueError, re.escape(msg))
):
turtle.TurtleScreen.save(screen, os.path.join(tmpdir, "file.png"))

def test_save_raises_if_parent_not_found(self) -> None:
screen = unittest.mock.Mock()

with tempfile.TemporaryDirectory() as tmpdir:
parent = os.path.join(tmpdir, "unknown_parent")
msg = f"The directory '{parent}' does not exist. Cannot save to it"

with self.assertRaisesRegex(FileNotFoundError, re.escape(msg)):
turtle.TurtleScreen.save(screen, os.path.join(parent, "a.ps"))

def test_save_raises_if_file_found(self) -> None:
screen = unittest.mock.Mock()

with tempfile.TemporaryDirectory() as tmpdir:
file_path = os.path.join(tmpdir, "some_file.ps")
with open(file_path, "w") as f:
f.write("some text")

msg = (
f"The file '{file_path}' already exists. To overwrite it use"
" the 'overwrite=True' argument of the save function."
)
with self.assertRaisesRegex(FileExistsError, re.escape(msg)):
turtle.TurtleScreen.save(screen, file_path)

def test_save_overwrites_if_specified(self) -> None:
screen = unittest.mock.Mock()
screen.cv.postscript.return_value = "postscript"

with tempfile.TemporaryDirectory() as tmpdir:
file_path = os.path.join(tmpdir, "some_file.ps")
with open(file_path, "w") as f:
f.write("some text")

turtle.TurtleScreen.save(screen, file_path, overwrite=True)
with open(file_path) as f:
assert f.read() == "postscript"

def test_save(self) -> None:
screen = unittest.mock.Mock()
screen.cv.postscript.return_value = "postscript"

with tempfile.TemporaryDirectory() as tmpdir:
file_path = os.path.join(tmpdir, "some_file.ps")

turtle.TurtleScreen.save(screen, file_path)
with open(file_path) as f:
assert f.read() == "postscript"


class TestModuleLevel(unittest.TestCase):
def test_all_signatures(self):
import inspect
Expand Down
36 changes: 35 additions & 1 deletion Lib/turtle.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
import sys

from os.path import isfile, split, join
from pathlib import Path
from copy import deepcopy
from tkinter import simpledialog

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

def save(self, filename, *, overwrite=False):
"""Save the drawing as a PostScript file

Arguments:
filename -- a string, the path of the created file.
Must end with '.ps' or '.eps'.

Optional arguments:
overwrite -- boolean, if true, then existing files will be overwritten

Example (for a TurtleScreen instance named screen):
>>> screen.save('my_drawing.eps')
"""
filename = Path(filename)
if not filename.parent.exists():
raise FileNotFoundError(
f"The directory '{filename.parent}' does not exist."
" Cannot save to it."
)
if not overwrite and filename.exists():
raise FileExistsError(
f"The file '{filename}' already exists. To overwrite it use"
" the 'overwrite=True' argument of the save function."
)
if (ext := filename.suffix) not in {".ps", ".eps"}:
raise ValueError(
f"Unknown file extension: '{ext}',"
" must be one of {'.ps', '.eps'}"
)

postscript = self.cv.postscript()
filename.write_text(postscript)

onscreenclick = onclick
resetscreen = reset
clearscreen = clear
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`turtle.save` to easily save Turtle drawings as PostScript files.
Patch by Marie Roald and Yngve Mardal Moe.
Loading