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

This commit is contained in:
Yngve Mardal Moe 2024-09-13 06:36:17 +02:00 committed by GitHub
parent 1f9d163850
commit 584cdf8d41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 122 additions and 1 deletions

View File

@ -427,6 +427,7 @@ Input methods
Methods specific to Screen Methods specific to Screen
| :func:`bye` | :func:`bye`
| :func:`exitonclick` | :func:`exitonclick`
| :func:`save`
| :func:`setup` | :func:`setup`
| :func:`title` | :func:`title`
@ -2269,6 +2270,24 @@ Methods specific to Screen, not inherited from TurtleScreen
client script. 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"]) .. 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 Set the size and position of the main window. Default values of arguments

View File

@ -1,5 +1,9 @@
import os
import pickle import pickle
import re
import unittest import unittest
import unittest.mock
import tempfile
from test import support from test import support
from test.support import import_helper from test.support import import_helper
from test.support import os_helper from test.support import os_helper
@ -130,6 +134,7 @@ class VectorComparisonMixin:
self.assertAlmostEqual( self.assertAlmostEqual(
i, j, msg='values at index {} do not match'.format(idx)) i, j, msg='values at index {} do not match'.format(idx))
class Multiplier: class Multiplier:
def __mul__(self, other): def __mul__(self, other):
@ -461,6 +466,67 @@ class TestTPen(unittest.TestCase):
self.assertTrue(tpen.isdown()) 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): class TestModuleLevel(unittest.TestCase):
def test_all_signatures(self): def test_all_signatures(self):
import inspect import inspect

View File

@ -106,6 +106,7 @@ import inspect
import sys import sys
from os.path import isfile, split, join from os.path import isfile, split, join
from pathlib import Path
from copy import deepcopy from copy import deepcopy
from tkinter import simpledialog from tkinter import simpledialog
@ -115,7 +116,7 @@ _tg_screen_functions = ['addshape', 'bgcolor', 'bgpic', 'bye',
'clearscreen', 'colormode', 'delay', 'exitonclick', 'getcanvas', 'clearscreen', 'colormode', 'delay', 'exitonclick', 'getcanvas',
'getshapes', 'listen', 'mainloop', 'mode', 'numinput', 'getshapes', 'listen', 'mainloop', 'mode', 'numinput',
'onkey', 'onkeypress', 'onkeyrelease', 'onscreenclick', 'ontimer', 'onkey', 'onkeypress', 'onkeyrelease', 'onscreenclick', 'ontimer',
'register_shape', 'resetscreen', 'screensize', 'setup', 'register_shape', 'resetscreen', 'screensize', 'save', 'setup',
'setworldcoordinates', 'textinput', 'title', 'tracer', 'turtles', 'update', 'setworldcoordinates', 'textinput', 'title', 'tracer', 'turtles', 'update',
'window_height', 'window_width'] 'window_height', 'window_width']
_tg_turtle_functions = ['back', 'backward', 'begin_fill', 'begin_poly', 'bk', _tg_turtle_functions = ['back', 'backward', 'begin_fill', 'begin_poly', 'bk',
@ -1492,6 +1493,39 @@ class TurtleScreen(TurtleScreenBase):
""" """
return self._resize(canvwidth, canvheight, bg) 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 onscreenclick = onclick
resetscreen = reset resetscreen = reset
clearscreen = clear clearscreen = clear

View File

@ -0,0 +1,2 @@
Add :func:`turtle.save` to easily save Turtle drawings as PostScript files.
Patch by Marie Roald and Yngve Mardal Moe.