diff --git a/arcade/__init__.py b/arcade/__init__.py index f24adfa7d..db48232a0 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -9,6 +9,7 @@ # Error out if we import Arcade with an incompatible version of Python. import sys import os +from datetime import datetime, tzinfo from typing import Optional from pathlib import Path @@ -34,6 +35,50 @@ def configure_logging(level: Optional[int] = None): LOG.addHandler(ch) +def get_timestamp( + how: str = "%Y_%m_%d_%H%M_%S_%f%Z", + when: Optional[types.HasStrftime] = None, + tzinfo: Optional[tzinfo] = None +) -> str: + """Return a timestamp as a formatted string. + + .. tip:: To print text to the console, see :ref:`logging`! + + This function :ref:`helps people who can't ` + use a :ref:`better alternative `. + + Calling this function without any arguments returns a string + with the current system time down to microseconds: + + .. code-block:: python + + # This code assumes the function is called at exactly 3PM + # on April 3rd, 2024 in the computer's local time zone. + >>> arcade.get_timestamp() + `2024_04_03_1500_00_000000' + + + See the following to learn more: + + * For general information, see :ref:`debug-timestamps` + * For custom formatting & times, see :ref:`debug-timestamps-example-when-how` + * To use time zones such as UTC, see :ref:`debug-timestamps-example-timezone` + * The general :py:mod:`datetime` documentation + * Python's guide to + :ref:`datetime-like behavior ` + + + :param how: A :ref:`valid datetime format string ` + :param tzinfo: A :py:class:`datetime.tzinfo` instance. + :param when: ``None`` or a :ref:`a datetime-like object ` + :return: A formatted string for either a passed ``when`` or + :py:meth:`datetime.now ` + + """ + when = when or datetime.now(tzinfo) + return when.strftime(how) + + # The following is used to load ffmpeg libraries. # Currently Arcade is only shipping binaries for Mac OS # as ffmpeg is not needed for support on Windows and Linux. @@ -83,6 +128,7 @@ def configure_logging(level: Optional[int] = None): from .window_commands import start_render from .window_commands import unschedule from .window_commands import schedule_once +from .window_commands import save_screenshot from .sections import Section, SectionManager @@ -332,6 +378,7 @@ def configure_logging(level: Optional[int] = None): 'get_screens', 'get_sprites_at_exact_point', 'get_sprites_at_point', + 'get_timestamp', 'SpatialHash', 'get_timings', 'create_text_sprite', @@ -355,6 +402,7 @@ def configure_logging(level: Optional[int] = None): 'read_tmx', 'load_tilemap', 'run', + 'save_screenshot', 'schedule', 'set_background_color', 'set_window', diff --git a/arcade/application.py b/arcade/application.py index 5e6ba95ac..03b758819 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -7,8 +7,8 @@ import logging import os import time -from typing import List, Tuple, Optional - +from typing import List, Tuple, Optional, Union +from pathlib import Path import pyglet import pyglet.gl as gl @@ -23,6 +23,9 @@ from arcade.types import Color, RGBOrA255, RGBANormalized from arcade import SectionManager from arcade.utils import is_raspberry_pi +from arcade.types import Rect +from PIL import Image + LOG = logging.getLogger(__name__) @@ -957,6 +960,58 @@ def center_y(self) -> float: """Returns the Y-coordinate of the center of the window.""" return self.height / 2 + def save_screenshot( + self, + path: Union[Path, str], + format: Optional[str] = None, + **kwargs + ) -> None: + """Save a screenshot to a specified file name. + + .. warning:: This may overwrite existing files! + + .. code-block:: python + + # By default, the image format is detected from the + # file extension on the path you pass. + window_instance.save_screenshot("screenshot.png") + + You can also use the same arguments as :py:meth:`PIL.Image.save`: + + * You can pass a ``format`` to stop Pillow from guessing the + format from the file name + * The pillow documentation provides a list of supported + :external+PIL:ref:`image-file-formats` + + :param path: The full path and the png image filename to save. + :param format: A :py:mod:`PIL` format name. + :param kwargs: Varies with :external+PIL:ref:`selected format ` + """ + img = self.ctx.get_framebuffer_image(self.ctx.screen) + img.save(path, format=format, **kwargs) + + def get_image( + self, + viewport: Rect | None + ) -> Image.Image: + """Get an image from the window. + + .. code-block:: python + + # Get an image from a portion of the window by specfying the viewport. + viewport = Rect(10, 16, 20, 20) + image = window_instance.get_image(viewport) + + # Get an image of the whole Window + image = window_instance.get_image() + + :param viewport: The area of the screen to get defined by the x, y, width, height values + """ + return self.ctx.get_framebuffer_image(self.ctx.screen, viewport=viewport) + + def get_pixel(self): + pass + def open_window( width: int, diff --git a/arcade/context.py b/arcade/context.py index 4077e9ae0..2deaf42c2 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -24,6 +24,8 @@ from arcade.texture_atlas import TextureAtlas from arcade.camera import Projector from arcade.camera.default import DefaultProjector +from arcade.types import Rect + __all__ = ["ArcadeContext"] @@ -524,6 +526,7 @@ def get_framebuffer_image( fbo: Framebuffer, components: int = 4, flip: bool = True, + viewport: Optional[Rect] = None ) -> Image.Image: """ Shortcut method for reading data from a framebuffer and converting it to a PIL image. @@ -531,12 +534,20 @@ def get_framebuffer_image( :param fbo: Framebuffer to get image from :param components: Number of components to read :param flip: Flip the image upside down + :param viewport: x, y, width, height to read """ mode = "RGBA"[:components] + if viewport: + width = viewport[2] - viewport[0] + height = viewport[3] - viewport[1] + else: + width = fbo.width + height = fbo.height + image = Image.frombuffer( mode, - (fbo.width, fbo.height), - fbo.read(components=components), + (width, height), + fbo.read(viewport=viewport, components=components), ) if flip: image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) diff --git a/arcade/examples/debug_screenshot.py b/arcade/examples/debug_screenshot.py new file mode 100644 index 000000000..8c2cb2c8b --- /dev/null +++ b/arcade/examples/debug_screenshot.py @@ -0,0 +1,65 @@ +"""Take screenshots for debugging. + +This example shows you how to take debug screenshots by: + +1. Setting the window's background to a non-transparent color +2. Randomly arranging sprites to display a pattern over it +3. Using arcade.save_screenshot + +After installing arcade version 3.0.0 or higher, this example can be run +from the command line with: +python -m arcade.examples.debug_screenshot +""" +import random +import arcade +from arcade.types import Color + + +SCREENSHOT_FILE_NAME = "debug_screenshot_image.png" + +# How many sprites to draw and how big they'll be +NUM_SPRITES = 100 +MIN_RADIUS_PX = 5 +MAX_RADIUS_PX = 50 + +# Window size +WIDTH_PX = 800 +HEIGHT_PX = 600 + + +class ScreenshotWindow(arcade.Window): + + def __init__(self): + super().__init__(WIDTH_PX, HEIGHT_PX, "Press space to save a screenshot") + + # Important: we have to set a non-transparent background color, + # or else the screenshot will have a transparent background. + self.background_color = arcade.color.AMAZON + + # Randomize circle sprite positions, sizes, and colors + self.sprites = arcade.SpriteList() + for i in range(NUM_SPRITES): + sprite = arcade.SpriteCircle( + random.randint(MIN_RADIUS_PX, MAX_RADIUS_PX), + Color.random(a=255) + ) + sprite.position = ( + random.uniform(0, self.width), + random.uniform(0, self.height) + ) + self.sprites.append(sprite) + + def on_draw(self): + self.clear() + self.sprites.draw() + + def on_key_press(self, key, modifiers): + if key == arcade.key.SPACE: + arcade.save_screenshot(SCREENSHOT_FILE_NAME) + # You can also use the format below instead. + # self.save_screenshot(SCREENSHOT_FILE_NAME) + + +if __name__ == "__main__": + window = ScreenshotWindow() + arcade.run() diff --git a/arcade/window_commands.py b/arcade/window_commands.py index 6b64cb420..63dbbd898 100644 --- a/arcade/window_commands.py +++ b/arcade/window_commands.py @@ -9,12 +9,13 @@ import os import pyglet - +from pathlib import Path from typing import ( Callable, Optional, Tuple, - TYPE_CHECKING + TYPE_CHECKING, + Union ) from arcade.types import RGBA255, Color @@ -36,7 +37,8 @@ "set_background_color", "schedule", "unschedule", - "schedule_once" + "schedule_once", + "save_screenshot" ] @@ -302,3 +304,29 @@ def some_action(delta_time): :param delay: Delay in seconds """ pyglet.clock.schedule_once(function_pointer, delay) + + +def save_screenshot( + path: Union[ Path, str], + format: Optional[str] = None, + **kwargs +) -> None: + """Save a screenshot to a specified file name. + + .. warning:: This may overwrite existing files! + + .. code-block:: python + + # By default, the image format is detected from the + # file extension on the path you pass. + window_instance.save_screenshot("screenshot.png") + + This works identically to + :py:meth:`Window.save_screenshot ` + + :param path: The full path and the png image filename to save. + :param format: A :py:mod:`PIL` format name. + :param kwargs: Varies with :external+PIL:ref:`selected format ` + """ + window = get_window() + window.save_screenshot(path, format=format, **kwargs) diff --git a/doc/example_code/how_to_examples/debug_screenshot.png b/doc/example_code/how_to_examples/debug_screenshot.png new file mode 100644 index 000000000..a5962fbd3 Binary files /dev/null and b/doc/example_code/how_to_examples/debug_screenshot.png differ diff --git a/doc/index.rst b/doc/index.rst index 9cac29faf..217505ca0 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -112,6 +112,7 @@ The Python Arcade Library programming_guide/texture_atlas programming_guide/edge_artifacts/index programming_guide/logging + programming_guide/screenshots_timestamps programming_guide/opengl_notes programming_guide/performance_tips programming_guide/headless diff --git a/doc/programming_guide/logging.rst b/doc/programming_guide/logging.rst index a1efc570b..2f4aabee7 100644 --- a/doc/programming_guide/logging.rst +++ b/doc/programming_guide/logging.rst @@ -7,6 +7,9 @@ Arcade has a few options to log additional information around timings and how things are working internally. The two major ways to do this by turning on logging, and by querying the OpenGL context. +To export data as part of debugging rather than logging, you may want to see +the :ref:`debug-helpers`. + Turn on logging --------------- diff --git a/doc/programming_guide/screenshots_timestamps.rst b/doc/programming_guide/screenshots_timestamps.rst new file mode 100644 index 000000000..2ace8eb7d --- /dev/null +++ b/doc/programming_guide/screenshots_timestamps.rst @@ -0,0 +1,301 @@ +.. _debug-helpers: + +Screenshots & Timestamps +======================== + +Sometimes, you need to export data to separate files +instead of :ref:`logging with lines of text.`. + +Arcade has a limited set of convenience functions to help you save +screenshots and other data this way. + +Keep the following in mind: + +* These convenience functions are mostly for debugging +* They are built for flexibility and ease of use +* They are not optimized performance, especially video +* Arcade's timestamp helper is a fallback for when you can't use + :ref:`much better third-party options ` + +To learn about better tools for screen recording, please see +:ref:`debug-screenshot-i-need-video`. + + +.. _debug-screenshots: + +Screenshots +----------- + +Arcade's screenshot helpers do one of three things: + +* Save to a file path +* Return a :py:class:`PIL.Image` +* Query pixels at coordinates + +All of them have simliar :ref:`limitations ` +due to how they copy data. + +Saving Screenshots to Files +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The most convenient ones are often the ones which save to disk +immediately: + +* :py:meth:`Window.save_screenshot ` +* :py:func:`arcade.save_screenshot` + +All of them have the same API and behavior. Since Arcade assumes +you will only have one window, :py:func:`arcade.save_screenshot` +is an easy-access wrapper around the other. + +You can also get a raw :py:class:`PIL.Image` object by using the +following functions: + + +.. _debug-screenshots-howto: + +Screenshot Saving Example +""""""""""""""""""""""""" + +The following code saves a screenshot to a file: + +.. literalinclude:: ../../arcade/examples/debug_screenshot.py + :caption: How to take a debug screenshot. + +Since it randomizes the circles, the result will be similar yet not +identical to the output below. + +.. image:: ../example_code/how_to_examples/debug_screenshot.png + :width: 800px + :align: center + :alt: Screen shot produced by the screenshot method. + +.. _debug-screenhots-pil-image: + +Getting PIL Images +^^^^^^^^^^^^^^^^^^ + +You can also get a :py:class:`PIL.Image` object back if you need +finer control. + +.. _dbug-screenshots-pixels: + +Pixel Queries +^^^^^^^^^^^^^ + +It's possible to query pixels directly. + +However, this is best reserved for tests since it may have performance +costs. + + +.. _debug-screenshot-limitations: + +Screenshot Limitations +^^^^^^^^^^^^^^^^^^^^^^ + +Arcade's screenshot helpers are not intended for rendering real-time +video. They will probably be too slow for many reasons, including: + +* Copying data from the GPU back to a computer's main memory +* Using the CPU to run :py:meth:`Image.save ` instead + of the GPU +* Systems with slow drives or heavy disk usage may get stuck waiting + to save individual files + + +.. _debug-screenshot-i-need-video: + +I Need to Save Video! +^^^^^^^^^^^^^^^^^^^^^ + +Sometimess, it's easier to :ref:`get help ` if you +record a bug on video. + +The cheapest and most reliable tools have tradeoffs: + +* Pre-installed video records are often easy to use but limmited +* `OBS `_ is powerful but complicated + +For :ref:`getting help `, the first set of tools +is often best. + +There are ways of recording video with Python, but they have variable +quality. Very advanced users may be able to get `ffmpeg `_ +or other libraries to work. However, these can come with risks or costs: + +* Your project must be able to use (L)GPL-licensed code +* It may be difficult to implement or get adequate performance +* The binaries can be very large + +Like OBS, ffmpeg is powerful and complicated. To learn more, see +:external+pyglet:ref:`pyglet's ffmpeg overview ` +in their programming guide. Note that pyglet might only support reading media +files. Wrting may require additional dependencies or non-trivial work. + +.. _debug-timestamps: + +Filename Timestamps +------------------- + +In addition to Arcade's :ref:`logging` features, +:py:func:`arcade.get_timestamp` is a minimal helper for +generating timestamped filenames. + +Calling it without arguments returns a string which specifies the +current time down six places of microseconds. + +.. code-block:: python + + # This output happens at exactly 3PM on April 3rd, 2024 in local time + >>> arcade.get_timestamp() + '2024_04_02_1500_00_000000' + +.. _debug-timestamps-who: + +Who Is This For? +^^^^^^^^^^^^^^^^ + +Everyone who can't use :ref:`a better alternative `: + +* Students required to use only Arcade and Python builtins +* Anyone who needs to minimize install size +* Game jam participants with a deadline + +In each of these cases, :py:func:`~arcade.get_timestamp` can help +write cleaner data export code a little faster. + +.. code-block:: python + + # This example dumps character data to a file with a timestamped name. + import json + from pathlib import Path + from typing import Dict, Any + from arcade import get_timestamp + + BASE_DIR = Path("debug") + + def log_game_character(char: Dict[str, Any]) -> None: + + # Decide on a filename + filename = f"{char['name']}{get_timestamp()}.json" + character_path = BASE_DIR / filename + + # Write the data + with character_path.open("w") as out: + json.dump(char, out) + + + # Set up our data + parrot = dict(name='parrot', funny=True, alive=False) + + # Write it to a timestamped file + log_game_character(parrot) + +.. _debug-timestamps-custom: + +Customizing Output +^^^^^^^^^^^^^^^^^^ + +Argument Overview +""""""""""""""""" + +The ``when``, ``how``, and ``tzinfo`` keyword arguments allow using +:ref:`compatible ` objects and format +strings: + +.. list-table:: + :header-rows: 1 + + * - Keyword Argument + - What it Takes + - Default + + * - ``when`` + - ``None`` or anything with a :ref:`datetime-like ` + ``strftime`` method + - Calling + :py:meth:`datetime.now(tzinfo) ` + + * - ``how`` + - A :ref:`C89-stlye date format string ` + - ``"%Y_%m_%d_%H%M_%S_%f%Z"`` + + * - ``tzinfo`` + - ``None`` or a valid :py:class:`datetime.tzinfo` instance + - ``None`` + + +.. _debug-timestamps-example-when-how: + +Example of Custom When and How +"""""""""""""""""""""""""""""" + +.. code-block:: python + + >>> from datetime import date + >>> DAY_MONTH_YEAR = '%d-%m-%Y' + >>> today = date.today() + >>> today + datetime.date(2024, 4, 3) + >>> arcade.get_timestamp(when=today, how=DAY_MONTH_YEAR) + '03-04-2024' + + +.. _debug-timestamps-example-timezone: + +Example of Custom Time Zones +"""""""""""""""""""""""""""" + +.. _UTC_Wiki: https://en.wikipedia.org/wiki/Coordinated_Universal_Time + +Using `UTC `_ is a common way to reduce confusion about when +something happened. On Python 3.8, you can use +:py:class:`datetime.timezone`'s ``utc`` constant with this function: + +.. code-block:: python + + >>> from datetime import timezone + >>> arcade.get_timestamp(tzinfo=timezone.utc) + '2024_12_11_1009_08_000007UTC` + +Starting with Python 3.11, you can use :py:attr:`datetime.UTC` as a +more readable shortcut. However, the built-in date & time tools can +still be confusing and incomplete. To learn about the most popular +alternatives, see the heading below. + + +.. _debug-better-datetime: + +Better Date & Time Handling +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There are plenty of excellent replacements for both +:py:func:`arcade.get_timestamp`and Python's :py:mod:`datetime`. + +In addition to beautiful syntax, the best of them are +:ref:`backward compatible ` with +:py:mod:`datetime` types. + +.. list-table:: + :header-rows: 1 + + * - Library + - Pros + - Cons + + * - `Arrow `_ + - Very popular and mature + - Fewer features + + * - `Pendulum `_ + - Popular and mature + - Included by other libraries which build on it + + * - `Moment `_ + - Well-liked and clean + - "Beta-quality" according to creator + + * - `Maya `_ + - Clean syntax + - `Currently unmaintained `_ diff --git a/pyproject.toml b/pyproject.toml index d7799366b..92e8acf27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,8 @@ dev = [ "coveralls", "pytest-mock", "pytest-cov", + # Can't monkeypatch.setitem datetime.now + freezegun has tz problems + "time-machine==2.14.1", "pygments==2.17.2", "docutils==0.20.1", "furo", @@ -66,6 +68,8 @@ testing_libraries = [ "pytest-mock", "pytest-cov", "pyyaml==6.0.1", + # Can't monkeypatch.setitem datetime.now + freezegun has tz problems + "time-machine==2.14.1" ] [project.scripts] diff --git a/tests/unit/test_get_timestamp.py b/tests/unit/test_get_timestamp.py new file mode 100644 index 000000000..d739a6caa --- /dev/null +++ b/tests/unit/test_get_timestamp.py @@ -0,0 +1,103 @@ +"""Minimal tests for arcade.get_timestamp. + +Since the function is a minimal fallback for people who can't use any of +the alternatives to :py:mod:`datetime`, we'll only check the following: + +* Local system time on the test machine +* A made-up time zone guaranteed not to clash with it +* Two built-ins which implement strftime-like behavior: + + * :py:class:`datetime.date` + * :py:class:`datetime.time` + +It's not our responsibility to check 3rd party date & time types for +compatibility. Instead, we cover it in doc and advise users to choose +one of the popular backward-compatible date & time replacements. + +""" +from datetime import date, datetime, time, timezone, timedelta +from arcade import get_timestamp + +import time_machine +from dateutil.tz import tzlocal, tzoffset + + +# System time zone + a zone guaranteed not to clash with it +LOCAL_TZ = tzlocal() +CUSTOM_TZ_NAME = 'ARC' +CUSTOM_TZ = tzoffset( + CUSTOM_TZ_NAME, + timedelta(hours=-5, minutes=4, seconds=3)) + +# Set up date and time constants to use with strfime checks +DATE = date( + year=2024, + month=12, + day=11) + +TIME = time( + hour=10, + minute=9, + second=8, + microsecond=7) + +DATETIME_NOW = datetime.combine(DATE, TIME, tzinfo=LOCAL_TZ) +DATETIME_NOW_UTC = datetime.combine(DATE, TIME, tzinfo=timezone.utc) +DATETIME_NOW_CUSTOM = datetime.combine(DATE, TIME, tzinfo=CUSTOM_TZ) + + +def test_get_timestamp() -> None: + + # Check default usage and all flags individually w/o combinations + with time_machine.travel(DATETIME_NOW, tick=False): + # Since we don't pass in a tz here, the following happens: + # 1. datetime.now() returns a datetime with a None time zone + # 2. The default format string's %Z treats lack of tz as '' + assert get_timestamp() == '2024_12_11_1009_08_000007' + assert get_timestamp(how="%Y") == '2024' + assert get_timestamp(how="%m") == '12' + assert get_timestamp(how="%d") == '11' + assert get_timestamp(how="%H") == "10" + assert get_timestamp(how="%M") == "09" + assert get_timestamp(how="%S") == "08" + assert get_timestamp(how="%f") == "000007" + assert get_timestamp(how="%Z") == '' + + # Make sure passing time zones works the same way as above + with time_machine.travel(DATETIME_NOW_CUSTOM, tick=False): + assert get_timestamp(tzinfo=CUSTOM_TZ) == ( + f'2024_12_11_1009_08_000007' + f'{CUSTOM_TZ_NAME}' + ) + assert get_timestamp(how="%Y", tzinfo=CUSTOM_TZ) == '2024' + assert get_timestamp(how="%m", tzinfo=CUSTOM_TZ) == '12' + assert get_timestamp(how="%d", tzinfo=CUSTOM_TZ) == '11' + assert get_timestamp(how="%H", tzinfo=CUSTOM_TZ) == "10" + assert get_timestamp(how="%M", tzinfo=CUSTOM_TZ) == "09" + assert get_timestamp(how="%S", tzinfo=CUSTOM_TZ) == "08" + assert get_timestamp(how="%f", tzinfo=CUSTOM_TZ) == "000007" + assert get_timestamp(how="%Z", tzinfo=CUSTOM_TZ) == CUSTOM_TZ_NAME + + # Test the 3.8-compatible UTC example exactly as in the docstring + with time_machine.travel(DATETIME_NOW_UTC, tick=False): + assert get_timestamp(tzinfo=timezone.utc) == '2024_12_11_1009_08_000007UTC' + assert get_timestamp(how="%Y", tzinfo=timezone.utc) == '2024' + assert get_timestamp(how="%m", tzinfo=timezone.utc) == '12' + assert get_timestamp(how="%d", tzinfo=timezone.utc) == '11' + assert get_timestamp(how="%H", tzinfo=timezone.utc) == "10" + assert get_timestamp(how="%M", tzinfo=timezone.utc) == "09" + assert get_timestamp(how="%S", tzinfo=timezone.utc) == "08" + assert get_timestamp(how="%f", tzinfo=timezone.utc) == "000007" + assert get_timestamp(how="%Z", tzinfo=timezone.utc) == 'UTC' + + # Spot-check two other built-in strftime-providing objects + assert get_timestamp(how="%Y-%m-%d", when=DATE) == "2024-12-11" + assert get_timestamp(how="%Y", when=DATE) == '2024' + assert get_timestamp(how="%m", when=DATE) == '12' + assert get_timestamp(how="%d", when=DATE) == '11' + + assert get_timestamp(how="%H:%M:%S.%f", when=TIME) == "10:09:08.000007" + assert get_timestamp(how="%H", when=TIME) == "10" + assert get_timestamp(how="%M", when=TIME) == "09" + assert get_timestamp(how="%S", when=TIME) == "08" + assert get_timestamp(how="%f", when=TIME) == "000007" diff --git a/tests/unit/window/test_screenshot.py b/tests/unit/window/test_screenshot.py new file mode 100644 index 000000000..ea8f97a92 --- /dev/null +++ b/tests/unit/window/test_screenshot.py @@ -0,0 +1,40 @@ +"""Make sure window screenshots work. + +We use pytests's ``tmp_path`` instead of :py:mod:`tempdir` or manually +creating a temp dir through :py:mod:`os` or other modules. The +``tmp_path`` fixture passes a :py:class:`~pathlib.Path` of a temp dir +unique to the test invocation to all tests with a ``tmp_path`` argument. + +See https://docs.pytest.org/en/8.0.x/tmpdir.html#the-tmp-path-fixture + +""" +import arcade +from pathlib import Path + + +def test_save_screenshot_window(window: arcade.Window, tmp_path: Path): + # Test Path support + file_1 = tmp_path / "screen.png" + assert not file_1.exists() + window.save_screenshot(file_1) + assert file_1.is_file() + + # Test str support + file_2 = tmp_path / "screen2.png" + assert not file_2.exists() + window.save_screenshot(str(file_2)) + assert file_2.is_file() + + +def test_command_with_location(window: arcade.Window, tmp_path): + # Test Path support + file_1 = tmp_path / "screen.png" + assert not file_1.exists() + window.save_screenshot(file_1) + assert file_1.is_file() + + # Test str support + file_2 = tmp_path / "screen2.png" + assert not file_2.exists() + window.save_screenshot(str(file_2)) + assert file_2.is_file()