From 6436a59bd9ecf0e5250c94a3fb4a4038dd1208f3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 24 Jun 2023 16:39:24 -0700 Subject: [PATCH 001/134] Tutorial: Print events and justify more decisions. --- docs/tutorial/part-01.rst | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/tutorial/part-01.rst b/docs/tutorial/part-01.rst index fac8d4e6..103ab99b 100644 --- a/docs/tutorial/part-01.rst +++ b/docs/tutorial/part-01.rst @@ -9,6 +9,7 @@ Initial script ============================================================================== First start with a modern top-level script. +You should have ``main.py`` script from :ref:`part-0`: .. code-block:: python @@ -25,7 +26,7 @@ Loading a tileset and opening a window ============================================================================== From here it is time to setup a ``tcod`` program. -Download `Alloy_curses_12x12.png `_ and place this file in your projects ``data/`` directory. +Download `Alloy_curses_12x12.png `_ [#tileset]_ and place this file in your projects ``data/`` directory. This tileset is from the `Dwarf Fortress tileset repository `_. These kinds of tilesets are always loaded with :python:`columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437`. @@ -62,7 +63,7 @@ Configuring an event loop The next step is to keep the window open until the user closes it. Since nothing is displayed yet a :any:`Console` should be created with :python:`"Hello World"` printed to it. -The size of the console can be used as a reference to create the context by adding the console to :any:`tcod.context.new`. +The size of the console can be used as a reference to create the context by adding the console to :any:`tcod.context.new`. [#init_context]_ Begin the main game loop with a :python:`while True:` statement. @@ -71,11 +72,13 @@ Do this first in the game loop before handing events. Events are checked by iterating over all pending events with :any:`tcod.event.wait`. Use the code :python:`for event in tcod.event.wait():` to begin handing events. -Test if an event is for closing the window with :python:`if isinstance(event, tcod.event.Quit):`. -If this is True then you should exit the function with :python:`raise SystemExit`. + +In the event loop start with the line :python:`print(event)` so that all events can be viewed from the program output. +Then test if an event is for closing the window with :python:`if isinstance(event, tcod.event.Quit):`. +If this is True then you should exit the function with :python:`raise SystemExit()`. [#why_raise]_ .. code-block:: python - :emphasize-lines: 2,3,11-18 + :emphasize-lines: 2,3,11-19 ... import tcod.console @@ -93,12 +96,14 @@ If this is True then you should exit the function with :python:`raise SystemExit while True: # Main loop context.present(console) # Render the console to the window and show it for event in tcod.event.wait(): # Event loop, blocks until pending events exist + print(event) if isinstance(event, tcod.event.Quit): raise SystemExit() ... If you run this then you get a window saying :python:`"Hello World"`. The window can be resized and the console will be stretched to fit the new resolution. +When you do anything such as press a key or interact with the window the event for that action will be printed to the program output. An example game state ============================================================================== @@ -165,6 +170,7 @@ Modify the drawing routine so that the console is cleared, then passed to :pytho state.on_draw(console) # Draw the current state context.present(console) # Display the console on the window for event in tcod.event.wait(): + print(event) if isinstance(event, tcod.event.Quit): raise SystemExit() ... @@ -182,6 +188,7 @@ Begin matching with :python:`match event:`. The equivalent to :python:`if isinstance(event, tcod.event.Quit):` is :python:`case tcod.event.Quit():`. Keyboard keys can be checked with :python:`case tcod.event.KeyDown(sym=tcod.event.KeySym.LEFT):`. Make a case for each arrow key: ``LEFT`` ``RIGHT`` ``UP`` ``DOWN`` and move the player in the direction of that key. +Since events are printed you can check the :any:`KeySym` of a key by pressing that key and looking at the printed output. See :any:`KeySym` for a list of all keys. .. code-block:: python @@ -209,7 +216,7 @@ See :any:`KeySym` for a list of all keys. Now replace the event handling code in ``main`` to defer to the states ``on_event`` method. .. code-block:: python - :emphasize-lines: 11 + :emphasize-lines: 12 ... def main() -> None: @@ -221,9 +228,21 @@ Now replace the event handling code in ``main`` to defer to the states ``on_even state.on_draw(console) context.present(console) for event in tcod.event.wait(): + print(event) state.on_event(event) # Pass events to the state ... Now when you run this script you have a player character you can move around with the arrow keys before closing the window. You can review the part-1 source code `here `_. + +.. rubric:: Footnotes + +.. [#tileset] The choice of tileset came down to what looked nice while also being square. + Other options such as using a BDF font were considered, but in the end this tutorial won't go too much into Unicode. + +.. [#init_context] This tutorial follows the setup for a fixed-size console. + The alternatives shown in :ref:`getting-started` are outside the scope of this tutorial. + +.. [#why_raise] You could use :python:`return` here to exit the ``main`` function and end the program, but :python:`raise SystemExit()` is used because it will close the program from anywhere. + :python:`raise SystemExit()` is also more useful to teach than :any:`sys.exit`. From f62949f4bf6af25b341aaf0a6c9cc99d5b973bfc Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 27 Jun 2023 09:29:18 -0700 Subject: [PATCH 002/134] Update tutorial notice. --- docs/tutorial/notice.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/notice.rst b/docs/tutorial/notice.rst index 47bd3d6b..707baf09 100644 --- a/docs/tutorial/notice.rst +++ b/docs/tutorial/notice.rst @@ -1,6 +1,6 @@ .. note:: This tutorial is still a work-in-progress. `The resources being used are tracked here `_. - Feel free to discuss this tutorial on the `Github Discussions`_ forum. + Feel free to discuss this tutorial or share your progress on the `Github Discussions`_ forum. .. _Github Discussions: https://github.com/libtcod/python-tcod/discussions From 75d59e1e73b9863e51e93f2a95e72a137aae918b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 27 Jun 2023 09:30:25 -0700 Subject: [PATCH 003/134] Enable all additional formats for Sphinx. --- .readthedocs.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 390d6115..81bb11ae 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -20,8 +20,7 @@ sphinx: fail_on_warning: true # If using Sphinx, optionally build your docs in additional formats such as PDF -# formats: -# - pdf +formats: all # Optionally declare the Python requirements required to build your docs python: From 1a6d9bea0167dd1769764dfbfd0668cfc7c659c8 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 27 Jun 2023 23:27:40 -0700 Subject: [PATCH 004/134] Add `__slots__` to EventDispatch. This class is meant to be inherited, the subclass should be allowed to use `__slots__`. --- CHANGELOG.md | 2 ++ tcod/event.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fac3613..b812ee71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Changed +- Added an empty `__slots__` to `EventDispatch`. ## [16.1.0] - 2023-06-23 ### Added diff --git a/tcod/event.py b/tcod/event.py index d808436b..16eaf498 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1365,6 +1365,8 @@ def cmd_quit(self) -> None: state.dispatch(event) ''' + __slots__ = () + def dispatch(self, event: Any) -> T | None: """Send an event to an `ev_*` method. From a7923ed728993ac409241b85a8e5105f6ae75b26 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 28 Jun 2023 08:59:48 -0700 Subject: [PATCH 005/134] Fix comments on Ruff noqa directives. --- build_sdl.py | 3 ++- tcod/libtcodpy.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build_sdl.py b/build_sdl.py index ef3dd4be..cc66626b 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -16,7 +16,8 @@ import pcpp # type: ignore import requests -# ruff: noqa: S603, S607 # This script calls a lot of programs. +# This script calls a lot of programs. +# ruff: noqa: S603, S607 BIT_SIZE, LINKAGE = platform.architecture() diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 88086c1c..1e026ce3 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -52,7 +52,8 @@ NOISE_DEFAULT, ) -# ruff: noqa: ANN401 PLR0913 # Functions are too deprecated to make changes. +# Functions are too deprecated to make changes. +# ruff: noqa: ANN401 PLR0913 Bsp = tcod.bsp.BSP From d20e1388f1464c38b372902773f4644795e044d6 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 28 Jun 2023 11:57:10 -0700 Subject: [PATCH 006/134] Drop end-of-life Python 3.7. --- .github/workflows/python-package.yml | 10 +++++----- CHANGELOG.md | 3 +++ README.rst | 2 +- pyproject.toml | 9 ++++----- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 38ad5cb1..1b1ecec6 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -105,14 +105,14 @@ jobs: strategy: matrix: os: ["ubuntu-latest", "windows-latest"] - python-version: ["3.7", "3.8", "3.9", "pypy-3.7"] + python-version: ["3.8", "3.9", "pypy-3.8"] architecture: ["x64"] include: - os: "windows-latest" - python-version: "3.7" + python-version: "3.8" architecture: "x86" - os: "windows-latest" - python-version: "pypy-3.7" + python-version: "pypy-3.8" architecture: "x86" fail-fast: false @@ -224,7 +224,7 @@ jobs: strategy: matrix: arch: ["x86_64", "aarch64"] - build: ["cp37-manylinux*", "pp37-manylinux*"] + build: ["cp38-manylinux*", "pp38-manylinux*"] steps: - uses: actions/checkout@v3 with: @@ -273,7 +273,7 @@ jobs: strategy: fail-fast: true matrix: - python: ["cp38-*_universal2", "cp38-*_x86_64", "cp38-*_arm64", "pp37-*"] + python: ["cp38-*_universal2", "cp38-*_x86_64", "cp38-*_arm64", "pp38-*"] steps: - uses: actions/checkout@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index b812ee71..2d66d040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ### Changed - Added an empty `__slots__` to `EventDispatch`. +### Removed +- Dropped support for Python 3.7. + ## [16.1.0] - 2023-06-23 ### Added - Added the enums `tcod.event.MouseButton` and `tcod.event.MouseButtonMask`. diff --git a/README.rst b/README.rst index f84370ee..82aad962 100755 --- a/README.rst +++ b/README.rst @@ -46,7 +46,7 @@ For the most part it's just:: ============== Requirements ============== -* Python 3.7+ +* Python 3.8+ * Windows, Linux, or MacOS X 10.9+. * On Linux, requires libsdl2 (2.0.10+). diff --git a/pyproject.toml b/pyproject.toml index 2fbc6ebb..325c0baa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dynamic = ["version"] description = "The official Python port of libtcod." authors = [{ name = "Kyle Benesch", email = "4b796c65+tcod@gmail.com" }] readme = "README.rst" -requires-python = ">=3.7" +requires-python = ">=3.8" license = { text = "Simplified BSD License" } dependencies = [ "cffi>=1.15", @@ -45,7 +45,6 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -70,18 +69,18 @@ Tracker = "https://github.com/libtcod/python-tcod/issues" Forum = "https://github.com/libtcod/python-tcod/discussions" [tool.distutils.bdist_wheel] -py-limited-api = "cp37" +py-limited-api = "cp38" [tool.setuptools_scm] write_to = "tcod/version.py" [tool.black] line-length = 120 -target-version = ["py37"] +target-version = ["py38"] [tool.isort] profile = "black" -py_version = "37" +py_version = "38" skip_gitignore = true line_length = 120 From e39c25d7d5da9ec262ede9c28970469d98bbea01 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 29 Jun 2023 13:35:06 -0700 Subject: [PATCH 007/134] Reorganize tutorial TOC into a sub-tree. --- docs/index.rst | 5 ++--- docs/tutorial/index.rst | 10 ++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 docs/tutorial/index.rst diff --git a/docs/index.rst b/docs/index.rst index 50a87f4a..afc353a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,10 +22,9 @@ Contents: .. toctree:: :maxdepth: 2 - :caption: Tutorial - :glob: + :caption: How To - tutorial/part-* + tutorial/index .. toctree:: :maxdepth: 2 diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst new file mode 100644 index 00000000..2277ab62 --- /dev/null +++ b/docs/tutorial/index.rst @@ -0,0 +1,10 @@ +Tutorial +############################################################################## + +.. include:: notice.rst + +.. toctree:: + :maxdepth: 1 + :glob: + + part-* From abdb1698f2ad189eb46fe340ce1566d37d2c41c7 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 10 Jul 2023 00:01:00 -0700 Subject: [PATCH 008/134] Update SDL to 2.28.1 The latest version fixes SDL's rendering error. Closes #131 --- CHANGELOG.md | 5 +++++ build_sdl.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d66d040..67303d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] ### Changed - Added an empty `__slots__` to `EventDispatch`. +- Bundle `SDL 2.28.1` on Windows and MacOS. + +### Fixed +- Fixed "SDL failed to get a vertex buffer for this Direct3D 9 rendering batch!" + https://github.com/libtcod/python-tcod/issues/131 ### Removed - Dropped support for Python 3.7. diff --git a/build_sdl.py b/build_sdl.py index cc66626b..b47b426c 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -26,7 +26,7 @@ # The SDL2 version to parse and export symbols from. SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.20") # The SDL2 version to include in binary distributions. -SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.26.0") +SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.28.1") # Used to remove excessive newlines in debug outputs. From c7936eb31e95874ba7153d92097539173812b458 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 10 Jul 2023 00:10:32 -0700 Subject: [PATCH 009/134] Fix more samples deprecations --- examples/samples_tcod.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 43b53d92..58d35a15 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -6,6 +6,7 @@ from __future__ import annotations import copy +import importlib.metadata import math import random import sys @@ -18,6 +19,7 @@ from numpy.typing import NDArray import tcod.cffi +import tcod.context import tcod.event import tcod.noise import tcod.render @@ -160,8 +162,8 @@ def print_banner(self) -> None: string="The Doryen library uses 24 bits colors, for both " "background and foreground.", fg=WHITE, bg=GREY, - bg_blend=tcod.BKGND_MULTIPLY, - alignment=tcod.CENTER, + bg_blend=libtcodpy.BKGND_MULTIPLY, + alignment=libtcodpy.CENTER, ) @@ -297,7 +299,7 @@ def on_draw(self) -> None: # in python the easiest way is to use the line iterator for x, y in tcod.los.bresenham((xo, yo), (xd, yd)).tolist(): if 0 <= x < sample_console.width and 0 <= y < sample_console.height: - tcod.console_set_char_background(sample_console, x, y, LIGHT_BLUE, self.bk_flag) + libtcodpy.console_set_char_background(sample_console, x, y, LIGHT_BLUE, self.bk_flag) sample_console.print( 2, 2, @@ -581,8 +583,8 @@ def on_draw(self) -> None: self.draw_ui() sample_console.print(self.player_x, self.player_y, "@") # Draw windows. - sample_console.tiles_rgb["ch"][SAMPLE_MAP == "="] = 0x2550 # BOX DRAWINGS DOUBLE HORIZONTAL - sample_console.tiles_rgb["fg"][SAMPLE_MAP == "="] = BLACK + sample_console.rgb["ch"][SAMPLE_MAP == "="] = 0x2550 # BOX DRAWINGS DOUBLE HORIZONTAL + sample_console.rgb["fg"][SAMPLE_MAP == "="] = BLACK # Get a 2D boolean array of visible cells. fov = tcod.map.compute_fov( @@ -625,7 +627,7 @@ def on_draw(self) -> None: dark_bg: NDArray[np.float16] = self.dark_map_bg.astype(np.float16) # Linear interpolation between colors. - sample_console.tiles_rgb["bg"] = dark_bg + (light_bg - dark_bg) * light[..., np.newaxis] + sample_console.rgb["bg"] = dark_bg + (light_bg - dark_bg) * light[..., np.newaxis] else: sample_console.bg[...] = np.where(fov[:, :, np.newaxis], self.light_map_bg, self.dark_map_bg) @@ -1411,7 +1413,7 @@ def init_context(renderer: int) -> None: context = tcod.context.new( columns=root_console.width, rows=root_console.height, - title=f"python-tcod samples (python-tcod {tcod.__version__}, libtcod {libtcod_version})", + title=f"""python-tcod samples (python-tcod {importlib.metadata.version("tcod")}, libtcod {libtcod_version})""", vsync=False, # VSync turned off since this is for benchmarking. tileset=tileset, ) From e597ceb8b5a63a62f2c99fdeffaab16adad2be0b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 10 Jul 2023 01:06:50 -0700 Subject: [PATCH 010/134] Prepare 16.1.1 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67303d41..ad769aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [16.1.1] - 2023-07-10 ### Changed - Added an empty `__slots__` to `EventDispatch`. - Bundle `SDL 2.28.1` on Windows and MacOS. From 9c29b1a60fbe019f409f9abdb47fc5f1dfde0da4 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 11 Jul 2023 18:31:23 -0700 Subject: [PATCH 011/134] Update tutorial part 1 --- docs/tutorial/part-01.rst | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/part-01.rst b/docs/tutorial/part-01.rst index 103ab99b..028fc40a 100644 --- a/docs/tutorial/part-01.rst +++ b/docs/tutorial/part-01.rst @@ -5,6 +5,10 @@ Part 1 - Moving a player around the screen .. include:: notice.rst +In part 1 you will become familiar with the initialization, rendering, and event system of tcod. +This will be done as a series of small implementations. +It is recommend to save your progress after each section is finished and tested. + Initial script ============================================================================== @@ -29,10 +33,16 @@ From here it is time to setup a ``tcod`` program. Download `Alloy_curses_12x12.png `_ [#tileset]_ and place this file in your projects ``data/`` directory. This tileset is from the `Dwarf Fortress tileset repository `_. These kinds of tilesets are always loaded with :python:`columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437`. +Use the string :python:`"data/Alloy_curses_12x12.png"` to refer to the path of the tileset. [#why_not_pathlib]_ + +Load the tileset with :any:`tcod.tileset.load_tilesheet`. +Then pass the tileset to :any:`tcod.context.new`, you only need to provide the ``tileset`` parameter. -Load the tileset with :any:`tcod.tileset.load_tilesheet` and then pass it to :any:`tcod.context.new`. -These functions are part of modules which have not been imported yet, so new imports need to be added. -:any:`tcod.context.new` returns a :any:`Context` which is used with the ``with`` statement. +:any:`tcod.context.new` returns a :any:`Context` which will be used with Python's :python:`with` statement. +We want to keep the name of the context, so use the syntax: :python:`with tcod.context.new(tileset=tileset) as context:`. +The new block can't be empty, so add :python:`pass` to the with statement body. + +These functions are part of modules which have not been imported yet, so new imports for ``tcod.context`` and ``tcod.tileset`` must be added to the top of the script. .. code-block:: python :emphasize-lines: 2,3,8-12 @@ -54,7 +64,7 @@ These functions are part of modules which have not been imported yet, so new imp If an import fails that means you do not have ``tcod`` installed on the Python environment you just used to run the script. If you use an IDE then make sure the Python environment it is using is correct and then run :shell:`pip install tcod` from the shell terminal within that IDE. -If you run this script now then a window will open and then immediately close. +There is no game loop, so if you run this script now then a window will open and then immediately close. If that happens without seeing a traceback in your terminal then the script is correct. Configuring an event loop @@ -241,6 +251,11 @@ You can review the part-1 source code `here Date: Wed, 20 Sep 2023 20:01:47 -0700 Subject: [PATCH 012/134] Fix gauss function typos --- CHANGELOG.md | 2 ++ tcod/random.py | 19 +++++++++++++++++-- tests/test_deprecated.py | 9 +++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad769aaf..0057b82f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Changed +- Renamed `gauss` methods to fix typos. ## [16.1.1] - 2023-07-10 ### Changed diff --git a/tcod/random.py b/tcod/random.py index b574cab8..c9e54286 100644 --- a/tcod/random.py +++ b/tcod/random.py @@ -14,6 +14,7 @@ from typing import Any, Hashable import tcod.constants +from tcod._internal import deprecate from tcod.cffi import ffi, lib MERSENNE_TWISTER = tcod.constants.RNG_MT @@ -110,7 +111,7 @@ def uniform(self, low: float, high: float) -> float: """ return float(lib.TCOD_random_get_double(self.random_c, low, high)) - def guass(self, mu: float, sigma: float) -> float: + def gauss(self, mu: float, sigma: float) -> float: """Return a random number using Gaussian distribution. Args: @@ -119,10 +120,17 @@ def guass(self, mu: float, sigma: float) -> float: Returns: float: A random float. + + .. versionchanged:: Unreleased + Renamed from `guass` to `gauss`. """ return float(lib.TCOD_random_get_gaussian_double(self.random_c, mu, sigma)) - def inverse_guass(self, mu: float, sigma: float) -> float: + @deprecate("This is a typo, rename this to 'gauss'", category=FutureWarning) + def guass(self, mu: float, sigma: float) -> float: # noqa: D102 + return self.gauss(mu, sigma) + + def inverse_gauss(self, mu: float, sigma: float) -> float: """Return a random Gaussian number using the Box-Muller transform. Args: @@ -131,9 +139,16 @@ def inverse_guass(self, mu: float, sigma: float) -> float: Returns: float: A random float. + + .. versionchanged:: Unreleased + Renamed from `inverse_guass` to `inverse_gauss`. """ return float(lib.TCOD_random_get_gaussian_double_inv(self.random_c, mu, sigma)) + @deprecate("This is a typo, rename this to 'inverse_gauss'", category=FutureWarning) + def inverse_guass(self, mu: float, sigma: float) -> float: # noqa: D102 + return self.inverse_gauss(mu, sigma) + def __getstate__(self) -> Any: """Pack the self.random_c attribute into a portable state.""" state = self.__dict__.copy() diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index 502d9ea9..356de7d3 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -8,6 +8,7 @@ import tcod.constants import tcod.event import tcod.libtcodpy +import tcod.random with pytest.warns(): import libtcodpy @@ -53,3 +54,11 @@ def test_line_where() -> None: with pytest.warns(): where = tcod.libtcodpy.line_where(1, 0, 3, 4) np.testing.assert_array_equal(where, [[1, 1, 2, 2, 3], [0, 1, 2, 3, 4]]) + + +def test_gauss_typo() -> None: + rng = tcod.random.Random() + with pytest.warns(FutureWarning, match=r"gauss"): + rng.guass(1, 1) + with pytest.warns(FutureWarning, match=r"inverse_gauss"): + rng.inverse_guass(1, 1) From 422ccee4ba9b68ff33b5573c21c36923e5dd563b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 20 Sep 2023 20:06:06 -0700 Subject: [PATCH 013/134] Update docs --- tcod/console.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tcod/console.py b/tcod/console.py index 36cc0aba..a014b8af 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -1,4 +1,6 @@ -"""Libtcod consoles are a strictly tile-based representation of text and color. +"""Libtcod tile-based Consoles and printing functions. + +Libtcod consoles are a strictly tile-based representation of colored glyphs/tiles. To render a console you need a tileset and a window to render to. See :ref:`getting-started` for info on how to set those up. """ From bf7f1c105ab5501738a85dbbf9394cd4873ded0e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 20 Sep 2023 20:06:37 -0700 Subject: [PATCH 014/134] Remove outdated Mypy ignore --- tcod/sdl/_internal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/sdl/_internal.py b/tcod/sdl/_internal.py index 00dc868c..28b47460 100644 --- a/tcod/sdl/_internal.py +++ b/tcod/sdl/_internal.py @@ -59,7 +59,7 @@ def __exit__( return False if _sys.version_info < (3, 8): return False - _sys.unraisablehook(_UnraisableHookArgs(exc_type, value, traceback, None, self.obj)) # type: ignore[arg-type] + _sys.unraisablehook(_UnraisableHookArgs(exc_type, value, traceback, None, self.obj)) return True From deeeeca45cfba004283faf9c8b789c32f819cbe1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 20 Sep 2023 20:10:47 -0700 Subject: [PATCH 015/134] Replace constant class var with an immutable type --- tcod/console.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/tcod/console.py b/tcod/console.py index a014b8af..c8786703 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -443,24 +443,28 @@ def put_char( """ lib.TCOD_console_put_char(self.console_c, x, y, ch, bg_blend) - __ALIGNMENT_LOOKUP = {0: "tcod.LEFT", 1: "tcod.RIGHT", 2: "tcod.CENTER"} - - __BG_BLEND_LOOKUP = { - 0: "tcod.BKGND_NONE", - 1: "tcod.BKGND_SET", - 2: "tcod.BKGND_MULTIPLY", - 3: "tcod.BKGND_LIGHTEN", - 4: "tcod.BKGND_DARKEN", - 5: "tcod.BKGND_SCREEN", - 6: "tcod.BKGND_COLOR_DODGE", - 7: "tcod.BKGND_COLOR_BURN", - 8: "tcod.BKGND_ADD", - 9: "tcod.BKGND_ADDA", - 10: "tcod.BKGND_BURN", - 11: "tcod.BKGND_OVERLAY", - 12: "tcod.BKGND_ALPH", - 13: "tcod.BKGND_DEFAULT", - } + __ALIGNMENT_LOOKUP = ( + "tcod.LEFT", + "tcod.RIGHT", + "tcod.CENTER", + ) + + __BG_BLEND_LOOKUP = ( + "tcod.BKGND_NONE", + "tcod.BKGND_SET", + "tcod.BKGND_MULTIPLY", + "tcod.BKGND_LIGHTEN", + "tcod.BKGND_DARKEN", + "tcod.BKGND_SCREEN", + "tcod.BKGND_COLOR_DODGE", + "tcod.BKGND_COLOR_BURN", + "tcod.BKGND_ADD", + "tcod.BKGND_ADDA", + "tcod.BKGND_BURN", + "tcod.BKGND_OVERLAY", + "tcod.BKGND_ALPH", + "tcod.BKGND_DEFAULT", + ) def __deprecate_defaults( # noqa: C901, PLR0912 self, From 7b75e019b19417badc9b21747c534fd2147ded94 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 20 Sep 2023 20:18:27 -0700 Subject: [PATCH 016/134] Upgrade cibuildwheel --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1b1ecec6..784fef9a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -242,7 +242,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install cibuildwheel==2.3.1 + pip install cibuildwheel==2.16.0 - name: Build wheels run: | python -m cibuildwheel --output-dir wheelhouse From 688fc663ca08138afb8efed192b620bbf3f3bee1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 20 Sep 2023 20:46:39 -0700 Subject: [PATCH 017/134] Prepare 16.2.0 release. --- CHANGELOG.md | 2 ++ tcod/random.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0057b82f..284d5da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [16.2.0] - 2023-09-20 ### Changed - Renamed `gauss` methods to fix typos. diff --git a/tcod/random.py b/tcod/random.py index c9e54286..b6042d8a 100644 --- a/tcod/random.py +++ b/tcod/random.py @@ -121,7 +121,7 @@ def gauss(self, mu: float, sigma: float) -> float: Returns: float: A random float. - .. versionchanged:: Unreleased + .. versionchanged:: 16.2 Renamed from `guass` to `gauss`. """ return float(lib.TCOD_random_get_gaussian_double(self.random_c, mu, sigma)) @@ -140,7 +140,7 @@ def inverse_gauss(self, mu: float, sigma: float) -> float: Returns: float: A random float. - .. versionchanged:: Unreleased + .. versionchanged:: 16.2 Renamed from `inverse_guass` to `inverse_gauss`. """ return float(lib.TCOD_random_get_gaussian_double_inv(self.random_c, mu, sigma)) From 2eb854dd7107aabbd5d5252e664871c660d7ca7d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 24 Sep 2023 22:39:13 -0700 Subject: [PATCH 018/134] Use the C locale when passing file paths to libtcod Fixes encoding errors which prevented existing files from loading correctly. --- CHANGELOG.md | 2 ++ tcod/_internal.py | 13 +++++++++++++ tcod/console.py | 8 ++++---- tcod/image.py | 8 ++++---- tcod/libtcodpy.py | 31 ++++++++++++++++--------------- tcod/tileset.py | 10 +++++----- 6 files changed, 44 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 284d5da0..e5c77fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Fixed +- Fixed errors loading files where their paths are non-ASCII and the C locale is not UTF-8. ## [16.2.0] - 2023-09-20 ### Changed diff --git a/tcod/_internal.py b/tcod/_internal.py index cd92a45c..56ac7c50 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -2,7 +2,10 @@ from __future__ import annotations import functools +import locale +import sys import warnings +from pathlib import Path from types import TracebackType from typing import Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar, cast @@ -126,6 +129,16 @@ def _fmt(string: str, stacklevel: int = 2) -> bytes: return string.encode("utf-8").replace(b"%", b"%%") +def _path_encode(path: Path) -> bytes: + """Return a bytes file path for the current C locale.""" + try: + return str(path).encode(locale.getlocale()[1] or "utf-8") + except UnicodeEncodeError as exc: + if sys.version_info >= (3, 11): + exc.add_note("""Consider calling 'locale.setlocale(locale.LC_CTYPES, ".UTF8")' to support Unicode paths.""") + raise + + class _PropagateException: """Context manager designed to propagate exceptions outside of a cffi callback context. diff --git a/tcod/console.py b/tcod/console.py index c8786703..af0b3324 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -17,7 +17,7 @@ import tcod._internal import tcod.constants -from tcod._internal import _check, deprecate +from tcod._internal import _check, _path_encode, deprecate from tcod.cffi import ffi, lib @@ -1301,9 +1301,9 @@ def load_xp(path: str | PathLike[str], order: Literal["C", "F"] = "C") -> tuple[ console.rgba[is_transparent] = (ord(" "), (0,), (0,)) """ path = Path(path).resolve(strict=True) - layers = _check(tcod.lib.TCOD_load_xp(bytes(path), 0, ffi.NULL)) + layers = _check(tcod.lib.TCOD_load_xp(_path_encode(path), 0, ffi.NULL)) consoles = ffi.new("TCOD_Console*[]", layers) - _check(tcod.lib.TCOD_load_xp(bytes(path), layers, consoles)) + _check(tcod.lib.TCOD_load_xp(_path_encode(path), layers, consoles)) return tuple(Console._from_cdata(console_p, order=order) for console_p in consoles) @@ -1364,7 +1364,7 @@ def save_xp( tcod.lib.TCOD_save_xp( len(consoles_c), consoles_c, - bytes(path), + _path_encode(path), compress_level, ) ) diff --git a/tcod/image.py b/tcod/image.py index b404361a..826ea33e 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -18,7 +18,7 @@ from numpy.typing import ArrayLike, NDArray import tcod.console -from tcod._internal import _console, deprecate +from tcod._internal import _console, _path_encode, deprecate from tcod.cffi import ffi, lib @@ -72,7 +72,7 @@ def from_file(cls, path: str | PathLike[str]) -> Image: .. versionadded:: 16.0 """ path = Path(path).resolve(strict=True) - return cls._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(path)), lib.TCOD_image_delete)) + return cls._from_cdata(ffi.gc(lib.TCOD_image_load(_path_encode(path)), lib.TCOD_image_delete)) def clear(self, color: tuple[int, int, int]) -> None: """Fill this entire Image with color. @@ -306,7 +306,7 @@ def save_as(self, filename: str | PathLike[str]) -> None: .. versionchanged:: 16.0 Added PathLike support. """ - lib.TCOD_image_save(self.image_c, bytes(Path(filename))) + lib.TCOD_image_save(self.image_c, _path_encode(Path(filename))) @property def __array_interface__(self) -> dict[str, Any]: @@ -364,7 +364,7 @@ def load(filename: str | PathLike[str]) -> NDArray[np.uint8]: .. versionadded:: 11.4 """ - image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(Path(filename))), lib.TCOD_image_delete)) + image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(_path_encode(Path(filename))), lib.TCOD_image_delete)) array: NDArray[np.uint8] = np.asarray(image, dtype=np.uint8) height, width, depth = array.shape if depth == 3: diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 1e026ce3..a9162eb3 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -31,6 +31,7 @@ _console, _fmt, _int, + _path_encode, _PropagateException, _unicode, _unpack_char_p, @@ -991,7 +992,7 @@ def console_set_custom_font( Added PathLike support. `fontFile` no longer takes bytes. """ fontFile = Path(fontFile).resolve(strict=True) - _check(lib.TCOD_console_set_custom_font(bytes(fontFile), flags, nb_char_horiz, nb_char_vertic)) + _check(lib.TCOD_console_set_custom_font(_path_encode(fontFile), flags, nb_char_horiz, nb_char_vertic)) @deprecate("Check `con.width` instead.") @@ -1806,7 +1807,7 @@ def console_from_file(filename: str | PathLike[str]) -> tcod.console.Console: Added PathLike support. """ filename = Path(filename).resolve(strict=True) - return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(bytes(filename)))) + return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(_path_encode(filename)))) @deprecate("Call the `Console.blit` method instead.") @@ -1985,7 +1986,7 @@ def console_load_asc(con: tcod.console.Console, filename: str | PathLike[str]) - Added PathLike support. """ filename = Path(filename).resolve(strict=True) - return bool(lib.TCOD_console_load_asc(_console(con), bytes(filename))) + return bool(lib.TCOD_console_load_asc(_console(con), _path_encode(filename))) @deprecate("This format is not actively supported") @@ -1998,7 +1999,7 @@ def console_save_asc(con: tcod.console.Console, filename: str | PathLike[str]) - .. versionchanged:: 16.0 Added PathLike support. """ - return bool(lib.TCOD_console_save_asc(_console(con), bytes(Path(filename)))) + return bool(lib.TCOD_console_save_asc(_console(con), _path_encode(Path(filename)))) @deprecate("This format is not actively supported") @@ -2012,7 +2013,7 @@ def console_load_apf(con: tcod.console.Console, filename: str | PathLike[str]) - Added PathLike support. """ filename = Path(filename).resolve(strict=True) - return bool(lib.TCOD_console_load_apf(_console(con), bytes(filename))) + return bool(lib.TCOD_console_load_apf(_console(con), _path_encode(filename))) @deprecate("This format is not actively supported") @@ -2025,7 +2026,7 @@ def console_save_apf(con: tcod.console.Console, filename: str | PathLike[str]) - .. versionchanged:: 16.0 Added PathLike support. """ - return bool(lib.TCOD_console_save_apf(_console(con), bytes(Path(filename)))) + return bool(lib.TCOD_console_save_apf(_console(con), _path_encode(Path(filename)))) @deprecate("Use tcod.console.load_xp to load this file.") @@ -2040,7 +2041,7 @@ def console_load_xp(con: tcod.console.Console, filename: str | PathLike[str]) -> Added PathLike support. """ filename = Path(filename).resolve(strict=True) - return bool(lib.TCOD_console_load_xp(_console(con), bytes(filename))) + return bool(lib.TCOD_console_load_xp(_console(con), _path_encode(filename))) @deprecate("Use tcod.console.save_xp to save this console.") @@ -2050,7 +2051,7 @@ def console_save_xp(con: tcod.console.Console, filename: str | PathLike[str], co .. versionchanged:: 16.0 Added PathLike support. """ - return bool(lib.TCOD_console_save_xp(_console(con), bytes(Path(filename)), compress_level)) + return bool(lib.TCOD_console_save_xp(_console(con), _path_encode(Path(filename)), compress_level)) @deprecate("Use tcod.console.load_xp to load this file.") @@ -2061,7 +2062,7 @@ def console_from_xp(filename: str | PathLike[str]) -> tcod.console.Console: Added PathLike support. """ filename = Path(filename).resolve(strict=True) - return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(bytes(filename)))) + return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(_path_encode(filename)))) @deprecate("Use tcod.console.load_xp to load this file.") @@ -2074,7 +2075,7 @@ def console_list_load_xp( Added PathLike support. """ filename = Path(filename).resolve(strict=True) - tcod_list = lib.TCOD_console_list_from_xp(bytes(filename)) + tcod_list = lib.TCOD_console_list_from_xp(_path_encode(filename)) if tcod_list == ffi.NULL: return None try: @@ -2102,7 +2103,7 @@ def console_list_save_xp( try: for console in console_list: lib.TCOD_list_push(tcod_list, _console(console)) - return bool(lib.TCOD_console_list_save_xp(tcod_list, bytes(Path(filename)), compress_level)) + return bool(lib.TCOD_console_list_save_xp(tcod_list, _path_encode(Path(filename)), compress_level)) finally: lib.TCOD_list_delete(tcod_list) @@ -3436,7 +3437,7 @@ def mouse_get_status() -> Mouse: @pending_deprecate() def namegen_parse(filename: str | PathLike[str], random: tcod.random.Random | None = None) -> None: - lib.TCOD_namegen_parse(bytes(Path(filename)), random or ffi.NULL) + lib.TCOD_namegen_parse(_path_encode(Path(filename)), random or ffi.NULL) @pending_deprecate() @@ -3639,7 +3640,7 @@ def _pycall_parser_error(msg: Any) -> None: def parser_run(parser: Any, filename: str | PathLike[str], listener: Any = None) -> None: global _parser_listener if not listener: - lib.TCOD_parser_run(parser, bytes(Path(filename)), ffi.NULL) + lib.TCOD_parser_run(parser, _path_encode(Path(filename)), ffi.NULL) return propagate_manager = _PropagateException() @@ -3658,7 +3659,7 @@ def parser_run(parser: Any, filename: str | PathLike[str], listener: Any = None) with _parser_callback_lock: _parser_listener = listener with propagate_manager: - lib.TCOD_parser_run(parser, bytes(Path(filename)), c_listener) + lib.TCOD_parser_run(parser, _path_encode(Path(filename)), c_listener) @deprecate("libtcod objects are deleted automatically.") @@ -4079,7 +4080,7 @@ def sys_save_screenshot(name: str | PathLike[str] | None = None) -> None: .. versionchanged:: 16.0 Added PathLike support. """ - lib.TCOD_sys_save_screenshot(bytes(Path(name)) if name is not None else ffi.NULL) + lib.TCOD_sys_save_screenshot(_path_encode(Path(name)) if name is not None else ffi.NULL) # custom fullscreen resolution diff --git a/tcod/tileset.py b/tcod/tileset.py index b53b0149..1ebf2122 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -21,7 +21,7 @@ from numpy.typing import ArrayLike, NDArray import tcod.console -from tcod._internal import _check, _console, _raise_tcod_error, deprecate +from tcod._internal import _check, _console, _path_encode, _raise_tcod_error, deprecate from tcod.cffi import ffi, lib @@ -268,7 +268,7 @@ def load_truetype_font(path: str | PathLike[str], tile_width: int, tile_height: This function is provisional. The API may change. """ path = Path(path).resolve(strict=True) - cdata = lib.TCOD_load_truetype_font_(bytes(path), tile_width, tile_height) + cdata = lib.TCOD_load_truetype_font_(_path_encode(path), tile_width, tile_height) if not cdata: raise RuntimeError(ffi.string(lib.TCOD_get_error())) return Tileset._claim(cdata) @@ -296,7 +296,7 @@ def set_truetype_font(path: str | PathLike[str], tile_width: int, tile_height: i Use :any:`load_truetype_font` instead. """ path = Path(path).resolve(strict=True) - if lib.TCOD_tileset_load_truetype_(bytes(path), tile_width, tile_height): + if lib.TCOD_tileset_load_truetype_(_path_encode(path), tile_width, tile_height): raise RuntimeError(ffi.string(lib.TCOD_get_error())) @@ -314,7 +314,7 @@ def load_bdf(path: str | PathLike[str]) -> Tileset: .. versionadded:: 11.10 """ path = Path(path).resolve(strict=True) - cdata = lib.TCOD_load_bdf(bytes(path)) + cdata = lib.TCOD_load_bdf(_path_encode(path)) if not cdata: raise RuntimeError(ffi.string(lib.TCOD_get_error()).decode()) return Tileset._claim(cdata) @@ -343,7 +343,7 @@ def load_tilesheet(path: str | PathLike[str], columns: int, rows: int, charmap: mapping = [] if charmap is not None: mapping = list(itertools.islice(charmap, columns * rows)) - cdata = lib.TCOD_tileset_load(bytes(path), columns, rows, len(mapping), mapping) + cdata = lib.TCOD_tileset_load(_path_encode(path), columns, rows, len(mapping), mapping) if not cdata: _raise_tcod_error() return Tileset._claim(cdata) From b032d80bcfe1557ea5f8a9ffdf9e65320c4981d4 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 24 Sep 2023 23:07:44 -0700 Subject: [PATCH 019/134] Prepare 16.2.1 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c77fa4..720afbd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [16.2.1] - 2023-09-24 ### Fixed - Fixed errors loading files where their paths are non-ASCII and the C locale is not UTF-8. From c8520307e77f439cee48b70b71c010ab545c6132 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 25 Sep 2023 01:08:02 -0700 Subject: [PATCH 020/134] Ignore locale outside of Windows The C calls I use are okay with UTF-8 on Unix platforms. --- CHANGELOG.md | 4 +++- tcod/_internal.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 720afbd4..2de3647a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,12 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Fixed +- Ignore the locale when encoding file paths outside of Windows. ## [16.2.1] - 2023-09-24 ### Fixed -- Fixed errors loading files where their paths are non-ASCII and the C locale is not UTF-8. +- Fixed errors loading files on Windows where their paths are non-ASCII and the locale is not UTF-8. ## [16.2.0] - 2023-09-20 ### Changed diff --git a/tcod/_internal.py b/tcod/_internal.py index 56ac7c50..1bf5e211 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -130,9 +130,11 @@ def _fmt(string: str, stacklevel: int = 2) -> bytes: def _path_encode(path: Path) -> bytes: - """Return a bytes file path for the current C locale.""" + """Return a bytes file path for the current locale when on Windows, uses fsdecode for other platforms.""" + if sys.platform != "win32": + return bytes(path) # Sane and expected behavior for converting Path into bytes try: - return str(path).encode(locale.getlocale()[1] or "utf-8") + return str(path).encode(locale.getlocale()[1] or "utf-8") # Stay classy, Windows except UnicodeEncodeError as exc: if sys.version_info >= (3, 11): exc.add_note("""Consider calling 'locale.setlocale(locale.LC_CTYPES, ".UTF8")' to support Unicode paths.""") From 05b6e53a85615677a44ac283e2e8714132088018 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 05:35:18 +0000 Subject: [PATCH 021/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.3.0 → 23.9.1](https://github.com/psf/black/compare/23.3.0...23.9.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54a9a88e..fb79db5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.9.1 hooks: - id: black - repo: https://github.com/pycqa/isort From 3632ec6b854deea624077b14e6fad1624c50f7b9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:05:18 +0000 Subject: [PATCH 022/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb79db5e..abf69940 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 58c553cb097ce92041cab6f56bc66dd19ef80b83 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 20:00:56 +0000 Subject: [PATCH 023/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.9.1 → 23.10.0](https://github.com/psf/black/compare/23.9.1...23.10.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index abf69940..f49c6c76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.0 hooks: - id: black - repo: https://github.com/pycqa/isort From d841fe7f86856bebb1f0290d4d7cdede632e7212 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 24 Oct 2023 22:49:08 -0700 Subject: [PATCH 024/134] Apply Ruff auto fixes --- examples/samples_tcod.py | 2 +- examples/termbox/termbox.py | 2 +- tcod/event.py | 18 ++++-------------- tcod/map.py | 8 ++++---- tcod/path.py | 6 +----- tcod/render.py | 4 +--- tcod/sdl/audio.py | 4 ++-- tcod/sdl/joystick.py | 4 ++-- tcod/sdl/render.py | 4 ++-- 9 files changed, 18 insertions(+), 34 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 58d35a15..a53f81c3 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -1295,7 +1295,7 @@ def on_draw(self) -> None: texture = np.roll(texture, -int_t, 1) # replace new stretch of texture with new values for v in range(RES_V - int_t, RES_V): - for u in range(0, RES_U): + for u in range(RES_U): tex_v = (v + int_abs_t) / float(RES_V) texture[u, v] = tcod.noise_get_fbm(noise2d, [u / float(RES_U), tex_v], 32.0) + tcod.noise_get_fbm( noise2d, [1 - u / float(RES_U), tex_v], 32.0 diff --git a/examples/termbox/termbox.py b/examples/termbox/termbox.py index 26ab2d7a..714a28a5 100755 --- a/examples/termbox/termbox.py +++ b/examples/termbox/termbox.py @@ -264,7 +264,7 @@ def peek_event(self, timeout=0): else: uch = None """ - pass # return (e.type, uch, e.key, e.mod, e.w, e.h, e.x, e.y) + # return (e.type, uch, e.key, e.mod, e.w, e.h, e.x, e.y) def poll_event(self): """Wait for an event and return it. diff --git a/tcod/event.py b/tcod/event.py index 16eaf498..41623a93 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -83,11 +83,11 @@ import enum import warnings -from typing import Any, Callable, Generic, Iterator, Mapping, NamedTuple, TypeVar +from typing import Any, Callable, Final, Generic, Iterator, Mapping, NamedTuple, TypeVar import numpy as np from numpy.typing import NDArray -from typing_extensions import Final, Literal +from typing_extensions import Literal import tcod.event_constants import tcod.sdl.joystick @@ -796,12 +796,7 @@ def __init__(self, x: int, y: int) -> None: self.y = y def __repr__(self) -> str: - return "tcod.event.{}(type={!r}, x={!r}, y={!r})".format( - self.__class__.__name__, - self.type, - self.x, - self.y, - ) + return f"tcod.event.{self.__class__.__name__}(type={self.type!r}, x={self.x!r}, y={self.y!r})" def __str__(self) -> str: return "<{}, x={!r}, y={!r})".format( @@ -828,12 +823,7 @@ def __init__(self, type: str, width: int, height: int) -> None: self.height = height def __repr__(self) -> str: - return "tcod.event.{}(type={!r}, width={!r}, height={!r})".format( - self.__class__.__name__, - self.type, - self.width, - self.height, - ) + return f"tcod.event.{self.__class__.__name__}(type={self.type!r}, width={self.width!r}, height={self.height!r})" def __str__(self) -> str: return "<{}, width={!r}, height={!r})".format( diff --git a/tcod/map.py b/tcod/map.py index 820c981d..8cd6ea68 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -137,8 +137,8 @@ def compute_fov( """ if not (0 <= x < self.width and 0 <= y < self.height): warnings.warn( - "Index ({}, {}) is outside of this maps shape ({}, {})." - "\nThis will raise an error in future versions.".format(x, y, self.width, self.height), + f"Index ({x}, {y}) is outside of this maps shape ({self.width}, {self.height})." + "\nThis will raise an error in future versions.", RuntimeWarning, stacklevel=2, ) @@ -239,8 +239,8 @@ def compute_fov( raise TypeError(msg) if not (0 <= pov[0] < transparency.shape[0] and 0 <= pov[1] < transparency.shape[1]): warnings.warn( - "Given pov index {!r} is outside the array of shape {!r}." - "\nThis will raise an error in future versions.".format(pov, transparency.shape), + f"Given pov index {pov!r} is outside the array of shape {transparency.shape!r}." + "\nThis will raise an error in future versions.", RuntimeWarning, stacklevel=2, ) diff --git a/tcod/path.py b/tcod/path.py index 84d54138..09f6d985 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -83,11 +83,7 @@ def get_tcod_path_ffi(self) -> tuple[Any, Any, tuple[int, int]]: return self._CALLBACK_P, ffi.new_handle(self._userdata), self.shape def __repr__(self) -> str: - return "{}({!r}, shape={!r})".format( - self.__class__.__name__, - self._userdata, - self.shape, - ) + return f"{self.__class__.__name__}({self._userdata!r}, shape={self.shape!r})" class EdgeCostCallback(_EdgeCostFunc): diff --git a/tcod/render.py b/tcod/render.py index d01d8129..8825d076 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -29,9 +29,7 @@ from __future__ import annotations -from typing import Any - -from typing_extensions import Final +from typing import Any, Final import tcod.console import tcod.sdl.render diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 8b9addab..b623c58d 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -48,11 +48,11 @@ import threading import time from types import TracebackType -from typing import Any, Callable, Hashable, Iterator +from typing import Any, Callable, Final, Hashable, Iterator import numpy as np from numpy.typing import ArrayLike, DTypeLike, NDArray -from typing_extensions import Final, Literal, Self +from typing_extensions import Literal, Self import tcod.sdl.sys from tcod.cffi import ffi, lib diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index 8ab8637f..2e918202 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -5,10 +5,10 @@ from __future__ import annotations import enum -from typing import Any, ClassVar +from typing import Any, ClassVar, Final from weakref import WeakValueDictionary -from typing_extensions import Final, Literal +from typing_extensions import Literal import tcod.sdl.sys from tcod.cffi import ffi, lib diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index b5489f18..cb365215 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -5,11 +5,11 @@ from __future__ import annotations import enum -from typing import Any +from typing import Any, Final import numpy as np from numpy.typing import NDArray -from typing_extensions import Final, Literal +from typing_extensions import Literal import tcod.sdl.video from tcod.cffi import ffi, lib From 95e605a7c633c6d1c50a5966d642d550746e7563 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 24 Oct 2023 23:22:55 -0700 Subject: [PATCH 025/134] Fix or ignore several Ruff issues --- build_libtcod.py | 4 ++-- examples/termbox/termbox.py | 2 ++ examples/termbox/termboxtest.py | 4 +++- examples/thread_jobs.py | 12 +++++----- tcod/color.py | 12 +++++----- tcod/libtcodpy.py | 42 ++++++++++++++++++--------------- 6 files changed, 42 insertions(+), 34 deletions(-) diff --git a/build_libtcod.py b/build_libtcod.py index b912ecc1..59447e9f 100755 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -9,7 +9,7 @@ import re import sys from pathlib import Path -from typing import Any, Iterable, Iterator +from typing import Any, ClassVar, Iterable, Iterator from cffi import FFI @@ -45,7 +45,7 @@ class ParsedHeader: """ # Class dictionary of all parsed headers. - all_headers: dict[Path, ParsedHeader] = {} + all_headers: ClassVar[dict[Path, ParsedHeader]] = {} def __init__(self, path: Path) -> None: """Initialize and organize a header file.""" diff --git a/examples/termbox/termbox.py b/examples/termbox/termbox.py index 714a28a5..4634c990 100755 --- a/examples/termbox/termbox.py +++ b/examples/termbox/termbox.py @@ -15,6 +15,8 @@ [ ] not all keys/events are mapped """ +# ruff: noqa + class TermboxException(Exception): def __init__(self, msg) -> None: diff --git a/examples/termbox/termboxtest.py b/examples/termbox/termboxtest.py index c0d57065..696be1ce 100755 --- a/examples/termbox/termboxtest.py +++ b/examples/termbox/termboxtest.py @@ -1,7 +1,9 @@ -#!/usr/bin/python +#!/usr/bin/env python import termbox +# ruff: noqa + spaceord = ord(" ") diff --git a/examples/thread_jobs.py b/examples/thread_jobs.py index d6fa54fd..f4154f9a 100755 --- a/examples/thread_jobs.py +++ b/examples/thread_jobs.py @@ -31,32 +31,32 @@ REPEAT = 10 # Number to times to run a test. Only the fastest result is shown. -def test_fov(map_: tcod.map.Map) -> tcod.map.Map: +def test_fov(map_: tcod.map.Map) -> tcod.map.Map: # noqa: D103 map_.compute_fov(MAP_WIDTH // 2, MAP_HEIGHT // 2) return map_ -def test_fov_single(maps: List[tcod.map.Map]) -> None: +def test_fov_single(maps: List[tcod.map.Map]) -> None: # noqa: D103 for map_ in maps: test_fov(map_) -def test_fov_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: +def test_fov_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: # noqa: D103 for _result in executor.map(test_fov, maps): pass -def test_astar(map_: tcod.map.Map) -> List[Tuple[int, int]]: +def test_astar(map_: tcod.map.Map) -> List[Tuple[int, int]]: # noqa: D103 astar = tcod.path.AStar(map_) return astar.get_path(0, 0, MAP_WIDTH - 1, MAP_HEIGHT - 1) -def test_astar_single(maps: List[tcod.map.Map]) -> None: +def test_astar_single(maps: List[tcod.map.Map]) -> None: # noqa: D103 for map_ in maps: test_astar(map_) -def test_astar_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: +def test_astar_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: # noqa: D103 for _result in executor.map(test_astar, maps): pass diff --git a/tcod/color.py b/tcod/color.py index dfd97b8c..37312413 100644 --- a/tcod/color.py +++ b/tcod/color.py @@ -17,7 +17,7 @@ class Color(List[int]): b (int): Blue value, from 0 to 255. """ - def __init__(self, r: int = 0, g: int = 0, b: int = 0) -> None: + def __init__(self, r: int = 0, g: int = 0, b: int = 0) -> None: # noqa: D107 list.__setitem__(self, slice(None), (r & 0xFF, g & 0xFF, b & 0xFF)) @property @@ -82,13 +82,13 @@ def __getitem__(self, index: Any) -> Any: # noqa: ANN401 return super().__getitem__(index) @deprecate("This class will not be mutable in the future.", FutureWarning) - def __setitem__(self, index: Any, value: Any) -> None: # noqa: ANN401 + def __setitem__(self, index: Any, value: Any) -> None: # noqa: ANN401, D105 if isinstance(index, str): super().__setitem__("rgb".index(index), value) else: super().__setitem__(index, value) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Compare equality between colors. Also compares with standard sequences such as 3-item tuples or lists. @@ -99,7 +99,7 @@ def __eq__(self, other: Any) -> bool: return False @deprecate("Use NumPy instead for color math operations.", FutureWarning) - def __add__(self, other: Any) -> Color: # type: ignore[override] + def __add__(self, other: object) -> Color: # type: ignore[override] """Add two colors together. .. deprecated:: 9.2 @@ -108,7 +108,7 @@ def __add__(self, other: Any) -> Color: # type: ignore[override] return Color._new_from_cdata(lib.TCOD_color_add(self, other)) @deprecate("Use NumPy instead for color math operations.", FutureWarning) - def __sub__(self, other: Any) -> Color: + def __sub__(self, other: object) -> Color: """Subtract one color from another. .. deprecated:: 9.2 @@ -117,7 +117,7 @@ def __sub__(self, other: Any) -> Color: return Color._new_from_cdata(lib.TCOD_color_subtract(self, other)) @deprecate("Use NumPy instead for color math operations.", FutureWarning) - def __mul__(self, other: Any) -> Color: + def __mul__(self, other: object) -> Color: """Multiply with a scaler or another color. .. deprecated:: 9.2 diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index a9162eb3..d3db588c 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -54,7 +54,7 @@ ) # Functions are too deprecated to make changes. -# ruff: noqa: ANN401 PLR0913 +# ruff: noqa: ANN401 PLR0913 D102 D103 D105 D107 Bsp = tcod.bsp.BSP @@ -1477,6 +1477,9 @@ def console_print_ex( con (Console): Any Console instance. x (int): Character x position from the left. y (int): Character y position from the top. + flag: Blending mode to use. + alignment: The libtcod alignment constant. + fmt: A unicode or bytes string, optionally using color codes. .. deprecated:: 8.5 Use :any:`Console.print_` instead. @@ -1726,8 +1729,7 @@ def console_wait_for_keypress(flush: bool) -> Key: """Block until the user presses a key, then returns a new Key. Args: - flush bool: If True then the event queue is cleared before waiting - for the next event. + flush: If True then the event queue is cleared before waiting for the next event. Returns: Key: A new Key instance. @@ -2136,8 +2138,8 @@ def path_new_using_function( Args: w (int): Clipping width. h (int): Clipping height. - func (Callable[[int, int, int, int, Any], float]): - userData (Any): + func: Callback function with the format: `f(origin_x, origin_y, dest_x, dest_y, userData) -> float` + userData (Any): An object passed to the callback. dcost (float): A multiplier for the cost of diagonal movement. Can be set to 0 to disable diagonal movement. @@ -2485,6 +2487,7 @@ def heightmap_copy(hm1: NDArray[np.float32], hm2: NDArray[np.float32]) -> None: """Copy the heightmap ``hm1`` to ``hm2``. Args: + hm: A numpy.ndarray formatted for heightmap functions. hm1 (numpy.ndarray): The source heightmap. hm2 (numpy.ndarray): The destination heightmap. @@ -2499,6 +2502,7 @@ def heightmap_normalize(hm: NDArray[np.float32], mi: float = 0.0, ma: float = 1. """Normalize heightmap values between ``mi`` and ``ma``. Args: + hm: A numpy.ndarray formatted for heightmap functions. mi (float): The lowest value after normalization. ma (float): The highest value after normalization. """ @@ -2944,7 +2948,7 @@ def heightmap_has_land_on_border(hm: NDArray[np.float32], waterlevel: float) -> Args: hm (numpy.ndarray): A numpy.ndarray formatted for heightmap functions. - waterLevel (float): The water level to use. + waterlevel (float): The water level to use. Returns: bool: True if the map edges are below ``waterlevel``, otherwise False. @@ -3493,6 +3497,7 @@ def noise_set_type(n: tcod.noise.Noise, typ: int) -> None: """Set a Noise objects default noise algorithm. Args: + n: Noise object. typ (int): Any NOISE_* constant. """ n.algorithm = typ @@ -3531,7 +3536,7 @@ def noise_get_fbm( n (Noise): A Noise instance. f (Sequence[float]): The point to sample the noise from. typ (int): The noise algorithm to use. - octaves (float): The level of level. Should be more than 1. + oc (float): The level of level. Should be more than 1. Returns: float: The sampled noise value. @@ -3552,7 +3557,7 @@ def noise_get_turbulence( n (Noise): A Noise instance. f (Sequence[float]): The point to sample the noise from. typ (int): The noise algorithm to use. - octaves (float): The level of level. Should be more than 1. + oc (float): The level of level. Should be more than 1. Returns: float: The sampled noise value. @@ -3781,8 +3786,8 @@ def random_get_int(rnd: tcod.random.Random | None, mi: int, ma: int) -> int: Args: rnd (Optional[Random]): A Random instance, or None to use the default. - low (int): The lower bound of the random range, inclusive. - high (int): The upper bound of the random range, inclusive. + mi (int): The lower bound of the random range, inclusive. + ma (int): The upper bound of the random range, inclusive. Returns: int: A random integer in the range ``mi`` <= n <= ``ma``. @@ -3798,8 +3803,8 @@ def random_get_float(rnd: tcod.random.Random | None, mi: float, ma: float) -> fl Args: rnd (Optional[Random]): A Random instance, or None to use the default. - low (float): The lower bound of the random range, inclusive. - high (float): The upper bound of the random range, inclusive. + mi (float): The lower bound of the random range, inclusive. + ma (float): The upper bound of the random range, inclusive. Returns: float: A random double precision float @@ -3827,8 +3832,8 @@ def random_get_int_mean(rnd: tcod.random.Random | None, mi: int, ma: int, mean: Args: rnd (Optional[Random]): A Random instance, or None to use the default. - low (int): The lower bound of the random range, inclusive. - high (int): The upper bound of the random range, inclusive. + mi (int): The lower bound of the random range, inclusive. + ma (int): The upper bound of the random range, inclusive. mean (int): The mean return value. Returns: @@ -3845,8 +3850,8 @@ def random_get_float_mean(rnd: tcod.random.Random | None, mi: float, ma: float, Args: rnd (Optional[Random]): A Random instance, or None to use the default. - low (float): The lower bound of the random range, inclusive. - high (float): The upper bound of the random range, inclusive. + mi (float): The lower bound of the random range, inclusive. + ma (float): The upper bound of the random range, inclusive. mean (float): The mean return value. Returns: @@ -4071,7 +4076,7 @@ def sys_save_screenshot(name: str | PathLike[str] | None = None) -> None: screenshot000.png, screenshot001.png, etc. Whichever is available first. Args: - file Optional[AnyStr]: File path to save screenshot. + name: File path to save screenshot. .. deprecated:: 11.13 This function is not supported by contexts. @@ -4179,8 +4184,7 @@ def sys_register_SDL_renderer(callback: Callable[[Any], None]) -> None: The callback is called on every call to :any:`libtcodpy.console_flush`. Args: - callback Callable[[CData], None]: - A function which takes a single argument. + callback: A function which takes a single argument. .. deprecated:: 11.13 This function is not supported by contexts. From 086684f92253237d2ab1bbe4d7ebbeb0f2bfdb04 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 24 Oct 2023 23:37:32 -0700 Subject: [PATCH 026/134] Switch linters and formatters to Ruff --- .github/workflows/python-package.yml | 36 ++++++++++------------------ .pre-commit-config.yaml | 12 ++++------ .vscode/extensions.json | 2 +- .vscode/settings.json | 3 +-- build_sdl.py | 6 ++++- examples/samples_tcod.py | 5 +--- pyproject.toml | 2 ++ 7 files changed, 28 insertions(+), 38 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 784fef9a..5fa48608 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,26 +16,16 @@ env: git-depth: 0 # Depth to search for tags. jobs: - black: + ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install Black - run: pip install black - - name: Run Black - run: black --check --diff examples/ scripts/ tcod/ tests/ *.py - - isort: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install isort - run: pip install isort - - name: isort - uses: liskin/gh-problem-matcher-wrap@v2 - with: - linters: isort - run: isort scripts/ tcod/ tests/ examples/ --check --diff + - name: Install Ruff + run: pip install ruff + - name: Ruff Check + run: ruff check . --fix-only --exit-non-zero-on-fix --output-format=github + - name: Ruff Format + run: ruff format . --check mypy: runs-on: ubuntu-latest @@ -76,7 +66,7 @@ jobs: # This makes sure that the latest versions of the SDL headers parse correctly. parse_sdl: - needs: [black, isort, mypy] + needs: [ruff, mypy] runs-on: ${{ matrix.os }} strategy: matrix: @@ -100,7 +90,7 @@ jobs: SDL_VERSION: ${{ matrix.sdl-version }} build: - needs: [black, isort, mypy] + needs: [ruff, mypy] runs-on: ${{ matrix.os }} strategy: matrix: @@ -164,7 +154,7 @@ jobs: retention-days: 7 test-docs: - needs: [black, isort, mypy] + needs: [ruff, mypy] runs-on: ubuntu-latest steps: - name: Install APT dependencies @@ -189,7 +179,7 @@ jobs: run: python -m sphinx -T -E -W --keep-going . _build/html isolated: # Test installing the package from source. - needs: [black, isort, mypy, sdist] + needs: [ruff, mypy, sdist] runs-on: ${{ matrix.os }} strategy: matrix: @@ -219,7 +209,7 @@ jobs: python -c "import tcod.context" linux-wheels: - needs: [black, isort, mypy] + needs: [ruff, mypy] runs-on: "ubuntu-latest" strategy: matrix: @@ -268,7 +258,7 @@ jobs: retention-days: 7 build-macos: - needs: [black, isort, mypy] + needs: [ruff, mypy] runs-on: "macos-11" strategy: fail-fast: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f49c6c76..a591e078 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,13 +14,11 @@ repos: - id: debug-statements - id: fix-byte-order-marker - id: detect-private-key - - repo: https://github.com/psf/black - rev: 23.10.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.2 hooks: - - id: black - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort + - id: ruff + args: [--fix-only, --exit-non-zero-on-fix] + - id: ruff-format default_language_version: python: python3.11 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d589600c..15ad54f7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,13 +6,13 @@ "austin.code-gnu-global", "editorconfig.editorconfig", "ms-python.python", - "ms-python.black-formatter", "ms-python.vscode-pylance", "ms-vscode.cpptools", "redhat.vscode-yaml", "streetsidesoftware.code-spell-checker", "tamasfe.even-better-toml", "xaver.clang-format", + "charliermarsh.ruff" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a02db40..aeeb53d8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,6 @@ "--follow-imports=silent", "--show-column-numbers" ], - "python.formatting.provider": "none", "files.associations": { "*.spec": "python", }, @@ -483,7 +482,7 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" + "editor.defaultFormatter": "charliermarsh.ruff" }, "cSpell.enableFiletypes": [ "github-actions-workflow" diff --git a/build_sdl.py b/build_sdl.py index b47b426c..0c46df14 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -183,7 +183,11 @@ def _should_track_define(self, tokens: list[Any]) -> bool: ) def on_directive_handle( - self, directive: Any, tokens: list[Any], if_passthru: bool, preceding_tokens: list[Any] # noqa: ANN401 + self, + directive: Any, # noqa: ANN401 + tokens: list[Any], + if_passthru: bool, + preceding_tokens: list[Any], ) -> Any: # noqa: ANN401 """Catch and store definitions.""" if directive.value == "define" and self._should_track_define(tokens): diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index a53f81c3..7f65a817 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -565,10 +565,7 @@ def draw_ui(self) -> None: sample_console.print( 1, 1, - "IJKL : move around\n" - "T : torch fx {}\n" - "W : light walls {}\n" - "+-: algo {}".format( + "IJKL : move around\nT : torch fx {}\nW : light walls {}\n+-: algo {}".format( "on " if self.torch else "off", "on " if self.light_walls else "off", FOV_ALGO_NAMES[self.algo_num], diff --git a/pyproject.toml b/pyproject.toml index 325c0baa..3edd28ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,6 +186,8 @@ ignore = [ "D407", # dashed-underline-after-section "D408", # section-underline-after-name "D409", # section-underline-matches-section-length + "D206", # indent-with-spaces + "W191", # tab-indentation ] extend-exclude = ["libtcod"] # Ignore submodule line-length = 120 From 027f52229b5004807580a01883a1c11a795e34c0 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 25 Oct 2023 00:13:30 -0700 Subject: [PATCH 027/134] Convert isolated tests to use Tox Remove setuptools upper bound Skip more tests requiring SDL windows --- .github/workflows/python-package.yml | 22 ++++++++++------------ pyproject.toml | 4 ++-- tests/conftest.py | 9 +++++++++ tests/test_sdl.py | 6 +++--- tests/test_tcod.py | 2 +- tox.ini | 17 +++++++++++++++++ 6 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 tox.ini diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5fa48608..fdd34ead 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -178,35 +178,33 @@ jobs: working-directory: docs run: python -m sphinx -T -E -W --keep-going . _build/html - isolated: # Test installing the package from source. - needs: [ruff, mypy, sdist] + tox: + needs: [ruff] runs-on: ${{ matrix.os }} strategy: matrix: os: ["ubuntu-latest", "windows-latest"] steps: + - uses: actions/checkout@v3 + with: + fetch-depth: ${{ env.git-depth }} + - name: Checkout submodules + run: git submodule update --init --depth 1 - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.x - name: Install Python dependencies run: | - python -m pip install --upgrade pip - pip install wheel + python -m pip install --upgrade pip tox - name: Install APT dependencies if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install libsdl2-dev - - uses: actions/download-artifact@v3 - with: - name: sdist - - name: Build package in isolation - run: | - pip install tcod-*.tar.gz - - name: Confirm package import + - name: Run tox run: | - python -c "import tcod.context" + tox -vv linux-wheels: needs: [ruff, mypy] diff --git a/pyproject.toml b/pyproject.toml index 3edd28ce..94f1ddc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [build-system] requires = [ - # Newer versions of setuptools break editable installs + # setuptools >=64.0.0 might break editable installs # https://github.com/pypa/setuptools/issues/3548 - "setuptools >=61.0.0, <64.0.0", + "setuptools >=61.0.0", "setuptools_scm[toml]>=6.2", "wheel>=0.37.1", "cffi>=1.15", diff --git a/tests/conftest.py b/tests/conftest.py index 163d0918..83e2dc8e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,15 @@ def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption("--no-window", action="store_true", help="Skip tests which need a rendering context.") +@pytest.fixture() +def uses_window(request: pytest.FixtureRequest) -> Iterator[None]: + """Marks tests which require a rendering context.""" + if request.config.getoption("--no-window"): + pytest.skip("This test needs a rendering context.") + yield None + return + + @pytest.fixture(scope="session", params=["SDL", "SDL2"]) def session_console(request: pytest.FixtureRequest) -> Iterator[tcod.console.Console]: if request.config.getoption("--no-window"): diff --git a/tests/test_sdl.py b/tests/test_sdl.py index fa2ac29a..10cdbb50 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -11,7 +11,7 @@ # ruff: noqa: D103 -def test_sdl_window() -> None: +def test_sdl_window(uses_window: None) -> None: assert tcod.sdl.video.get_grabbed_window() is None window = tcod.sdl.video.new_window(1, 1) window.raise_window() @@ -47,14 +47,14 @@ def test_sdl_window_bad_types() -> None: tcod.sdl.video.Window(tcod.ffi.new("SDL_Rect*")) -def test_sdl_screen_saver() -> None: +def test_sdl_screen_saver(uses_window: None) -> None: tcod.sdl.sys.init() assert tcod.sdl.video.screen_saver_allowed(False) is False assert tcod.sdl.video.screen_saver_allowed(True) is True assert tcod.sdl.video.screen_saver_allowed() is True -def test_sdl_render() -> None: +def test_sdl_render(uses_window: None) -> None: window = tcod.sdl.video.new_window(1, 1) render = tcod.sdl.render.new_renderer(window, software=True, vsync=False, target_textures=True) render.present() diff --git a/tests/test_tcod.py b/tests/test_tcod.py index 33e50195..38b1d5c2 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -186,7 +186,7 @@ def test_recommended_size(console: tcod.console.Console) -> None: @pytest.mark.filterwarnings("ignore") -def test_context() -> None: +def test_context(uses_window: None) -> None: with tcod.context.new_window(32, 32, renderer=tcod.RENDERER_SDL2): pass WIDTH, HEIGHT = 16, 4 diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..a9bf7fbf --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +isolated_build = True +env_list = + py311 +minversion = 4.4.11 + +[testenv] +description = run the tests with pytest +package = wheel +wheel_build_env = .pkg +deps = + pytest>=6 + pytest-cov + pytest-benchmark + pytest-timeout +commands = + pytest --no-window {tty:--color=yes} {posargs} From b399f48289c0aed48b2e5c0668a4f0752d348f9b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 25 Oct 2023 16:23:40 -0700 Subject: [PATCH 028/134] Refactor tutorial More verbose code examples Include future annotations Explain procedural block elements function call --- docs/tutorial/part-00.rst | 4 ++ docs/tutorial/part-01.rst | 87 ++++++++++++++++++++++++++++++--------- 2 files changed, 72 insertions(+), 19 deletions(-) diff --git a/docs/tutorial/part-00.rst b/docs/tutorial/part-00.rst index cb97db3f..a00a7fe9 100644 --- a/docs/tutorial/part-00.rst +++ b/docs/tutorial/part-00.rst @@ -23,9 +23,13 @@ First script First start with a modern top-level script. Create a script in the project root folder called ``main.py`` which checks :python:`if __name__ == "__main__":` and calls a ``main`` function. +Any modern script using type-hinting will also have :python:`from __future__ import annotations` near the top. .. code-block:: python + from __future__ import annotations + + def main() -> None: print("Hello World!") diff --git a/docs/tutorial/part-01.rst b/docs/tutorial/part-01.rst index 028fc40a..4f1827f5 100644 --- a/docs/tutorial/part-01.rst +++ b/docs/tutorial/part-01.rst @@ -17,6 +17,9 @@ You should have ``main.py`` script from :ref:`part-0`: .. code-block:: python + from __future__ import annotations + + def main() -> None: ... @@ -36,6 +39,7 @@ These kinds of tilesets are always loaded with :python:`columns=16, rows=16, cha Use the string :python:`"data/Alloy_curses_12x12.png"` to refer to the path of the tileset. [#why_not_pathlib]_ Load the tileset with :any:`tcod.tileset.load_tilesheet`. +Pass the tileset to :any:`tcod.tileset.procedural_block_elements` which will fill in most `Block Elements `_ missing from `Code Page 437 `_. Then pass the tileset to :any:`tcod.context.new`, you only need to provide the ``tileset`` parameter. :any:`tcod.context.new` returns a :any:`Context` which will be used with Python's :python:`with` statement. @@ -45,9 +49,10 @@ The new block can't be empty, so add :python:`pass` to the with statement body. These functions are part of modules which have not been imported yet, so new imports for ``tcod.context`` and ``tcod.tileset`` must be added to the top of the script. .. code-block:: python - :emphasize-lines: 2,3,8-12 + :emphasize-lines: 3,4,8-14 + + from __future__ import annotations - ... import tcod.context # Add these imports import tcod.tileset @@ -57,9 +62,13 @@ These functions are part of modules which have not been imported yet, so new imp tileset = tcod.tileset.load_tilesheet( "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 ) + tcod.tileset.procedural_block_elements(tileset=tileset) with tcod.context.new(tileset=tileset) as context: pass # The window will stay open for the duration of this block - ... + + + if __name__ == "__main__": + main() If an import fails that means you do not have ``tcod`` installed on the Python environment you just used to run the script. If you use an IDE then make sure the Python environment it is using is correct and then run :shell:`pip install tcod` from the shell terminal within that IDE. @@ -88,11 +97,14 @@ Then test if an event is for closing the window with :python:`if isinstance(even If this is True then you should exit the function with :python:`raise SystemExit()`. [#why_raise]_ .. code-block:: python - :emphasize-lines: 2,3,11-19 + :emphasize-lines: 3,5,15-23 + + from __future__ import annotations - ... import tcod.console + import tcod.context import tcod.event + import tcod.tileset def main() -> None: @@ -100,6 +112,7 @@ If this is True then you should exit the function with :python:`raise SystemExit tileset = tcod.tileset.load_tilesheet( "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 ) + tcod.tileset.procedural_block_elements(tileset=tileset) console = tcod.console.Console(80, 50) console.print(0, 0, "Hello World") # Test text by printing "Hello World" to the console with tcod.context.new(console=console, tileset=tileset) as context: @@ -109,7 +122,10 @@ If this is True then you should exit the function with :python:`raise SystemExit print(event) if isinstance(event, tcod.event.Quit): raise SystemExit() - ... + + + if __name__ == "__main__": + main() If you run this then you get a window saying :python:`"Hello World"`. The window can be resized and the console will be stretched to fit the new resolution. @@ -135,9 +151,15 @@ The parameters for ``on_draw`` are ``self`` because this is an instance method a Call this method using the players current coordinates and the :python:`"@"` character. .. code-block:: python + :emphasize-lines: 3,10-21 + + from __future__ import annotations - ... import attrs + import tcod.console + import tcod.context + import tcod.event + import tcod.tileset @attrs.define(eq=False) @@ -152,6 +174,7 @@ Call this method using the players current coordinates and the :python:`"@"` cha def on_draw(self, console: tcod.console.Console) -> None: """Draw the player glyph.""" console.print(self.player_x, self.player_y, "@") + ... Now remove the :python:`console.print(0, 0, "Hello World")` line from ``main``. @@ -183,7 +206,10 @@ Modify the drawing routine so that the console is cleared, then passed to :pytho print(event) if isinstance(event, tcod.event.Quit): raise SystemExit() - ... + + + if __name__ == "__main__": + main() Now if you run the script you'll see ``@``. @@ -201,12 +227,33 @@ Make a case for each arrow key: ``LEFT`` ``RIGHT`` ``UP`` ``DOWN`` and move the Since events are printed you can check the :any:`KeySym` of a key by pressing that key and looking at the printed output. See :any:`KeySym` for a list of all keys. +Finally replace the event handling code in ``main`` to defer to the states ``on_event`` method. +The full script so far is: + .. code-block:: python + :emphasize-lines: 23-35,53 + + from __future__ import annotations + + import attrs + import tcod.console + import tcod.context + import tcod.event + import tcod.tileset + - ... @attrs.define(eq=False) class ExampleState: - ... + """Example state with a hard-coded player position.""" + + player_x: int + """Player X position, left-most position is zero.""" + player_y: int + """Player Y position, top-most position is zero.""" + + def on_draw(self, console: tcod.console.Console) -> None: + """Draw the player glyph.""" + console.print(self.player_x, self.player_y, "@") def on_event(self, event: tcod.event.Event) -> None: """Move the player on events and handle exiting. Movement is hard-coded.""" @@ -221,16 +268,15 @@ See :any:`KeySym` for a list of all keys. self.player_y -= 1 case tcod.event.KeyDown(sym=tcod.event.KeySym.DOWN): self.player_y += 1 - ... - -Now replace the event handling code in ``main`` to defer to the states ``on_event`` method. -.. code-block:: python - :emphasize-lines: 12 - ... def main() -> None: - ... + """Run ExampleState.""" + tileset = tcod.tileset.load_tilesheet( + "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 + ) + tcod.tileset.procedural_block_elements(tileset=tileset) + console = tcod.console.Console(80, 50) state = ExampleState(player_x=console.width // 2, player_y=console.height // 2) with tcod.context.new(console=console, tileset=tileset) as context: while True: @@ -240,7 +286,10 @@ Now replace the event handling code in ``main`` to defer to the states ``on_even for event in tcod.event.wait(): print(event) state.on_event(event) # Pass events to the state - ... + + + if __name__ == "__main__": + main() Now when you run this script you have a player character you can move around with the arrow keys before closing the window. @@ -253,7 +302,7 @@ You can review the part-1 source code `here Date: Wed, 25 Oct 2023 15:15:55 -0700 Subject: [PATCH 029/134] Add SDL audio device example --- examples/audio_tone.py | 48 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 2 files changed, 49 insertions(+) create mode 100755 examples/audio_tone.py diff --git a/examples/audio_tone.py b/examples/audio_tone.py new file mode 100755 index 00000000..dc24d3ec --- /dev/null +++ b/examples/audio_tone.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Shows how to use tcod.sdl.audio to play a custom-made audio stream. + +Opens an audio device using SDL and plays a square wave for 1 second. +""" +import math +import time +from typing import Any + +import attrs +import numpy as np +from numpy.typing import NDArray +from scipy import signal # type: ignore + +import tcod.sdl.audio + +VOLUME = 10 ** (-12 / 10) # -12dB, square waves can be loud + + +@attrs.define +class PullWave: + """Square wave stream generator for an SDL audio device in pull mode.""" + + time: float = 0.0 + + def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) -> None: + """Stream a square wave to SDL on demand. + + This function must run faster than the stream duration. + Numpy is used to keep performance within these limits. + """ + sample_rate = device.frequency + n_samples = device.buffer_samples + duration = n_samples / sample_rate + print(f"{duration=} {self.time=}") + + t = np.linspace(self.time, self.time + duration, n_samples, endpoint=False) + self.time += duration + wave = signal.square(t * (math.tau * 440)).astype(np.float32) + wave *= VOLUME + + stream[:] = device.convert(wave) + + +if __name__ == "__main__": + with tcod.sdl.audio.open(callback=PullWave()) as device: + print(device) + time.sleep(1) diff --git a/requirements.txt b/requirements.txt index ee1ea8a0..eb2f7d65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +attrs>=23.1.0 cffi>=1.15 numpy>=1.21.4 pycparser>=2.14 From 32005bb43364979f566a9d8442fc584ea16a35b4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 19:48:44 +0000 Subject: [PATCH 030/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.2 → v0.1.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.2...v0.1.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a591e078..a0778f86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.2 + rev: v0.1.3 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 326f92945a28bce6d5d6b8f722d8a0e8cfe2aa00 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 30 Oct 2023 14:44:24 -0700 Subject: [PATCH 031/134] Have Tox use any Python 3 version --- .github/workflows/python-package.yml | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fdd34ead..8eeac91c 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -193,7 +193,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.x + python-version: "3.x" - name: Install Python dependencies run: | python -m pip install --upgrade pip tox diff --git a/tox.ini b/tox.ini index a9bf7fbf..784e5f57 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] isolated_build = True env_list = - py311 + py3 minversion = 4.4.11 [testenv] From 6eb6d6d90b525c1764543c551a9fc5cb19923f25 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 19:41:41 +0000 Subject: [PATCH 032/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.3 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.3...v0.1.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0778f86..b58a31b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.3 + rev: v0.1.4 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 04664d305ac11e4d52074b10470399ab52bd3cc5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 10 Nov 2023 14:36:49 -0800 Subject: [PATCH 033/134] Remove unused type ignores New Mypy version --- tcod/libtcodpy.py | 2 +- tcod/sdl/render.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index d3db588c..e6649497 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -2920,7 +2920,7 @@ def heightmap_get_normal(hm: NDArray[np.float32], x: float, y: float, waterLevel """ cn = ffi.new("float[3]") lib.TCOD_heightmap_get_normal(_heightmap_cdata(hm), x, y, cn, waterLevel) - return tuple(cn) # type: ignore + return tuple(cn) @deprecate("This function is deprecated, see documentation.") diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index cb365215..b1dafe8c 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -360,7 +360,7 @@ def draw_color(self) -> tuple[int, int, int, int]: """ rgba = ffi.new("uint8_t[4]") _check(lib.SDL_GetRenderDrawColor(self.p, rgba, rgba + 1, rgba + 2, rgba + 3)) - return tuple(rgba) # type: ignore[return-value] + return tuple(rgba) @draw_color.setter def draw_color(self, rgba: tuple[int, int, int, int]) -> None: From 7ef79333f32273c0d35c6b9c5b26feda53667393 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:05:16 +0000 Subject: [PATCH 034/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.5) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b58a31b1..e8feb5a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.1.5 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 5788303ea960d11d31fee5d2844a02a7e1957955 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 19:38:11 +0000 Subject: [PATCH 035/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.5 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.5...v0.1.6) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8feb5a7..ff7a3bd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.5 + rev: v0.1.6 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 1ea1db28008029d2165b0705ed32b8aef8c65cd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:44:12 +0000 Subject: [PATCH 036/134] Bump pyinstaller in /examples/distribution/PyInstaller Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 4.3 to 5.13.1. - [Release notes](https://github.com/pyinstaller/pyinstaller/releases) - [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst) - [Commits](https://github.com/pyinstaller/pyinstaller/compare/v4.3...v5.13.1) --- updated-dependencies: - dependency-name: pyinstaller dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- examples/distribution/PyInstaller/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/distribution/PyInstaller/requirements.txt b/examples/distribution/PyInstaller/requirements.txt index f28443fd..390bb9dd 100644 --- a/examples/distribution/PyInstaller/requirements.txt +++ b/examples/distribution/PyInstaller/requirements.txt @@ -1,3 +1,3 @@ -tcod==12.2.0 -pyinstaller==4.3 -pypiwin32; sys_platform=="win32" +tcod==12.2.0 +pyinstaller==5.13.1 +pypiwin32; sys_platform=="win32" From 0c3035c5f472713b72c013c7a3e53806c3e8d1d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:00:05 +0000 Subject: [PATCH 037/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.1.7) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff7a3bd4..2b5e7322 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.1.7 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 36bd2ec9e33f9d26f64f0ac56930cc209032211b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 20:06:55 +0000 Subject: [PATCH 038/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.7 → v0.1.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.7...v0.1.8) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b5e7322..538602ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.7 + rev: v0.1.8 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 53926e18521dd8539157eb3cb3f57dd4e85ecf61 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 19:50:08 +0000 Subject: [PATCH 039/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.8 → v0.1.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.8...v0.1.9) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 538602ff..b944cbb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.8 + rev: v0.1.9 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From f919219178b8bdb7c134bc288409428990fd855e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 19:52:24 +0000 Subject: [PATCH 040/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.1.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.9...v0.1.11) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b944cbb4..63cc2295 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.1.11 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From d9fdb067436d5729c929a9aeb2b942c74572d030 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:26:35 +0000 Subject: [PATCH 041/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.11 → v0.1.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.11...v0.1.13) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63cc2295..b3619574 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.11 + rev: v0.1.13 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From b738d1ee848d1ce0307267cd46b18b5040f745bb Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 20:58:18 -0800 Subject: [PATCH 042/134] Update Python version for pre-commit. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3619574..9f04c539 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,4 +21,4 @@ repos: args: [--fix-only, --exit-non-zero-on-fix] - id: ruff-format default_language_version: - python: python3.11 + python: python3.12 From ea62af615419a136960ccea3dc43f26809ffb4de Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 20:58:58 -0800 Subject: [PATCH 043/134] Modernize VSCode settings --- .vscode/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index aeeb53d8..67f05c37 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,8 +5,8 @@ ], "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": false + "source.fixAll": "always", + "source.organizeImports": "never" }, "files.trimFinalNewlines": true, "files.insertFinalNewline": true, From 390fe35c00faff657c3aee78dfbbaa04e461c3a2 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 21:05:24 -0800 Subject: [PATCH 044/134] Initialize SDL joystick subsystems lazily Should fix performance issues caused by calling functions such as get_controller every frame. --- CHANGELOG.md | 1 + tcod/sdl/joystick.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de3647a..0abccdd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] ### Fixed - Ignore the locale when encoding file paths outside of Windows. +- Fix performance when calling joystick functions. ## [16.2.1] - 2023-09-24 ### Fixed diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index 2e918202..62241dc5 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -363,7 +363,10 @@ def _touchpad(self) -> bool: def init() -> None: """Initialize SDL's joystick and game controller subsystems.""" - tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK | tcod.sdl.sys.Subsystem.GAMECONTROLLER) + CONTROLLER_SYSTEMS = tcod.sdl.sys.Subsystem.JOYSTICK | tcod.sdl.sys.Subsystem.GAMECONTROLLER + if tcod.sdl.sys.Subsystem(lib.SDL_WasInit(CONTROLLER_SYSTEMS)) == CONTROLLER_SYSTEMS: + return # Already initialized + tcod.sdl.sys.init(CONTROLLER_SYSTEMS) def _get_number() -> int: From 7904d0fd2f93ae6968db450fa5a5c08074aa5b89 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 21:12:53 -0800 Subject: [PATCH 045/134] Apply auto-formatting on markdown files from VSCode --- CHANGELOG.md | 797 ++++++++++++++++++++++++++++++------- CONTRIBUTING.md | 2 +- examples/README.md | 4 +- examples/termbox/README.md | 26 +- 4 files changed, 667 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0abccdd6..cb802f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1294 +1,1790 @@ # Changelog + Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + ### Fixed + - Ignore the locale when encoding file paths outside of Windows. - Fix performance when calling joystick functions. ## [16.2.1] - 2023-09-24 + ### Fixed + - Fixed errors loading files on Windows where their paths are non-ASCII and the locale is not UTF-8. ## [16.2.0] - 2023-09-20 + ### Changed + - Renamed `gauss` methods to fix typos. ## [16.1.1] - 2023-07-10 + ### Changed + - Added an empty `__slots__` to `EventDispatch`. - Bundle `SDL 2.28.1` on Windows and MacOS. ### Fixed + - Fixed "SDL failed to get a vertex buffer for this Direct3D 9 rendering batch!" https://github.com/libtcod/python-tcod/issues/131 ### Removed + - Dropped support for Python 3.7. ## [16.1.0] - 2023-06-23 + ### Added + - Added the enums `tcod.event.MouseButton` and `tcod.event.MouseButtonMask`. ### Changed + - Using `libtcod 1.24.0`. ### Deprecated + - Mouse button and mask constants have been replaced by enums. ### Fixed + - `WindowResized` literal annotations were in the wrong case. ## [16.0.3] - 2023-06-04 + ### Changed + - Enabled logging for libtcod and SDL. ### Deprecated + - Deprecated using `tcod` as an implicit alias for `libtcodpy`. You should use `from tcod import libtcodpy` if you want to access this module. - Deprecated constants being held directly in `tcod`, get these from `tcod.libtcodpy` instead. - Deprecated `tcod.Console` which should be accessed from `tcod.console.Console` instead. ## [16.0.2] - 2023-06-02 + ### Fixed + - Joystick/controller device events would raise `RuntimeError` when accessed after removal. ## [16.0.1] - 2023-05-28 + ### Fixed + - `AudioDevice.stopped` was inverted. - Fixed the audio mixer stop and fadeout methods. - Exceptions raised in the audio mixer callback no longer cause a messy crash, they now go to `sys.unraisablehook`. ## [16.0.0] - 2023-05-27 + ### Added + - Added PathLike support to more libtcodpy functions. - New `tcod.sdl.mouse.show` function for querying or setting mouse visibility. -- New class method `tcod.image.Image.from_file` to load images with. This replaces `tcod.image_load`. +- New class method `tcod.image.Image.from_file` to load images with. This replaces `tcod.image_load`. - `tcod.sdl.audio.AudioDevice` is now a context manager. ### Changed + - SDL audio conversion will now pass unconvertible floating types as float32 instead of raising. ### Deprecated + - Deprecated the libtcodpy functions for images and noise generators. ### Removed + - `tcod.console_set_custom_font` can no longer take bytes as the file path. ### Fixed + - Fix `tcod.sdl.mouse.warp_in_window` function. - Fix `TypeError: '_AudioCallbackUserdata' object is not callable` when using an SDL audio device callback. [#128](https://github.com/libtcod/python-tcod/issues/128) ## [15.0.3] - 2023-05-25 + ### Deprecated -- Deprecated all libtcod color constants. Replace these with your own manually defined colors. + +- Deprecated all libtcod color constants. Replace these with your own manually defined colors. Using a color will tell you the color values of the deprecated color in the warning. -- Deprecated older scancode and keysym constants. These were replaced with the Scancode and KeySym enums. +- Deprecated older scancode and keysym constants. These were replaced with the Scancode and KeySym enums. ### Fixed + - DLL loader could fail to load `SDL2.dll` when other tcod namespace packages were installed. ## [15.0.1] - 2023-03-30 + ### Added + - Added support for `tcod.sdl` namespace packages. ### Fixed -- ``Renderer.read_pixels`` method was completely broken. + +- `Renderer.read_pixels` method was completely broken. ## [15.0.0] - 2023-01-04 + ### Changed + - Modified the letter case of window event types to match their type annotations. - This may cause regressions. Run Mypy to check for ``[comparison-overlap]`` errors. -- Mouse event attributes have been changed ``.pixel -> .position`` and ``.pixel_motion -> .motion``. + This may cause regressions. Run Mypy to check for `[comparison-overlap]` errors. +- Mouse event attributes have been changed `.pixel -> .position` and `.pixel_motion -> .motion`. - `Context.convert_event` now returns copies of events with mouse coordinates converted into tile positions. ### Deprecated + - Mouse event pixel and tile attributes have been deprecated. ## [14.0.0] - 2022-12-09 + ### Added + - Added explicit support for namespace packages. ### Changed + - Using `libtcod 1.23.1`. - Bundle `SDL 2.26.0` on Windows and MacOS. - Code Page 437: Character 0x7F is now assigned to 0x2302 (HOUSE). -- Forced all renderers to ``RENDERER_SDL2`` to fix rare graphical artifacts with OpenGL. +- Forced all renderers to `RENDERER_SDL2` to fix rare graphical artifacts with OpenGL. ### Deprecated + - The `renderer` parameter of new contexts is now deprecated. ## [13.8.1] - 2022-09-23 + ### Fixed + - `EventDispatch` was missing new event names. ## [13.8.0] - 2022-09-22 + ### Added + - Ported SDL2 joystick handing as `tcod.sdl.joystick`. - New joystick related events. ### Changed + - Using `libtcod 1.22.3`. - Bundle `SDL 2.24.0` on Windows and MacOS. ### Deprecated + - Renderers other than `tcod.RENDERER_SDL2` are now discouraged. ### Fixed + - Fixed double present bug in non-context flush functions. This was affecting performance and also caused a screen flicker whenever the global fade color was active. - Fixed the parsing of SDL 2.24.0 headers on Windows. ## [13.7.0] - 2022-08-07 + ### Added + - You can new use `SDLConsoleRender.atlas` to access the `SDLTilesetAtlas` used to create it. [#121](https://github.com/libtcod/python-tcod/issues/121) ### Fixed -- Fixed the parsing of SDL 2.0.22 headers. Specifically `SDL_FLT_EPSILON`. + +- Fixed the parsing of SDL 2.0.22 headers. Specifically `SDL_FLT_EPSILON`. ## [13.6.2] - 2022-05-02 + ### Fixed + - SDL renderers were ignoring tiles where only the background red channel was changed. ## [13.6.1] - 2022-03-29 + ### Changed + - The SDL2 renderer has had a major performance update when compiled with SDL 2.0.18. - SDL2 is now the default renderer to avoid rare issues with the OpenGL 2 renderer. ## [13.6.0] - 2022-02-19 + ### Added -- `BasicMixer` and `Channel` classes added to `tcod.sdl.audio`. These handle simple audio mixing. + +- `BasicMixer` and `Channel` classes added to `tcod.sdl.audio`. These handle simple audio mixing. - `AudioDevice.convert` added to handle simple conversions to the active devices format. - `tcod.sdl.audio.convert_audio` added to handle any other conversions needed. ## [13.5.0] - 2022-02-11 + ### Added -- `tcod.sdl.audio`, a new module exposing SDL audio devices. This does not include an audio mixer yet. + +- `tcod.sdl.audio`, a new module exposing SDL audio devices. This does not include an audio mixer yet. - `tcod.sdl.mouse`, for SDL mouse and cursor handing. - `Context.sdl_atlas`, which provides the relevant `SDLTilesetAtlas` when one is being used by the context. - Several missing features were added to `tcod.sdl.render`. - `Window.mouse_rect` added to SDL windows to set the mouse confinement area. + ### Changed + - `Texture.access` and `Texture.blend_mode` properties now return enum instances. You can still set `blend_mode` with `int` but Mypy will complain. ## [13.4.0] - 2022-02-04 + ### Added + - Adds `sdl_window` and `sdl_renderer` properties to tcod contexts. - Adds `tcod.event.add_watch` and `tcod.event.remove_watch` to handle SDL events via callback. - Adds the `tcod.sdl.video` module to handle SDL windows. - Adds the `tcod.sdl.render` module to handle SDL renderers. - Adds the `tcod.render` module which gives more control over the rendering of consoles and tilesets. + ### Fixed + - Fixed handling of non-Path PathLike parameters and filepath encodings. ## [13.3.0] - 2022-01-07 + ### Added + - New experimental renderer `tcod.context.RENDERER_XTERM`. + ### Changed + - Using `libtcod 1.20.1`. + ### Fixed + - Functions accepting `Path`-like parameters now accept the more correct `os.PathLike` type. - BDF files with blank lines no longer fail to load with an "Unknown keyword" error. ## [13.2.0] - 2021-12-24 + ### Added + - New `console` parameter in `tcod.context.new` which sets parameters from an existing Console. ### Changed + - Using `libtcod 1.20.0`. ### Fixed + - Fixed segfault when an OpenGL2 context fails to load. - Gaussian number generation no longer affects the results of unrelated RNG's. - Gaussian number generation is now reentrant and thread-safe. - Fixed potential crash in PNG image loading. ## [13.1.0] - 2021-10-22 + ### Added + - Added the `tcod.tileset.procedural_block_elements` function. ### Removed + - Python 3.6 is no longer supported. ## [13.0.0] - 2021-09-20 + ### Changed + - Console print and drawing functions now always use absolute coordinates for negative numbers. ## [12.7.3] - 2021-08-13 + ### Deprecated + - `tcod.console_is_key_pressed` was replaced with `tcod.event.get_keyboard_state`. - `tcod.console_from_file` is deprecated. - The `.asc` and `.apf` formats are no longer actively supported. ### Fixed + - Fixed the parsing of SDL 2.0.16 headers. ## [12.7.2] - 2021-07-01 + ### Fixed -- *Scancode* and *KeySym* enums no longer crash when SDL returns an unexpected value. + +- _Scancode_ and _KeySym_ enums no longer crash when SDL returns an unexpected value. ## [12.7.1] - 2021-06-30 + ### Added + - Started uploading wheels for ARM64 macOS. ## [12.7.0] - 2021-06-29 + ### Added -- *tcod.image* and *tcod.tileset* now support *pathlib*. + +- _tcod.image_ and _tcod.tileset_ now support _pathlib_. ### Fixed + - Wheels for 32-bit Windows now deploy again. ## [12.6.2] - 2021-06-15 + ### Fixed + - Git is no longer required to install from source. ## [12.6.1] - 2021-06-09 + ### Fixed + - Fixed version mismatch when building from sources. ## [12.6.0] - 2021-06-09 + ### Added -- Added the *decoration* parameter to *Console.draw_frame*. - You may use this parameter to designate custom glyphs as the frame border. + +- Added the _decoration_ parameter to _Console.draw_frame_. + You may use this parameter to designate custom glyphs as the frame border. ### Deprecated + - The handling of negative indexes given to console drawing and printing - functions will be changed to be used as absolute coordinates in the future. + functions will be changed to be used as absolute coordinates in the future. ## [12.5.1] - 2021-05-30 + ### Fixed + - The setup script should no longer fail silently when cffi is unavailable. ## [12.5.0] - 2021-05-21 + ### Changed + - `KeyboardEvent`'s '`scancode`, `sym`, and `mod` attributes now use their respective enums. ## [12.4.0] - 2021-05-21 + ### Added + - Added modernized REXPaint saving/loading functions. - - `tcod.console.load_xp` - - `tcod.console.save_xp` + - `tcod.console.load_xp` + - `tcod.console.save_xp` ### Changed + - Using `libtcod 1.18.1`. - `tcod.event.KeySym` and `tcod.event.Scancode` can now be hashed. ## [12.3.2] - 2021-05-15 + ### Changed + - Using `libtcod 1.17.1`. ### Fixed + - Fixed regression with loading PNG images. ## [12.3.1] - 2021-05-13 + ### Fixed + - Fix Windows deployment. ## [12.3.0] - 2021-05-13 + ### Added + - New keyboard enums: - - `tcod.event.KeySym` - - `tcod.event.Scancode` - - `tcod.event.Modifier` + - `tcod.event.KeySym` + - `tcod.event.Scancode` + - `tcod.event.Modifier` - New functions: - - `tcod.event.get_keyboard_state` - - `tcod.event.get_modifier_state` + - `tcod.event.get_keyboard_state` + - `tcod.event.get_modifier_state` - Added `tcod.console.rgb_graphic` and `tcod.console.rgba_graphic` dtypes. - Another name for the Console array attributes: `Console.rgb` and `Console.rgba`. ### Changed + - Using `libtcod 1.17.0`. ### Deprecated + - `Console_tiles_rgb` is being renamed to `Console.rgb`. - `Console_tiles` being renamed to `Console.rgba`. ### Fixed + - Contexts now give a more useful error when pickled. - Fixed regressions with `tcod.console_print_frame` and `Console.print_frame` - when given empty strings as the banner. + when given empty strings as the banner. ## [12.2.0] - 2021-04-09 + ### Added + - Added `tcod.noise.Algorithm` and `tcod.noise.Implementation` enums. - Added `tcod.noise.grid` helper function. ### Deprecated + - The non-enum noise implementation names have been deprecated. ### Fixed + - Indexing Noise classes now works with the FBM implementation. ## [12.1.0] - 2021-04-01 + ### Added + - Added package-level PyInstaller hook. ### Changed + - Using `libtcod 1.16.7`. - `tcod.path.dijkstra2d` now returns the output and accepts an `out` parameter. ### Deprecated -- In the future `tcod.path.dijkstra2d` will no longer modify the input by default. Until then an `out` parameter must be given. + +- In the future `tcod.path.dijkstra2d` will no longer modify the input by default. Until then an `out` parameter must be given. ### Fixed + - Fixed crashes from loading tilesets with non-square tile sizes. - Tilesets with a size of 0 should no longer crash when used. - Prevent division by zero from recommended-console-size functions. ## [12.0.0] - 2021-03-05 + ### Added + - Now includes PyInstaller hooks within the package itself. ### Deprecated + - The Random class will now warn if the seed it's given will not used - deterministically. It will no longer accept non-integer seeds in the future. + deterministically. It will no longer accept non-integer seeds in the future. ### Changed + - Now bundles SDL 2.0.14 for MacOS. - `tcod.event` can now detect and will warn about uninitialized tile - attributes on mouse events. + attributes on mouse events. ### Removed + - Python 3.5 is no longer supported. - The `tdl` module has been dropped. ## [11.19.3] - 2021-01-07 + ### Fixed + - Some wheels had broken version metadata. ## [11.19.2] - 2020-12-30 + ### Changed + - Now bundles SDL 2.0.10 for MacOS and SDL 2.0.14 for Windows. ### Fixed + - MacOS wheels were failing to bundle dependencies for SDL2. ## [11.19.1] - 2020-12-29 + ### Fixed + - MacOS wheels failed to deploy for the previous version. ## [11.19.0] - 2020-12-29 + ### Added + - Added the important `order` parameter to `Context.new_console`. ## [11.18.3] - 2020-12-28 + ### Changed + - Now bundles SDL 2.0.14 for Windows/MacOS. ### Deprecated + - Support for Python 3.5 will be dropped. - `tcod.console_load_xp` has been deprecated, `tcod.console_from_xp` can load - these files without modifying an existing console. + these files without modifying an existing console. ### Fixed + - `tcod.console_from_xp` now has better error handling (instead of crashing.) - Can now compile with SDL 2.0.14 headers. ## [11.18.2] - 2020-12-03 + ### Fixed + - Fixed missing `tcod.FOV_SYMMETRIC_SHADOWCAST` constant. -- Fixed regression in `tcod.sys_get_current_resolution` behavior. This - function now returns the monitor resolution as was previously expected. +- Fixed regression in `tcod.sys_get_current_resolution` behavior. This + function now returns the monitor resolution as was previously expected. ## [11.18.1] - 2020-11-30 + ### Fixed + - Code points from the Private Use Area will now print correctly. ## [11.18.0] - 2020-11-13 + ### Added + - New context method `Context.new_console`. ### Changed + - Using `libtcod 1.16.0-alpha.15`. ## [11.17.0] - 2020-10-30 + ### Added + - New FOV implementation: `tcod.FOV_SYMMETRIC_SHADOWCAST`. ### Changed + - Using `libtcod 1.16.0-alpha.14`. ## [11.16.1] - 2020-10-28 + ### Deprecated + - Changed context deprecations to PendingDeprecationWarning to reduce mass - panic from tutorial followers. + panic from tutorial followers. ### Fixed + - Fixed garbled titles and crashing on some platforms. ## [11.16.0] - 2020-10-23 + ### Added + - Added `tcod.context.new` function. - Contexts now support a CLI. - You can now provide the window x,y position when making contexts. - `tcod.noise.Noise` instances can now be indexed to generate noise maps. ### Changed + - Using `libtcod 1.16.0-alpha.13`. - The OpenGL 2 renderer can now use `SDL_HINT_RENDER_SCALE_QUALITY` to - determine the tileset upscaling filter. + determine the tileset upscaling filter. - Improved performance of the FOV_BASIC algorithm. ### Deprecated + - `tcod.context.new_window` and `tcod.context.new_terminal` have been replaced - by `tcod.context.new`. + by `tcod.context.new`. ### Fixed + - Pathfinders will now work with boolean arrays. - Console blits now ignore alpha compositing which would result in division by - zero. + zero. - `tcod.console_is_key_pressed` should work even if libtcod events are ignored. - The `TCOD_RENDERER` and `TCOD_VSYNC` environment variables should work now. - `FOV_PERMISSIVE` algorithm is now reentrant. ## [11.15.3] - 2020-07-30 + ### Fixed + - `tcod.tileset.Tileset.remap`, codepoint and index were swapped. ## [11.15.2] - 2020-07-27 + ### Fixed + - `tcod.path.dijkstra2d`, fixed corrupted output with int8 arrays. ## [11.15.1] - 2020-07-26 + ### Changed + - `tcod.event.EventDispatch` now uses the absolute names for event type hints - so that IDE's can better auto-complete method overrides. + so that IDE's can better auto-complete method overrides. ### Fixed + - Fixed libtcodpy heightmap data alignment issues on non-square maps. ## [11.15.0] - 2020-06-29 + ### Added + - `tcod.path.SimpleGraph` for pathfinding on simple 2D arrays. ### Changed + - `tcod.path.CustomGraph` now accepts an `order` parameter. ## [11.14.0] - 2020-06-23 + ### Added + - New `tcod.los` module for NumPy-based line-of-sight algorithms. - Includes `tcod.los.bresenham`. + Includes `tcod.los.bresenham`. ### Deprecated + - `tcod.line_where` and `tcod.line_iter` have been deprecated. ## [11.13.6] - 2020-06-19 + ### Deprecated + - `console_init_root` and `console_set_custom_font` have been replaced by the - modern API. + modern API. - All functions which handle SDL windows without a context are deprecated. - All functions which modify a globally active tileset are deprecated. - `tcod.map.Map` is deprecated, NumPy arrays should be passed to functions - directly instead of through this class. + directly instead of through this class. ## [11.13.5] - 2020-06-15 + ### Fixed + - Install requirements will no longer try to downgrade `cffi`. ## [11.13.4] - 2020-06-15 ## [11.13.3] - 2020-06-13 + ### Fixed + - `cffi` requirement has been updated to version `1.13.0`. - The older versions raise TypeError's. + The older versions raise TypeError's. ## [11.13.2] - 2020-06-12 + ### Fixed + - SDL related errors during package installation are now more readable. ## [11.13.1] - 2020-05-30 + ### Fixed + - `tcod.event.EventDispatch`: `ev_*` methods now allow `Optional[T]` return - types. + types. ## [11.13.0] - 2020-05-22 + ### Added + - `tcod.path`: New `Pathfinder` and `CustomGraph` classes. ### Changed + - Added `edge_map` parameter to `tcod.path.dijkstra2d` and - `tcod.path.hillclimb2d`. + `tcod.path.hillclimb2d`. ### Fixed + - tcod.console_init_root` and context initializing functions were not - raising exceptions on failure. + raising exceptions on failure. ## [11.12.1] - 2020-05-02 + ### Fixed + - Prevent adding non-existent 2nd halves to potential double-wide charterers. ## [11.12.0] - 2020-04-30 + ### Added -- Added `tcod.context` module. You now have more options for making libtcod - controlled contexts. + +- Added `tcod.context` module. You now have more options for making libtcod + controlled contexts. - `tcod.tileset.load_tilesheet`: Load a simple tilesheet as a Tileset. - `Tileset.remap`: Reassign codepoints to tiles on a Tileset. - `tcod.tileset.CHARMAP_CP437`: Character mapping for `load_tilesheet`. - `tcod.tileset.CHARMAP_TCOD`: Older libtcod layout. ### Changed + - `EventDispatch.dispatch` can now return the values returned by the `ev_*` - methods. The class is now generic to support type checking these values. + methods. The class is now generic to support type checking these values. - Event mouse coordinates are now strictly int types. - Submodules are now implicitly imported. ## [11.11.4] - 2020-04-26 + ### Changed + - Using `libtcod 1.16.0-alpha.10`. ### Fixed + - Fixed characters being dropped when color codes were used. ## [11.11.3] - 2020-04-24 + ### Changed + - Using `libtcod 1.16.0-alpha.9`. ### Fixed + - `FOV_DIAMOND` and `FOV_RESTRICTIVE` algorithms are now reentrant. - [libtcod#48](https://github.com/libtcod/libtcod/pull/48) + [libtcod#48](https://github.com/libtcod/libtcod/pull/48) - The `TCOD_VSYNC` environment variable was being ignored. ## [11.11.2] - 2020-04-22 ## [11.11.1] - 2020-04-03 + ### Changed + - Using `libtcod 1.16.0-alpha.8`. ### Fixed + - Changing the active tileset now redraws tiles correctly on the next frame. ## [11.11.0] - 2020-04-02 + ### Added + - Added `Console.close` as a more obvious way to close the active window of a - root console. + root console. ### Changed + - GCC is no longer needed to compile the library on Windows. - Using `libtcod 1.16.0-alpha.7`. - `tcod.console_flush` will now accept an RGB tuple as a `clear_color`. ### Fixed + - Changing the active tileset will now properly show it on the next render. ## [11.10.0] - 2020-03-26 + ### Added + - Added `tcod.tileset.load_bdf`, you can now load BDF fonts. - `tcod.tileset.set_default` and `tcod.tileset.get_default` are now stable. ### Changed + - Using `libtcod 1.16.0-alpha.6`. ### Deprecated + - The `snap_to_integer` parameter in `tcod.console_flush` has been deprecated - since it can cause minor scaling issues which don't exist when using - `integer_scaling` instead. + since it can cause minor scaling issues which don't exist when using + `integer_scaling` instead. ## [11.9.2] - 2020-03-17 + ### Fixed + - Fixed segfault after the Tileset returned by `tcod.tileset.get_default` goes - out of scope. + out of scope. ## [11.9.1] - 2020-02-28 + ### Changed + - Using `libtcod 1.16.0-alpha.5`. - Mouse tile coordinates are now always zero before the first call to - `tcod.console_flush`. + `tcod.console_flush`. ## [11.9.0] - 2020-02-22 + ### Added + - New method `Tileset.render` renders an RGBA NumPy array from a tileset and - a console. + a console. ## [11.8.2] - 2020-02-22 + ### Fixed + - Prevent KeyError when representing unusual keyboard symbol constants. ## [11.8.1] - 2020-02-22 + ### Changed + - Using `libtcod 1.16.0-alpha.4`. ### Fixed + - Mouse tile coordinates are now correct on any resized window. ## [11.8.0] - 2020-02-21 + ### Added + - Added `tcod.console.recommended_size` for when you want to change your main - console size at runtime. + console size at runtime. - Added `Console.tiles_rgb` as a replacement for `Console.tiles2`. ### Changed + - Using `libtcod 1.16.0-alpha.3`. - Added parameters to `tcod.console_flush`, you can now manually provide a - console and adjust how it is presented. + console and adjust how it is presented. ### Deprecated + - `Console.tiles2` is deprecated in favour of `Console.tiles_rgb`. - `Console.buffer` is now deprecated in favour of `Console.tiles`, instead of - the other way around. + the other way around. ### Fixed + - Fixed keyboard state and mouse state functions losing state when events were - flushed. + flushed. ## [11.7.2] - 2020-02-16 + ### Fixed + - Fixed regression in `tcod.console_clear`. ## [11.7.1] - 2020-02-16 + ### Fixed + - Fixed regression in `Console.draw_frame`. - The wavelet noise generator now excludes -1.0f and 1.0f as return values. - Fixed console fading color regression. ## [11.7.0] - 2020-02-14 + ### Changed + - Using `libtcod 1.16.0-alpha.2`. - When a renderer fails to load it will now fallback to a different one. - The order is: OPENGL2 -> OPENGL -> SDL2. + The order is: OPENGL2 -> OPENGL -> SDL2. - The default renderer is now SDL2. - The SDL and OPENGL renderers are no longer deprecated, but they now point to - slightly different backward compatible implementations. + slightly different backward compatible implementations. ### Deprecated + - The use of `libtcod.cfg` and `terminal.png` is deprecated. ### Fixed + - `tcod.sys_update_char` now works with the newer renderers. - Fixed buffer overflow in name generator. - `tcod.image_from_console` now works with the newer renderers. - New renderers now auto-load fonts from `libtcod.cfg` or `terminal.png`. ## [11.6.0] - 2019-12-05 + ### Changed + - Console blit operations now perform per-cell alpha transparency. ## [11.5.1] - 2019-11-23 + ### Fixed + - Python 3.8 wheels failed to deploy. ## [11.5.0] - 2019-11-22 + ### Changed + - Quarter block elements are now rendered using Unicode instead of a custom - encoding. + encoding. ### Fixed + - `OPENGL` and `GLSL` renderers were not properly clearing space characters. ## [11.4.1] - 2019-10-15 + ### Added + - Uploaded Python 3.8 wheels to PyPI. ## [11.4.0] - 2019-09-20 + ### Added + - Added `__array_interface__` to the Image class. - Added `Console.draw_semigraphics` as a replacement for blit_2x functions. - `draw_semigraphics` can handle array-like objects. + `draw_semigraphics` can handle array-like objects. - `Image.from_array` class method creates an Image from an array-like object. - `tcod.image.load` loads a PNG file as an RGBA array. ### Changed + - `Console.tiles` is now named `Console.buffer`. ## [11.3.0] - 2019-09-06 + ### Added + - New attribute `Console.tiles2` is similar to `Console.tiles` but without an - alpha channel. + alpha channel. ## [11.2.2] - 2019-08-25 + ### Fixed + - Fixed a regression preventing PyInstaller distributions from loading SDL2. ## [11.2.1] - 2019-08-25 ## [11.2.0] - 2019-08-24 + ### Added + - `tcod.path.dijkstra2d`: Computes Dijkstra from an arbitrary initial state. - `tcod.path.hillclimb2d`: Returns a path from a distance array. - `tcod.path.maxarray`: Creates arrays filled with maximum finite values. ### Fixed + - Changing the tiles of an active tileset on OPENGL2 will no longer leave - temporary artifact tiles. + temporary artifact tiles. - It's now harder to accidentally import tcod's internal modules. ## [11.1.2] - 2019-08-02 + ### Changed + - Now bundles SDL 2.0.10 for Windows/MacOS. ### Fixed + - Can now parse SDL 2.0.10 headers during installation without crashing. ## [11.1.1] - 2019-08-01 + ### Deprecated + - Using an out-of-bounds index for field-of-view operations now raises a - warning, which will later become an error. + warning, which will later become an error. ### Fixed + - Changing the tiles of an active tileset will now work correctly. ## [11.1.0] - 2019-07-05 + ### Added + - You can now set the `TCOD_RENDERER` and `TCOD_VSYNC` environment variables to - force specific options to be used. - Example: ``TCOD_RENDERER=sdl2 TCOD_VSYNC=1`` + force specific options to be used. + Example: `TCOD_RENDERER=sdl2 TCOD_VSYNC=1` ### Changed + - `tcod.sys_set_renderer` now raises an exception if it fails. ### Fixed + - `tcod.console_map_ascii_code_to_font` functions will now work when called - before `tcod.console_init_root`. + before `tcod.console_init_root`. ## [11.0.2] - 2019-06-21 + ### Changed + - You no longer need OpenGL to build python-tcod. ## [11.0.1] - 2019-06-21 + ### Changed + - Better runtime checks for Windows dependencies should now give distinct - errors depending on if the issue is SDL2 or missing redistributables. + errors depending on if the issue is SDL2 or missing redistributables. ### Fixed + - Changed NumPy type hints from `np.array` to `np.ndarray` which should - resolve issues. + resolve issues. ## [11.0.0] - 2019-06-14 + ### Changed + - `tcod.map.compute_fov` now takes a 2-item tuple instead of separate `x` and - `y` parameters. This causes less confusion over how axes are aligned. + `y` parameters. This causes less confusion over how axes are aligned. ## [10.1.1] - 2019-06-02 + ### Changed + - Better string representations for `tcod.event.Event` subclasses. ### Fixed + - Fixed regressions in text alignment for non-rectangle print functions. ## [10.1.0] - 2019-05-24 + ### Added + - `tcod.console_init_root` now has an optional `vsync` parameter. ## [10.0.5] - 2019-05-17 + ### Fixed + - Fixed shader compilation issues in the OPENGL2 renderer. - Fallback fonts should fail less on Linux. ## [10.0.4] - 2019-05-17 + ### Changed + - Now depends on cffi 0.12 or later. ### Fixed + - `tcod.console_init_root` and `tcod.console_set_custom_font` will raise - exceptions instead of terminating. + exceptions instead of terminating. - Fixed issues preventing `tcod.event` from working on 32-bit Windows. ## [10.0.3] - 2019-05-10 + ### Fixed + - Corrected bounding box issues with the `Console.print_box` method. ## [10.0.2] - 2019-04-26 + ### Fixed + - Resolved Color warnings when importing tcod. - When compiling, fixed a name conflict with endianness macros on FreeBSD. ## [10.0.1] - 2019-04-19 + ### Fixed + - Fixed horizontal alignment for TrueType fonts. - Fixed taking screenshots with the older SDL renderer. ## [10.0.0] - 2019-03-29 + ### Added + - New `Console.tiles` array attribute. + ### Changed + - `Console.DTYPE` changed to add alpha to its color types. + ### Fixed + - Console printing was ignoring color codes at the beginning of a string. ## [9.3.0] - 2019-03-15 + ### Added + - The SDL2/OPENGL2 renderers can potentially use a fall-back font when none - are provided. + are provided. - New function `tcod.event.get_mouse_state`. - New function `tcod.map.compute_fov` lets you get a visibility array directly - from a transparency array. + from a transparency array. + ### Deprecated + - The following functions and classes have been deprecated. - - `tcod.Key` - - `tcod.Mouse` - - `tcod.mouse_get_status` - - `tcod.console_is_window_closed` - - `tcod.console_check_for_keypress` - - `tcod.console_wait_for_keypress` - - `tcod.console_delete` - - `tcod.sys_check_for_event` - - `tcod.sys_wait_for_event` + - `tcod.Key` + - `tcod.Mouse` + - `tcod.mouse_get_status` + - `tcod.console_is_window_closed` + - `tcod.console_check_for_keypress` + - `tcod.console_wait_for_keypress` + - `tcod.console_delete` + - `tcod.sys_check_for_event` + - `tcod.sys_wait_for_event` - The SDL, OPENGL, and GLSL renderers have been deprecated. - Many libtcodpy functions have been marked with PendingDeprecationWarning's. + ### Fixed + - To be more compatible with libtcodpy `tcod.console_init_root` will default - to the SDL render, but will raise warnings when an old renderer is used. + to the SDL render, but will raise warnings when an old renderer is used. ## [9.2.5] - 2019-03-04 + ### Fixed + - Fixed `tcod.namegen_generate_custom`. ## [9.2.4] - 2019-03-02 + ### Fixed + - The `tcod` package is has been marked as typed and will now work with MyPy. ## [9.2.3] - 2019-03-01 + ### Deprecated + - The behavior for negative indexes on the new print functions may change in - the future. + the future. - Methods and functionality preventing `tcod.Color` from behaving like a tuple - have been deprecated. + have been deprecated. ## [9.2.2] - 2019-02-26 + ### Fixed + - `Console.print_box` wasn't setting the background color by default. ## [9.2.1] - 2019-02-25 + ### Fixed + - `tcod.sys_get_char_size` fixed on the new renderers. ## [9.2.0] - 2019-02-24 + ### Added + - New `tcod.console.get_height_rect` function, which can be used to get the - height of a print call without an existing console. + height of a print call without an existing console. - New `tcod.tileset` module, with a `set_truetype_font` function. + ### Fixed + - The new print methods now handle alignment according to how they were - documented. + documented. - `SDL2` and `OPENGL2` now support screenshots. - Windows and MacOS builds now restrict exported SDL2 symbols to only - SDL 2.0.5; This will avoid hard to debug import errors when the wrong - version of SDL is dynamically linked. + SDL 2.0.5; This will avoid hard to debug import errors when the wrong + version of SDL is dynamically linked. - The root console now starts with a white foreground. ## [9.1.0] - 2019-02-23 + ### Added + - Added the `tcod.random.MULTIPLY_WITH_CARRY` constant. + ### Changed + - The overhead for warnings has been reduced when running Python with the - optimize `-O` flag. + optimize `-O` flag. - `tcod.random.Random` now provides a default algorithm. ## [9.0.0] - 2019-02-17 + ### Changed + - New console methods now default to an `fg` and `bg` of None instead of - white-on-black. + white-on-black. ## [8.5.0] - 2019-02-15 + ### Added + - `tcod.console.Console` now supports `str` and `repr`. - Added new Console methods which are independent from the console defaults. - You can now give an array when initializing a `tcod.console.Console` - instance. + instance. - `Console.clear` can now take `ch`, `fg`, and `bg` parameters. + ### Changed + - Updated libtcod to 1.10.6 - Printing generates more compact layouts. + ### Deprecated + - Most libtcodpy console functions have been replaced by the tcod.console - module. -- Deprecated the `set_key_color` functions. You can pass key colors to - `Console.blit` instead. + module. +- Deprecated the `set_key_color` functions. You can pass key colors to + `Console.blit` instead. - `Console.clear` should be given the colors to clear with as parameters, - rather than by using `default_fg` or `default_bg`. + rather than by using `default_fg` or `default_bg`. - Most functions which depend on console default values have been deprecated. - The new deprecation warnings will give details on how to make default values - explicit. + The new deprecation warnings will give details on how to make default values + explicit. + ### Fixed + - `tcod.console.Console.blit` was ignoring the key color set by - `Console.set_key_color`. + `Console.set_key_color`. - The `SDL2` and `OPENGL2` renders can now large numbers of tiles. ## [8.4.3] - 2019-02-06 + ### Changed + - Updated libtcod to 1.10.5 - The SDL2/OPENGL2 renderers will now auto-detect a custom fonts key-color. ## [8.4.2] - 2019-02-05 + ### Deprecated + - The tdl module has been deprecated. - The libtcodpy parser functions have been deprecated. + ### Fixed + - `tcod.image_is_pixel_transparent` and `tcod.image_get_alpha` now return - values. + values. - `Console.print_frame` was clearing tiles outside if its bounds. - The `FONT_LAYOUT_CP437` layout was incorrect. ## [8.4.1] - 2019-02-01 + ### Fixed + - Window event types were not upper-case. - Fixed regression where libtcodpy mouse wheel events unset mouse coordinates. ## [8.4.0] - 2019-01-31 + ### Added + - Added tcod.event module, based off of the sdlevent.py shim. + ### Changed + - Updated libtcod to 1.10.3 + ### Fixed + - Fixed libtcodpy `struct_add_value_list` function. - Use correct math for tile-based delta in mouse events. - New renderers now support tile-based mouse coordinates. - SDL2 renderer will now properly refresh after the window is resized. ## [8.3.2] - 2018-12-28 + ### Fixed + - Fixed rare access violations for some functions which took strings as - parameters, such as `tcod.console_init_root`. + parameters, such as `tcod.console_init_root`. ## [8.3.1] - 2018-12-28 + ### Fixed + - libtcodpy key and mouse functions will no longer accept the wrong types. - The `new_struct` method was not being called for libtcodpy's custom parsers. ## [8.3.0] - 2018-12-08 + ### Added + - Added BSP traversal methods in tcod.bsp for parity with libtcodpy. + ### Deprecated + - Already deprecated bsp functions are now even more deprecated. ## [8.2.0] - 2018-11-27 + ### Added + - New layout `tcod.FONT_LAYOUT_CP437`. + ### Changed + - Updated libtcod to 1.10.2 - `tcod.console_print_frame` and `Console.print_frame` now support Unicode - strings. + strings. + ### Deprecated + - Deprecated using bytes strings for all printing functions. + ### Fixed + - Console objects are now initialized with spaces. This fixes some blit - operations. + operations. - Unicode code-points above U+FFFF will now work on all platforms. ## [8.1.1] - 2018-11-16 + ### Fixed + - Printing a frame with an empty string no longer displays a title bar. ## [8.1.0] - 2018-11-15 + ### Changed + - Heightmap functions now support 'F_CONTIGUOUS' arrays. - `tcod.heightmap_new` now has an `order` parameter. - Updated SDL to 2.0.9 + ### Deprecated + - Deprecated heightmap functions which sample noise grids, this can be done - using the `Noise.sample_ogrid` method. + using the `Noise.sample_ogrid` method. ## [8.0.0] - 2018-11-02 + ### Changed + - The default renderer can now be anything if not set manually. - Better error message for when a font file isn't found. ## [7.0.1] - 2018-10-27 + ### Fixed + - Building from source was failing because `console_2tris.glsl*` was missing - from source distributions. + from source distributions. ## [7.0.0] - 2018-10-25 + ### Added + - New `RENDERER_SDL2` and `RENDERER_OPENGL2` renderers. + ### Changed + - Updated libtcod to 1.9.0 - Now requires SDL 2.0.5, which is not trivially installable on - Ubuntu 16.04 LTS. + Ubuntu 16.04 LTS. + ### Removed + - Dropped support for Python versions before 3.5 - Dropped support for MacOS versions before 10.9 Mavericks. ## [6.0.7] - 2018-10-24 + ### Fixed + - The root console no longer loses track of buffers and console defaults on a - renderer change. + renderer change. ## [6.0.6] - 2018-10-01 + ### Fixed + - Replaced missing wheels for older and 32-bit versions of MacOS. ## [6.0.5] - 2018-09-28 + ### Fixed + - Resolved CDefError error during source installs. ## [6.0.4] - 2018-09-11 + ### Fixed + - tcod.Key right-hand modifiers are now set independently at initialization, - instead of mirroring the left-hand modifier value. + instead of mirroring the left-hand modifier value. ## [6.0.3] - 2018-09-05 + ### Fixed + - tcod.Key and tcod.Mouse no longer ignore initiation parameters. ## [6.0.2] - 2018-08-28 + ### Fixed + - Fixed color constants missing at build-time. ## [6.0.1] - 2018-08-24 + ### Fixed + - Source distributions were missing C++ source files. ## [6.0.0] - 2018-08-23 + ### Changed + - Project renamed to tcod on PyPI. + ### Deprecated + - Passing bytes strings to libtcodpy print functions is deprecated. + ### Fixed + - Fixed libtcodpy print functions not accepting bytes strings. - libtcod constants are now generated at build-time fixing static analysis - tools. + tools. ## [5.0.1] - 2018-07-08 + ### Fixed + - tdl.event no longer crashes with StopIteration on Python 3.7 ## [5.0.0] - 2018-07-05 + ### Changed + - tcod.path: all classes now use `shape` instead of `width` and `height`. - tcod.path now respects NumPy array shape, instead of assuming that arrays - need to be transposed from C memory order. From now on `x` and `y` mean - 1st and 2nd axis. This doesn't affect non-NumPy code. + need to be transposed from C memory order. From now on `x` and `y` mean + 1st and 2nd axis. This doesn't affect non-NumPy code. - tcod.path now has full support of non-contiguous memory. ## [4.6.1] - 2018-06-30 + ### Added + - New function `tcod.line_where` for indexing NumPy arrays using a Bresenham - line. + line. + ### Deprecated + - Python 2.7 support will be dropped in the near future. ## [4.5.2] - 2018-06-29 + ### Added + - New wheels for Python3.7 on Windows. + ### Fixed + - Arrays from `tcod.heightmap_new` are now properly zeroed out. ## [4.5.1] - 2018-06-23 + ### Deprecated + - Deprecated all libtcodpy map functions. + ### Fixed + - `tcod.map_copy` could break the `tcod.map.Map` class. - `tcod.map_clear` `transparent` and `walkable` parameters were reversed. - When multiple SDL2 headers were installed, the wrong ones would be used when - the library is built. + the library is built. - Fails to build via pip unless Numpy is installed first. ## [4.5.0] - 2018-06-12 + ### Changed + - Updated libtcod to v1.7.0 - Updated SDL to v2.0.8 - Error messages when failing to create an SDL window should be a less vague. - You no longer need to initialize libtcod before you can print to an - off-screen console. + off-screen console. + ### Fixed + - Avoid crashes if the root console has a character code higher than expected. + ### Removed + - No more debug output when loading fonts. ## [4.4.0] - 2018-05-02 + ### Added -- Added the libtcodpy module as an alias for tcod. Actual use of it is - deprecated, it exists primarily for backward compatibility. + +- Added the libtcodpy module as an alias for tcod. Actual use of it is + deprecated, it exists primarily for backward compatibility. - Adding missing libtcodpy functions `console_has_mouse_focus` and - `console_is_active`. + `console_is_active`. + ### Changed + - Updated libtcod to v1.6.6 ## [4.3.2] - 2018-03-18 + ### Deprecated + - Deprecated the use of falsy console parameters with libtcodpy functions. + ### Fixed + - Fixed libtcodpy image functions not supporting falsy console parameters. - Fixed tdl `Window.get_char` method. (Kaczor2704) ## [4.3.1] - 2018-03-07 + ### Fixed + - Fixed cffi.api.FFIError "unsupported expression: expected a simple numeric - constant" error when building on platforms with an older cffi module and - newer SDL headers. + constant" error when building on platforms with an older cffi module and + newer SDL headers. - tcod/tdl Map and Console objects were not saving stride data when pickled. ## [4.3.0] - 2018-02-01 + ### Added + - You can now set the numpy memory order on tcod.console.Console, - tcod.map.Map, and tdl.map.Map objects well as from the - tcod.console_init_root function. + tcod.map.Map, and tdl.map.Map objects well as from the + tcod.console_init_root function. + ### Changed + - The `console_init_root` `title` parameter is now optional. + ### Fixed + - OpenGL renderer alpha blending is now consistent with all other render - modes. + modes. ## [4.2.3] - 2018-01-06 + ### Fixed + - Fixed setup.py regression that could prevent building outside of the git - repository. + repository. ## [4.2.2] - 2018-01-06 + ### Fixed + - The Windows dynamic linker will now prefer the bundled version of SDL. - This fixes: - "ImportError: DLL load failed: The specified procedure could not be found." + This fixes: + "ImportError: DLL load failed: The specified procedure could not be found." - `key.c` is no longer set when `key.vk == KEY_TEXT`, this fixes a regression - which was causing events to be heard twice in the libtcod/Python tutorial. + which was causing events to be heard twice in the libtcod/Python tutorial. ## [4.2.0] - 2018-01-02 + ### Changed + - Updated libtcod backend to v1.6.4 - Updated SDL to v2.0.7 for Windows/MacOS. + ### Removed + - Source distributions no longer include tests, examples, or fonts. - [Find these on GitHub.](https://github.com/libtcod/python-tcod) + [Find these on GitHub.](https://github.com/libtcod/python-tcod) + ### Fixed + - Fixed "final link failed: Nonrepresentable section on output" error - when compiling for Linux. + when compiling for Linux. - `tcod.console_init_root` defaults to the SDL renderer, other renderers - cause issues with mouse movement events. + cause issues with mouse movement events. ## [4.1.1] - 2017-11-02 + ### Fixed + - Fixed `ConsoleBuffer.blit` regression. - Console defaults corrected, the root console's blend mode and alignment is - the default value for newly made Console's. + the default value for newly made Console's. - You can give a byte string as a filename to load parsers. ## [4.1.0] - 2017-07-19 + ### Added + - tdl Map class can now be pickled. + ### Changed + - Added protection to the `transparent`, `walkable`, and `fov` - attributes in tcod and tdl Map classes, to prevent them from being - accidentally overridden. + attributes in tcod and tdl Map classes, to prevent them from being + accidentally overridden. - tcod and tdl Map classes now use numpy arrays as their attributes. ## [4.0.1] - 2017-07-12 + ### Fixed + - tdl: Fixed NameError in `set_fps`. ## [4.0.0] - 2017-07-08 + ### Changed + - tcod.bsp: `BSP.split_recursive` parameter `random` is now `seed`. - tcod.console: `Console.blit` parameters have been rearranged. - Most of the parameters are now optional. + Most of the parameters are now optional. - tcod.noise: `Noise.__init__` parameter `rand` is now named `seed`. - tdl: Changed `set_fps` parameter name to `fps`. + ### Fixed + - tcod.bsp: Corrected spelling of max_vertical_ratio. ## [3.2.0] - 2017-07-04 + ### Changed + - Merged libtcod-cffi dependency with TDL. + ### Fixed + - Fixed boolean related crashes with Key 'text' events. -- tdl.noise: Fixed crash when given a negative seed. As well as cases - where an instance could lose its seed being pickled. +- tdl.noise: Fixed crash when given a negative seed. As well as cases + where an instance could lose its seed being pickled. ## [3.1.0] - 2017-05-28 + ### Added + - You can now pass tdl Console instances as parameters to libtcod-cffi - functions expecting a tcod Console. + functions expecting a tcod Console. + ### Changed + - Dependencies updated: `libtcod-cffi>=2.5.0,<3` - The `Console.tcod_console` attribute is being renamed to - `Console.console_c`. + `Console.console_c`. + ### Deprecated + - The tdl.noise and tdl.map modules will be deprecated in the future. + ### Fixed + - Resolved crash-on-exit issues for Windows platforms. ## [3.0.2] - 2017-04-13 + ### Changed + - Dependencies updated: `libtcod-cffi>=2.4.3,<3` - You can now create Console instances before a call to `tdl.init`. + ### Removed + - Dropped support for Python 3.3 + ### Fixed + - Resolved issues with MacOS builds. - 'OpenGL' and 'GLSL' renderers work again. ## [3.0.1] - 2017-03-22 + ### Changed + - `KeyEvent`'s with `text` now have all their modifier keys set to False. + ### Fixed + - Undefined behavior in text events caused crashes on 32-bit builds. ## [3.0.0] - 2017-03-21 + ### Added + - `KeyEvent` supports libtcod text and meta keys. + ### Changed + - `KeyEvent` parameters have been moved. - This version requires `libtcod-cffi>=2.3.0`. + ### Deprecated + - `KeyEvent` camel capped attribute names are deprecated. + ### Fixed + - Crashes with key-codes undefined by libtcod. - `tdl.map` typedef issues with libtcod-cffi. - ## [2.0.1] - 2017-02-22 + ### Fixed + - `tdl.init` renderer was defaulted to OpenGL which is not supported in the - current version of libtcod. + current version of libtcod. ## [2.0.0] - 2017-02-15 + ### Changed + - Dependencies updated, tdl now requires libtcod-cffi 2.x.x - Some event behaviors have changed with SDL2, event keys might be different - than what you expect. + than what you expect. + ### Removed + - Key repeat functions were removed from SDL2. - `set_key_repeat` is now stubbed, and does nothing. + `set_key_repeat` is now stubbed, and does nothing. ## [1.6.0] - 2016-11-18 + - Console.blit methods can now take fg_alpha and bg_alpha parameters. ## [1.5.3] - 2016-06-04 + - set_font no longer crashes when loading a file without the implied font size in its name ## [1.5.2] - 2016-03-11 + - Fixed non-square Map instances ## [1.5.1] - 2015-12-20 + - Fixed errors with Unicode and non-Unicode literals on Python 2 - Fixed attribute error in compute_fov ## [1.5.0] - 2015-07-13 + - python-tdl distributions are now universal builds - New Map class - map.bresenham now returns a list - This release will require libtcod-cffi v0.2.3 or later ## [1.4.0] - 2015-06-22 + - The DLL's have been moved into another library which you can find at https://github.com/HexDecimal/libtcod-cffi You can use this library to have some raw access to libtcod if you want. Plus it can be used alongside TDL. - The libtcod console objects in Console instances have been made public. -- Added tdl.event.wait function. This function can called with a timeout and +- Added tdl.event.wait function. This function can called with a timeout and can automatically call tdl.flush. ## [1.3.1] - 2015-06-19 + - Fixed pathfinding regressions. ## [1.3.0] - 2015-06-19 -- Updated backend to use python-cffi instead of ctypes. This gives decent + +- Updated backend to use python-cffi instead of ctypes. This gives decent boost to speed in CPython and a drastic to boost in speed in PyPy. ## [1.2.0] - 2015-06-06 -- The set_colors method now changes the default colors used by the draw_* - methods. You can use Python's Ellipsis to explicitly select default colors + +- The set*colors method now changes the default colors used by the draw*\* + methods. You can use Python's Ellipsis to explicitly select default colors this way. - Functions and Methods renamed to match Python's style-guide PEP 8, the old function names still exist and are depreciated. - The fgcolor and bgcolor parameters have been shortened to fg and bg. ## [1.1.7] - 2015-03-19 + - Noise generator now seeds properly. - The OS event queue will now be handled during a call to tdl.flush. This prevents a common newbie programmer hang where events are handled @@ -1296,11 +1792,13 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - Fixed a major bug that would cause a crash in later versions of Python 3 ## [1.1.6] - 2014-06-27 + - Fixed a race condition when importing on some platforms. - Fixed a type issue with quickFOV on Linux. - Added a bresenham function to the tdl.map module. ## [1.1.5] - 2013-11-10 + - A for loop can iterate over all coordinates of a Console. - drawStr can be configured to scroll or raise an error. - You can now configure or disable key repeating with tdl.event.setKeyRepeat @@ -1308,6 +1806,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - setColors method fixed. ## [1.1.4] - 2013-03-06 + - Merged the Typewriter and MetaConsole classes, You now have a virtual cursor with Console and Window objects. - Fixed the clear method on the Window class. @@ -1318,11 +1817,13 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - Fixed event.keyWait, and now converts window closed events into Alt+F4. ## [1.1.3] - 2012-12-17 + - Some of the setFont parameters were incorrectly labeled and documented. - setFont can auto-detect tilesets if the font sizes are in the filenames. - Added some X11 unicode tilesets, including Unifont. ## [1.1.2] - 2012-12-13 + - Window title now defaults to the running scripts filename. - Fixed incorrect deltaTime for App.update - App will no longer call tdl.flush on its own, you'll need to call this @@ -1331,6 +1832,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - clear method now defaults to black on black. ## [1.1.1] - 2012-12-05 + - Map submodule added with AStar class and quickFOV function. - New Typewriter class. - Most console functions can use Python-style negative indexes now. @@ -1338,6 +1840,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - Rectangle geometry is less strict. ## [1.1.0] - 2012-10-04 + - KeyEvent.keyname is now KeyEvent.key - MouseButtonEvent.button now behaves like KeyEvent.keyname does. - event.App class added. @@ -1345,20 +1848,24 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - KeyEvent.ctrl is now KeyEvent.control ## [1.0.8] - 2010-04-07 + - No longer works in Python 2.5 but now works in 3.x and has been partly tested. - Many bug fixes. ## [1.0.5] - 2010-04-06 + - Got rid of setuptools dependency, this will make it much more compatible with Python 3.x - Fixed a typo with the MacOS library import. ## [1.0.4] - 2010-04-06 -- All constant colors (C_*) have been removed, they may be put back in later. + +- All constant colors (C\_\*) have been removed, they may be put back in later. - Made some type assertion failures show the value they received to help in - general debugging. Still working on it. + general debugging. Still working on it. - Added MacOS and 64-bit Linux support. ## [1.0.0] - 2009-01-31 + - First public release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4f7d5ea..846d492c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ pre-commit install ## Building python-tcod To work with the tcod source, your environment must be set up to build -Python C extensions. You'll also need `cpp` installed for +Python C extensions. You'll also need `cpp` installed for use with pycparser. ### Windows diff --git a/examples/README.md b/examples/README.md index 0594efe1..8f13fa79 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ This directory contains a few example scripts for using python-tcod. -`samples_tcod.py` is the mail example which uses most of the newer API. This +`samples_tcod.py` is the mail example which uses most of the newer API. This can be compared to `samples_libtcodpy.py` which mostly uses deprecated functions from the old API. @@ -8,5 +8,5 @@ Examples in the `distribution/` folder show how to distribute projects made using python-tcod. Examples in the `experimental/` folder show off features that might later be -added the python-tcod API. You can use those features by copying those modules +added the python-tcod API. You can use those features by copying those modules into your own project. diff --git a/examples/termbox/README.md b/examples/termbox/README.md index 3ec77f56..36f13c20 100644 --- a/examples/termbox/README.md +++ b/examples/termbox/README.md @@ -4,7 +4,6 @@ The code here are modified files from [termbox repository](https://github.com/nsf/termbox/), so please consult it for the license and other info. - The code consists of two part - `termbox.py` module with API, translation of official binding form the description below into `tld`: @@ -14,7 +13,6 @@ And the example `termboxtest.py` which is copied verbatim from: https://github.com/nsf/termbox/blob/b20c0a11/test_termboxmodule.py - ### API Mapping Notes Notes taken while mapping the Termbox class: @@ -41,19 +39,19 @@ Notes taken while mapping the Termbox class: - init... - tdl doesn't allow to resize window (or rather libtcod) - tb works in existing terminal window and queries it rather than making own + init... + tdl doesn't allow to resize window (or rather libtcod) + tb works in existing terminal window and queries it rather than making own - colors... - tdl uses RGB values - tb uses it own constants + colors... + tdl uses RGB values + tb uses it own constants - event... - tb returns event one by one - tdl return an event iterator + event... + tb returns event one by one + tdl return an event iterator - tb Event tdl Event - .type .type - EVENT_KEY KEYDOWN + tb Event tdl Event + .type .type + EVENT_KEY KEYDOWN From 1c8064cc70b5eb73fe93fdf8e753f5800c88866c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 21:15:24 -0800 Subject: [PATCH 046/134] Remove outdated version block --- tcod/sdl/_internal.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tcod/sdl/_internal.py b/tcod/sdl/_internal.py index 28b47460..58b7b030 100644 --- a/tcod/sdl/_internal.py +++ b/tcod/sdl/_internal.py @@ -57,8 +57,6 @@ def __exit__( ) -> bool: if exc_type is None: return False - if _sys.version_info < (3, 8): - return False _sys.unraisablehook(_UnraisableHookArgs(exc_type, value, traceback, None, self.obj)) return True From 70ec2cc03fe8ec70f7bf14b9eff4c2acefda01ff Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 21:18:43 -0800 Subject: [PATCH 047/134] Add Python 3.12 to supported versions Fix string type in pyproject.toml --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 94f1ddc2..b4c0490a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Games/Entertainment", @@ -106,7 +107,7 @@ filterwarnings = [ [tool.mypy] files = ["."] -python_version = 3.9 +python_version = "3.9" warn_unused_configs = true show_error_codes = true disallow_subclassing_any = true From 392b0ce5f5eaf0dfad47d272b5202d95d8d4c44c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 22:20:16 -0800 Subject: [PATCH 048/134] Refactoring pass, fix or suppress warnings Fix critical issues with samples_tcod.py --- .vscode/settings.json | 3 +++ examples/samples_tcod.py | 50 ++++++++++++++++++++-------------------- tcod/_internal.py | 20 ++++++++-------- tcod/bsp.py | 4 ++-- tcod/console.py | 32 ++++++++++++------------- tcod/context.py | 22 +++++++++--------- tcod/image.py | 10 ++++---- tcod/libtcodpy.py | 37 ++++++++++++++--------------- tcod/map.py | 6 ++--- tcod/noise.py | 10 ++++---- tcod/path.py | 45 ++++++++++++++++++------------------ tcod/random.py | 6 ++--- tcod/render.py | 2 +- tcod/sdl/_internal.py | 6 ++--- tcod/sdl/audio.py | 8 +++---- tcod/sdl/joystick.py | 4 ++-- tcod/sdl/mouse.py | 4 ++-- tcod/sdl/render.py | 42 ++++++++++++++++----------------- tcod/sdl/sys.py | 3 +-- tcod/sdl/video.py | 10 ++++---- tcod/tileset.py | 6 ++--- tests/test_console.py | 2 +- tests/test_libtcodpy.py | 4 ++-- tests/test_tcod.py | 4 ++-- 24 files changed, 170 insertions(+), 170 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 67f05c37..1ea25a26 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,6 +33,7 @@ "ALTERASE", "arange", "ARCHS", + "arctan", "asarray", "ascontiguousarray", "astar", @@ -190,6 +191,7 @@ "intersphinx", "isinstance", "isort", + "issubdtype", "itemsize", "itleref", "ivar", @@ -338,6 +340,7 @@ "pypiwin", "pypy", "pytest", + "PYTHONHASHSEED", "PYTHONOPTIMIZE", "Pyup", "quickstart", diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 7f65a817..197a1bc9 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -118,7 +118,7 @@ def on_draw(self) -> None: self.slide_corner_colors() self.interpolate_corner_colors() self.darken_background_characters() - self.randomize_sample_conole() + self.randomize_sample_console() self.print_banner() def slide_corner_colors(self) -> None: @@ -143,7 +143,7 @@ def darken_background_characters(self) -> None: sample_console.fg[:] = sample_console.bg[:] sample_console.fg[:] //= 2 - def randomize_sample_conole(self) -> None: + def randomize_sample_console(self) -> None: # randomize sample console characters sample_console.ch[:] = np.random.randint( low=ord("a"), @@ -406,7 +406,7 @@ def on_draw(self) -> None: rect_h = 13 if self.implementation == tcod.noise.Implementation.SIMPLE: rect_h = 10 - sample_console.draw_semigraphics(self.img) + sample_console.draw_semigraphics(np.asarray(self.img)) sample_console.draw_rect( 2, 2, @@ -669,7 +669,7 @@ def __init__(self) -> None: self.using_astar = True self.recalculate = False self.busy = 0.0 - self.oldchar = " " + self.old_char = " " self.map = tcod.map.Map(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) for y in range(SAMPLE_SCREEN_HEIGHT): @@ -778,33 +778,33 @@ def on_draw(self) -> None: def ev_keydown(self, event: tcod.event.KeyDown) -> None: if event.sym == tcod.event.KeySym.i and self.dy > 0: # destination move north - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.oldchar, libtcodpy.BKGND_NONE) + libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) self.dy -= 1 - self.oldchar = sample_console.ch[self.dx, self.dy] + self.old_char = sample_console.ch[self.dx, self.dy] libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) if SAMPLE_MAP[self.dx, self.dy] == " ": self.recalculate = True elif event.sym == tcod.event.KeySym.k and self.dy < SAMPLE_SCREEN_HEIGHT - 1: # destination move south - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.oldchar, libtcodpy.BKGND_NONE) + libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) self.dy += 1 - self.oldchar = sample_console.ch[self.dx, self.dy] + self.old_char = sample_console.ch[self.dx, self.dy] libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) if SAMPLE_MAP[self.dx, self.dy] == " ": self.recalculate = True elif event.sym == tcod.event.KeySym.j and self.dx > 0: # destination move west - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.oldchar, libtcodpy.BKGND_NONE) + libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) self.dx -= 1 - self.oldchar = sample_console.ch[self.dx, self.dy] + self.old_char = sample_console.ch[self.dx, self.dy] libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) if SAMPLE_MAP[self.dx, self.dy] == " ": self.recalculate = True elif event.sym == tcod.event.KeySym.l and self.dx < SAMPLE_SCREEN_WIDTH - 1: # destination move east - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.oldchar, libtcodpy.BKGND_NONE) + libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) self.dx += 1 - self.oldchar = sample_console.ch[self.dx, self.dy] + self.old_char = sample_console.ch[self.dx, self.dy] libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) if SAMPLE_MAP[self.dx, self.dy] == " ": self.recalculate = True @@ -822,10 +822,10 @@ def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None: mx = event.tile.x - SAMPLE_SCREEN_X my = event.tile.y - SAMPLE_SCREEN_Y if 0 <= mx < SAMPLE_SCREEN_WIDTH and 0 <= my < SAMPLE_SCREEN_HEIGHT and (self.dx != mx or self.dy != my): - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.oldchar, libtcodpy.BKGND_NONE) + libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) self.dx = mx self.dy = my - self.oldchar = sample_console.ch[self.dx, self.dy] + self.old_char = sample_console.ch[self.dx, self.dy] libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) if SAMPLE_MAP[self.dx, self.dy] == " ": self.recalculate = True @@ -1139,7 +1139,7 @@ class NameGeneratorSample(Sample): def __init__(self) -> None: self.name = "Name generator" - self.curset = 0 + self.current_set = 0 self.delay = 0.0 self.names: list[str] = [] self.sets: list[str] = [] @@ -1159,7 +1159,7 @@ def on_draw(self) -> None: sample_console.print( 1, 1, - "%s\n\n+ : next generator\n- : prev generator" % self.sets[self.curset], + "%s\n\n+ : next generator\n- : prev generator" % self.sets[self.current_set], fg=WHITE, bg=None, ) @@ -1175,18 +1175,18 @@ def on_draw(self) -> None: self.delay += frame_length[-1] if self.delay > 0.5: self.delay -= 0.5 - self.names.append(libtcodpy.namegen_generate(self.sets[self.curset])) + self.names.append(libtcodpy.namegen_generate(self.sets[self.current_set])) def ev_keydown(self, event: tcod.event.KeyDown) -> None: if event.sym == tcod.event.KeySym.EQUALS: - self.curset += 1 + self.current_set += 1 self.names.append("======") elif event.sym == tcod.event.KeySym.MINUS: - self.curset -= 1 + self.current_set -= 1 self.names.append("======") else: super().ev_keydown(event) - self.curset %= len(self.sets) + self.current_set %= len(self.sets) ############################################# @@ -1294,9 +1294,9 @@ def on_draw(self) -> None: for v in range(RES_V - int_t, RES_V): for u in range(RES_U): tex_v = (v + int_abs_t) / float(RES_V) - texture[u, v] = tcod.noise_get_fbm(noise2d, [u / float(RES_U), tex_v], 32.0) + tcod.noise_get_fbm( - noise2d, [1 - u / float(RES_U), tex_v], 32.0 - ) + texture[u, v] = libtcodpy.noise_get_fbm( + noise2d, [u / float(RES_U), tex_v], 32.0 + ) + libtcodpy.noise_get_fbm(noise2d, [1 - u / float(RES_U), tex_v], 32.0) # squared distance from center, # clipped to sensible minimum and maximum values @@ -1324,9 +1324,9 @@ def on_draw(self) -> None: y = random.uniform(-0.5, 0.5) strength = random.uniform(MIN_LIGHT_STRENGTH, 1.0) - color = tcod.Color(0, 0, 0) # create bright colors with random hue + color = libtcodpy.Color(0, 0, 0) # create bright colors with random hue hue = random.uniform(0, 360) - tcod.color_set_hsv(color, hue, 0.5, strength) + libtcodpy.color_set_hsv(color, hue, 0.5, strength) self.lights.append(Light(x, y, TEX_STRETCH, color.r, color.g, color.b, strength)) # eliminate lights that are going to be out of view diff --git a/tcod/_internal.py b/tcod/_internal.py index 1bf5e211..ef6305bf 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -7,7 +7,7 @@ import warnings from pathlib import Path from types import TracebackType -from typing import Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar, cast +from typing import TYPE_CHECKING, Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar, cast import numpy as np from numpy.typing import ArrayLike, NDArray @@ -15,6 +15,9 @@ from tcod.cffi import ffi, lib +if TYPE_CHECKING: + import tcod.image + FuncType = Callable[..., Any] F = TypeVar("F", bound=FuncType) T = TypeVar("T") @@ -198,18 +201,17 @@ def _get_cdata_from_args(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 def __hash__(self) -> int: return hash(self.cdata) - def __eq__(self, other: Any) -> Any: - try: - return self.cdata == other.cdata - except AttributeError: + def __eq__(self, other: object) -> bool: + if not isinstance(other, _CDataWrapper): return NotImplemented + return bool(self.cdata == other.cdata) - def __getattr__(self, attr: str) -> Any: + def __getattr__(self, attr: str) -> Any: # noqa: ANN401 if "cdata" in self.__dict__: return getattr(self.__dict__["cdata"], attr) raise AttributeError(attr) - def __setattr__(self, attr: str, value: Any) -> None: + def __setattr__(self, attr: str, value: Any) -> None: # noqa: ANN401 if hasattr(self, "cdata") and hasattr(self.cdata, attr): setattr(self.cdata, attr, value) else: @@ -240,7 +242,7 @@ def __init__(self, array: ArrayLike) -> None: """Initialize an image from the given array. May copy or reference the array.""" self._array: NDArray[np.uint8] = np.ascontiguousarray(array, dtype=np.uint8) height, width, depth = self._array.shape - if depth != 3: + if depth != 3: # noqa: PLR2004 msg = f"Array must have RGB channels. Shape is: {self._array.shape!r}" raise TypeError(msg) self._buffer = ffi.from_buffer("TCOD_color_t[]", self._array) @@ -265,7 +267,7 @@ def __init__(self, array: ArrayLike) -> None: ) -def _as_image(image: Any) -> TempImage: +def _as_image(image: ArrayLike | tcod.image.Image) -> TempImage | tcod.image.Image: """Convert this input into an Image-like object.""" if hasattr(image, "image_c"): return image # type: ignore diff --git a/tcod/bsp.py b/tcod/bsp.py index a53b8f67..cfb6c021 100644 --- a/tcod/bsp.py +++ b/tcod/bsp.py @@ -88,7 +88,7 @@ def h(self) -> int: # noqa: D102 def h(self, value: int) -> None: self.height = value - def _as_cdata(self) -> Any: + def _as_cdata(self) -> Any: # noqa: ANN401 cdata = ffi.gc( lib.TCOD_bsp_new_with_size(self.x, self.y, self.width, self.height), lib.TCOD_bsp_delete, @@ -115,7 +115,7 @@ def __repr__(self) -> str: status, ) - def _unpack_bsp_tree(self, cdata: Any) -> None: + def _unpack_bsp_tree(self, cdata: Any) -> None: # noqa: ANN401 self.x = cdata.x self.y = cdata.y self.width = cdata.w diff --git a/tcod/console.py b/tcod/console.py index af0b3324..3512c893 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -12,7 +12,7 @@ from typing import Any, Iterable import numpy as np -from numpy.typing import NDArray +from numpy.typing import ArrayLike, NDArray from typing_extensions import Literal import tcod._internal @@ -168,7 +168,7 @@ def _get_root(cls, order: Literal["C", "F"] | None = None) -> Console: This function will also update an already active root console. """ - global _root_console + global _root_console # noqa: PLW0603 if _root_console is None: _root_console = object.__new__(cls) self: Console = _root_console @@ -180,7 +180,7 @@ def _get_root(cls, order: Literal["C", "F"] | None = None) -> Console: def _init_setup_console_data(self, order: Literal["C", "F"] = "C") -> None: """Setup numpy arrays over libtcod data buffers.""" - global _root_console + global _root_console # noqa: PLW0603 self._key_color = None if self.console_c == ffi.NULL: _root_console = self @@ -517,7 +517,7 @@ def __deprecate_defaults( # noqa: C901, PLR0912 stacklevel=3, ) - def print_( + def print_( # noqa: PLR0913 self, x: int, y: int, @@ -544,7 +544,7 @@ def print_( alignment = self.default_alignment if alignment is None else alignment lib.TCOD_console_printf_ex(self.console_c, x, y, bg_blend, alignment, _fmt(string)) - def print_rect( + def print_rect( # noqa: PLR0913 self, x: int, y: int, @@ -594,7 +594,7 @@ def print_rect( ) ) - def get_height_rect(self, x: int, y: int, width: int, height: int, string: str) -> int: + def get_height_rect(self, x: int, y: int, width: int, height: int, string: str) -> int: # noqa: PLR0913 """Return the height of this text word-wrapped into this rectangle. Args: @@ -610,7 +610,7 @@ def get_height_rect(self, x: int, y: int, width: int, height: int, string: str) string_ = string.encode("utf-8") return int(lib.TCOD_console_get_height_rect_n(self.console_c, x, y, width, height, len(string_), string_)) - def rect( + def rect( # noqa: PLR0913 self, x: int, y: int, @@ -696,7 +696,7 @@ def vline( self.__deprecate_defaults("draw_rect", bg_blend) lib.TCOD_console_vline(self.console_c, x, y, height, bg_blend) - def print_frame( + def print_frame( # noqa: PLR0913 self, x: int, y: int, @@ -738,7 +738,7 @@ def print_frame( string = _fmt(string) if string else ffi.NULL _check(lib.TCOD_console_printf_frame(self.console_c, x, y, width, height, clear, bg_blend, string)) - def blit( + def blit( # noqa: PLR0913 self, dest: Console, dest_x: int = 0, @@ -871,7 +871,7 @@ def close(self) -> None: raise NotImplementedError(msg) lib.TCOD_console_delete(self.console_c) - def __exit__(self, *args: Any) -> None: + def __exit__(self, *_: object) -> None: """Close the graphical window on exit. Some tcod functions may have undefined behavior after this point. @@ -931,7 +931,7 @@ def __str__(self) -> str: """Return a simplified representation of this consoles contents.""" return "<%s>" % "\n ".join("".join(chr(c) for c in line) for line in self._tiles["ch"]) - def print( + def print( # noqa: PLR0913 self, x: int, y: int, @@ -980,7 +980,7 @@ def print( alignment, ) - def print_box( + def print_box( # noqa: PLR0913 self, x: int, y: int, @@ -1039,7 +1039,7 @@ def print_box( ) ) - def draw_frame( + def draw_frame( # noqa: PLR0913 self, x: int, y: int, @@ -1138,7 +1138,7 @@ def draw_frame( ) return decoration_ = [ord(c) for c in decoration] if isinstance(decoration, str) else decoration - if len(decoration_) != 9: + if len(decoration_) != 9: # noqa: PLR2004 msg = f"Decoration must have a length of 9 (len(decoration) is {len(decoration_)}.)" raise TypeError(msg) _check( @@ -1156,7 +1156,7 @@ def draw_frame( ) ) - def draw_rect( + def draw_rect( # noqa: PLR0913 self, x: int, y: int, @@ -1204,7 +1204,7 @@ def draw_rect( bg_blend, ) - def draw_semigraphics(self, pixels: Any, x: int = 0, y: int = 0) -> None: + def draw_semigraphics(self, pixels: ArrayLike | tcod.image.Image, x: int = 0, y: int = 0) -> None: """Draw a block of 2x2 semi-graphics into this console. `pixels` is an Image or an array-like object. It will be down-sampled diff --git a/tcod/context.py b/tcod/context.py index 4b2d33e3..8d5e5259 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -124,12 +124,12 @@ """ -def _handle_tileset(tileset: tcod.tileset.Tileset | None) -> Any: +def _handle_tileset(tileset: tcod.tileset.Tileset | None) -> Any: # noqa: ANN401 """Get the TCOD_Tileset pointer from a Tileset or return a NULL pointer.""" return tileset._tileset_p if tileset else ffi.NULL -def _handle_title(title: str | None) -> Any: +def _handle_title(title: str | None) -> Any: # noqa: ANN401 """Return title as a CFFI string. If title is None then return a decent default title is returned. @@ -145,12 +145,12 @@ class Context: Use :any:`tcod.context.new` to create a new context. """ - def __init__(self, context_p: Any) -> None: + def __init__(self, context_p: Any) -> None: # noqa: ANN401 """Create a context from a cffi pointer.""" self._context_p = context_p @classmethod - def _claim(cls, context_p: Any) -> Context: + def _claim(cls, context_p: Any) -> Context: # noqa: ANN401 """Return a new instance wrapping a context pointer.""" return cls(ffi.gc(context_p, lib.TCOD_context_delete)) @@ -176,11 +176,11 @@ def close(self) -> None: ffi.release(self._context_p) del self._context_p - def __exit__(self, *args: Any) -> None: + def __exit__(self, *_: object) -> None: """Automatically close on the context on exit.""" self.close() - def present( + def present( # noqa: PLR0913 self, console: tcod.console.Console, *, @@ -366,7 +366,7 @@ def renderer_type(self) -> int: return _check(lib.TCOD_context_get_renderer_type(self._p)) @property - def sdl_window_p(self) -> Any: + def sdl_window_p(self) -> Any: # noqa: ANN401 '''A cffi `SDL_Window*` pointer. This pointer might be NULL. This pointer will become invalid if the context is closed or goes out @@ -446,7 +446,7 @@ def __reduce__(self) -> NoReturn: @ffi.def_extern() # type: ignore -def _pycall_cli_output(catch_reference: Any, output: Any) -> None: +def _pycall_cli_output(catch_reference: Any, output: Any) -> None: # noqa: ANN401 """Callback for the libtcod context CLI. Catches the CLI output. @@ -455,7 +455,7 @@ def _pycall_cli_output(catch_reference: Any, output: Any) -> None: catch.append(ffi.string(output).decode("utf-8")) -def new( +def new( # noqa: PLR0913 *, x: int | None = None, y: int | None = None, @@ -573,7 +573,7 @@ def new( @pending_deprecate("Call tcod.context.new with width and height as keyword parameters.") -def new_window( +def new_window( # noqa: PLR0913 width: int, height: int, *, @@ -600,7 +600,7 @@ def new_window( @pending_deprecate("Call tcod.context.new with columns and rows as keyword parameters.") -def new_terminal( +def new_terminal( # noqa: PLR0913 columns: int, rows: int, *, diff --git a/tcod/image.py b/tcod/image.py index 826ea33e..9480c08d 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -40,7 +40,7 @@ def __init__(self, width: int, height: int) -> None: self.image_c = ffi.gc(lib.TCOD_image_new(width, height), lib.TCOD_image_delete) @classmethod - def _from_cdata(cls, cdata: Any) -> Image: + def _from_cdata(cls, cdata: Any) -> Image: # noqa: ANN401 self: Image = object.__new__(cls) self.image_c = cdata self.width, self.height = self._get_size() @@ -207,7 +207,7 @@ def put_pixel(self, x: int, y: int, color: tuple[int, int, int]) -> None: """ lib.TCOD_image_put_pixel(self.image_c, x, y, color) - def blit( + def blit( # noqa: PLR0913 self, console: tcod.console.Console, x: float, @@ -242,7 +242,7 @@ def blit( angle, ) - def blit_rect( + def blit_rect( # noqa: PLR0913 self, console: tcod.console.Console, x: int, @@ -263,7 +263,7 @@ def blit_rect( """ lib.TCOD_image_blit_rect(self.image_c, _console(console), x, y, width, height, bg_blend) - def blit_2x( + def blit_2x( # noqa: PLR0913 self, console: tcod.console.Console, dest_x: int, @@ -367,7 +367,7 @@ def load(filename: str | PathLike[str]) -> NDArray[np.uint8]: image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(_path_encode(Path(filename))), lib.TCOD_image_delete)) array: NDArray[np.uint8] = np.asarray(image, dtype=np.uint8) height, width, depth = array.shape - if depth == 3: + if depth == 3: # noqa: PLR2004 array = np.concatenate( ( array, diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index e6649497..d6c7e730 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -1226,7 +1226,7 @@ def console_flush( DeprecationWarning, stacklevel=2, ) - if len(clear_color) == 3: + if len(clear_color) == 3: # noqa: PLR2004 clear_color = clear_color[0], clear_color[1], clear_color[2], 255 options = { "keep_aspect": keep_aspect, @@ -2393,11 +2393,10 @@ def heightmap_new(w: int, h: int, order: str = "C") -> NDArray[np.float32]: """ if order == "C": return np.zeros((h, w), np.float32, order="C") - elif order == "F": + if order == "F": return np.zeros((w, h), np.float32, order="F") - else: - msg = "Invalid order parameter, should be 'C' or 'F'." - raise ValueError(msg) + msg = "Invalid order parameter, should be 'C' or 'F'." + raise ValueError(msg) @deprecate("Assign to heightmaps as a NumPy array instead.") @@ -2863,16 +2862,15 @@ def heightmap_get_value(hm: NDArray[np.float32], x: int, y: int) -> float: stacklevel=2, ) return hm[y, x] # type: ignore - elif hm.flags["F_CONTIGUOUS"]: + if hm.flags["F_CONTIGUOUS"]: warnings.warn( "Get a value from this heightmap with hm[x,y]", DeprecationWarning, stacklevel=2, ) return hm[x, y] # type: ignore - else: - msg = "This array is not contiguous." - raise ValueError(msg) + msg = "This array is not contiguous." + raise ValueError(msg) @pending_deprecate() @@ -3574,26 +3572,25 @@ def noise_delete(n: tcod.noise.Noise) -> None: """ -def _unpack_union(type_: int, union: Any) -> Any: +def _unpack_union(type_: int, union: Any) -> Any: # noqa: PLR0911 """Unpack items from parser new_property (value_converter).""" if type_ == lib.TCOD_TYPE_BOOL: return bool(union.b) - elif type_ == lib.TCOD_TYPE_CHAR: + if type_ == lib.TCOD_TYPE_CHAR: return union.c.decode("latin-1") - elif type_ == lib.TCOD_TYPE_INT: + if type_ == lib.TCOD_TYPE_INT: return union.i - elif type_ == lib.TCOD_TYPE_FLOAT: + if type_ == lib.TCOD_TYPE_FLOAT: return union.f - elif type_ == lib.TCOD_TYPE_STRING or lib.TCOD_TYPE_VALUELIST15 >= type_ >= lib.TCOD_TYPE_VALUELIST00: + if type_ == lib.TCOD_TYPE_STRING or lib.TCOD_TYPE_VALUELIST15 >= type_ >= lib.TCOD_TYPE_VALUELIST00: return _unpack_char_p(union.s) - elif type_ == lib.TCOD_TYPE_COLOR: + if type_ == lib.TCOD_TYPE_COLOR: return Color._new_from_cdata(union.col) - elif type_ == lib.TCOD_TYPE_DICE: + if type_ == lib.TCOD_TYPE_DICE: return Dice(union.dice) - elif type_ & lib.TCOD_TYPE_LIST: + if type_ & lib.TCOD_TYPE_LIST: return _convert_TCODList(union.list, type_ & 0xFF) - else: - raise RuntimeError("Unknown libtcod type: %i" % type_) + raise RuntimeError("Unknown libtcod type: %i" % type_) def _convert_TCODList(c_list: Any, type_: int) -> Any: @@ -3643,7 +3640,7 @@ def _pycall_parser_error(msg: Any) -> None: @deprecate("Parser functions have been deprecated.") def parser_run(parser: Any, filename: str | PathLike[str], listener: Any = None) -> None: - global _parser_listener + global _parser_listener # noqa: PLW0603 if not listener: lib.TCOD_parser_run(parser, _path_encode(Path(filename)), ffi.NULL) return diff --git a/tcod/map.py b/tcod/map.py index 8cd6ea68..b339cee1 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -87,7 +87,7 @@ def __init__( self.__buffer: NDArray[np.bool_] = np.zeros((height, width, 3), dtype=np.bool_) self.map_c = self.__as_cdata() - def __as_cdata(self) -> Any: + def __as_cdata(self) -> Any: # noqa: ANN401 return ffi.new( "struct TCOD_Map*", ( @@ -113,7 +113,7 @@ def fov(self) -> NDArray[np.bool_]: buffer: np.ndarray[Any, np.dtype[np.bool_]] = self.__buffer[:, :, 2] return buffer.T if self._order == "F" else buffer - def compute_fov( + def compute_fov( # noqa: PLR0913 self, x: int, y: int, @@ -232,7 +232,7 @@ def compute_fov( conditions. """ transparency = np.asarray(transparency) - if len(transparency.shape) != 2: + if len(transparency.shape) != 2: # noqa: PLR2004 raise TypeError("transparency must be an array of 2 dimensions" " (shape is %r)" % transparency.shape) if isinstance(pov, int): msg = "The tcod.map.compute_fov function has changed. The `x` and `y` parameters should now be given as a single tuple." diff --git a/tcod/noise.py b/tcod/noise.py index 92b7636e..66cba1d1 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -122,7 +122,7 @@ class Noise: noise_c (CData): A cffi pointer to a TCOD_noise_t object. """ - def __init__( + def __init__( # noqa: PLR0913 self, dimensions: int, algorithm: int = Algorithm.SIMPLEX, @@ -132,7 +132,7 @@ def __init__( octaves: float = 4, seed: int | tcod.random.Random | None = None, ) -> None: - if not 0 < dimensions <= 4: + if not 0 < dimensions <= 4: # noqa: PLR2004 msg = f"dimensions must be in range 0 < n <= 4, got {dimensions}" raise ValueError(msg) self._seed = seed @@ -190,7 +190,7 @@ def implementation(self) -> int: @implementation.setter def implementation(self, value: int) -> None: - if not 0 <= value < 3: + if not 0 <= value < 3: # noqa: PLR2004 msg = f"{value!r} is not a valid implementation. " raise ValueError(msg) self._tdl_noise_c.implementation = value @@ -336,7 +336,7 @@ def sample_ogrid(self, ogrid: Sequence[ArrayLike]) -> NDArray[np.float32]: def __getstate__(self) -> dict[str, Any]: state = self.__dict__.copy() - if self.dimensions < 4 and self.noise_c.waveletTileData == ffi.NULL: + if self.dimensions < 4 and self.noise_c.waveletTileData == ffi.NULL: # noqa: PLR2004 # Trigger a side effect of wavelet, so that copies will be synced. saved_algo = self.algorithm self.algorithm = tcod.constants.NOISE_WAVELET @@ -385,7 +385,7 @@ def __setstate__(self, state: dict[str, Any]) -> None: self.__dict__.update(state) return None - def _setstate_old(self, state: Any) -> None: + def _setstate_old(self, state: tuple[Any, ...]) -> None: self._random = state[0] self.noise_c = ffi.new("struct TCOD_Noise*") self.noise_c.ndim = state[3] diff --git a/tcod/path.py b/tcod/path.py index 09f6d985..f708a072 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -20,10 +20,10 @@ import functools import itertools import warnings -from typing import Any, Callable +from typing import Any, Callable, Final import numpy as np -from numpy.typing import ArrayLike, NDArray +from numpy.typing import ArrayLike, DTypeLike, NDArray from typing_extensions import Literal from tcod._internal import _check @@ -31,26 +31,26 @@ @ffi.def_extern() # type: ignore -def _pycall_path_old(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: +def _pycall_path_old(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 """Libtcodpy style callback, needs to preserve the old userData issue.""" func, userData = ffi.from_handle(handle) return func(x1, y1, x2, y2, userData) # type: ignore @ffi.def_extern() # type: ignore -def _pycall_path_simple(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: +def _pycall_path_simple(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 """Does less and should run faster, just calls the handle function.""" return ffi.from_handle(handle)(x1, y1, x2, y2) # type: ignore @ffi.def_extern() # type: ignore -def _pycall_path_swap_src_dest(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: +def _pycall_path_swap_src_dest(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 """A TDL function dest comes first to match up with a dest only call.""" return ffi.from_handle(handle)(x2, y2, x1, y1) # type: ignore @ffi.def_extern() # type: ignore -def _pycall_path_dest_only(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: +def _pycall_path_dest_only(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 """A TDL function which samples the dest coordinate only.""" return ffi.from_handle(handle)(x2, y2) # type: ignore @@ -74,7 +74,7 @@ class _EdgeCostFunc: _CALLBACK_P = lib._pycall_path_old - def __init__(self, userdata: Any, shape: tuple[int, int]) -> None: + def __init__(self, userdata: object, shape: tuple[int, int]) -> None: self._userdata = userdata self.shape = shape @@ -117,7 +117,7 @@ class NodeCostArray(np.ndarray): # type: ignore A cost of 0 means the node is blocking. """ - _C_ARRAY_CALLBACKS = { + _C_ARRAY_CALLBACKS: Final = { np.float32: ("float*", _get_path_cost_func("PathCostArrayFloat32")), np.bool_: ("int8_t*", _get_path_cost_func("PathCostArrayInt8")), np.int8: ("int8_t*", _get_path_cost_func("PathCostArrayInt8")), @@ -130,14 +130,13 @@ class NodeCostArray(np.ndarray): # type: ignore def __new__(cls, array: ArrayLike) -> NodeCostArray: """Validate a numpy array and setup a C callback.""" - self = np.asarray(array).view(cls) - return self + return np.asarray(array).view(cls) def __repr__(self) -> str: return f"{self.__class__.__name__}({repr(self.view(np.ndarray))!r})" def get_tcod_path_ffi(self) -> tuple[Any, Any, tuple[int, int]]: - if len(self.shape) != 2: + if len(self.shape) != 2: # noqa: PLR2004 msg = f"Array must have a 2d shape, shape is {self.shape!r}" raise ValueError(msg) if self.dtype.type not in self._C_ARRAY_CALLBACKS: @@ -194,7 +193,7 @@ def __init__(self, cost: Any, diagonal: float = 1.41) -> None: def __repr__(self) -> str: return f"{self.__class__.__name__}(cost={self.cost!r}, diagonal={self.diagonal!r})" - def __getstate__(self) -> Any: + def __getstate__(self) -> dict[str, Any]: state = self.__dict__.copy() del state["_path_c"] del state["shape"] @@ -202,7 +201,7 @@ def __getstate__(self) -> Any: del state["_userdata"] return state - def __setstate__(self, state: Any) -> None: + def __setstate__(self, state: dict[str, Any]) -> None: self.__dict__.update(state) self.__init__(self.cost, self.diagonal) # type: ignore @@ -286,7 +285,7 @@ def get_path(self, x: int, y: int) -> list[tuple[int, int]]: def maxarray( shape: tuple[int, ...], - dtype: Any = np.int32, + dtype: DTypeLike = np.int32, order: Literal["C", "F"] = "C", ) -> NDArray[Any]: """Return a new array filled with the maximum finite value for `dtype`. @@ -321,7 +320,7 @@ def _export_dict(array: NDArray[Any]) -> dict[str, Any]: } -def _export(array: NDArray[Any]) -> Any: +def _export(array: NDArray[Any]) -> Any: # noqa: ANN401 """Convert a NumPy array into a cffi object.""" return ffi.new("struct NArray*", _export_dict(array)) @@ -329,7 +328,7 @@ def _export(array: NDArray[Any]) -> Any: def _compile_cost_edges(edge_map: ArrayLike) -> tuple[NDArray[np.intc], int]: """Return an edge_cost array using an integer map.""" edge_map = np.array(edge_map, copy=True) - if edge_map.ndim != 2: + if edge_map.ndim != 2: # noqa: PLR2004 raise ValueError("edge_map must be 2 dimensional. (Got %i)" % edge_map.ndim) edge_center = edge_map.shape[0] // 2, edge_map.shape[1] // 2 edge_map[edge_center] = 0 @@ -344,13 +343,13 @@ def _compile_cost_edges(edge_map: ArrayLike) -> tuple[NDArray[np.intc], int]: return c_edges, len(edge_array) -def dijkstra2d( +def dijkstra2d( # noqa: PLR0913 distance: ArrayLike, cost: ArrayLike, cardinal: int | None = None, diagonal: int | None = None, *, - edge_map: Any = None, + edge_map: ArrayLike | None = None, out: np.ndarray | None = ..., # type: ignore ) -> NDArray[Any]: """Return the computed distance of all nodes on a 2D Dijkstra grid. @@ -523,7 +522,7 @@ def hillclimb2d( cardinal: bool | None = None, diagonal: bool | None = None, *, - edge_map: Any = None, + edge_map: ArrayLike | None = None, ) -> NDArray[Any]: """Return a path on a grid from `start` to the lowest point. @@ -577,7 +576,7 @@ def hillclimb2d( return path -def _world_array(shape: tuple[int, ...], dtype: Any = np.int32) -> NDArray[Any]: +def _world_array(shape: tuple[int, ...], dtype: DTypeLike = np.int32) -> NDArray[Any]: """Return an array where ``ij == arr[ij]``.""" return np.ascontiguousarray( np.transpose( @@ -666,7 +665,7 @@ def __init__(self, shape: tuple[int, ...], *, order: str = "C") -> None: self._order = order if self._order == "F": self._shape_c = self._shape_c[::-1] - if not 0 < self._ndim <= 4: + if not 0 < self._ndim <= 4: # noqa: PLR2004 msg = "Graph dimensions must be 1 <= n <= 4." raise TypeError(msg) self._graph: dict[tuple[Any, ...], dict[str, Any]] = {} @@ -957,7 +956,7 @@ def set_heuristic(self, *, cardinal: int = 0, diagonal: int = 0, z: int = 0, w: raise ValueError(msg) self._heuristic = (cardinal, diagonal, z, w) - def _compile_rules(self) -> Any: + def _compile_rules(self) -> Any: # noqa: ANN401 """Compile this graph into a C struct array.""" if not self._edge_rules_p: self._edge_rules_keep_alive = [] @@ -1029,7 +1028,7 @@ class SimpleGraph: def __init__(self, *, cost: ArrayLike, cardinal: int, diagonal: int, greed: int = 1) -> None: cost = np.asarray(cost) - if cost.ndim != 2: + if cost.ndim != 2: # noqa: PLR2004 msg = f"The cost array must e 2 dimensional, array of shape {cost.shape!r} given." raise TypeError(msg) if greed <= 0: diff --git a/tcod/random.py b/tcod/random.py index b6042d8a..148331df 100644 --- a/tcod/random.py +++ b/tcod/random.py @@ -81,7 +81,7 @@ def __init__( ) @classmethod - def _new_from_cdata(cls, cdata: Any) -> Random: + def _new_from_cdata(cls, cdata: Any) -> Random: # noqa: ANN401 """Return a new instance encapsulating this cdata.""" self: Random = object.__new__(cls) self.random_c = cdata @@ -149,7 +149,7 @@ def inverse_gauss(self, mu: float, sigma: float) -> float: def inverse_guass(self, mu: float, sigma: float) -> float: # noqa: D102 return self.inverse_gauss(mu, sigma) - def __getstate__(self) -> Any: + def __getstate__(self) -> dict[str, Any]: """Pack the self.random_c attribute into a portable state.""" state = self.__dict__.copy() state["random_c"] = { @@ -165,7 +165,7 @@ def __getstate__(self) -> Any: } return state - def __setstate__(self, state: Any) -> None: + def __setstate__(self, state: dict[str, Any]) -> None: """Create a new cdata object with the stored parameters.""" if "algo" in state["random_c"]: # Handle old/deprecated format. Covert to libtcod's new union type. diff --git a/tcod/render.py b/tcod/render.py index 8825d076..80558dfb 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -50,7 +50,7 @@ def __init__(self, renderer: tcod.sdl.render.Renderer, tileset: tcod.tileset.Til ) @classmethod - def _from_ref(cls, renderer_p: Any, atlas_p: Any) -> SDLTilesetAtlas: + def _from_ref(cls, renderer_p: Any, atlas_p: Any) -> SDLTilesetAtlas: # noqa: ANN401 self = object.__new__(cls) # Ignore Final reassignment type errors since this is an alternative constructor. # This could be a sign that the current constructor was badly implemented. diff --git a/tcod/sdl/_internal.py b/tcod/sdl/_internal.py index 58b7b030..761ca9a1 100644 --- a/tcod/sdl/_internal.py +++ b/tcod/sdl/_internal.py @@ -5,7 +5,7 @@ import sys as _sys from dataclasses import dataclass from types import TracebackType -from typing import Any, Callable, TypeVar +from typing import Any, Callable, NoReturn, TypeVar from tcod.cffi import ffi, lib @@ -80,7 +80,7 @@ def _check(result: int) -> int: return result -def _check_p(result: Any) -> Any: +def _check_p(result: Any) -> Any: # noqa: ANN401 """Check if an SDL function returned NULL, and raise an exception if it did.""" if not result: raise RuntimeError(_get_error()) @@ -111,7 +111,7 @@ def _required_version(required: tuple[int, int, int]) -> Callable[[T], T]: if required <= _compiled_version(): return lambda x: x - def replacement(*_args: Any, **_kwargs: Any) -> Any: + def replacement(*_args: object, **_kwargs: object) -> NoReturn: msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" raise RuntimeError(msg) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index b623c58d..8ed3fd38 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -136,7 +136,7 @@ def convert_audio( in_array: NDArray[Any] = np.asarray(in_sound) if len(in_array.shape) == 1: in_array = in_array[:, np.newaxis] - if len(in_array.shape) != 2: + if len(in_array.shape) != 2: # noqa: PLR2004 msg = f"Expected a 1 or 2 ndim input, got {in_array.shape} instead." raise TypeError(msg) cvt = ffi.new("SDL_AudioCVT*") @@ -191,7 +191,7 @@ def __init__( self, device_id: int, capture: bool, - spec: Any, # SDL_AudioSpec* + spec: Any, # SDL_AudioSpec* # noqa: ANN401 ) -> None: assert device_id >= 0 assert ffi.typeof(spec) is ffi.typeof("SDL_AudioSpec*") @@ -284,7 +284,7 @@ def _convert_array(self, samples_: ArrayLike) -> NDArray[Any]: if isinstance(samples_, np.ndarray): samples_ = self._verify_array_format(samples_) samples: NDArray[Any] = np.asarray(samples_, dtype=self.format) - if len(samples.shape) < 2: + if len(samples.shape) < 2: # noqa: PLR2004 samples = samples[:, np.newaxis] return np.ascontiguousarray(np.broadcast_to(samples, (samples.shape[0], self.channels)), dtype=self.format) @@ -605,7 +605,7 @@ class AllowedChanges(enum.IntFlag): """""" -def open( +def open( # noqa: PLR0913 name: str | None = None, capture: bool = False, *, diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index 62241dc5..b65d7339 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -126,7 +126,7 @@ class Joystick: _by_instance_id: ClassVar[WeakValueDictionary[int, Joystick]] = WeakValueDictionary() """Currently opened joysticks.""" - def __init__(self, sdl_joystick_p: Any) -> None: + def __init__(self, sdl_joystick_p: Any) -> None: # noqa: ANN401 self.sdl_joystick_p: Final = sdl_joystick_p """The CFFI pointer to an SDL_Joystick struct.""" self.axes: Final[int] = _check(lib.SDL_JoystickNumAxes(self.sdl_joystick_p)) @@ -200,7 +200,7 @@ class GameController: _by_instance_id: ClassVar[WeakValueDictionary[int, GameController]] = WeakValueDictionary() """Currently opened controllers.""" - def __init__(self, sdl_controller_p: Any) -> None: + def __init__(self, sdl_controller_p: Any) -> None: # noqa: ANN401 self.sdl_controller_p: Final = sdl_controller_p self.joystick: Final = Joystick(lib.SDL_GameControllerGetJoystick(self.sdl_controller_p)) """The :any:`Joystick` associated with this controller.""" diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index 6a93955a..d212afb6 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -23,7 +23,7 @@ class Cursor: """A cursor icon for use with :any:`set_cursor`.""" - def __init__(self, sdl_cursor_p: Any) -> None: + def __init__(self, sdl_cursor_p: Any) -> None: # noqa: ANN401 if ffi.typeof(sdl_cursor_p) is not ffi.typeof("struct SDL_Cursor*"): msg = f"Expected a {ffi.typeof('struct SDL_Cursor*')} type (was {ffi.typeof(sdl_cursor_p)})." raise TypeError(msg) @@ -82,7 +82,7 @@ def new_cursor(data: NDArray[np.bool_], mask: NDArray[np.bool_], hot_xy: tuple[i :any:`set_cursor` https://wiki.libsdl.org/SDL_CreateCursor """ - if len(data.shape) != 2: + if len(data.shape) != 2: # noqa: PLR2004 msg = "Data and mask arrays must be 2D." raise TypeError(msg) if data.shape != mask.shape: diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index b1dafe8c..2e879c4d 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -115,7 +115,7 @@ class BlendMode(enum.IntEnum): """""" -def compose_blend_mode( +def compose_blend_mode( # noqa: PLR0913 source_color_factor: BlendFactor, dest_color_factor: BlendFactor, color_operation: BlendOperation, @@ -239,7 +239,7 @@ def __init__(self, renderer: Renderer) -> None: def __enter__(self) -> None: pass - def __exit__(self, *_: Any) -> None: + def __exit__(self, *_: object) -> None: _check(lib.SDL_SetRenderTarget(self.renderer.p, self.old_texture_p)) @@ -262,7 +262,7 @@ def __eq__(self, other: object) -> bool: return bool(self.p == other.p) return NotImplemented - def copy( + def copy( # noqa: PLR0913 self, texture: Texture, source: tuple[float, float, float, float] | None = None, @@ -334,11 +334,11 @@ def upload_texture(self, pixels: NDArray[Any], *, format: int | None = None, acc See :any:`TextureAccess` for more options. """ if format is None: - assert len(pixels.shape) == 3 + assert len(pixels.shape) == 3 # noqa: PLR2004 assert pixels.dtype == np.uint8 - if pixels.shape[2] == 4: + if pixels.shape[2] == 4: # noqa: PLR2004 format = int(lib.SDL_PIXELFORMAT_RGBA32) - elif pixels.shape[2] == 3: + elif pixels.shape[2] == 3: # noqa: PLR2004 format = int(lib.SDL_PIXELFORMAT_RGB24) else: msg = f"Can't determine the format required for an array of shape {pixels.shape}." @@ -594,8 +594,8 @@ def fill_rects(self, rects: NDArray[np.intc | np.float32]) -> None: .. versionadded:: 13.5 """ - assert len(rects.shape) == 2 - assert rects.shape[1] == 4 + assert len(rects.shape) == 2 # noqa: PLR2004 + assert rects.shape[1] == 4 # noqa: PLR2004 rects = np.ascontiguousarray(rects) if rects.dtype == np.intc: _check(lib.SDL_RenderFillRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0])) @@ -610,8 +610,8 @@ def draw_rects(self, rects: NDArray[np.intc | np.float32]) -> None: .. versionadded:: 13.5 """ - assert len(rects.shape) == 2 - assert rects.shape[1] == 4 + assert len(rects.shape) == 2 # noqa: PLR2004 + assert rects.shape[1] == 4 # noqa: PLR2004 rects = np.ascontiguousarray(rects) if rects.dtype == np.intc: _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0])) @@ -626,8 +626,8 @@ def draw_points(self, points: NDArray[np.intc | np.float32]) -> None: .. versionadded:: 13.5 """ - assert len(points.shape) == 2 - assert points.shape[1] == 2 + assert len(points.shape) == 2 # noqa: PLR2004 + assert points.shape[1] == 2 # noqa: PLR2004 points = np.ascontiguousarray(points) if points.dtype == np.intc: _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0])) @@ -642,8 +642,8 @@ def draw_lines(self, points: NDArray[np.intc | np.float32]) -> None: .. versionadded:: 13.5 """ - assert len(points.shape) == 2 - assert points.shape[1] == 2 + assert len(points.shape) == 2 # noqa: PLR2004 + assert points.shape[1] == 2 # noqa: PLR2004 points = np.ascontiguousarray(points) if points.dtype == np.intc: _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0] - 1)) @@ -654,7 +654,7 @@ def draw_lines(self, points: NDArray[np.intc | np.float32]) -> None: raise TypeError(msg) @_required_version((2, 0, 18)) - def geometry( + def geometry( # noqa: PLR0913 self, texture: Texture | None, xy: NDArray[np.float32], @@ -667,18 +667,18 @@ def geometry( .. versionadded:: 13.5 """ assert xy.dtype == np.float32 - assert len(xy.shape) == 2 - assert xy.shape[1] == 2 + assert len(xy.shape) == 2 # noqa: PLR2004 + assert xy.shape[1] == 2 # noqa: PLR2004 assert xy[0].flags.c_contiguous assert color.dtype == np.uint8 - assert len(color.shape) == 2 - assert color.shape[1] == 4 + assert len(color.shape) == 2 # noqa: PLR2004 + assert color.shape[1] == 4 # noqa: PLR2004 assert color[0].flags.c_contiguous assert uv.dtype == np.float32 - assert len(uv.shape) == 2 - assert uv.shape[1] == 2 + assert len(uv.shape) == 2 # noqa: PLR2004 + assert uv.shape[1] == 2 # noqa: PLR2004 assert uv[0].flags.c_contiguous if indices is not None: assert indices.dtype.type in (np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32) diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index b2a71a66..dff4d4c0 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -2,7 +2,6 @@ import enum import warnings -from typing import Any from tcod.cffi import ffi, lib from tcod.sdl._internal import _check, _get_error @@ -44,7 +43,7 @@ def __del__(self) -> None: def __enter__(self) -> _ScopeInit: return self - def __exit__(self, *args: Any) -> None: + def __exit__(self, *_: object) -> None: self.close() diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 64b8cf20..41fec458 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -97,10 +97,10 @@ class _TempSurface: def __init__(self, pixels: ArrayLike) -> None: self._array: NDArray[np.uint8] = np.ascontiguousarray(pixels, dtype=np.uint8) - if len(self._array.shape) != 3: + if len(self._array.shape) != 3: # noqa: PLR2004 msg = f"NumPy shape must be 3D [y, x, ch] (got {self._array.shape})" raise TypeError(msg) - if not (3 <= self._array.shape[2] <= 4): + if not (3 <= self._array.shape[2] <= 4): # noqa: PLR2004 msg = f"NumPy array must have RGB or RGBA channels. (got {self._array.shape})" raise TypeError(msg) self.p = ffi.gc( @@ -113,7 +113,7 @@ def __init__(self, pixels: ArrayLike) -> None: 0x000000FF, 0x0000FF00, 0x00FF0000, - 0xFF000000 if self._array.shape[2] == 4 else 0, + 0xFF000000 if self._array.shape[2] == 4 else 0, # noqa: PLR2004 ), lib.SDL_FreeSurface, ) @@ -122,7 +122,7 @@ def __init__(self, pixels: ArrayLike) -> None: class Window: """An SDL2 Window object.""" - def __init__(self, sdl_window_p: Any) -> None: + def __init__(self, sdl_window_p: Any) -> None: # noqa: ANN401 if ffi.typeof(sdl_window_p) is not ffi.typeof("struct SDL_Window*"): msg = "sdl_window_p must be {!r} type (was {!r}).".format( ffi.typeof("struct SDL_Window*"), ffi.typeof(sdl_window_p) @@ -334,7 +334,7 @@ def hide(self) -> None: lib.SDL_HideWindow(self.p) -def new_window( +def new_window( # noqa: PLR0913 width: int, height: int, *, diff --git a/tcod/tileset.py b/tcod/tileset.py index 1ebf2122..4f1aeef8 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -38,7 +38,7 @@ def __init__(self, tile_width: int, tile_height: int) -> None: ) @classmethod - def _claim(cls, cdata: Any) -> Tileset: + def _claim(cls, cdata: Any) -> Tileset: # noqa: ANN401 """Return a new Tileset that owns the provided TCOD_Tileset* object.""" self = object.__new__(cls) if cdata == ffi.NULL: @@ -48,7 +48,7 @@ def _claim(cls, cdata: Any) -> Tileset: return self @classmethod - def _from_ref(cls, tileset_p: Any) -> Tileset: + def _from_ref(cls, tileset_p: Any) -> Tileset: # noqa: ANN401 self = object.__new__(cls) self._tileset_p = tileset_p return self @@ -147,7 +147,7 @@ def set_tile(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8]) -> None: required = (*self.tile_shape, 4) if tile.shape != required: note = "" - if len(tile.shape) == 3 and tile.shape[2] == 3: + if len(tile.shape) == 3 and tile.shape[2] == 3: # noqa: PLR2004 note = ( "\nNote: An RGB array is too ambiguous," " an alpha channel must be added to this array to divide the background/foreground areas." diff --git a/tests/test_console.py b/tests/test_console.py index f8fa5d91..cd6df8f7 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -103,7 +103,7 @@ def test_console_pickle_fortran() -> None: def test_console_repr() -> None: from numpy import array # noqa: F401 # Used for eval - eval(repr(tcod.console.Console(10, 2))) + eval(repr(tcod.console.Console(10, 2))) # noqa: S307 def test_console_str() -> None: diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py index 9eb151bb..50c8be7e 100644 --- a/tests/test_libtcodpy.py +++ b/tests/test_libtcodpy.py @@ -518,7 +518,7 @@ def test_color() -> None: color_a["b"] = color_a["b"] assert list(color_a) == [0, 3, 2] - assert color_a == color_a + assert color_a == color_a # noqa: PLR0124 color_b = libtcodpy.Color(255, 255, 255) assert color_a != color_b @@ -532,7 +532,7 @@ def test_color() -> None: def test_color_repr() -> None: Color = libtcodpy.Color col = Color(0, 1, 2) - assert eval(repr(col)) == col + assert eval(repr(col)) == col # noqa: S307 @pytest.mark.filterwarnings("ignore") diff --git a/tests/test_tcod.py b/tests/test_tcod.py index 38b1d5c2..aee98c0c 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -160,7 +160,7 @@ def test_key_repr() -> None: assert key.vk == 1 assert key.c == 2 # noqa: PLR2004 assert key.shift - key_copy = eval(repr(key)) + key_copy = eval(repr(key)) # noqa: S307 assert key.vk == key_copy.vk assert key.c == key_copy.c assert key.shift == key_copy.shift @@ -169,7 +169,7 @@ def test_key_repr() -> None: def test_mouse_repr() -> None: Mouse = tcod.Mouse mouse = Mouse(x=1, lbutton=True) - mouse_copy = eval(repr(mouse)) + mouse_copy = eval(repr(mouse)) # noqa: S307 assert mouse.x == mouse_copy.x assert mouse.lbutton == mouse_copy.lbutton From 153ce40b129669cec7fb38068f5dda610b427ccd Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 22:29:48 -0800 Subject: [PATCH 049/134] Update Python shebangs --- build_libtcod.py | 2 +- build_sdl.py | 2 +- docs/tcod/getting-started.rst | 4 ++-- examples/audio_tone.py | 2 +- examples/cavegen.py | 2 +- examples/distribution/PyInstaller/main.py | 2 +- examples/distribution/cx_Freeze/main.py | 2 +- examples/distribution/cx_Freeze/setup.py | 2 +- examples/eventget.py | 2 +- examples/framerate.py | 2 +- examples/samples_tcod.py | 2 +- examples/thread_jobs.py | 2 +- examples/ttf.py | 2 +- scripts/generate_charmap_table.py | 2 +- scripts/get_release_description.py | 2 +- scripts/tag_release.py | 2 +- setup.py | 2 +- 17 files changed, 18 insertions(+), 18 deletions(-) diff --git a/build_libtcod.py b/build_libtcod.py index 59447e9f..00b6036f 100755 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """Parse and compile libtcod and SDL sources for CFFI.""" from __future__ import annotations diff --git a/build_sdl.py b/build_sdl.py index 0c46df14..56fa87de 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """Build script to parse SDL headers and generate CFFI bindings.""" from __future__ import annotations diff --git a/docs/tcod/getting-started.rst b/docs/tcod/getting-started.rst index c7634149..7d5ec175 100644 --- a/docs/tcod/getting-started.rst +++ b/docs/tcod/getting-started.rst @@ -21,7 +21,7 @@ integer increments. Example:: - #!/usr/bin/env python3 + #!/usr/bin/env python # Make sure 'dejavu10x10_gs_tc.png' is in the same directory as this script. import tcod.console import tcod.context @@ -89,7 +89,7 @@ clearing the console every frame and replacing it only on resizing the window. Example:: - #!/usr/bin/env python3 + #!/usr/bin/env python import tcod.context import tcod.event diff --git a/examples/audio_tone.py b/examples/audio_tone.py index dc24d3ec..052f8a39 100755 --- a/examples/audio_tone.py +++ b/examples/audio_tone.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """Shows how to use tcod.sdl.audio to play a custom-made audio stream. Opens an audio device using SDL and plays a square wave for 1 second. diff --git a/examples/cavegen.py b/examples/cavegen.py index eedf0575..4ae22ac3 100755 --- a/examples/cavegen.py +++ b/examples/cavegen.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """A basic cellular automata cave generation example using SciPy. http://www.roguebasin.com/index.php?title=Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels diff --git a/examples/distribution/PyInstaller/main.py b/examples/distribution/PyInstaller/main.py index e1c6090d..51e31e79 100755 --- a/examples/distribution/PyInstaller/main.py +++ b/examples/distribution/PyInstaller/main.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights for the "hello world" PyInstaller # example script. This work is published from: United States. diff --git a/examples/distribution/cx_Freeze/main.py b/examples/distribution/cx_Freeze/main.py index 8f608af7..0c85ca83 100755 --- a/examples/distribution/cx_Freeze/main.py +++ b/examples/distribution/cx_Freeze/main.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """cx_Freeze main script example.""" import tcod.console import tcod.context diff --git a/examples/distribution/cx_Freeze/setup.py b/examples/distribution/cx_Freeze/setup.py index 446a0f42..50a588db 100755 --- a/examples/distribution/cx_Freeze/setup.py +++ b/examples/distribution/cx_Freeze/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python import sys from cx_Freeze import Executable, setup # type: ignore diff --git a/examples/eventget.py b/examples/eventget.py index 0895e90f..0af84d04 100755 --- a/examples/eventget.py +++ b/examples/eventget.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights for this example. This work is # published from: United States. diff --git a/examples/framerate.py b/examples/framerate.py index 3a9e9f81..a51f83ff 100755 --- a/examples/framerate.py +++ b/examples/framerate.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights for this example. This work is # published from: United States. diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 197a1bc9..f68fed86 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """This code demonstrates various usages of python-tcod.""" # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights to these samples. diff --git a/examples/thread_jobs.py b/examples/thread_jobs.py index f4154f9a..fa99a465 100755 --- a/examples/thread_jobs.py +++ b/examples/thread_jobs.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights for this example. This work is # published from: United States. diff --git a/examples/ttf.py b/examples/ttf.py index da412b4b..fb0336cb 100755 --- a/examples/ttf.py +++ b/examples/ttf.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """A TrueType Font example using the FreeType library. You will need to get this external library from PyPI: diff --git a/scripts/generate_charmap_table.py b/scripts/generate_charmap_table.py index 0591cb9b..8154b5b8 100755 --- a/scripts/generate_charmap_table.py +++ b/scripts/generate_charmap_table.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """This script is used to generate the tables for `charmap-reference.rst`. Uses the tabulate module from PyPI. diff --git a/scripts/get_release_description.py b/scripts/get_release_description.py index 7d47d9f9..547f8ca6 100755 --- a/scripts/get_release_description.py +++ b/scripts/get_release_description.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """Print the description used for GitHub Releases.""" from __future__ import annotations diff --git a/scripts/tag_release.py b/scripts/tag_release.py index 70b77fb7..2d528306 100755 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """Automate tagged releases of this project.""" from __future__ import annotations diff --git a/setup.py b/setup.py index dc2139e7..4027fbae 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """Python-tcod setup script.""" from __future__ import annotations From 3e02e28489f0de3555f1cb18bb40d79bea22728c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 22:49:09 -0800 Subject: [PATCH 050/134] Clean up warnings in scripts --- scripts/generate_charmap_table.py | 6 +++--- scripts/tag_release.py | 8 +++----- setup.py | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/scripts/generate_charmap_table.py b/scripts/generate_charmap_table.py index 8154b5b8..c4137e8a 100755 --- a/scripts/generate_charmap_table.py +++ b/scripts/generate_charmap_table.py @@ -14,8 +14,8 @@ import tcod.tileset -def get_charmaps() -> Iterator[str]: - """Return an iterator of the current character maps from tcod.tilest.""" +def get_character_maps() -> Iterator[str]: + """Return an iterator of the current character maps from tcod.tileset.""" for name in dir(tcod.tileset): if name.startswith("CHARMAP_"): yield name[len("CHARMAP_") :].lower() @@ -60,7 +60,7 @@ def main() -> None: parser.add_argument( "charmap", action="store", - choices=list(get_charmaps()), + choices=list(get_character_maps()), type=str, help="which character map to generate a table from", ) diff --git a/scripts/tag_release.py b/scripts/tag_release.py index 2d528306..7b946513 100755 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -34,11 +34,9 @@ def parse_changelog(args: argparse.Namespace) -> tuple[str, str]: ) assert match header, changes, tail = match.groups() - tagged = "\n## [{}] - {}\n{}".format( - args.tag, - datetime.date.today().isoformat(), # Local timezone is fine, probably. # noqa: DTZ011 - changes, - ) + + iso_date = datetime.datetime.now(tz=datetime.timezone.utc).date().isoformat() + tagged = f"\n## [{args.tag}] - {iso_date}\n{changes}" if args.verbose: print("--- Tagged section:") print(tagged) diff --git a/setup.py b/setup.py index 4027fbae..6e17d0f8 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ def check_sdl_version() -> None: return needed_version = "{}.{}.{}".format(*SDL_VERSION_NEEDED) try: - sdl_version_str = subprocess.check_output(["sdl2-config", "--version"], universal_newlines=True).strip() + sdl_version_str = subprocess.check_output(["sdl2-config", "--version"], universal_newlines=True).strip() # noqa: S603, S607 except FileNotFoundError as exc: msg = ( f"libsdl2-dev or equivalent must be installed on your system and must be at least version {needed_version}." From 5dcbcd42c14464f0f1e1680bde42438d597502e1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 15 Jan 2024 22:54:42 -0800 Subject: [PATCH 051/134] Prepare 16.2.2 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb802f94..acd852e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +## [16.2.2] - 2024-01-16 + ### Fixed - Ignore the locale when encoding file paths outside of Windows. From ede0886eb04bf3c8bf4ca98e6d2925b96ad67e1a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 19:33:20 +0000 Subject: [PATCH 052/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.13 → v0.1.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.13...v0.1.14) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f04c539..3bdb74e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.13 + rev: v0.1.14 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 60ea79c9c6db6bed69bf699aa8530741fc9bd1d2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 19:40:04 +0000 Subject: [PATCH 053/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.14 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.14...v0.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3bdb74e6..520d663a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 + rev: v0.2.0 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 5726f0db22150633a47cdc70761cdee4aa0eb72b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 19:40:12 +0000 Subject: [PATCH 054/134] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- build_libtcod.py | 2 +- examples/samples_tcod.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/build_libtcod.py b/build_libtcod.py index 00b6036f..56161182 100755 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -15,7 +15,7 @@ sys.path.append(str(Path(__file__).parent)) # Allow importing local modules. -import build_sdl # noqa: E402 +import build_sdl Py_LIMITED_API = 0x03060000 diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index f68fed86..7baf6af2 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -768,12 +768,11 @@ def on_draw(self) -> None: libtcodpy.console_put_char(sample_console, self.px, self.py, " ", libtcodpy.BKGND_NONE) self.px, self.py = libtcodpy.path_walk(self.path, True) # type: ignore libtcodpy.console_put_char(sample_console, self.px, self.py, "@", libtcodpy.BKGND_NONE) - else: - if not libtcodpy.dijkstra_is_empty(self.dijkstra): - libtcodpy.console_put_char(sample_console, self.px, self.py, " ", libtcodpy.BKGND_NONE) - self.px, self.py = libtcodpy.dijkstra_path_walk(self.dijkstra) # type: ignore - libtcodpy.console_put_char(sample_console, self.px, self.py, "@", libtcodpy.BKGND_NONE) - self.recalculate = True + elif not libtcodpy.dijkstra_is_empty(self.dijkstra): + libtcodpy.console_put_char(sample_console, self.px, self.py, " ", libtcodpy.BKGND_NONE) + self.px, self.py = libtcodpy.dijkstra_path_walk(self.dijkstra) # type: ignore + libtcodpy.console_put_char(sample_console, self.px, self.py, "@", libtcodpy.BKGND_NONE) + self.recalculate = True def ev_keydown(self, event: tcod.event.KeyDown) -> None: if event.sym == tcod.event.KeySym.i and self.dy > 0: From 9101ca0e397b476f7b6d8a8bc6b946bd439d7fe0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 23:11:12 +0000 Subject: [PATCH 055/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 520d663a..447a431b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.2.1 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 98cc42411376b331fd1374c3aab7e868daecdfe2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 23:30:42 +0000 Subject: [PATCH 056/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.2.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.2.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 447a431b..7badadae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.1 + rev: v0.2.2 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From bcbd672c404164f5bc8116d0bf8d0a0f18ab1fcb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 23:30:50 +0000 Subject: [PATCH 057/134] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_sdl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sdl.py b/tests/test_sdl.py index 10cdbb50..709d3647 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -23,7 +23,7 @@ def test_sdl_window(uses_window: None) -> None: assert window.title == sys.argv[0] window.title = "Title" assert window.title == "Title" - assert window.opacity == 1.0 # noqa: PLR2004 + assert window.opacity == 1.0 window.position = window.position window.fullscreen = window.fullscreen window.resizable = window.resizable From 63e98d9acaa4bfb8b53243918c4bbca859049ed4 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 20 Feb 2024 09:29:52 -0800 Subject: [PATCH 058/134] Tutorial part 2 --- .vscode/settings.json | 5 + docs/conf.py | 7 +- docs/epilog.rst | 3 + docs/tutorial/index.rst | 6 + docs/tutorial/part-02.rst | 701 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 719 insertions(+), 3 deletions(-) create mode 100644 docs/tutorial/part-02.rst diff --git a/.vscode/settings.json b/.vscode/settings.json index 1ea25a26..62ea12d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -127,6 +127,7 @@ "DHLINE", "DISPLAYSWITCH", "dlopen", + "docstrings", "documentclass", "Doryen", "DPAD", @@ -136,6 +137,7 @@ "DTEEW", "dtype", "dtypes", + "dunder", "DVLINE", "elif", "endianness", @@ -148,6 +150,7 @@ "ffade", "fgcolor", "fheight", + "Flecs", "flto", "fmean", "fontx", @@ -332,6 +335,7 @@ "printn", "PRINTSCREEN", "propname", + "pushdown", "pycall", "pycparser", "pyinstaller", @@ -346,6 +350,7 @@ "quickstart", "QUOTEDBL", "RALT", + "randint", "randomizer", "rbutton", "RCTRL", diff --git a/docs/conf.py b/docs/conf.py index 2fece512..a323f906 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,7 +103,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "/epilog.rst"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "epilog.rst", "prolog.rst"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -378,13 +378,14 @@ napoleon_use_param = True napoleon_use_rtype = True -rst_prolog = ".. include:: /prolog.rst" # Added to the beginning of every source file. -rst_epilog = ".. include:: /epilog.rst" # Added to the end of every source file. +rst_prolog = Path("prolog.rst").read_text(encoding="utf-8") # Added to the beginning of every source file. +rst_epilog = Path("epilog.rst").read_text(encoding="utf-8") # Added to the end of every source file. # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "numpy": ("https://numpy.org/doc/stable/", None), + "tcod-ecs": ("https://python-tcod-ecs.readthedocs.io/en/latest/", None), } os.environ["READTHEDOCS"] = "True" diff --git a/docs/epilog.rst b/docs/epilog.rst index e69de29b..c3ee806c 100644 --- a/docs/epilog.rst +++ b/docs/epilog.rst @@ -0,0 +1,3 @@ + +.. _tcod-ecs: https://github.com/HexDecimal/python-tcod-ecs +.. _Flecs: https://github.com/SanderMertens/flecs diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 2277ab62..b43a8971 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -3,6 +3,12 @@ Tutorial .. include:: notice.rst +.. note:: + This a Python tutorial reliant on a Modern ECS implementation. + In this case `tcod-ecs`_ will be used. + Most other Python ECS libraries do not support entity relationships and arbitrary tags required by this tutorial. + If you wish to use this tutorial with another language you may need a Modern ECS implementation on par with `Flecs`_. + .. toctree:: :maxdepth: 1 :glob: diff --git a/docs/tutorial/part-02.rst b/docs/tutorial/part-02.rst new file mode 100644 index 00000000..460e5016 --- /dev/null +++ b/docs/tutorial/part-02.rst @@ -0,0 +1,701 @@ +.. _part-2: + +Part 2 - Entities +############################################################################## + +.. include:: notice.rst + +In part 2 entities will be added and the state system will be refactored to be more generic. +This part will also begin to split logic into multiple Python modules using a namespace called ``game``. + +Entities will be handled with an ECS implementation, in this case: `tcod-ecs`_. +``tcod-ecs`` is a standalone package and is installed separately from ``tcod``. +Use :shell:`pip install tcod-ecs` to install this package. + +Namespace package +============================================================================== + +Create a new folder called ``game`` and inside the folder create a new python file named ``__init__.py``. +``game/__init__.py`` only needs a docstring describing that it is a namespace package: + +.. code-block:: python + + """Game namespace package.""" + +This package will be used to organize new modules. + +State protocol +============================================================================== + +To have more states than ``ExampleState`` one must use an abstract type which can be used to refer to any state. +In this case a `Protocol`_ will be used, called ``State``. + +Create a new module: ``game/state.py``. +In this module add the class :python:`class State(Protocol):`. +``Protocol`` is from Python's ``typing`` module. +``State`` should have the ``on_event`` and ``on_draw`` methods from ``ExampleState`` but these methods will be empty other than the docstrings describing what they are for. +These methods refer to types from ``tcod`` and those types will need to be imported. +``State`` should also have :python:`__slots__ = ()` [#slots]_ in case the class is used for a subclass. + +``game/state.py`` should look like this: + +.. code-block:: python + + """Base classes for states.""" + from __future__ import annotations + + from typing import Protocol + + import tcod.console + import tcod.event + + + class State(Protocol): + """An abstract game state.""" + + __slots__ = () + + def on_event(self, event: tcod.event.Event) -> None: + """Called on events.""" + + def on_draw(self, console: tcod.console.Console) -> None: + """Called when the state is being drawn.""" + +The ``ExampleState`` class does not need to be updated since it is already a structural subtype of ``State``. +Note that subclasses of ``State`` will never be in same module as ``State``, this will be the same for all abstract classes. + +Organizing globals +============================================================================== + +There are a few variables which will need to be accessible from multiple modules. +Any global variables which might be assigned from other modules will need to a tracked and handled with care. + +Create a new module: ``g.py`` [#g]_. +This module is exceptional and will be placed at the top-level instead of in the ``game`` folder. + +``console`` and ``context`` from ``main.py`` will now be annotated in ``g.py``. +These will not be assigned here, only annotated with a type-hint. + +A new global will be added: :python:`states: list[game.state.State] = []`. +States are implemented as a list/stack to support `pushdown automata `_. +Representing states as a stack makes it easier to implement popup windows, menus, and other "history aware" states. + +Finally :python:`world: tcod.ecs.Registry` will be added to hold the ECS scope. + +It is important to document all variables placed in this module with docstrings. + +.. code-block:: python + + """This module stores globally mutable variables used by this program.""" + from __future__ import annotations + + import tcod.console + import tcod.context + import tcod.ecs + + import game.state + + console: tcod.console.Console + """The main console.""" + + context: tcod.context.Context + """The window managed by tcod.""" + + states: list[game.state.State] = [] + """A stack of states with the last item being the active state.""" + + world: tcod.ecs.Registry + """The active ECS registry and current session.""" + +Now other modules can :python:`import g` to access global variables. + +Ideally you should not overuse this module for too many things. +When a variables can either be taken as a function parameter or accessed as a global then passing as a parameter is always preferable. + +State functions +============================================================================== + +Create a new module: ``game/state_tools.py``. +This module will handle events and rendering of the global state. + +In this module add the function :python:`def main_draw() -> None:`. +This will hold the "clear, draw, present" logic from the ``main`` function which will be moved to this function. +Render the active state with :python:`g.states[-1].on_draw(g.console)`. +If ``g.states`` is empty then this function should immediately :python:`return` instead of doing anything. +Empty containers in Python are :python:`False` when checked for truthiness. + +Next the function :python:`def main_loop() -> None:` is created. +The :python:`while` loop from ``main`` will be moved to this function. +The while loop will be replaced by :python:`while g.states:` so that this function will exit if no state exists. +Drawing will be replaced by a call to ``main_draw``. +Events in the for-loop will be passed to the active state :python:`g.states[-1].on_event(event)`. +Any states ``on_event`` method could potentially change the state so ``g.states`` must be checked to be non-empty for every handled event. + +.. code-block:: python + + """State handling functions.""" + from __future__ import annotations + + import tcod.console + + import g + + + def main_draw() -> None: + """Render and present the active state.""" + if not g.states: + return + g.console.clear() + g.states[-1].on_draw(g.console) + g.context.present(g.console) + + + def main_loop() -> None: + """Run the active state forever.""" + while g.states: + main_draw() + for event in tcod.event.wait(): + if g.states: + g.states[-1].on_event(event) + +Now ``main.py`` can be edited to use the global variables and the new game loop. + +Add :python:`import g` and :python:`import game.state_tools`. +Replace references to ``console`` with ``g.console``. +Replace references to ``context`` with ``g.context``. + +States are initialed by assigning a list with the initial state to ``g.states``. +The previous game loop is replaced by a call to :python:`game.state_tools.main_loop()`. + +.. code-block:: python + :emphasize-lines: 3-4,12-15 + + ... + + import g + import game.state_tools + + def main() -> None: + """Entry point function.""" + tileset = tcod.tileset.load_tilesheet( + "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 + ) + tcod.tileset.procedural_block_elements(tileset=tileset) + g.console = tcod.console.Console(80, 50) + g.states = [ExampleState(player_x=console.width // 2, player_y=console.height // 2)] + with tcod.context.new(console=g.console, tileset=tileset) as g.context: + game.state_tools.main_loop() + ... + +After this you can test the game. +There should be no visible differences from before. + + +ECS tags +============================================================================== + +Create ``game/tags.py``. +This will hold some sentinel values to be used as tags for ``tcod-ecs``. +These tags can be anything that's both unique and unchanging, in this case Python strings are used. + +For example :python:`IsPlayer: Final = "IsPlayer"` will tag an object as being controlled by the player. +The name is ``IsPlayer`` and string is the same as the name. +The ``Final`` annotation clarifies that this a constant. +Sentinel values for ``tcod-ecs`` are named like classes, similar to names like :python:`None` or :python:`False`. + +Repeat this for ``IsActor`` and ``IsItem`` tags. +The ``game/tags.py`` module should look like this: + +.. code-block:: python + + """Collection of common tags.""" + from __future__ import annotations + + from typing import Final + + IsPlayer: Final = "IsPlayer" + """Entity is the player.""" + + IsActor: Final = "IsActor" + """Entity is an actor.""" + + IsItem: Final = "IsItem" + """Entity is an item.""" + +ECS components +============================================================================== + +Next is a new ``game/components.py`` module. +This will hold the components for the graphics and position of entities. + +Start by adding an import for ``attrs``. +The ability to easily design small classes which are frozen/immutable is important for working with ``tcod-ecs``. + +The first component will be a ``Position`` class. +This class will be decorated with :python:`@attrs.define(frozen=True)`. +For attributes this class will have :python:`x: int` and :python:`y: int`. + +It will be common to add vectors to a ``Position`` with code such as :python:`new_pos: Position = Position(0, 0) + (0, 1)`. +Create the dunder method :python:`def __add__(self, direction: tuple[int, int]) -> Self:` to allow this syntax. +Unpack the input with :python:`x, y = direction`. +:python:`self.__class__` is the current class so :python:`self.__class__(self.x + x, self.y + y)` will create a new instance with the direction added to the previous values. + +The new class will look like this: + +.. code-block:: python + + @attrs.define(frozen=True) + class Position: + """An entities position.""" + + x: int + y: int + + def __add__(self, direction: tuple[int, int]) -> Self: + """Add a vector to this position.""" + x, y = direction + return self.__class__(self.x + x, self.y + y) + +Because ``Position`` is immutable, ``tcod-ecs`` is able to reliably track changes to this component. +Normally you can only query entities by which components they have. +A callback can be registered with ``tcod-ecs`` to mirror component values as tags. +This allows querying an entity by its exact position. + +Add :python:`import tcod.ecs.callbacks` and :python:`from tcod.ecs import Entity`. +Then create the new function :python:`def on_position_changed(entity: Entity, old: Position | None, new: Position | None) -> None:` decorated with :python:`@tcod.ecs.callbacks.register_component_changed(component=Position)`. +This function is called when the ``Position`` component is either added, removed, or modified by assignment. +The goal of this function is to mirror the current position to the :class:`set`-like attribute ``entity.tags``. + +:python:`if old == new:` then a position was assigned its own value or an equivalent value. +The cost of discarding and adding the same value can sometimes be high so this case should be guarded and ignored. +:python:`if old is not None:` then the value tracked by ``entity.tags`` is outdated and must be removed. +:python:`if new is not None:` then ``new`` is the up-to-date value to be tracked by ``entity.tags``. + +The function should look like this: + +.. code-block:: python + + @tcod.ecs.callbacks.register_component_changed(component=Position) + def on_position_changed(entity: Entity, old: Position | None, new: Position | None) -> None: + """Mirror position components as a tag.""" + if old == new: # New position is equivalent to its previous value + return # Ignore and return + if old is not None: # Position component removed or changed + entity.tags.discard(old) # Remove old position from tags + if new is not None: # Position component added or changed + entity.tags.add(new) # Add new position to tags + +Next is the ``Graphic`` component. +This will have the attributes :python:`ch: int = ord("!")` and :python:`fg: tuple[int, int, int] = (255, 255, 255)`. +By default all new components should be marked as frozen. + +.. code-block:: python + + @attrs.define(frozen=True) + class Graphic: + """An entities icon and color.""" + + ch: int = ord("!") + fg: tuple[int, int, int] = (255, 255, 255) + +One last component: ``Gold``. +Define this as :python:`Gold: Final = ("Gold", int)`. +``(name, type)`` is tcod-ecs specific syntax to handle multiple components sharing the same type. + +.. code-block:: python + + Gold: Final = ("Gold", int) + """Amount of gold.""" + +That was the last component. +The ``game/components.py`` module should look like this: + +.. code-block:: python + + """Collection of common components.""" + from __future__ import annotations + + from typing import Final, Self + + import attrs + import tcod.ecs.callbacks + from tcod.ecs import Entity + + + @attrs.define(frozen=True) + class Position: + """An entities position.""" + + x: int + y: int + + def __add__(self, direction: tuple[int, int]) -> Self: + """Add a vector to this position.""" + x, y = direction + return self.__class__(self.x + x, self.y + y) + + + @tcod.ecs.callbacks.register_component_changed(component=Position) + def on_position_changed(entity: Entity, old: Position | None, new: Position | None) -> None: + """Mirror position components as a tag.""" + if old == new: + return + if old is not None: + entity.tags.discard(old) + if new is not None: + entity.tags.add(new) + + + @attrs.define(frozen=True) + class Graphic: + """An entities icon and color.""" + + ch: int = ord("!") + fg: tuple[int, int, int] = (255, 255, 255) + + + Gold: Final = ("Gold", int) + """Amount of gold.""" + +ECS entities and registry +============================================================================== + +Now it is time to create entities. +To do that you need to create the ECS registry. + +Make a new script called ``game/world_tools.py``. +This module will be used to create the ECS registry. + +Random numbers from :mod:`random` will be used. +In this case we want to use ``Random`` as a component so add :python:`from random import Random`. +Get the registry with :python:`from tcod.ecs import Registry`. +Collect all our components and tags with :python:`from game.components import Gold, Graphic, Position` and :python:`from game.tags import IsActor, IsItem, IsPlayer`. + +This module will have one function: :python:`def new_world() -> Registry:`. +Think of the ECS registry as containing the world since this is how it will be used. +Start this function with :python:`world = Registry()`. + +Entities are referenced with the syntax :python:`world[unique_id]`. +If the same ``unique_id`` is used then you will access the same entity. +:python:`new_entity = world[object()]` is the syntax to spawn new entities because ``object()`` is always unique. +Whenever a global entity is needed then :python:`world[None]` will be used. + +Create an instance of :python:`Random()` and assign it to both :python:`world[None].components[Random]` and ``rng``. +This can done on one line with :python:`rng = world[None].components[Random] = Random()`. + +Next create the player entity with :python:`player = world[object()]`. +Assign the following components to the new player entity: :python:`player.components[Position] = Position(5, 5)`, :python:`player.components[Graphic] = Graphic(ord("@"))`, and :python:`player.components[Gold] = 0`. +Then update the players tags with :python:`player.tags |= {IsPlayer, IsActor}`. + +To add some variety we will scatter gold randomly across the world. +Start a for-loop with :python:`for _ in range(10):` then create a ``gold`` entity in this loop. + +The ``Random`` instance ``rng`` has access to functions from Python's random module such as :any:`random.randint`. +Set ``Position`` to :python:`Position(rng.randint(0, 20), rng.randint(0, 20))`. +Set ``Graphic`` to :python:`Graphic(ord("$"), fg=(255, 255, 0))`. +Set ``Gold`` to :python:`rng.randint(1, 10)`. +Then add ``IsItem`` as a tag. + +Once the for-loop exits then :python:`return world`. +Make sure :python:`return` has the correct indentation and is not part of the for-loop or else you will only spawn one gold. + +``game/world_tools.py`` should look like this: + +.. code-block:: python + + """Functions for working with worlds.""" + from __future__ import annotations + + from random import Random + + from tcod.ecs import Registry + + from game.components import Gold, Graphic, Position + from game.tags import IsActor, IsItem, IsPlayer + + + def new_world() -> Registry: + """Return a freshly generated world.""" + world = Registry() + + rng = world[None].components[Random] = Random() + + player = world[object()] + player.components[Position] = Position(5, 5) + player.components[Graphic] = Graphic(ord("@")) + player.components[Gold] = 0 + player.tags |= {IsPlayer, IsActor} + + for _ in range(10): + gold = world[object()] + gold.components[Position] = Position(rng.randint(0, 20), rng.randint(0, 20)) + gold.components[Graphic] = Graphic(ord("$"), fg=(255, 255, 0)) + gold.components[Gold] = rng.randint(1, 10) + gold.tags |= {IsItem} + + return world + +New in-game state +============================================================================== + +Now there is a new ECS world but the example state does not know how to render it. +A new state needs to be made which is aware of the new entities. + +Create a new script called ``game/states.py``. +``states`` is for derived classes, ``state`` is for the abstract class. +New states will be created in this module and this module will be allowed to import many first party modules without issues. + +Before adding a new state it is time to add a more complete set of directional keys. +These will be added as a dictionary and can be reused anytime we want to know how a key translates to a direction. +Use :python:`from tcod.event import KeySym` to make ``KeySym`` enums easier to write. +Then add the following: + +.. code-block:: python + + DIRECTION_KEYS: Final = { + # Arrow keys + KeySym.LEFT: (-1, 0), + KeySym.RIGHT: (1, 0), + KeySym.UP: (0, -1), + KeySym.DOWN: (0, 1), + # Arrow key diagonals + KeySym.HOME: (-1, -1), + KeySym.END: (-1, 1), + KeySym.PAGEUP: (1, -1), + KeySym.PAGEDOWN: (1, 1), + # Keypad + KeySym.KP_4: (-1, 0), + KeySym.KP_6: (1, 0), + KeySym.KP_8: (0, -1), + KeySym.KP_2: (0, 1), + KeySym.KP_7: (-1, -1), + KeySym.KP_1: (-1, 1), + KeySym.KP_9: (1, -1), + KeySym.KP_3: (1, 1), + # VI keys + KeySym.h: (-1, 0), + KeySym.l: (1, 0), + KeySym.k: (0, -1), + KeySym.j: (0, 1), + KeySym.y: (-1, -1), + KeySym.b: (-1, 1), + KeySym.u: (1, -1), + KeySym.n: (1, 1), + } + +Create a new :python:`class InGame:` decorated with :python:`@attrs.define(eq=False)`. +States will always use ``g.world`` to access the ECS registry. +States prefer ``console`` as a parameter over the global ``g.console`` so always use ``console`` when it exists. + +.. code-block:: python + + @attrs.define(eq=False) + class InGame: + """Primary in-game state.""" + ... + +Create an ``on_event`` method matching the ``State`` protocol. +Copying these methods from ``State`` or ``ExampleState`` should be enough. + +Now to do an tcod-ecs query to fetch the player entity. +In tcod-ecs queries most often start with :python:`g.world.Q.all_of(components=[], tags=[])`. +Which components and tags are asked for will narrow down the returned set of entities to only those matching the requirements. +The query to fetch player entities is :python:`g.world.Q.all_of(tags=[IsPlayer])`. +We expect only one player so the result will be unpacked into a single name: :python:`(player,) = g.world.Q.all_of(tags=[IsPlayer])`. + +Next is to handle the event. +Handling :python:`case tcod.event.Quit():` is the same as before: :python:`raise SystemExit()`. + +The case for direction keys will now be done in a single case: :python:`case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS:`. +``sym=sym`` assigns from the event attribute to a local name. +The left side is the ``event.sym`` attribute and right side is the local name ``sym`` being assigned to. +The case also has a condition which must pass for this branch to be taken and in this case we ensure that only keys from the ``DIRECTION_KEYS`` dictionary are valid ``sym``'s. + +Inside this branch moving the player is simple. +Access the ``(x, y)`` vector with :python:`DIRECTION_KEYS[sym]` and use ``+=`` to add it to the players current ``Position`` component. +This triggers the earlier written ``__add__`` dunder method and ``on_position_changed`` callback. + +Now that the player has moved it would be a good time to interact with the gold entities. +The query to see if the player has stepped on gold is to check for whichever entities have a ``Gold`` component, an ``IsItem`` tag, and the players current position as a tag. +The query for this is :python:`g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]):`. + +We will iterate over whatever matches this query using a :python:`for gold in ...:` loop. +Add the entities ``Gold`` component to the player. +Keep in mind that ``Gold`` is treated like an ``int`` so its usage is predictable. +Now print the current amount of gold using :python:`print(f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g")`. +Then use :python:`gold.clear()` at the end to remove all components and tags from the gold entity which will effectively delete it. + +.. code-block:: python + + ... + def on_event(self, event: tcod.event.Event) -> None: + """Handle events for the in-game state.""" + (player,) = g.world.Q.all_of(tags=[IsPlayer]) + match event: + case tcod.event.Quit(): + raise SystemExit() + case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: + player.components[Position] += DIRECTION_KEYS[sym] + # Auto pickup gold + for gold in g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]): + player.components[Gold] += gold.components[Gold] + print(f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g") + gold.clear() + ... + +Now start with the ``on_draw`` method. +Any entity with both a ``Position`` and a ``Graphic`` is drawable. +Iterate over these entities with :python:`for entity in g.world.Q.all_of(components=[Position, Graphic]):`. +Accessing components can be slow in a loop, so assign components to local names before using them (:python:`pos = entity.components[Position]` and :python:`graphic = entity.components[Graphic]`). + +Check if a components position is in the bounds of the console. +:python:`0 <= pos.x < console.width and 0 <= pos.y < console.height` tells if the position is in bounds. +Instead of nesting this method further, this check should be a guard using :python:`if not (...):` and :python:`continue`. + +Draw the graphic by assigning it to the consoles Numpy array directly with :python:`console.rgb[["ch", "fg"]][pos.y, pos.x] = graphic.ch, graphic.fg`. +``console.rgb`` is a ``ch,fg,bg`` array and :python:`[["ch", "fg"]]` narrows it down to only ``ch,fg``. +The array is in C row-major memory order so you access it with yx (or ij) ordering. + +.. code-block:: python + + ... + def on_draw(self, console: tcod.console.Console) -> None: + """Draw the standard screen.""" + for entity in g.world.Q.all_of(components=[Position, Graphic]): + pos = entity.components[Position] + if not (0 <= pos.x < console.width and 0 <= pos.y < console.height): + continue + graphic = entity.components[Graphic] + console.rgb[["ch", "fg"]][pos.y, pos.x] = graphic.ch, graphic.fg + +``game/states.py`` should now look like this: + +.. code-block:: python + + """A collection of game states.""" + from __future__ import annotations + + from typing import Final + + import attrs + import tcod.console + import tcod.event + from tcod.event import KeySym + + import g + from game.components import Gold, Graphic, Position + from game.tags import IsItem, IsPlayer + + DIRECTION_KEYS: Final = { + # Arrow keys + KeySym.LEFT: (-1, 0), + KeySym.RIGHT: (1, 0), + KeySym.UP: (0, -1), + KeySym.DOWN: (0, 1), + # Arrow key diagonals + KeySym.HOME: (-1, -1), + KeySym.END: (-1, 1), + KeySym.PAGEUP: (1, -1), + KeySym.PAGEDOWN: (1, 1), + # Keypad + KeySym.KP_4: (-1, 0), + KeySym.KP_6: (1, 0), + KeySym.KP_8: (0, -1), + KeySym.KP_2: (0, 1), + KeySym.KP_7: (-1, -1), + KeySym.KP_1: (-1, 1), + KeySym.KP_9: (1, -1), + KeySym.KP_3: (1, 1), + # VI keys + KeySym.h: (-1, 0), + KeySym.l: (1, 0), + KeySym.k: (0, -1), + KeySym.j: (0, 1), + KeySym.y: (-1, -1), + KeySym.b: (-1, 1), + KeySym.u: (1, -1), + KeySym.n: (1, 1), + } + + + @attrs.define(eq=False) + class InGame: + """Primary in-game state.""" + + def on_event(self, event: tcod.event.Event) -> None: + """Handle events for the in-game state.""" + (player,) = g.world.Q.all_of(tags=[IsPlayer]) + match event: + case tcod.event.Quit(): + raise SystemExit() + case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: + player.components[Position] += DIRECTION_KEYS[sym] + # Auto pickup gold + for gold in g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]): + player.components[Gold] += gold.components[Gold] + print(f"Picked up ${gold.components[Gold]}, total: ${player.components[Gold]}") + gold.clear() + + def on_draw(self, console: tcod.console.Console) -> None: + """Draw the standard screen.""" + for entity in g.world.Q.all_of(components=[Position, Graphic]): + pos = entity.components[Position] + if not (0 <= pos.x < console.width and 0 <= pos.y < console.height): + continue + graphic = entity.components[Graphic] + console.rgb[["ch", "fg"]][pos.y, pos.x] = graphic.ch, graphic.fg + +Back to ``main.py``. +At this point you should know which imports to add and which are no longed needed. +``ExampleState`` should be removed. +``g.state`` will be initialized with :python:`[game.states.InGame()]` instead. +Add :python:`g.world = game.world_tools.new_world()`. + +``main.py`` will look like this: + +.. code-block:: python + :emphasize-lines: 5-12,22-23 + + #!/usr/bin/env python3 + """Main entry-point module. This script is used to start the program.""" + from __future__ import annotations + + import tcod.console + import tcod.context + import tcod.tileset + + import g + import game.state_tools + import game.states + import game.world_tools + + + def main() -> None: + """Entry point function.""" + tileset = tcod.tileset.load_tilesheet( + "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 + ) + tcod.tileset.procedural_block_elements(tileset=tileset) + g.console = tcod.console.Console(80, 50) + g.states = [game.states.InGame()] + g.world = game.world_tools.new_world() + with tcod.context.new(console=g.console, tileset=tileset) as g.context: + game.state_tools.main_loop() + + + if __name__ == "__main__": + main() + +Now you can play a simple game where you wander around collecting gold. + +You can review the part-2 source code `here `_. + +.. rubric:: Footnotes + +.. [#slots] This is done to prevent subclasses from requiring a ``__dict__`` attribute. + If you are still wondering what ``__slots__`` is then `the Python docs have a detailed explanation `_. + +.. [#g] ``global``, ``globals``, and ``glob`` were already taken by keywords, built-ins, and the standard library. + The alternatives are to either put this in the ``game`` namespace or to add an underscore such as ``globals_.py``. + +.. _Protocol: https://mypy.readthedocs.io/en/stable/protocols.html From 336aa0a428180804b572695196a54dc7eb7e1669 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 20 Feb 2024 09:37:34 -0800 Subject: [PATCH 059/134] conf.py should always open files relative to script Fixes tests importing this script from other places --- docs/conf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a323f906..f66b990d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,6 +25,8 @@ sys.path.insert(0, str(Path("..").resolve(strict=True))) +THIS_DIR = Path(__file__).parent + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -378,8 +380,8 @@ napoleon_use_param = True napoleon_use_rtype = True -rst_prolog = Path("prolog.rst").read_text(encoding="utf-8") # Added to the beginning of every source file. -rst_epilog = Path("epilog.rst").read_text(encoding="utf-8") # Added to the end of every source file. +rst_prolog = (THIS_DIR / "prolog.rst").read_text(encoding="utf-8") # Added to the beginning of every source file. +rst_epilog = (THIS_DIR / "epilog.rst").read_text(encoding="utf-8") # Added to the end of every source file. # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { From f1c6e5041f477ec84491248f44b44e12715f2a28 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 24 Feb 2024 05:43:23 -0800 Subject: [PATCH 060/134] Refactor part 2 and split state changes into part 3 --- docs/tutorial/part-02.rst | 226 +++++++++++--------------------------- docs/tutorial/part-03.rst | 145 ++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 160 deletions(-) create mode 100644 docs/tutorial/part-03.rst diff --git a/docs/tutorial/part-02.rst b/docs/tutorial/part-02.rst index 460e5016..432f4d16 100644 --- a/docs/tutorial/part-02.rst +++ b/docs/tutorial/part-02.rst @@ -5,7 +5,7 @@ Part 2 - Entities .. include:: notice.rst -In part 2 entities will be added and the state system will be refactored to be more generic. +In part 2 entities will be added and a new state will be created to handle them. This part will also begin to split logic into multiple Python modules using a namespace called ``game``. Entities will be handled with an ECS implementation, in this case: `tcod-ecs`_. @@ -24,46 +24,6 @@ Create a new folder called ``game`` and inside the folder create a new python fi This package will be used to organize new modules. -State protocol -============================================================================== - -To have more states than ``ExampleState`` one must use an abstract type which can be used to refer to any state. -In this case a `Protocol`_ will be used, called ``State``. - -Create a new module: ``game/state.py``. -In this module add the class :python:`class State(Protocol):`. -``Protocol`` is from Python's ``typing`` module. -``State`` should have the ``on_event`` and ``on_draw`` methods from ``ExampleState`` but these methods will be empty other than the docstrings describing what they are for. -These methods refer to types from ``tcod`` and those types will need to be imported. -``State`` should also have :python:`__slots__ = ()` [#slots]_ in case the class is used for a subclass. - -``game/state.py`` should look like this: - -.. code-block:: python - - """Base classes for states.""" - from __future__ import annotations - - from typing import Protocol - - import tcod.console - import tcod.event - - - class State(Protocol): - """An abstract game state.""" - - __slots__ = () - - def on_event(self, event: tcod.event.Event) -> None: - """Called on events.""" - - def on_draw(self, console: tcod.console.Console) -> None: - """Called when the state is being drawn.""" - -The ``ExampleState`` class does not need to be updated since it is already a structural subtype of ``State``. -Note that subclasses of ``State`` will never be in same module as ``State``, this will be the same for all abstract classes. - Organizing globals ============================================================================== @@ -73,14 +33,15 @@ Any global variables which might be assigned from other modules will need to a t Create a new module: ``g.py`` [#g]_. This module is exceptional and will be placed at the top-level instead of in the ``game`` folder. -``console`` and ``context`` from ``main.py`` will now be annotated in ``g.py``. -These will not be assigned here, only annotated with a type-hint. +In ``g.py`` import ``tcod.context`` and ``tcod.ecs``. + +``context`` from ``main.py`` will now be annotated in ``g.py`` by adding the line :python:`context: tcod.context.Context` by itself. +Notice that is this only a type-hinted name and nothing is assigned to it. +This means that type-checking will assume the variable always exists but using it before it is assigned will crash at run-time. -A new global will be added: :python:`states: list[game.state.State] = []`. -States are implemented as a list/stack to support `pushdown automata `_. -Representing states as a stack makes it easier to implement popup windows, menus, and other "history aware" states. +``main.py`` should add :python:`import g` and replace the variables named ``context`` with ``g.context``. -Finally :python:`world: tcod.ecs.Registry` will be added to hold the ECS scope. +Then add the :python:`world: tcod.ecs.Registry` global to hold the ECS scope. It is important to document all variables placed in this module with docstrings. @@ -89,107 +50,17 @@ It is important to document all variables placed in this module with docstrings. """This module stores globally mutable variables used by this program.""" from __future__ import annotations - import tcod.console import tcod.context import tcod.ecs - import game.state - - console: tcod.console.Console - """The main console.""" - context: tcod.context.Context """The window managed by tcod.""" - states: list[game.state.State] = [] - """A stack of states with the last item being the active state.""" - world: tcod.ecs.Registry """The active ECS registry and current session.""" -Now other modules can :python:`import g` to access global variables. - Ideally you should not overuse this module for too many things. -When a variables can either be taken as a function parameter or accessed as a global then passing as a parameter is always preferable. - -State functions -============================================================================== - -Create a new module: ``game/state_tools.py``. -This module will handle events and rendering of the global state. - -In this module add the function :python:`def main_draw() -> None:`. -This will hold the "clear, draw, present" logic from the ``main`` function which will be moved to this function. -Render the active state with :python:`g.states[-1].on_draw(g.console)`. -If ``g.states`` is empty then this function should immediately :python:`return` instead of doing anything. -Empty containers in Python are :python:`False` when checked for truthiness. - -Next the function :python:`def main_loop() -> None:` is created. -The :python:`while` loop from ``main`` will be moved to this function. -The while loop will be replaced by :python:`while g.states:` so that this function will exit if no state exists. -Drawing will be replaced by a call to ``main_draw``. -Events in the for-loop will be passed to the active state :python:`g.states[-1].on_event(event)`. -Any states ``on_event`` method could potentially change the state so ``g.states`` must be checked to be non-empty for every handled event. - -.. code-block:: python - - """State handling functions.""" - from __future__ import annotations - - import tcod.console - - import g - - - def main_draw() -> None: - """Render and present the active state.""" - if not g.states: - return - g.console.clear() - g.states[-1].on_draw(g.console) - g.context.present(g.console) - - - def main_loop() -> None: - """Run the active state forever.""" - while g.states: - main_draw() - for event in tcod.event.wait(): - if g.states: - g.states[-1].on_event(event) - -Now ``main.py`` can be edited to use the global variables and the new game loop. - -Add :python:`import g` and :python:`import game.state_tools`. -Replace references to ``console`` with ``g.console``. -Replace references to ``context`` with ``g.context``. - -States are initialed by assigning a list with the initial state to ``g.states``. -The previous game loop is replaced by a call to :python:`game.state_tools.main_loop()`. - -.. code-block:: python - :emphasize-lines: 3-4,12-15 - - ... - - import g - import game.state_tools - - def main() -> None: - """Entry point function.""" - tileset = tcod.tileset.load_tilesheet( - "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 - ) - tcod.tileset.procedural_block_elements(tileset=tileset) - g.console = tcod.console.Console(80, 50) - g.states = [ExampleState(player_x=console.width // 2, player_y=console.height // 2)] - with tcod.context.new(console=g.console, tileset=tileset) as g.context: - game.state_tools.main_loop() - ... - -After this you can test the game. -There should be no visible differences from before. - +When a variable can either be taken as a function parameter or accessed as a global then passing as a parameter is always preferable. ECS tags ============================================================================== @@ -435,7 +306,7 @@ Make sure :python:`return` has the correct indentation and is not part of the fo return world -New in-game state +New InGame state ============================================================================== Now there is a new ECS world but the example state does not know how to render it. @@ -485,7 +356,6 @@ Then add the following: Create a new :python:`class InGame:` decorated with :python:`@attrs.define(eq=False)`. States will always use ``g.world`` to access the ECS registry. -States prefer ``console`` as a parameter over the global ``g.console`` so always use ``console`` when it exists. .. code-block:: python @@ -494,8 +364,8 @@ States prefer ``console`` as a parameter over the global ``g.console`` so always """Primary in-game state.""" ... -Create an ``on_event`` method matching the ``State`` protocol. -Copying these methods from ``State`` or ``ExampleState`` should be enough. +Create an ``on_event`` and ``on_draw`` method matching the ``ExampleState`` class. +Copying ``ExampleState`` and modifying it should be enough since this wil replace ``ExampleState``. Now to do an tcod-ecs query to fetch the player entity. In tcod-ecs queries most often start with :python:`g.world.Q.all_of(components=[], tags=[])`. @@ -520,9 +390,13 @@ The query to see if the player has stepped on gold is to check for whichever ent The query for this is :python:`g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]):`. We will iterate over whatever matches this query using a :python:`for gold in ...:` loop. -Add the entities ``Gold`` component to the player. +Add the entities ``Gold`` component to the players similar component. Keep in mind that ``Gold`` is treated like an ``int`` so its usage is predictable. -Now print the current amount of gold using :python:`print(f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g")`. + +Format the added and total of gold using a Python f-string_: :python:`text = f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g"`. +Store ``text`` globally in the ECS registry with :python:`g.world[None].components[("Text", str)] = text`. +This is done as two lines to avoid creating a line with an excessive length. + Then use :python:`gold.clear()` at the end to remove all components and tags from the gold entity which will effectively delete it. .. code-block:: python @@ -539,7 +413,8 @@ Then use :python:`gold.clear()` at the end to remove all components and tags fro # Auto pickup gold for gold in g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]): player.components[Gold] += gold.components[Gold] - print(f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g") + text = f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g" + g.world[None].components[str] = text gold.clear() ... @@ -556,6 +431,17 @@ Draw the graphic by assigning it to the consoles Numpy array directly with :pyth ``console.rgb`` is a ``ch,fg,bg`` array and :python:`[["ch", "fg"]]` narrows it down to only ``ch,fg``. The array is in C row-major memory order so you access it with yx (or ij) ordering. +That ends the entity rendering loop. +Next is to print the ``("Text", str)`` component if it exists. +A normal access will raise ``KeyError`` if the component is accessed before being assigned. +This case will be handled by the ``.get`` method of the ``Entity.components`` attribute. +:python:`g.world[None].components.get(("Text", str))` will return :python:`None` instead of raising ``KeyError``. +Assigning this result to ``text`` and then checking :python:`if text:` will ensure that ``text`` within the branch is not None and that the string is not empty. +We will not use ``text`` outside of the branch, so an assignment expression can be used here to check and assign the name at the same time with :python:`if text := g.world[None].components.get(("Text", str)):`. + +In this branch you will print ``text`` to the bottom of the console with a white foreground and black background. +The call to do this is :python:`console.print(x=0, y=console.height - 1, string=text, fg=(255, 255, 255), bg=(0, 0, 0))`. + .. code-block:: python ... @@ -568,6 +454,12 @@ The array is in C row-major memory order so you access it with yx (or ij) orderi graphic = entity.components[Graphic] console.rgb[["ch", "fg"]][pos.y, pos.x] = graphic.ch, graphic.fg + if text := g.world[None].components.get(("Text", str)): + console.print(x=0, y=console.height - 1, string=text, fg=(255, 255, 255), bg=(0, 0, 0)) + +Verify the indentation of the ``if`` branch is correct. +It should be at the same level as the ``for`` loop and not inside of it. + ``game/states.py`` should now look like this: .. code-block:: python @@ -633,7 +525,8 @@ The array is in C row-major memory order so you access it with yx (or ij) orderi # Auto pickup gold for gold in g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]): player.components[Gold] += gold.components[Gold] - print(f"Picked up ${gold.components[Gold]}, total: ${player.components[Gold]}") + text = f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g" + g.world[None].components[("Text", str)] = text gold.clear() def on_draw(self, console: tcod.console.Console) -> None: @@ -645,16 +538,26 @@ The array is in C row-major memory order so you access it with yx (or ij) orderi graphic = entity.components[Graphic] console.rgb[["ch", "fg"]][pos.y, pos.x] = graphic.ch, graphic.fg + if text := g.world[None].components.get(("Text", str)): + console.print(x=0, y=console.height - 1, string=text, fg=(255, 255, 255), bg=(0, 0, 0)) + +Main script update +============================================================================== + Back to ``main.py``. -At this point you should know which imports to add and which are no longed needed. -``ExampleState`` should be removed. -``g.state`` will be initialized with :python:`[game.states.InGame()]` instead. -Add :python:`g.world = game.world_tools.new_world()`. +At this point you should know to import the modules needed. + +The ``ExampleState`` class is obsolete and will be removed. +``state`` will be created with :python:`game.states.InGame()` instead. + +If you have not replaced ``context`` with ``g.context`` yet then do it now. + +Add :python:`g.world = game.world_tools.new_world()` before the main loop. ``main.py`` will look like this: .. code-block:: python - :emphasize-lines: 5-12,22-23 + :emphasize-lines: 10-12,22-24,28 #!/usr/bin/env python3 """Main entry-point module. This script is used to start the program.""" @@ -662,10 +565,10 @@ Add :python:`g.world = game.world_tools.new_world()`. import tcod.console import tcod.context + import tcod.event import tcod.tileset import g - import game.state_tools import game.states import game.world_tools @@ -676,11 +579,17 @@ Add :python:`g.world = game.world_tools.new_world()`. "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 ) tcod.tileset.procedural_block_elements(tileset=tileset) - g.console = tcod.console.Console(80, 50) - g.states = [game.states.InGame()] + console = tcod.console.Console(80, 50) + state = game.states.InGame() g.world = game.world_tools.new_world() - with tcod.context.new(console=g.console, tileset=tileset) as g.context: - game.state_tools.main_loop() + with tcod.context.new(console=console, tileset=tileset) as g.context: + while True: # Main loop + console.clear() # Clear the console before any drawing + state.on_draw(console) # Draw the current state + g.context.present(console) # Render the console to the window and show it + for event in tcod.event.wait(): # Event loop, blocks until pending events exist + print(event) + state.on_event(event) # Dispatch events to the state if __name__ == "__main__": @@ -692,10 +601,7 @@ You can review the part-2 source code `here `_. - .. [#g] ``global``, ``globals``, and ``glob`` were already taken by keywords, built-ins, and the standard library. The alternatives are to either put this in the ``game`` namespace or to add an underscore such as ``globals_.py``. -.. _Protocol: https://mypy.readthedocs.io/en/stable/protocols.html +.. _f-string: https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals diff --git a/docs/tutorial/part-03.rst b/docs/tutorial/part-03.rst new file mode 100644 index 00000000..c3918521 --- /dev/null +++ b/docs/tutorial/part-03.rst @@ -0,0 +1,145 @@ +.. _part-3: + +Part 3 - UI State +############################################################################## + +.. include:: notice.rst + +.. warning:: + + **This part is still a draft and is being worked on. + Sections here will be incorrect as these examples were hastily moved from an earlier part.** + + +State protocol +============================================================================== + +To have more states than ``ExampleState`` one must use an abstract type which can be used to refer to any state. +In this case a `Protocol`_ will be used, called ``State``. + +Create a new module: ``game/state.py``. +In this module add the class :python:`class State(Protocol):`. +``Protocol`` is from Python's ``typing`` module. +``State`` should have the ``on_event`` and ``on_draw`` methods from ``ExampleState`` but these methods will be empty other than the docstrings describing what they are for. +These methods refer to types from ``tcod`` and those types will need to be imported. +``State`` should also have :python:`__slots__ = ()` [#slots]_ in case the class is used for a subclass. + +``game/state.py`` should look like this: + +.. code-block:: python + + """Base classes for states.""" + from __future__ import annotations + + from typing import Protocol + + import tcod.console + import tcod.event + + + class State(Protocol): + """An abstract game state.""" + + __slots__ = () + + def on_event(self, event: tcod.event.Event) -> None: + """Called on events.""" + + def on_draw(self, console: tcod.console.Console) -> None: + """Called when the state is being drawn.""" + +The ``InGame`` class does not need to be updated since it is already a structural subtype of ``State``. +Note that subclasses of ``State`` will never be in same module as ``State``, this will be the same for all abstract classes. + +State globals +============================================================================== + +A new global will be added: :python:`states: list[game.state.State] = []`. +States are implemented as a list/stack to support `pushdown automata `_. +Representing states as a stack makes it easier to implement popup windows, menus, and other "history aware" states. + +State functions +============================================================================== + +Create a new module: ``game/state_tools.py``. +This module will handle events and rendering of the global state. + +In this module add the function :python:`def main_draw() -> None:`. +This will hold the "clear, draw, present" logic from the ``main`` function which will be moved to this function. +Render the active state with :python:`g.states[-1].on_draw(g.console)`. +If ``g.states`` is empty then this function should immediately :python:`return` instead of doing anything. +Empty containers in Python are :python:`False` when checked for truthiness. + +Next the function :python:`def main_loop() -> None:` is created. +The :python:`while` loop from ``main`` will be moved to this function. +The while loop will be replaced by :python:`while g.states:` so that this function will exit if no state exists. +Drawing will be replaced by a call to ``main_draw``. +Events in the for-loop will be passed to the active state :python:`g.states[-1].on_event(event)`. +Any states ``on_event`` method could potentially change the state so ``g.states`` must be checked to be non-empty for every handled event. + +.. code-block:: python + + """State handling functions.""" + from __future__ import annotations + + import tcod.console + + import g + + + def main_draw() -> None: + """Render and present the active state.""" + if not g.states: + return + g.console.clear() + g.states[-1].on_draw(g.console) + g.context.present(g.console) + + + def main_loop() -> None: + """Run the active state forever.""" + while g.states: + main_draw() + for event in tcod.event.wait(): + if g.states: + g.states[-1].on_event(event) + +Now ``main.py`` can be edited to use the global variables and the new game loop. + +Add :python:`import g` and :python:`import game.state_tools`. +Replace references to ``console`` with ``g.console``. +Replace references to ``context`` with ``g.context``. + +States are initialed by assigning a list with the initial state to ``g.states``. +The previous game loop is replaced by a call to :python:`game.state_tools.main_loop()`. + +.. code-block:: python + :emphasize-lines: 3-4,12-15 + + ... + + import g + import game.state_tools + + def main() -> None: + """Entry point function.""" + tileset = tcod.tileset.load_tilesheet( + "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 + ) + tcod.tileset.procedural_block_elements(tileset=tileset) + g.console = tcod.console.Console(80, 50) + g.states = [ExampleState(player_x=console.width // 2, player_y=console.height // 2)] + with tcod.context.new(console=g.console, tileset=tileset) as g.context: + game.state_tools.main_loop() + ... + +After this you can test the game. +There should be no visible differences from before. + + +.. rubric:: Footnotes + +.. [#slots] This is done to prevent subclasses from requiring a ``__dict__`` attribute. + If you are still wondering what ``__slots__`` is then `the Python docs have a detailed explanation `_. + +.. _Protocol: https://mypy.readthedocs.io/en/stable/protocols.html From 4c0feb51e1e51dddb90ddc509fcc75ebf91b5c7d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 23:45:34 +0000 Subject: [PATCH 061/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.2 → v0.3.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.2...v0.3.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7badadae..718837ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.2 + rev: v0.3.2 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 396f279204b0f4b972cb856d5f8c8a35034f857e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 23:45:41 +0000 Subject: [PATCH 062/134] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- build_libtcod.py | 1 + build_sdl.py | 1 + examples/audio_tone.py | 1 + examples/cavegen.py | 1 + examples/distribution/PyInstaller/main.py | 1 + examples/distribution/cx_Freeze/main.py | 1 + examples/eventget.py | 1 + examples/framerate.py | 1 + examples/samples_tcod.py | 1 + examples/sdl-hello-world.py | 1 + examples/thread_jobs.py | 1 + examples/ttf.py | 1 + libtcodpy.py | 1 + scripts/generate_charmap_table.py | 1 + scripts/get_release_description.py | 1 + scripts/tag_release.py | 1 + setup.py | 1 + tcod/__init__.py | 1 + tcod/__pyinstaller/__init__.py | 1 + tcod/__pyinstaller/hook-tcod.py | 1 + tcod/_internal.py | 1 + tcod/bsp.py | 1 + tcod/cffi.py | 1 + tcod/color.py | 1 + tcod/console.py | 1 + tcod/constants.py | 1 + tcod/context.py | 1 + tcod/event.py | 24 ++++------------------- tcod/image.py | 1 + tcod/libtcodpy.py | 9 ++------- tcod/los.py | 1 + tcod/map.py | 1 + tcod/noise.py | 1 + tcod/path.py | 1 + tcod/random.py | 1 + tcod/sdl/__init__.py | 1 + tcod/sdl/_internal.py | 1 + tcod/sdl/audio.py | 1 + tcod/sdl/joystick.py | 1 + tcod/sdl/mouse.py | 1 + tcod/sdl/render.py | 1 + tcod/sdl/video.py | 1 + tcod/tcod.py | 1 + tcod/tileset.py | 1 + tests/conftest.py | 1 + tests/test_console.py | 1 + tests/test_deprecated.py | 1 + tests/test_libtcodpy.py | 1 + tests/test_noise.py | 1 + tests/test_parser.py | 1 + tests/test_random.py | 1 + tests/test_sdl.py | 1 + tests/test_sdl_audio.py | 1 + tests/test_tcod.py | 1 + tests/test_tileset.py | 1 + 55 files changed, 59 insertions(+), 27 deletions(-) diff --git a/build_libtcod.py b/build_libtcod.py index 56161182..2ffe6aad 100755 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Parse and compile libtcod and SDL sources for CFFI.""" + from __future__ import annotations import contextlib diff --git a/build_sdl.py b/build_sdl.py index 56fa87de..629c98f4 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Build script to parse SDL headers and generate CFFI bindings.""" + from __future__ import annotations import io diff --git a/examples/audio_tone.py b/examples/audio_tone.py index 052f8a39..22f21133 100755 --- a/examples/audio_tone.py +++ b/examples/audio_tone.py @@ -3,6 +3,7 @@ Opens an audio device using SDL and plays a square wave for 1 second. """ + import math import time from typing import Any diff --git a/examples/cavegen.py b/examples/cavegen.py index 4ae22ac3..8b95d3fc 100755 --- a/examples/cavegen.py +++ b/examples/cavegen.py @@ -6,6 +6,7 @@ This will print the result to the console, so be sure to run this from the command line. """ + from typing import Any import numpy as np diff --git a/examples/distribution/PyInstaller/main.py b/examples/distribution/PyInstaller/main.py index 51e31e79..b2767fed 100755 --- a/examples/distribution/PyInstaller/main.py +++ b/examples/distribution/PyInstaller/main.py @@ -4,6 +4,7 @@ # example script. This work is published from: United States. # https://creativecommons.org/publicdomain/zero/1.0/ """PyInstaller main script example.""" + import sys from pathlib import Path diff --git a/examples/distribution/cx_Freeze/main.py b/examples/distribution/cx_Freeze/main.py index 0c85ca83..79846377 100755 --- a/examples/distribution/cx_Freeze/main.py +++ b/examples/distribution/cx_Freeze/main.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """cx_Freeze main script example.""" + import tcod.console import tcod.context import tcod.event diff --git a/examples/eventget.py b/examples/eventget.py index 0af84d04..ada61668 100755 --- a/examples/eventget.py +++ b/examples/eventget.py @@ -4,6 +4,7 @@ # published from: United States. # https://creativecommons.org/publicdomain/zero/1.0/ """An demonstration of event handling using the tcod.event module.""" + from typing import List, Set import tcod.context diff --git a/examples/framerate.py b/examples/framerate.py index a51f83ff..25a1dd3f 100755 --- a/examples/framerate.py +++ b/examples/framerate.py @@ -4,6 +4,7 @@ # published from: United States. # https://creativecommons.org/publicdomain/zero/1.0/ """A system to control time since the original libtcod tools are deprecated.""" + import statistics import time from collections import deque diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 7baf6af2..d2a8cb86 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """This code demonstrates various usages of python-tcod.""" + # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights to these samples. # https://creativecommons.org/publicdomain/zero/1.0/ diff --git a/examples/sdl-hello-world.py b/examples/sdl-hello-world.py index b9c07547..4e314805 100644 --- a/examples/sdl-hello-world.py +++ b/examples/sdl-hello-world.py @@ -1,4 +1,5 @@ """Hello world using tcod's SDL API and using Pillow for the TTF rendering.""" + from pathlib import Path import numpy as np diff --git a/examples/thread_jobs.py b/examples/thread_jobs.py index fa99a465..0c616bba 100755 --- a/examples/thread_jobs.py +++ b/examples/thread_jobs.py @@ -13,6 +13,7 @@ Typically the field-of-view tasks run good but not great, and the path-finding tasks run poorly. """ + import concurrent.futures import multiprocessing import platform diff --git a/examples/ttf.py b/examples/ttf.py index fb0336cb..3e13a837 100755 --- a/examples/ttf.py +++ b/examples/ttf.py @@ -5,6 +5,7 @@ pip install freetype-py """ + # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights to this example script. # https://creativecommons.org/publicdomain/zero/1.0/ diff --git a/libtcodpy.py b/libtcodpy.py index 7947f394..76317e99 100644 --- a/libtcodpy.py +++ b/libtcodpy.py @@ -1,4 +1,5 @@ """Deprecated module alias for tcod.libtcodpy, use 'import tcod as libtcodpy' instead.""" + import warnings from tcod.libtcodpy import * # noqa: F403 diff --git a/scripts/generate_charmap_table.py b/scripts/generate_charmap_table.py index c4137e8a..50b6a779 100755 --- a/scripts/generate_charmap_table.py +++ b/scripts/generate_charmap_table.py @@ -3,6 +3,7 @@ Uses the tabulate module from PyPI. """ + from __future__ import annotations import argparse diff --git a/scripts/get_release_description.py b/scripts/get_release_description.py index 547f8ca6..889feced 100755 --- a/scripts/get_release_description.py +++ b/scripts/get_release_description.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Print the description used for GitHub Releases.""" + from __future__ import annotations import re diff --git a/scripts/tag_release.py b/scripts/tag_release.py index 7b946513..2155aaae 100755 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Automate tagged releases of this project.""" + from __future__ import annotations import argparse diff --git a/setup.py b/setup.py index 6e17d0f8..29f9cb3d 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Python-tcod setup script.""" + from __future__ import annotations import platform diff --git a/tcod/__init__.py b/tcod/__init__.py index b8ea0d25..63335ebc 100644 --- a/tcod/__init__.py +++ b/tcod/__init__.py @@ -6,6 +6,7 @@ Read the documentation online: https://python-tcod.readthedocs.io/en/latest/ """ + from __future__ import annotations from pkgutil import extend_path diff --git a/tcod/__pyinstaller/__init__.py b/tcod/__pyinstaller/__init__.py index 7224bb10..2b67c1c0 100644 --- a/tcod/__pyinstaller/__init__.py +++ b/tcod/__pyinstaller/__init__.py @@ -1,4 +1,5 @@ """PyInstaller entry point for tcod.""" + from __future__ import annotations from pathlib import Path diff --git a/tcod/__pyinstaller/hook-tcod.py b/tcod/__pyinstaller/hook-tcod.py index 9790d583..b0cabf38 100644 --- a/tcod/__pyinstaller/hook-tcod.py +++ b/tcod/__pyinstaller/hook-tcod.py @@ -5,6 +5,7 @@ If this hook is ever modified then the contributed hook needs to be removed from: https://github.com/pyinstaller/pyinstaller-hooks-contrib """ + from PyInstaller.utils.hooks import collect_dynamic_libs # type: ignore hiddenimports = ["_cffi_backend"] diff --git a/tcod/_internal.py b/tcod/_internal.py index ef6305bf..cebe249e 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -1,4 +1,5 @@ """Internal helper functions used by the rest of the library.""" + from __future__ import annotations import functools diff --git a/tcod/bsp.py b/tcod/bsp.py index cfb6c021..030bb04b 100644 --- a/tcod/bsp.py +++ b/tcod/bsp.py @@ -24,6 +24,7 @@ else: print('Dig a room for %s.' % node) """ + from __future__ import annotations from typing import Any, Iterator diff --git a/tcod/cffi.py b/tcod/cffi.py index d733abea..19a597b6 100644 --- a/tcod/cffi.py +++ b/tcod/cffi.py @@ -1,4 +1,5 @@ """This module handles loading of the libtcod cffi API.""" + from __future__ import annotations import logging diff --git a/tcod/color.py b/tcod/color.py index 37312413..ef75bfb8 100644 --- a/tcod/color.py +++ b/tcod/color.py @@ -1,4 +1,5 @@ """Old libtcod color management.""" + from __future__ import annotations import warnings diff --git a/tcod/console.py b/tcod/console.py index 3512c893..5cd80c3b 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -4,6 +4,7 @@ To render a console you need a tileset and a window to render to. See :ref:`getting-started` for info on how to set those up. """ + from __future__ import annotations import warnings diff --git a/tcod/constants.py b/tcod/constants.py index f0a596dc..98f1ef1f 100644 --- a/tcod/constants.py +++ b/tcod/constants.py @@ -2,6 +2,7 @@ This module is auto-generated by `build_libtcod.py`. """ + from tcod.color import Color FOV_BASIC = 0 diff --git a/tcod/context.py b/tcod/context.py index 8d5e5259..f22c78de 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -22,6 +22,7 @@ .. versionadded:: 11.12 """ + from __future__ import annotations import copy diff --git a/tcod/event.py b/tcod/event.py index 41623a93..cb48b047 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -79,6 +79,7 @@ .. versionadded:: 8.4 """ + from __future__ import annotations import enum @@ -444,12 +445,7 @@ def tile(self, xy: tuple[int, int]) -> None: self._tile = Point(*xy) def __repr__(self) -> str: - return ("tcod.event.{}(position={!r}, tile={!r}, state={})").format( - self.__class__.__name__, - tuple(self.position), - tuple(self.tile), - MouseButtonMask(self.state), - ) + return f"tcod.event.{self.__class__.__name__}(position={tuple(self.position)!r}, tile={tuple(self.tile)!r}, state={MouseButtonMask(self.state)})" def __str__(self) -> str: return ("<%s, position=(x=%i, y=%i), tile=(x=%i, y=%i), state=%s>") % ( @@ -554,14 +550,7 @@ def from_sdl_event(cls, sdl_event: Any) -> MouseMotion: return self def __repr__(self) -> str: - return ("tcod.event.{}(position={!r}, motion={!r}, tile={!r}, tile_motion={!r}, state={!r})").format( - self.__class__.__name__, - tuple(self.position), - tuple(self.motion), - tuple(self.tile), - tuple(self.tile_motion), - MouseButtonMask(self.state), - ) + return f"tcod.event.{self.__class__.__name__}(position={tuple(self.position)!r}, motion={tuple(self.motion)!r}, tile={tuple(self.tile)!r}, tile_motion={tuple(self.tile_motion)!r}, state={MouseButtonMask(self.state)!r})" def __str__(self) -> str: return ("<%s, motion=(x=%i, y=%i), tile_motion=(x=%i, y=%i)>") % ( @@ -621,12 +610,7 @@ def from_sdl_event(cls, sdl_event: Any) -> Any: return self def __repr__(self) -> str: - return "tcod.event.{}(position={!r}, tile={!r}, button={!r})".format( - self.__class__.__name__, - tuple(self.position), - tuple(self.tile), - MouseButton(self.button), - ) + return f"tcod.event.{self.__class__.__name__}(position={tuple(self.position)!r}, tile={tuple(self.tile)!r}, button={MouseButton(self.button)!r})" def __str__(self) -> str: return " str: ) def __repr__(self) -> str: - return "{}(nb_dices={!r},nb_faces={!r},multiplier={!r},addsub={!r})".format( - self.__class__.__name__, - self.nb_dices, - self.nb_faces, - self.multiplier, - self.addsub, - ) + return f"{self.__class__.__name__}(nb_dices={self.nb_dices!r},nb_faces={self.nb_faces!r},multiplier={self.multiplier!r},addsub={self.addsub!r})" # reverse lookup table for KEY_X attributes, used by Key.__repr__ diff --git a/tcod/los.py b/tcod/los.py index d6cd1362..6d30d0b8 100644 --- a/tcod/los.py +++ b/tcod/los.py @@ -1,4 +1,5 @@ """This modules holds functions for NumPy-based line of sight algorithms.""" + from __future__ import annotations from typing import Any diff --git a/tcod/map.py b/tcod/map.py index b339cee1..fc3c24cf 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -1,4 +1,5 @@ """libtcod map attributes and field-of-view functions.""" + from __future__ import annotations import warnings diff --git a/tcod/noise.py b/tcod/noise.py index 66cba1d1..22f5d9c2 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -31,6 +31,7 @@ [ 76, 54, 85, 144, 164], [ 63, 94, 159, 209, 203]], dtype=uint8) """ + from __future__ import annotations import enum diff --git a/tcod/path.py b/tcod/path.py index f708a072..5765f233 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -15,6 +15,7 @@ All path-finding functions now respect the NumPy array shape (if a NumPy array is used.) """ + from __future__ import annotations import functools diff --git a/tcod/random.py b/tcod/random.py index 148331df..73121578 100644 --- a/tcod/random.py +++ b/tcod/random.py @@ -6,6 +6,7 @@ However, you will need to use these generators to get deterministic results from the :any:`Noise` and :any:`BSP` classes. """ + from __future__ import annotations import os diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index 8133948b..22e82845 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -1,4 +1,5 @@ """tcod.sdl package.""" + from pkgutil import extend_path __path__ = extend_path(__path__, __name__) diff --git a/tcod/sdl/_internal.py b/tcod/sdl/_internal.py index 761ca9a1..8ea5e13f 100644 --- a/tcod/sdl/_internal.py +++ b/tcod/sdl/_internal.py @@ -1,4 +1,5 @@ """tcod.sdl private functions.""" + from __future__ import annotations import logging diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 8ed3fd38..33356132 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -41,6 +41,7 @@ .. versionadded:: 13.5 """ + from __future__ import annotations import enum diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index b65d7339..d5c66713 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -2,6 +2,7 @@ .. versionadded:: 13.8 """ + from __future__ import annotations import enum diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index d212afb6..fefd71d7 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -6,6 +6,7 @@ .. versionadded:: 13.5 """ + from __future__ import annotations import enum diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 2e879c4d..c57d2242 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -2,6 +2,7 @@ .. versionadded:: 13.4 """ + from __future__ import annotations import enum diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 41fec458..53a8e2db 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -6,6 +6,7 @@ .. versionadded:: 13.4 """ + from __future__ import annotations import enum diff --git a/tcod/tcod.py b/tcod/tcod.py index 6fab4ff4..8181da79 100644 --- a/tcod/tcod.py +++ b/tcod/tcod.py @@ -6,6 +6,7 @@ Read the documentation online: https://python-tcod.readthedocs.io/en/latest/ """ + from __future__ import annotations import warnings diff --git a/tcod/tileset.py b/tcod/tileset.py index 4f1aeef8..846aed32 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -10,6 +10,7 @@ `_ while continuing to use python-tcod's pathfinding and field-of-view algorithms. """ + from __future__ import annotations import itertools diff --git a/tests/conftest.py b/tests/conftest.py index 83e2dc8e..2998b4ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Test directory configuration.""" + import random import warnings from typing import Callable, Iterator, Union diff --git a/tests/test_console.py b/tests/test_console.py index cd6df8f7..e329fe6f 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -1,4 +1,5 @@ """Tests for tcod.console.""" + import pickle from pathlib import Path diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index 356de7d3..869c2b36 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -1,4 +1,5 @@ """Test deprecated features.""" + from __future__ import annotations import numpy as np diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py index 50c8be7e..676ab2bc 100644 --- a/tests/test_libtcodpy.py +++ b/tests/test_libtcodpy.py @@ -1,4 +1,5 @@ """Tests for the libtcodpy API.""" + from pathlib import Path from typing import Any, Callable, Iterator, List, Optional, Tuple, Union diff --git a/tests/test_noise.py b/tests/test_noise.py index 0f64d5fe..72149062 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -1,4 +1,5 @@ """Tests for the tcod.noise module.""" + import copy import pickle diff --git a/tests/test_parser.py b/tests/test_parser.py index ce200166..07f736a1 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,4 +1,5 @@ """Test old libtcodpy parser.""" + from pathlib import Path from typing import Any diff --git a/tests/test_random.py b/tests/test_random.py index e6b64e1e..5bed9d95 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -1,4 +1,5 @@ """Test random number generators.""" + import copy import pickle from pathlib import Path diff --git a/tests/test_sdl.py b/tests/test_sdl.py index 709d3647..f22b7a72 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -1,4 +1,5 @@ """Test SDL specific features.""" + import sys import numpy as np diff --git a/tests/test_sdl_audio.py b/tests/test_sdl_audio.py index 6441bda1..f7e55093 100644 --- a/tests/test_sdl_audio.py +++ b/tests/test_sdl_audio.py @@ -1,4 +1,5 @@ """Test tcod.sdl.audio module.""" + import contextlib import sys import time diff --git a/tests/test_tcod.py b/tests/test_tcod.py index aee98c0c..d15b145d 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -1,4 +1,5 @@ """Tests for newer tcod API.""" + import copy import pickle from typing import Any, NoReturn diff --git a/tests/test_tileset.py b/tests/test_tileset.py index aba88b98..dd272fe7 100644 --- a/tests/test_tileset.py +++ b/tests/test_tileset.py @@ -1,4 +1,5 @@ """Test for tcod.tileset module.""" + import tcod.tileset # ruff: noqa: D103 From c7d71e58f7b80a6ba5cd8ce77d0e38846d4e72e1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 12 Mar 2024 22:16:20 -0700 Subject: [PATCH 063/134] Fix pyproject Ruff deprecations --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b4c0490a..97ee6190 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,10 @@ module = "tcod._libtcod" ignore_missing_imports = true [tool.ruff] -# https://beta.ruff.rs/docs/rules/ +extend-exclude = ["libtcod"] # Ignore submodule +line-length = 120 + +[tool.ruff.lint] # https://docs.astral.sh/ruff/rules/ select = [ "C90", # mccabe "E", # pycodestyle @@ -190,9 +193,6 @@ ignore = [ "D206", # indent-with-spaces "W191", # tab-indentation ] -extend-exclude = ["libtcod"] # Ignore submodule -line-length = 120 -[tool.ruff.pydocstyle] -# Use Google-style docstrings. +[tool.ruff.lint.pydocstyle] # https://docs.astral.sh/ruff/settings/#lintpydocstyle convention = "google" From 5df804c91fc221805bfb59aee7a12f7571bbb91b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:14:04 +0000 Subject: [PATCH 064/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.2 → v0.3.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.2...v0.3.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 718837ab..02c3b670 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.2 + rev: v0.3.3 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 6fce3dd4fbd852db21d22e7e39972ab9bf3ef08a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:00:31 +0000 Subject: [PATCH 065/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.3 → v0.3.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.3...v0.3.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02c3b670..2f877f6e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.3 + rev: v0.3.4 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 40c0c865c37960e6f063812df49306dccbf997cb Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 25 Mar 2024 14:59:44 -0700 Subject: [PATCH 066/134] Reformat yml --- .github/workflows/python-package.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8eeac91c..9f625fb2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -63,7 +63,6 @@ jobs: path: dist/tcod-*.tar.gz retention-days: 7 - # This makes sure that the latest versions of the SDL headers parse correctly. parse_sdl: needs: [ruff, mypy] @@ -290,15 +289,15 @@ jobs: retention-days: 7 publish: - needs: [sdist, build, build-macos, linux-wheels] - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - environment: - name: release - url: https://pypi.org/p/tcod - permissions: - id-token: write - steps: + needs: [sdist, build, build-macos, linux-wheels] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + environment: + name: release + url: https://pypi.org/p/tcod + permissions: + id-token: write + steps: - uses: actions/download-artifact@v3 with: name: sdist From 729448991b222dd0d4301fd1a113937a9c7f3a1e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 25 Mar 2024 15:00:38 -0700 Subject: [PATCH 067/134] Set correct version on MacOS wheels --- .github/workflows/python-package.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9f625fb2..844f451a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -273,7 +273,7 @@ jobs: # Downloads SDL2 for the later step. run: python3 build_sdl.py - name: Build wheels - uses: pypa/cibuildwheel@v2.12.3 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BUILD: ${{ matrix.python }} CIBW_ARCHS_MACOS: x86_64 arm64 universal2 @@ -281,6 +281,7 @@ jobs: CIBW_BEFORE_TEST: pip install numpy CIBW_TEST_COMMAND: python -c "import tcod.context" CIBW_TEST_SKIP: "pp* *-macosx_arm64 *-macosx_universal2:arm64" + MACOSX_DEPLOYMENT_TARGET: "10.11" - name: Archive wheel uses: actions/upload-artifact@v3 with: From f4d3b1b8cca991cd788574802da9b002bf0dd9db Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:14:16 +0000 Subject: [PATCH 068/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.5) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f877f6e..40c8dba0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.3.5 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From b813146e38030fc696451f269b16f2057b9052d9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:14:23 +0000 Subject: [PATCH 069/134] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tcod/event.py | 2 +- tcod/libtcodpy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tcod/event.py b/tcod/event.py index cb48b047..0dc40dbe 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -2805,7 +2805,7 @@ def __getattr__(name: str) -> int: return value -__all__ = [ # noqa: F405 +__all__ = [ "Modifier", "Point", "BUTTON_LEFT", diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index dd23de45..13c9e5e3 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -4285,7 +4285,7 @@ def __getattr__(name: str) -> Color: raise AttributeError(msg) from None -__all__ = [ # noqa: F405 +__all__ = [ "Color", "Bsp", "NB_FOV_ALGORITHMS", From 8d39359f38854e0c298f7c0a590f3738445b8f1f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 21:33:30 +0000 Subject: [PATCH 070/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40c8dba0..e8b671c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 9c510f0130a1cc6ad8665466045c3fef9be23823 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 21:07:48 +0000 Subject: [PATCH 071/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.4.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8b671c1..8411e3fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.5 + rev: v0.4.3 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From f166816d5ee1396a528d33be1883f7e226b491a9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 9 May 2024 00:15:39 -0700 Subject: [PATCH 072/134] Skip checking events when the SDL event subsystem is offline. Fixes a known access violation. --- CHANGELOG.md | 4 ++++ tcod/event.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd852e6..10bed531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +### Fixed + +- Fixed access violation when events are polled before SDL is initialized. + ## [16.2.2] - 2024-01-16 ### Fixed diff --git a/tcod/event.py b/tcod/event.py index 0dc40dbe..c4592855 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -92,6 +92,7 @@ import tcod.event_constants import tcod.sdl.joystick +import tcod.sdl.sys from tcod.cffi import ffi, lib from tcod.event_constants import * # noqa: F403 from tcod.event_constants import KMOD_ALT, KMOD_CTRL, KMOD_GUI, KMOD_SHIFT @@ -1196,6 +1197,13 @@ def get() -> Iterator[Any]: It is also safe to call this function inside of a loop that is already handling events (the event iterator is reentrant.) """ + if not lib.SDL_WasInit(tcod.sdl.sys.Subsystem.EVENTS): + warnings.warn( + "Events polled before SDL was initialized.", + RuntimeWarning, + stacklevel=1, + ) + return sdl_event = ffi.new("SDL_Event*") while lib.SDL_PollEvent(sdl_event): if sdl_event.type in _SDL_TO_CLASS_TABLE: From ffa1a27690031db205b06cb8dd9f373713232bc1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 9 May 2024 00:31:08 -0700 Subject: [PATCH 073/134] Update Pillow code in example New version of Pillow has type hints and breaking changes. --- .vscode/settings.json | 1 + examples/sdl-hello-world.py | 7 ++++--- requirements.txt | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 62ea12d3..a6b3de95 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -163,6 +163,7 @@ "GAMECONTROLLER", "gamepad", "genindex", + "getbbox", "GFORCE", "GLES", "globaltoc", diff --git a/examples/sdl-hello-world.py b/examples/sdl-hello-world.py index 4e314805..6afd02ab 100644 --- a/examples/sdl-hello-world.py +++ b/examples/sdl-hello-world.py @@ -3,7 +3,7 @@ from pathlib import Path import numpy as np -from PIL import Image, ImageDraw, ImageFont # type: ignore # pip install Pillow +from PIL import Image, ImageDraw, ImageFont # pip install Pillow import tcod.event import tcod.sdl.render @@ -15,8 +15,9 @@ def render_text(renderer: tcod.sdl.render.Renderer, text: str) -> tcod.sdl.render.Texture: """Render text, upload it to VRAM, then return it as an SDL Texture.""" - # Use Pillow normally to render the font. This code is standard. - width, height = font.getsize(text) + # Use Pillow to render the font. + _left, _top, right, bottom = font.getbbox(text) + width, height = right, bottom image = Image.new("RGBA", (width, height)) draw = ImageDraw.Draw(image) draw.text((0, 0), text, font=font) diff --git a/requirements.txt b/requirements.txt index eb2f7d65..a94c3862 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ requests>=2.28.1 setuptools==65.5.1 types-cffi types-requests +types-Pillow types-setuptools types-tabulate typing_extensions From 0684c0830409f9057a084c1057ba04447e147c8a Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 9 May 2024 00:37:48 -0700 Subject: [PATCH 074/134] Update deprecated VSCode configuration --- .vscode/launch.json | 14 +++++++------- .vscode/settings.json | 8 +------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0f237387..a4d5d9e7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,24 +6,24 @@ "configurations": [ { "name": "Python: Current File", - "type": "python", + "type": "debugpy", "request": "launch", "program": "${file}", - "console": "internalConsole", + "console": "integratedTerminal", }, { // Run the Python samples. // tcod will be built and installed in editalbe mode. "name": "Python: Python samples", - "type": "python", + "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/examples/samples_tcod.py", "cwd": "${workspaceFolder}/examples", - "console": "internalConsole", + "console": "integratedTerminal", }, { "name": "Python: Run tests", - "type": "python", + "type": "debugpy", "request": "launch", "module": "pytest", "preLaunchTask": "develop python-tcod", @@ -31,7 +31,7 @@ { "name": "Documentation: Launch Chrome", "request": "launch", - "type": "pwa-chrome", + "type": "chrome", "url": "file://${workspaceFolder}/docs/_build/html/index.html", "webRoot": "${workspaceFolder}", "preLaunchTask": "build documentation", @@ -39,7 +39,7 @@ { "name": "Documentation: Launch Edge", "request": "launch", - "type": "pwa-msedge", + "type": "msedge", "url": "file://${workspaceFolder}/docs/_build/html/index.html", "webRoot": "${workspaceFolder}", "preLaunchTask": "build documentation", diff --git a/.vscode/settings.json b/.vscode/settings.json index a6b3de95..39cb6ad5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,16 +8,10 @@ "source.fixAll": "always", "source.organizeImports": "never" }, + "cmake.configureOnOpen": false, "files.trimFinalNewlines": true, "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, - "python.linting.enabled": true, - "python.linting.flake8Enabled": false, - "python.linting.mypyEnabled": true, - "python.linting.mypyArgs": [ - "--follow-imports=silent", - "--show-column-numbers" - ], "files.associations": { "*.spec": "python", }, From 61ce951b390a0a53f84ad3d22810a2017bc48c07 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 9 May 2024 00:52:57 -0700 Subject: [PATCH 075/134] Silence several Ruff errors --- tcod/event.py | 17 +++++++---------- tcod/libtcodpy.py | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tcod/event.py b/tcod/event.py index c4592855..5d7e9aef 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -174,16 +174,11 @@ def _verify_tile_coordinates(xy: Point | None) -> Point: return Point(0, 0) -_is_sdl_video_initialized = False - - def _init_sdl_video() -> None: """Keyboard layout stuff needs SDL to be initialized first.""" - global _is_sdl_video_initialized - if _is_sdl_video_initialized: + if lib.SDL_WasInit(lib.SDL_INIT_VIDEO): return lib.SDL_InitSubSystem(lib.SDL_INIT_VIDEO) - _is_sdl_video_initialized = True class Modifier(enum.IntFlag): @@ -744,7 +739,7 @@ def from_sdl_event(cls, sdl_event: Any) -> WindowEvent | Undefined: def __repr__(self) -> str: return f"tcod.event.{self.__class__.__name__}(type={self.type!r})" - __WINDOW_TYPES = { + __WINDOW_TYPES: Final = { lib.SDL_WINDOWEVENT_SHOWN: "WindowShown", lib.SDL_WINDOWEVENT_HIDDEN: "WindowHidden", lib.SDL_WINDOWEVENT_EXPOSED: "WindowExposed", @@ -1592,7 +1587,9 @@ def handle_events(event: tcod.event.Event) -> None: .. versionadded:: 13.4 """ if callback in _event_watch_handles: - warnings.warn(f"{callback} is already an active event watcher, nothing was added.", RuntimeWarning) + warnings.warn( + f"{callback} is already an active event watcher, nothing was added.", RuntimeWarning, stacklevel=2 + ) return callback handle = _event_watch_handles[callback] = ffi.new_handle(callback) lib.SDL_AddEventWatch(lib._sdl_event_watcher, handle) @@ -1609,7 +1606,7 @@ def remove_watch(callback: Callable[[Event], None]) -> None: .. versionadded:: 13.4 """ if callback not in _event_watch_handles: - warnings.warn(f"{callback} is not an active event watcher, nothing was removed.", RuntimeWarning) + warnings.warn(f"{callback} is not an active event watcher, nothing was removed.", RuntimeWarning, stacklevel=2) return handle = _event_watch_handles[callback] lib.SDL_DelEventWatch(lib._sdl_event_watcher, handle) @@ -2813,7 +2810,7 @@ def __getattr__(name: str) -> int: return value -__all__ = [ +__all__ = [ # noqa: F405 "Modifier", "Point", "BUTTON_LEFT", diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 13c9e5e3..dd23de45 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -4285,7 +4285,7 @@ def __getattr__(name: str) -> Color: raise AttributeError(msg) from None -__all__ = [ +__all__ = [ # noqa: F405 "Color", "Bsp", "NB_FOV_ALGORITHMS", From 835ca64e344126de3e966f3f5cf1c18b944b68ab Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 23 Jun 2024 16:48:07 -0700 Subject: [PATCH 076/134] Docs: add newline between module docstrings and future imports --- docs/tutorial/part-02.rst | 6 ++++++ docs/tutorial/part-03.rst | 2 ++ 2 files changed, 8 insertions(+) diff --git a/docs/tutorial/part-02.rst b/docs/tutorial/part-02.rst index 432f4d16..c939f88a 100644 --- a/docs/tutorial/part-02.rst +++ b/docs/tutorial/part-02.rst @@ -48,6 +48,7 @@ It is important to document all variables placed in this module with docstrings. .. code-block:: python """This module stores globally mutable variables used by this program.""" + from __future__ import annotations import tcod.context @@ -80,6 +81,7 @@ The ``game/tags.py`` module should look like this: .. code-block:: python """Collection of common tags.""" + from __future__ import annotations from typing import Final @@ -184,6 +186,7 @@ The ``game/components.py`` module should look like this: .. code-block:: python """Collection of common components.""" + from __future__ import annotations from typing import Final, Self @@ -275,6 +278,7 @@ Make sure :python:`return` has the correct indentation and is not part of the fo .. code-block:: python """Functions for working with worlds.""" + from __future__ import annotations from random import Random @@ -465,6 +469,7 @@ It should be at the same level as the ``for`` loop and not inside of it. .. code-block:: python """A collection of game states.""" + from __future__ import annotations from typing import Final @@ -561,6 +566,7 @@ Add :python:`g.world = game.world_tools.new_world()` before the main loop. #!/usr/bin/env python3 """Main entry-point module. This script is used to start the program.""" + from __future__ import annotations import tcod.console diff --git a/docs/tutorial/part-03.rst b/docs/tutorial/part-03.rst index c3918521..a70aadeb 100644 --- a/docs/tutorial/part-03.rst +++ b/docs/tutorial/part-03.rst @@ -29,6 +29,7 @@ These methods refer to types from ``tcod`` and those types will need to be impor .. code-block:: python """Base classes for states.""" + from __future__ import annotations from typing import Protocol @@ -80,6 +81,7 @@ Any states ``on_event`` method could potentially change the state so ``g.states` .. code-block:: python """State handling functions.""" + from __future__ import annotations import tcod.console From 69fd5fa8e34f3aa31a0e96a67925ea43a07527c3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 23 Jun 2024 17:21:31 -0700 Subject: [PATCH 077/134] Fix tests for newer Numpy versions New versions of Numpy return different types than before --- tcod/console.py | 12 ++++++------ tcod/map.py | 2 +- tcod/noise.py | 2 +- tcod/path.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tcod/console.py b/tcod/console.py index 5cd80c3b..c6767b59 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -313,8 +313,8 @@ def rgba(self) -> NDArray[Any]: ... (*WHITE, 255), ... (*BLACK, 255), ... ) - >>> con.rgba[0, 0] - (88, [255, 255, 255, 255], [ 0, 0, 0, 255]) + >>> print(f"{con.rgba[0, 0]=}") + con.rgba[0, 0]=...(88, [255, 255, 255, 255], [ 0, 0, 0, 255])... .. versionadded:: 12.3 """ @@ -334,11 +334,11 @@ def rgb(self) -> NDArray[Any]: >>> con = tcod.console.Console(10, 2) >>> BLUE, YELLOW, BLACK = (0, 0, 255), (255, 255, 0), (0, 0, 0) >>> con.rgb[0, 0] = ord("@"), YELLOW, BLACK - >>> con.rgb[0, 0] - (64, [255, 255, 0], [0, 0, 0]) + >>> print(f"{con.rgb[0, 0]=}") + con.rgb[0, 0]=...(64, [255, 255, 0], [0, 0, 0])... >>> con.rgb["bg"] = BLUE - >>> con.rgb[0, 0] - (64, [255, 255, 0], [ 0, 0, 255]) + >>> print(f"{con.rgb[0, 0]=}") + con.rgb[0, 0]=...(64, [255, 255, 0], [ 0, 0, 255])... .. versionadded:: 12.3 """ diff --git a/tcod/map.py b/tcod/map.py index fc3c24cf..68f4a04b 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -61,7 +61,7 @@ class Map: [ True, True, True], [False, True, True], [False, False, True]]...) - >>> m.fov[3,1] + >>> m.fov.item(3, 1) False .. deprecated:: 11.13 diff --git a/tcod/noise.py b/tcod/noise.py index 22f5d9c2..8d9dc3ff 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -236,7 +236,7 @@ def __getitem__(self, indexes: Any) -> NDArray[np.float32]: raise IndexError( "This noise generator has %i dimensions, but was indexed with %i." % (self.dimensions, len(indexes)) ) - indexes = np.broadcast_arrays(*indexes) + indexes = list(np.broadcast_arrays(*indexes)) c_input = [ffi.NULL, ffi.NULL, ffi.NULL, ffi.NULL] for i, index in enumerate(indexes): if index.dtype.type == np.object_: diff --git a/tcod/path.py b/tcod/path.py index 5765f233..8d9fc5db 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -1141,7 +1141,7 @@ def traversal(self) -> NDArray[Any]: >>> i, j = (3, 3) # Starting index. >>> path = [(i, j)] # List of nodes from the start to the root. >>> while not (pf.traversal[i, j] == (i, j)).all(): - ... i, j = pf.traversal[i, j] + ... i, j = pf.traversal[i, j].tolist() ... path.append((i, j)) >>> path # Slower. [(3, 3), (2, 2), (1, 1), (0, 0)] From 2d2775f1164689b12fa905e849dfc107ef1e7893 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 25 Jun 2024 21:23:30 -0700 Subject: [PATCH 078/134] Update tutorial part 3 --- .vscode/settings.json | 1 + docs/tutorial/part-01.rst | 6 +- docs/tutorial/part-02.rst | 6 +- docs/tutorial/part-03.rst | 336 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 331 insertions(+), 18 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 39cb6ad5..73a2b81e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -186,6 +186,7 @@ "imageio", "INCOL", "INROW", + "interactable", "intersphinx", "isinstance", "isort", diff --git a/docs/tutorial/part-01.rst b/docs/tutorial/part-01.rst index 4f1827f5..d4216982 100644 --- a/docs/tutorial/part-01.rst +++ b/docs/tutorial/part-01.rst @@ -140,7 +140,7 @@ The next step is to change state based on user input. Like ``tcod`` you'll need to install ``attrs`` with Pip, such as with :shell:`pip install attrs`. Start by adding an ``attrs`` class called ``ExampleState``. -This a normal class with the :python:`@attrs.define(eq=False)` decorator added. +This a normal class with the :python:`@attrs.define()` decorator added. This class should hold coordinates for the player. It should also have a ``on_draw`` method which takes :any:`tcod.console.Console` as a parameter and marks the player position on it. @@ -162,7 +162,7 @@ Call this method using the players current coordinates and the :python:`"@"` cha import tcod.tileset - @attrs.define(eq=False) + @attrs.define() class ExampleState: """Example state with a hard-coded player position.""" @@ -242,7 +242,7 @@ The full script so far is: import tcod.tileset - @attrs.define(eq=False) + @attrs.define() class ExampleState: """Example state with a hard-coded player position.""" diff --git a/docs/tutorial/part-02.rst b/docs/tutorial/part-02.rst index c939f88a..90b20625 100644 --- a/docs/tutorial/part-02.rst +++ b/docs/tutorial/part-02.rst @@ -358,12 +358,12 @@ Then add the following: KeySym.n: (1, 1), } -Create a new :python:`class InGame:` decorated with :python:`@attrs.define(eq=False)`. +Create a new :python:`class InGame:` decorated with :python:`@attrs.define()`. States will always use ``g.world`` to access the ECS registry. .. code-block:: python - @attrs.define(eq=False) + @attrs.define() class InGame: """Primary in-game state.""" ... @@ -515,7 +515,7 @@ It should be at the same level as the ``for`` loop and not inside of it. } - @attrs.define(eq=False) + @attrs.define() class InGame: """Primary in-game state.""" diff --git a/docs/tutorial/part-03.rst b/docs/tutorial/part-03.rst index a70aadeb..25b811e8 100644 --- a/docs/tutorial/part-03.rst +++ b/docs/tutorial/part-03.rst @@ -24,6 +24,16 @@ In this module add the class :python:`class State(Protocol):`. These methods refer to types from ``tcod`` and those types will need to be imported. ``State`` should also have :python:`__slots__ = ()` [#slots]_ in case the class is used for a subclass. +Now add a few small classes using :python:`@attrs.define()`: +A ``Push`` class with a :python:`state: State` attribute. +A ``Pop`` class with no attributes. +A ``Reset`` class with a :python:`state: State` attribute. + +Then add a :python:`StateResult: TypeAlias = "Push | Pop | Reset | None"`. +This is a type which combines all of the previous classes. + +Edit ``State``'s ``on_event`` method to return ``StateResult``. + ``game/state.py`` should look like this: .. code-block:: python @@ -32,8 +42,9 @@ These methods refer to types from ``tcod`` and those types will need to be impor from __future__ import annotations - from typing import Protocol + from typing import Protocol, TypeAlias + import attrs import tcod.console import tcod.event @@ -43,21 +54,73 @@ These methods refer to types from ``tcod`` and those types will need to be impor __slots__ = () - def on_event(self, event: tcod.event.Event) -> None: + def on_event(self, event: tcod.event.Event) -> StateResult: """Called on events.""" def on_draw(self, console: tcod.console.Console) -> None: """Called when the state is being drawn.""" + + @attrs.define() + class Push: + """Push a new state on top of the stack.""" + + state: State + + + @attrs.define() + class Pop: + """Remove the current state from the stack.""" + + + @attrs.define() + class Reset: + """Replace the entire stack with a new state.""" + + state: State + + + StateResult: TypeAlias = "Push | Pop | Reset | None" + """Union of state results.""" + The ``InGame`` class does not need to be updated since it is already a structural subtype of ``State``. Note that subclasses of ``State`` will never be in same module as ``State``, this will be the same for all abstract classes. -State globals +New globals ============================================================================== A new global will be added: :python:`states: list[game.state.State] = []`. States are implemented as a list/stack to support `pushdown automata `_. -Representing states as a stack makes it easier to implement popup windows, menus, and other "history aware" states. +Representing states as a stack makes it easier to implement popup windows, sub-menus, and other prompts. + +The ``console`` variable from ``main.py`` will be moved to ``g.py``. +Add :python:`console: tcod.console.Console` and replace all references to ``console`` in ``main.py`` with ``g.console``. + +.. code-block:: python + :emphasize-lines: 9,17-21 + + """This module stores globally mutable variables used by this program.""" + + from __future__ import annotations + + import tcod.console + import tcod.context + import tcod.ecs + + import game.state + + context: tcod.context.Context + """The window managed by tcod.""" + + world: tcod.ecs.Registry + """The active ECS registry and current session.""" + + states: list[game.state.State] = [] + """A stack of states with the last item being the active state.""" + + console: tcod.console.Console + """The current main console.""" + State functions ============================================================================== @@ -71,12 +134,39 @@ Render the active state with :python:`g.states[-1].on_draw(g.console)`. If ``g.states`` is empty then this function should immediately :python:`return` instead of doing anything. Empty containers in Python are :python:`False` when checked for truthiness. -Next the function :python:`def main_loop() -> None:` is created. +Next is to handle the ``StateResult`` type. +Start by adding the :python:`def apply_state_result(result: StateResult) -> None:` function. +This function will :python:`match result:` to decide on what to do. + +:python:`case Push(state=state):` should append ``state`` to ``g.states``. + +:python:`case Pop():` should simply call :python:`g.states.pop()`. + +:python:`case Reset(state=state):` should call :python:`apply_state_result(Pop())` until ``g.state`` is empty then call :python:`apply_state_result(Push(state))`. + +:python:`case None:` should be handled by explicitly ignoring it. + +:python:`case _:` handles anything else and should invoke :python:`raise TypeError(result)` since no other types are expected. + +Now the function :python:`def main_loop() -> None:` is created. The :python:`while` loop from ``main`` will be moved to this function. The while loop will be replaced by :python:`while g.states:` so that this function will exit if no state exists. Drawing will be replaced by a call to ``main_draw``. -Events in the for-loop will be passed to the active state :python:`g.states[-1].on_event(event)`. -Any states ``on_event`` method could potentially change the state so ``g.states`` must be checked to be non-empty for every handled event. +Events with mouse coordinates should be converted to tiles using :python:`tile_event = g.context.convert_event(event)` before being passed to a state. +:python:`apply_state_result(g.states[-1].on_event(tile_event))` will pass the event and handle the return result at the same time. +``g.states`` must be checked to be non-empty inside the event handing for-loop because ``apply_state_result`` could cause ``g.states`` to become empty. + +Next is the utility function :python:`def get_previous_state(state: State) -> State | None:`. +Get the index of ``state`` in ``g.states`` by identity [#identity]_ using :python:`current_index = next(index for index, value in enumerate(g.states) if value is state)`. +Return the previous state if :python:`current_index > 0` or else return None using :python:`return g.states[current_index - 1] if current_index > 0 else None`. + +Next is :python:`def draw_previous_state(state: State, console: tcod.console.Console, dim: bool = True) -> None:`. +Call ``get_previous_state`` to get the previous state and return early if the result is :python:`None`. +Then call the previous states :python:`State.on_draw` method as normal. +Afterwards test :python:`dim and state is g.states[-1]` to see if the console should be dimmed. +If it should be dimmed then reduce the color values of the console with :python:`console.rgb["fg"] //= 4` and :python:`console.rgb["bg"] //= 4`. +This is used to indicate that any graphics behind the active state are non-interactable. + .. code-block:: python @@ -87,6 +177,7 @@ Any states ``on_event`` method could potentially change the state so ``g.states` import tcod.console import g + from game.state import Pop, Push, Reset, StateResult def main_draw() -> None: @@ -98,30 +189,246 @@ Any states ``on_event`` method could potentially change the state so ``g.states` g.context.present(g.console) + def apply_state_result(result: StateResult) -> None: + """Apply a StateResult to `g.states`.""" + match result: + case Push(state=state): + g.states.append(state) + case Pop(): + g.states.pop() + case Reset(state=state): + while g.states: + apply_state_result(Pop()) + apply_state_result(Push(state)) + case None: + pass + case _: + raise TypeError(result) + + def main_loop() -> None: """Run the active state forever.""" while g.states: main_draw() for event in tcod.event.wait(): + tile_event = g.context.convert_event(event) if g.states: - g.states[-1].on_event(event) + apply_state_result(g.states[-1].on_event(tile_event)) + + + def get_previous_state(state: State) -> State | None: + """Return the state before `state` in the stack if it exists.""" + current_index = next(index for index, value in enumerate(g.states) if value is state) + return g.states[current_index - 1] if current_index > 0 else None + + + def draw_previous_state(state: State, console: tcod.console.Console, dim: bool = True) -> None: + """Draw previous states, optionally dimming all but the active state.""" + prev_state = get_previous_state(state) + if prev_state is None: + return + prev_state.on_draw(console) + if dim and state is g.states[-1]: + console.rgb["fg"] //= 4 + console.rgb["bg"] //= 4 + +Menus +============================================================================== + +.. code-block:: python + + """Menu UI classes.""" + + from __future__ import annotations + + from collections.abc import Callable + from typing import Protocol + + import attrs + import tcod.console + import tcod.event + from tcod.event import KeySym + + import game.state_tools + from game.constants import DIRECTION_KEYS + from game.state import Pop, State, StateResult + + + class MenuItem(Protocol): + """Menu item protocol.""" + + __slots__ = () + + def on_event(self, event: tcod.event.Event) -> StateResult: + """Handle events passed to the menu item.""" + + def on_draw(self, console: tcod.console.Console, x: int, y: int, highlight: bool) -> None: + """Draw is item at the given position.""" + + + @attrs.define() + class SelectItem(MenuItem): + """Clickable menu item.""" + + label: str + callback: Callable[[], StateResult] + + def on_event(self, event: tcod.event.Event) -> StateResult: + """Handle events selecting this item.""" + match event: + case tcod.event.KeyDown(sym=sym) if sym in {KeySym.RETURN, KeySym.RETURN2, KeySym.KP_ENTER}: + return self.callback() + case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.LEFT): + return self.callback() + case _: + return None + + def on_draw(self, console: tcod.console.Console, x: int, y: int, highlight: bool) -> None: + """Render this items label.""" + console.print(x, y, self.label, fg=(255, 255, 255), bg=(64, 64, 64) if highlight else (0, 0, 0)) + + + @attrs.define() + class ListMenu(State): + """Simple list menu state.""" + + items: tuple[MenuItem, ...] + selected: int | None = 0 + x: int = 0 + y: int = 0 + + def on_event(self, event: tcod.event.Event) -> StateResult: + """Handle events for menus.""" + match event: + case tcod.event.Quit(): + raise SystemExit() + case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: + dx, dy = DIRECTION_KEYS[sym] + if dx != 0 or dy == 0: + return self.activate_selected(event) + if self.selected is not None: + self.selected += dy + self.selected %= len(self.items) + else: + self.selected = 0 if dy == 1 else len(self.items) - 1 + return None + case tcod.event.MouseMotion(position=(_, y)): + y -= self.y + self.selected = y if 0 <= y < len(self.items) else None + return None + case tcod.event.KeyDown(sym=KeySym.ESCAPE): + return self.on_cancel() + case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.RIGHT): + return self.on_cancel() + case _: + return self.activate_selected(event) + + def activate_selected(self, event: tcod.event.Event) -> StateResult: + """Call the selected menu items callback.""" + if self.selected is not None: + return self.items[self.selected].on_event(event) + return None + + def on_cancel(self) -> StateResult: + """Handle escape or right click being pressed on menus.""" + return Pop() + + def on_draw(self, console: tcod.console.Console) -> None: + """Render the menu.""" + game.state_tools.draw_previous_state(self, console) + for i, item in enumerate(self.items): + item.on_draw(console, x=self.x, y=self.y + i, highlight=i == self.selected) + +Update states +============================================================================== + +.. code-block:: python + + class MainMenu(game.menus.ListMenu): + """Main/escape menu.""" + + __slots__ = () + + def __init__(self) -> None: + """Initialize the main menu.""" + items = [ + game.menus.SelectItem("New game", self.new_game), + game.menus.SelectItem("Quit", self.quit), + ] + if hasattr(g, "world"): + items.insert(0, game.menus.SelectItem("Continue", self.continue_)) + + super().__init__( + items=tuple(items), + selected=0, + x=5, + y=5, + ) + + @staticmethod + def continue_() -> StateResult: + """Return to the game.""" + return Reset(InGame()) + + @staticmethod + def new_game() -> StateResult: + """Begin a new game.""" + g.world = game.world_tools.new_world() + return Reset(InGame()) + + @staticmethod + def quit() -> StateResult: + """Close the program.""" + raise SystemExit() + +.. code-block:: python + :emphasize-lines: 2,5,19-23 + + @attrs.define() + class InGame(State): + """Primary in-game state.""" + + def on_event(self, event: tcod.event.Event) -> StateResult: + """Handle events for the in-game state.""" + (player,) = g.world.Q.all_of(tags=[IsPlayer]) + match event: + case tcod.event.Quit(): + raise SystemExit() + case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: + player.components[Position] += DIRECTION_KEYS[sym] + # Auto pickup gold + for gold in g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]): + player.components[Gold] += gold.components[Gold] + text = f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g" + g.world[None].components[("Text", str)] = text + gold.clear() + return None + case tcod.event.KeyDown(sym=KeySym.ESCAPE): + return Push(MainMenu()) + case _: + return None + + ... + +Update main.py +============================================================================== Now ``main.py`` can be edited to use the global variables and the new game loop. Add :python:`import g` and :python:`import game.state_tools`. Replace references to ``console`` with ``g.console``. -Replace references to ``context`` with ``g.context``. States are initialed by assigning a list with the initial state to ``g.states``. The previous game loop is replaced by a call to :python:`game.state_tools.main_loop()`. .. code-block:: python - :emphasize-lines: 3-4,12-15 + :emphasize-lines: 3-4,13-16 ... import g import game.state_tools + import game.states def main() -> None: """Entry point function.""" @@ -130,7 +437,7 @@ The previous game loop is replaced by a call to :python:`game.state_tools.main_l ) tcod.tileset.procedural_block_elements(tileset=tileset) g.console = tcod.console.Console(80, 50) - g.states = [ExampleState(player_x=console.width // 2, player_y=console.height // 2)] + g.states = [game.states.MainMenu()] with tcod.context.new(console=g.console, tileset=tileset) as g.context: game.state_tools.main_loop() ... @@ -138,10 +445,15 @@ The previous game loop is replaced by a call to :python:`game.state_tools.main_l After this you can test the game. There should be no visible differences from before. +You can review the part-3 source code `here `_. .. rubric:: Footnotes .. [#slots] This is done to prevent subclasses from requiring a ``__dict__`` attribute. - If you are still wondering what ``__slots__`` is then `the Python docs have a detailed explanation `_. + See :any:`slots` for a detailed explanation of what they are. + +.. [#identity] See :any:`is`. + Since ``State`` classes use ``attrs`` they might compare equal when they're not the same object. + This means :python:`list.index` won't work for this case. .. _Protocol: https://mypy.readthedocs.io/en/stable/protocols.html From 070cb120cdb99e8f2478b95b76c77b927a301143 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 26 Jun 2024 00:33:19 -0700 Subject: [PATCH 079/134] Tutorial: finish part 2 backports DIRECTION_KEYS moved to constants.py module --- docs/tutorial/part-02.rst | 55 +++++++++++---------------------------- 1 file changed, 15 insertions(+), 40 deletions(-) diff --git a/docs/tutorial/part-02.rst b/docs/tutorial/part-02.rst index 90b20625..fa4467a9 100644 --- a/docs/tutorial/part-02.rst +++ b/docs/tutorial/part-02.rst @@ -316,17 +316,21 @@ New InGame state Now there is a new ECS world but the example state does not know how to render it. A new state needs to be made which is aware of the new entities. -Create a new script called ``game/states.py``. -``states`` is for derived classes, ``state`` is for the abstract class. -New states will be created in this module and this module will be allowed to import many first party modules without issues. - Before adding a new state it is time to add a more complete set of directional keys. -These will be added as a dictionary and can be reused anytime we want to know how a key translates to a direction. +Create a new module called ``game/constants.py``. +Keys will be mapped to direction using a dictionary which can be reused anytime we want to know how a key translates to a direction. Use :python:`from tcod.event import KeySym` to make ``KeySym`` enums easier to write. -Then add the following: + +``game/constants.py`` should look like this: .. code-block:: python + """Global constants are stored here.""" + + from typing import Final + + from tcod.event import KeySym + DIRECTION_KEYS: Final = { # Arrow keys KeySym.LEFT: (-1, 0), @@ -358,6 +362,10 @@ Then add the following: KeySym.n: (1, 1), } +Create a new module called ``game/states.py``. +``states`` is for derived classes, ``state`` is for the abstract class. +New states will be created in this module and this module will be allowed to import many first party modules without issues. + Create a new :python:`class InGame:` decorated with :python:`@attrs.define()`. States will always use ``g.world`` to access the ECS registry. @@ -472,48 +480,15 @@ It should be at the same level as the ``for`` loop and not inside of it. from __future__ import annotations - from typing import Final - import attrs import tcod.console import tcod.event - from tcod.event import KeySym import g from game.components import Gold, Graphic, Position + from game.constants import DIRECTION_KEYS from game.tags import IsItem, IsPlayer - DIRECTION_KEYS: Final = { - # Arrow keys - KeySym.LEFT: (-1, 0), - KeySym.RIGHT: (1, 0), - KeySym.UP: (0, -1), - KeySym.DOWN: (0, 1), - # Arrow key diagonals - KeySym.HOME: (-1, -1), - KeySym.END: (-1, 1), - KeySym.PAGEUP: (1, -1), - KeySym.PAGEDOWN: (1, 1), - # Keypad - KeySym.KP_4: (-1, 0), - KeySym.KP_6: (1, 0), - KeySym.KP_8: (0, -1), - KeySym.KP_2: (0, 1), - KeySym.KP_7: (-1, -1), - KeySym.KP_1: (-1, 1), - KeySym.KP_9: (1, -1), - KeySym.KP_3: (1, 1), - # VI keys - KeySym.h: (-1, 0), - KeySym.l: (1, 0), - KeySym.k: (0, -1), - KeySym.j: (0, 1), - KeySym.y: (-1, -1), - KeySym.b: (-1, 1), - KeySym.u: (1, -1), - KeySym.n: (1, 1), - } - @attrs.define() class InGame: From 6a396e6c1a647cefa210b503b2604eb9dc815153 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 26 Jun 2024 01:15:20 -0700 Subject: [PATCH 080/134] Update CI workflows --- .github/workflows/python-package.yml | 98 ++++++++++++++++------------ .github/workflows/release-on-tag.yml | 2 +- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 844f451a..0f545dcc 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -19,7 +19,7 @@ jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Ruff run: pip install ruff - name: Ruff Check @@ -30,13 +30,13 @@ jobs: mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Checkout submodules run: git submodule update --init --recursive --depth 1 - name: Install typing dependencies run: pip install mypy pytest -r requirements.txt - name: Mypy - uses: liskin/gh-problem-matcher-wrap@v2 + uses: liskin/gh-problem-matcher-wrap@v3 with: linters: mypy run: mypy --show-column-numbers @@ -48,7 +48,7 @@ jobs: run: sudo apt-get update - name: Install APT dependencies run: sudo apt-get install libsdl2-dev - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: ${{ env.git-depth }} - name: Checkout submodules @@ -57,11 +57,12 @@ jobs: run: pip install build - name: Build source distribution run: python -m build --sdist - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: sdist path: dist/tcod-*.tar.gz retention-days: 7 + compression-level: 0 # This makes sure that the latest versions of the SDL headers parse correctly. parse_sdl: @@ -73,12 +74,12 @@ jobs: sdl-version: ["2.0.14", "2.0.16"] fail-fast: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: ${{ env.git-depth }} - name: Checkout submodules run: git submodule update --init --recursive --depth 1 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install build dependencies @@ -94,7 +95,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest", "windows-latest"] - python-version: ["3.8", "3.9", "pypy-3.8"] + python-version: ["3.8", "pypy-3.8"] architecture: ["x64"] include: - os: "windows-latest" @@ -106,14 +107,14 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: ${{ env.git-depth }} - name: Checkout submodules run: | git submodule update --init --recursive --depth 1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} @@ -144,13 +145,16 @@ jobs: - name: Xvfb logs if: runner.os != 'Windows' run: cat /tmp/xvfb.log - - uses: codecov/codecov-action@v3 - - uses: actions/upload-artifact@v3 + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + - uses: actions/upload-artifact@v4 if: runner.os == 'Windows' with: - name: wheels-windows + name: wheels-windows-${{ matrix.architecture }}-${{ matrix.python-version }} path: dist/*.whl retention-days: 7 + compression-level: 0 test-docs: needs: [ruff, mypy] @@ -160,7 +164,7 @@ jobs: run: | sudo apt-get update sudo apt-get install libsdl2-dev - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: ${{ env.git-depth }} - name: Checkout submodules @@ -184,13 +188,13 @@ jobs: matrix: os: ["ubuntu-latest", "windows-latest"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: ${{ env.git-depth }} - name: Checkout submodules run: git submodule update --init --depth 1 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install Python dependencies @@ -212,18 +216,20 @@ jobs: matrix: arch: ["x86_64", "aarch64"] build: ["cp38-manylinux*", "pp38-manylinux*"] + env: + BUILD_DESC: steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: ${{ env.git-depth }} - name: Set up QEMU if: ${{ matrix.arch == 'aarch64' }} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Checkout submodules run: | git submodule update --init --recursive --depth 1 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install Python dependencies @@ -247,33 +253,44 @@ jobs: CIBW_TEST_COMMAND: python -c "import tcod.context" # Skip test on emulated architectures CIBW_TEST_SKIP: "*_aarch64" + - name: Remove asterisk from label + run: | + BUILD_DESC=${{ matrix.build }} + BUILD_DESC=${BUILD_DESC//\*} + echo BUILD_DESC=${BUILD_DESC} >> $GITHUB_ENV - name: Archive wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheels-linux + name: wheels-linux-${{ matrix.arch }}-${{ env.BUILD_DESC }} path: wheelhouse/*.whl retention-days: 7 + compression-level: 0 build-macos: needs: [ruff, mypy] - runs-on: "macos-11" + runs-on: "macos-14" strategy: fail-fast: true matrix: - python: ["cp38-*_universal2", "cp38-*_x86_64", "cp38-*_arm64", "pp38-*"] + python: ["cp38-*_universal2", "cp38-*_x86_64", "pp38-*"] + env: + PYTHON_DESC: steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: ${{ env.git-depth }} - name: Checkout submodules run: git submodule update --init --recursive --depth 1 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" - name: Install Python dependencies - run: pip3 install -r requirements.txt + run: pip install -r requirements.txt - name: Prepare package # Downloads SDL2 for the later step. - run: python3 build_sdl.py + run: python build_sdl.py - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_BUILD: ${{ matrix.python }} CIBW_ARCHS_MACOS: x86_64 arm64 universal2 @@ -282,12 +299,18 @@ jobs: CIBW_TEST_COMMAND: python -c "import tcod.context" CIBW_TEST_SKIP: "pp* *-macosx_arm64 *-macosx_universal2:arm64" MACOSX_DEPLOYMENT_TARGET: "10.11" + - name: Remove asterisk from label + run: | + PYTHON_DESC=${{ matrix.python }} + PYTHON_DESC=${PYTHON_DESC//\*/X} + echo PYTHON_DESC=${PYTHON_DESC} >> $GITHUB_ENV - name: Archive wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheels-macos + name: wheels-macos-${{ env.PYTHON_DESC }} path: wheelhouse/*.whl retention-days: 7 + compression-level: 0 publish: needs: [sdist, build, build-macos, linux-wheels] @@ -295,26 +318,19 @@ jobs: if: startsWith(github.ref, 'refs/tags/') environment: name: release - url: https://pypi.org/p/tcod + url: https://pypi.org/project/tcod/ permissions: id-token: write steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: sdist path: dist/ - - uses: actions/download-artifact@v3 - with: - name: wheels-windows - path: dist/ - - uses: actions/download-artifact@v3 - with: - name: wheels-macos - path: dist/ - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: wheels-linux + pattern: wheels-* path: dist/ + merge-multiple: true - uses: pypa/gh-action-pypi-publish@release/v1 with: skip-existing: true diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index 95cf02d3..2171ddbf 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -13,7 +13,7 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Generate body run: | scripts/get_release_description.py | tee release_body.md From b92d433e81fd9b2a14f25cdab6107095a67eae84 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 26 Jun 2024 03:17:36 -0700 Subject: [PATCH 081/134] Add spelling --- .vscode/settings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 73a2b81e..07db160c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,6 +16,7 @@ "*.spec": "python", }, "cSpell.words": [ + "aarch", "ADDA", "ADDALPHA", "addressof", @@ -135,6 +136,7 @@ "DVLINE", "elif", "endianness", + "epel", "epub", "EQUALSAS", "errorvf", @@ -165,6 +167,7 @@ "greyscale", "groupwise", "guass", + "hasattr", "heapify", "heightmap", "heightmaps", @@ -402,6 +405,7 @@ "sourcelink", "sphinxstrong", "sphinxtitleref", + "staticmethod", "stdeb", "struct", "structs", From 9164b223dc6c7d9b8b8add7d68ad0c775bb1e968 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 20:57:24 +0000 Subject: [PATCH 082/134] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.3 → v0.5.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.3...v0.5.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8411e3fa..d075967a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.3 + rev: v0.5.1 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From efbd2641ca0824d2bde6a1cc9b70644c01dc30ee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 20:57:31 +0000 Subject: [PATCH 083/134] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index f66b990d..d4edf824 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -72,7 +72,7 @@ # # The full version, including alpha/beta/rc tags. git_describe = subprocess.run( - ["git", "describe", "--abbrev=0"], # noqa: S603, S607 + ["git", "describe", "--abbrev=0"], # noqa: S607 stdout=subprocess.PIPE, text=True, check=True, From a1c84ccd615f1630bac538bfe20bf5e85444fdd4 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 9 Jul 2024 06:49:08 -0700 Subject: [PATCH 084/134] Set pre-commit update schedule --- .pre-commit-config.yaml | 4 ++-- .vscode/settings.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d075967a..03143e06 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,7 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +ci: + autoupdate_schedule: quarterly repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 @@ -20,5 +22,3 @@ repos: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] - id: ruff-format -default_language_version: - python: python3.12 diff --git a/.vscode/settings.json b/.vscode/settings.json index 07db160c..fc7c18f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,6 +46,7 @@ "autofunction", "autogenerated", "automodule", + "autoupdate", "backlinks", "bdist", "Benesch", From b2de7747c7b7f75d25f026dea4dec538f55351d5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 9 Jul 2024 07:01:20 -0700 Subject: [PATCH 085/134] Update cibuildwheel --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0f545dcc..bfb4b7e4 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -235,7 +235,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install cibuildwheel==2.16.0 + pip install cibuildwheel==2.19.2 - name: Build wheels run: | python -m cibuildwheel --output-dir wheelhouse @@ -290,7 +290,7 @@ jobs: # Downloads SDL2 for the later step. run: python build_sdl.py - name: Build wheels - uses: pypa/cibuildwheel@v2.19.1 + uses: pypa/cibuildwheel@v2.19.2 env: CIBW_BUILD: ${{ matrix.python }} CIBW_ARCHS_MACOS: x86_64 arm64 universal2 From ce2027e209ebdb784c4e95d7926d3a74aff9c95e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:29:12 +0000 Subject: [PATCH 086/134] Bump setuptools from 65.5.1 to 70.0.0 Bumps [setuptools](https://github.com/pypa/setuptools) from 65.5.1 to 70.0.0. - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/setuptools/compare/v65.5.1...v70.0.0) --- updated-dependencies: - dependency-name: setuptools dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/requirements.txt b/requirements.txt index a94c3862..03ad70a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -attrs>=23.1.0 -cffi>=1.15 -numpy>=1.21.4 -pycparser>=2.14 -requests>=2.28.1 -setuptools==65.5.1 -types-cffi -types-requests -types-Pillow -types-setuptools -types-tabulate -typing_extensions -pcpp==1.30 +attrs>=23.1.0 +cffi>=1.15 +numpy>=1.21.4 +pycparser>=2.14 +requests>=2.28.1 +setuptools==70.0.0 +types-cffi +types-requests +types-Pillow +types-setuptools +types-tabulate +typing_extensions +pcpp==1.30 From 793b54151ed69a91e9277a7bddd35e9426cad92c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 16 Jul 2024 15:09:50 -0700 Subject: [PATCH 087/134] Fix image load NULL dereference and verify the existence of more files --- CHANGELOG.md | 2 ++ tcod/image.py | 9 ++++++++- tcod/libtcodpy.py | 7 ++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10bed531..2c82939c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ### Fixed - Fixed access violation when events are polled before SDL is initialized. +- Fixed access violation when libtcod images fail to load. +- Verify input files exist when calling `libtcodpy.parser_run`, `libtcodpy.namegen_parse`, `tcod.image.load`. ## [16.2.2] - 2024-01-16 diff --git a/tcod/image.py b/tcod/image.py index 38fe3e4d..7b2ee13d 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -39,10 +39,16 @@ def __init__(self, width: int, height: int) -> None: """Initialize a blank image.""" self.width, self.height = width, height self.image_c = ffi.gc(lib.TCOD_image_new(width, height), lib.TCOD_image_delete) + if self.image_c == ffi.NULL: + msg = "Failed to allocate image." + raise MemoryError(msg) @classmethod def _from_cdata(cls, cdata: Any) -> Image: # noqa: ANN401 self: Image = object.__new__(cls) + if cdata == ffi.NULL: + msg = "Pointer must not be NULL." + raise RuntimeError(msg) self.image_c = cdata self.width, self.height = self._get_size() return self @@ -365,7 +371,8 @@ def load(filename: str | PathLike[str]) -> NDArray[np.uint8]: .. versionadded:: 11.4 """ - image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(_path_encode(Path(filename))), lib.TCOD_image_delete)) + filename = Path(filename).resolve(strict=True) + image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(_path_encode(filename)), lib.TCOD_image_delete)) array: NDArray[np.uint8] = np.asarray(image, dtype=np.uint8) height, width, depth = array.shape if depth == 3: # noqa: PLR2004 diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index dd23de45..004053d8 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -3434,7 +3434,7 @@ def mouse_get_status() -> Mouse: @pending_deprecate() def namegen_parse(filename: str | PathLike[str], random: tcod.random.Random | None = None) -> None: - lib.TCOD_namegen_parse(_path_encode(Path(filename)), random or ffi.NULL) + lib.TCOD_namegen_parse(_path_encode(Path(filename).resolve(strict=True)), random or ffi.NULL) @pending_deprecate() @@ -3636,8 +3636,9 @@ def _pycall_parser_error(msg: Any) -> None: @deprecate("Parser functions have been deprecated.") def parser_run(parser: Any, filename: str | PathLike[str], listener: Any = None) -> None: global _parser_listener # noqa: PLW0603 + filename = Path(filename).resolve(strict=True) if not listener: - lib.TCOD_parser_run(parser, _path_encode(Path(filename)), ffi.NULL) + lib.TCOD_parser_run(parser, _path_encode(filename), ffi.NULL) return propagate_manager = _PropagateException() @@ -3656,7 +3657,7 @@ def parser_run(parser: Any, filename: str | PathLike[str], listener: Any = None) with _parser_callback_lock: _parser_listener = listener with propagate_manager: - lib.TCOD_parser_run(parser, _path_encode(Path(filename)), c_listener) + lib.TCOD_parser_run(parser, _path_encode(filename), c_listener) @deprecate("libtcod objects are deleted automatically.") From 49dea990349cb51be2a8c81322c53bdaed8cae33 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 16 Jul 2024 15:20:12 -0700 Subject: [PATCH 088/134] pre-commit update --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 03143e06..a312e948 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.1 + rev: v0.5.2 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 59ec79731507eec5d50dbada3ee766c4d3312621 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 16 Jul 2024 15:28:06 -0700 Subject: [PATCH 089/134] Prepare 16.2.3 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c82939c..9b5a2551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +## [16.2.3] - 2024-07-16 + ### Fixed - Fixed access violation when events are polled before SDL is initialized. From 350b6c7ec5c516e704c2dc69d4e7836ef3707bb8 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 16 Jul 2024 16:12:12 -0700 Subject: [PATCH 090/134] Update PyPI deployment Clean up ref_type check and add specific release URL --- .github/workflows/python-package.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bfb4b7e4..38d468c9 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -315,10 +315,10 @@ jobs: publish: needs: [sdist, build, build-macos, linux-wheels] runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') + if: github.ref_type == 'tag' environment: - name: release - url: https://pypi.org/project/tcod/ + name: pypi + url: https://pypi.org/project/tcod/${{ github.ref_name }} permissions: id-token: write steps: From 98093c1cda7d958c2c2b629e82b98b1954ce2776 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 16 Jul 2024 16:15:15 -0700 Subject: [PATCH 091/134] Skip macOS x86_64 wheel Redundant with the universal2 releases --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 38d468c9..0417777c 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -272,7 +272,7 @@ jobs: strategy: fail-fast: true matrix: - python: ["cp38-*_universal2", "cp38-*_x86_64", "pp38-*"] + python: ["cp38-*_universal2", "pp38-*"] env: PYTHON_DESC: steps: From 3bc0ce0b16787dcd613c66d9f647a9bdd7a7483e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 16 Jul 2024 16:15:27 -0700 Subject: [PATCH 092/134] Clean up minor formatting issues with the tutorial --- docs/tutorial/part-01.rst | 12 ++++++------ docs/tutorial/part-02.rst | 8 +++++--- docs/tutorial/part-03.rst | 6 +++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/tutorial/part-01.rst b/docs/tutorial/part-01.rst index d4216982..b18b7531 100644 --- a/docs/tutorial/part-01.rst +++ b/docs/tutorial/part-01.rst @@ -94,7 +94,7 @@ Use the code :python:`for event in tcod.event.wait():` to begin handing events. In the event loop start with the line :python:`print(event)` so that all events can be viewed from the program output. Then test if an event is for closing the window with :python:`if isinstance(event, tcod.event.Quit):`. -If this is True then you should exit the function with :python:`raise SystemExit()`. [#why_raise]_ +If this is True then you should exit the function with :python:`raise SystemExit`. [#why_raise]_ .. code-block:: python :emphasize-lines: 3,5,15-23 @@ -121,7 +121,7 @@ If this is True then you should exit the function with :python:`raise SystemExit for event in tcod.event.wait(): # Event loop, blocks until pending events exist print(event) if isinstance(event, tcod.event.Quit): - raise SystemExit() + raise SystemExit if __name__ == "__main__": @@ -205,7 +205,7 @@ Modify the drawing routine so that the console is cleared, then passed to :pytho for event in tcod.event.wait(): print(event) if isinstance(event, tcod.event.Quit): - raise SystemExit() + raise SystemExit if __name__ == "__main__": @@ -259,7 +259,7 @@ The full script so far is: """Move the player on events and handle exiting. Movement is hard-coded.""" match event: case tcod.event.Quit(): - raise SystemExit() + raise SystemExit case tcod.event.KeyDown(sym=tcod.event.KeySym.LEFT): self.player_x -= 1 case tcod.event.KeyDown(sym=tcod.event.KeySym.RIGHT): @@ -308,5 +308,5 @@ You can review the part-1 source code `here StateResult: """Close the program.""" - raise SystemExit() + raise SystemExit .. code-block:: python :emphasize-lines: 2,5,19-23 @@ -393,7 +393,7 @@ Update states (player,) = g.world.Q.all_of(tags=[IsPlayer]) match event: case tcod.event.Quit(): - raise SystemExit() + raise SystemExit case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: player.components[Position] += DIRECTION_KEYS[sym] # Auto pickup gold From 4b25bc4543b0dfb5d66509547ffa9fd401ed4384 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 16 Jul 2024 16:50:29 -0700 Subject: [PATCH 093/134] Fix Ruff warnings, update Mypy strategy to use local version --- .vscode/settings.json | 1 + tcod/console.py | 3 ++- tcod/context.py | 3 ++- tcod/libtcodpy.py | 15 ++++++++------- tcod/map.py | 4 +++- tcod/noise.py | 1 + tcod/path.py | 10 ++++++---- tcod/render.py | 2 ++ tcod/tileset.py | 11 +++++++---- tests/test_libtcodpy.py | 4 +--- 10 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fc7c18f2..a6f96553 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,7 @@ "files.associations": { "*.spec": "python", }, + "mypy-type-checker.importStrategy": "fromEnvironment", "cSpell.words": [ "aarch", "ADDA", diff --git a/tcod/console.py b/tcod/console.py index c6767b59..c38a4de7 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -120,6 +120,7 @@ def __init__( order: Literal["C", "F"] = "C", buffer: NDArray[Any] | None = None, ) -> None: + """Initialize the console.""" self._key_color: tuple[int, int, int] | None = None self._order = tcod._internal.verify_order(order) if buffer is not None: @@ -930,7 +931,7 @@ def __repr__(self) -> str: def __str__(self) -> str: """Return a simplified representation of this consoles contents.""" - return "<%s>" % "\n ".join("".join(chr(c) for c in line) for line in self._tiles["ch"]) + return "<{}>".format("\n ".join("".join(chr(c) for c in line) for line in self._tiles["ch"])) def print( # noqa: PLR0913 self, diff --git a/tcod/context.py b/tcod/context.py index f22c78de..3b3e8e0a 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -344,7 +344,8 @@ def new_console( scale = max(1, scale + event.y) """ if magnification < 0: - raise ValueError("Magnification must be greater than zero. (Got %f)" % magnification) + msg = f"Magnification must be greater than zero. (Got {magnification:f})" + raise ValueError(msg) size = ffi.new("int[2]") _check(lib.TCOD_context_recommended_console_size(self._p, magnification, size, size + 1)) width, height = max(min_columns, size[0]), max(min_rows, size[1]) diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 004053d8..feaa0d52 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -317,7 +317,7 @@ def nb_dices(self, value: int) -> None: self.nb_rolls = value def __str__(self) -> str: - add = "+(%s)" % self.addsub if self.addsub != 0 else "" + add = f"+({self.addsub})" if self.addsub != 0 else "" return "%id%ix%s%s" % ( self.nb_dices, self.nb_faces, @@ -330,7 +330,7 @@ def __repr__(self) -> str: # reverse lookup table for KEY_X attributes, used by Key.__repr__ -_LOOKUP_VK = {value: "KEY_%s" % key[6:] for key, value in lib.__dict__.items() if key.startswith("TCODK")} +_LOOKUP_VK = {value: f"KEY_{key[6:]}" for key, value in lib.__dict__.items() if key.startswith("TCODK")} class Key(_CDataWrapper): @@ -418,9 +418,9 @@ def __repr__(self) -> str: params = [] params.append(f"pressed={self.pressed!r}, vk=tcod.{_LOOKUP_VK[self.vk]}") if self.c: - params.append("c=ord(%r)" % chr(self.c)) + params.append(f"c=ord({chr(self.c)!r})") if self.text: - params.append("text=%r" % self.text) + params.append(f"text={self.text!r}") for attr in [ "shift", "lalt", @@ -432,7 +432,7 @@ def __repr__(self) -> str: ]: if getattr(self, attr): params.append(f"{attr}={getattr(self, attr)!r}") - return "tcod.Key(%s)" % ", ".join(params) + return "tcod.Key({})".format(", ".join(params)) @property def key_p(self) -> Any: @@ -510,7 +510,7 @@ def __repr__(self) -> str: ]: if getattr(self, attr): params.append(f"{attr}={getattr(self, attr)!r}") - return "tcod.Mouse(%s)" % ", ".join(params) + return "tcod.Mouse({})".format(", ".join(params)) @property def mouse_p(self) -> Any: @@ -2361,7 +2361,8 @@ def _heightmap_cdata(array: NDArray[np.float32]) -> ffi.CData: msg = "array must be a contiguous segment." raise ValueError(msg) if array.dtype != np.float32: - raise ValueError("array dtype must be float32, not %r" % array.dtype) + msg = f"array dtype must be float32, not {array.dtype!r}" + raise ValueError(msg) height, width = array.shape pointer = ffi.from_buffer("float *", array) return ffi.new("TCOD_heightmap_t *", (width, height, pointer)) diff --git a/tcod/map.py b/tcod/map.py index 68f4a04b..488c4878 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -76,6 +76,7 @@ def __init__( height: int, order: Literal["C", "F"] = "C", ) -> None: + """Initialize the map.""" warnings.warn( "This class may perform poorly and is no longer needed.", DeprecationWarning, @@ -234,7 +235,8 @@ def compute_fov( """ transparency = np.asarray(transparency) if len(transparency.shape) != 2: # noqa: PLR2004 - raise TypeError("transparency must be an array of 2 dimensions" " (shape is %r)" % transparency.shape) + msg = f"transparency must be an array of 2 dimensions (shape is {transparency.shape!r})" + raise TypeError(msg) if isinstance(pov, int): msg = "The tcod.map.compute_fov function has changed. The `x` and `y` parameters should now be given as a single tuple." raise TypeError(msg) diff --git a/tcod/noise.py b/tcod/noise.py index 8d9dc3ff..108fa2c0 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -133,6 +133,7 @@ def __init__( # noqa: PLR0913 octaves: float = 4, seed: int | tcod.random.Random | None = None, ) -> None: + """Initialize and seed the noise object.""" if not 0 < dimensions <= 4: # noqa: PLR2004 msg = f"dimensions must be in range 0 < n <= 4, got {dimensions}" raise ValueError(msg) diff --git a/tcod/path.py b/tcod/path.py index 8d9fc5db..8950505f 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -591,7 +591,7 @@ def _world_array(shape: tuple[int, ...], dtype: DTypeLike = np.int32) -> NDArray ) -def _as_hashable(obj: np.ndarray[Any, Any] | None) -> Any | None: +def _as_hashable(obj: np.ndarray[Any, Any] | None) -> object | None: """Return NumPy arrays as a more hashable form.""" if obj is None: return obj @@ -661,6 +661,7 @@ class CustomGraph: """ def __init__(self, shape: tuple[int, ...], *, order: str = "C") -> None: + """Initialize the custom graph.""" self._shape = self._shape_c = tuple(shape) self._ndim = len(self._shape) self._order = order @@ -894,8 +895,7 @@ def add_edges( edge_array = np.transpose(edge_nz) edge_array -= edge_center for edge, edge_cost in zip(edge_array, edge_costs): - edge = tuple(edge) - self.add_edge(edge, edge_cost, cost=cost, condition=condition) + self.add_edge(tuple(edge), edge_cost, cost=cost, condition=condition) def set_heuristic(self, *, cardinal: int = 0, diagonal: int = 0, z: int = 0, w: int = 0) -> None: """Set a pathfinder heuristic so that pathfinding can done with A*. @@ -1028,6 +1028,7 @@ class SimpleGraph: """ def __init__(self, *, cost: ArrayLike, cardinal: int, diagonal: int, greed: int = 1) -> None: + """Initialize the graph.""" cost = np.asarray(cost) if cost.ndim != 2: # noqa: PLR2004 msg = f"The cost array must e 2 dimensional, array of shape {cost.shape!r} given." @@ -1087,6 +1088,7 @@ class Pathfinder: """ def __init__(self, graph: CustomGraph | SimpleGraph) -> None: + """Initialize the pathfinder from a graph.""" self._graph = graph self._order = graph._order self._frontier_p = ffi.gc(lib.TCOD_frontier_new(self._graph._ndim), lib.TCOD_frontier_delete) @@ -1273,7 +1275,7 @@ def resolve(self, goal: tuple[int, ...] | None = None) -> None: if self._order == "F": # Goal is now ij indexed for the rest of this function. goal = goal[::-1] - if self._distance[goal] != np.iinfo(self._distance.dtype).max: + if self._distance[goal] != np.iinfo(self._distance.dtype).max: # noqa: SIM102 if not lib.frontier_has_index(self._frontier_p, goal): return self._update_heuristic(goal) diff --git a/tcod/render.py b/tcod/render.py index 80558dfb..09d3a137 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -42,6 +42,7 @@ class SDLTilesetAtlas: """Prepares a tileset for rendering using SDL.""" def __init__(self, renderer: tcod.sdl.render.Renderer, tileset: tcod.tileset.Tileset) -> None: + """Initialize the tileset atlas.""" self._renderer = renderer self.tileset: Final[tcod.tileset.Tileset] = tileset """The tileset used to create this SDLTilesetAtlas.""" @@ -64,6 +65,7 @@ class SDLConsoleRender: """Holds an internal cache console and texture which are used to optimized console rendering.""" def __init__(self, atlas: SDLTilesetAtlas) -> None: + """Initialize the console renderer.""" self.atlas: Final[SDLTilesetAtlas] = atlas """The SDLTilesetAtlas used to create this SDLConsoleRender. diff --git a/tcod/tileset.py b/tcod/tileset.py index 846aed32..022c57ac 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -22,7 +22,7 @@ from numpy.typing import ArrayLike, NDArray import tcod.console -from tcod._internal import _check, _console, _path_encode, _raise_tcod_error, deprecate +from tcod._internal import _check, _check_p, _console, _path_encode, _raise_tcod_error, deprecate from tcod.cffi import ffi, lib @@ -33,9 +33,12 @@ class Tileset: """ def __init__(self, tile_width: int, tile_height: int) -> None: - self._tileset_p = ffi.gc( - lib.TCOD_tileset_new(tile_width, tile_height), - lib.TCOD_tileset_delete, + """Initialize the tileset.""" + self._tileset_p = _check_p( + ffi.gc( + lib.TCOD_tileset_new(tile_width, tile_height), + lib.TCOD_tileset_delete, + ) ) @classmethod diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py index 676ab2bc..76762c8f 100644 --- a/tests/test_libtcodpy.py +++ b/tests/test_libtcodpy.py @@ -671,9 +671,7 @@ def map_() -> Iterator[tcod.map.Map]: @pytest.fixture() def path_callback(map_: tcod.map.Map) -> Callable[[int, int, int, int, None], bool]: def callback(ox: int, oy: int, dx: int, dy: int, user_data: None) -> bool: - if map_.walkable[dy, dx]: - return True - return False + return bool(map_.walkable[dy, dx]) return callback From 233e4a72803aebfba6ad8b0e3b350e7d27f7d2ea Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 16 Jul 2024 17:03:44 -0700 Subject: [PATCH 094/134] Update deprecations to a more standard syntax --- tcod/bsp.py | 9 +++++---- tcod/console.py | 24 ++++++++++++------------ tcod/context.py | 8 ++++---- tcod/image.py | 6 +++--- tcod/libtcodpy.py | 14 +++----------- tcod/random.py | 7 ++++--- tcod/tileset.py | 9 +++++---- 7 files changed, 36 insertions(+), 41 deletions(-) diff --git a/tcod/bsp.py b/tcod/bsp.py index 030bb04b..98783685 100644 --- a/tcod/bsp.py +++ b/tcod/bsp.py @@ -29,8 +29,9 @@ from typing import Any, Iterator +from typing_extensions import deprecated + import tcod.random -from tcod._internal import deprecate from tcod.cffi import ffi, lib @@ -72,7 +73,7 @@ def __init__(self, x: int, y: int, width: int, height: int) -> None: self.children: tuple[()] | tuple[BSP, BSP] = () @property - @deprecate("This attribute has been renamed to `width`.", FutureWarning) + @deprecated("This attribute has been renamed to `width`.", category=FutureWarning) def w(self) -> int: # noqa: D102 return self.width @@ -81,7 +82,7 @@ def w(self, value: int) -> None: self.width = value @property - @deprecate("This attribute has been renamed to `height`.", FutureWarning) + @deprecated("This attribute has been renamed to `height`.", category=FutureWarning) def h(self) -> int: # noqa: D102 return self.height @@ -177,7 +178,7 @@ def split_recursive( # noqa: PLR0913 ) self._unpack_bsp_tree(cdata) - @deprecate("Use pre_order method instead of walk.") + @deprecated("Use pre_order method instead of walk.") def walk(self) -> Iterator[BSP]: """Iterate over this BSP's hierarchy in pre order. diff --git a/tcod/console.py b/tcod/console.py index c38a4de7..8dc7309b 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -14,11 +14,11 @@ import numpy as np from numpy.typing import ArrayLike, NDArray -from typing_extensions import Literal +from typing_extensions import Literal, deprecated import tcod._internal import tcod.constants -from tcod._internal import _check, _path_encode, deprecate +from tcod._internal import _check, _path_encode from tcod.cffi import ffi, lib @@ -248,7 +248,7 @@ def ch(self) -> NDArray[np.intc]: return self._tiles["ch"].T if self._order == "F" else self._tiles["ch"] @property - @deprecate("This attribute has been renamed to `rgba`.", category=FutureWarning) + @deprecated("This attribute has been renamed to `rgba`.", category=FutureWarning) def tiles(self) -> NDArray[Any]: """An array of this consoles raw tile data. @@ -264,7 +264,7 @@ def tiles(self) -> NDArray[Any]: return self.rgba @property - @deprecate("This attribute has been renamed to `rgba`.", category=FutureWarning) + @deprecated("This attribute has been renamed to `rgba`.", category=FutureWarning) def buffer(self) -> NDArray[Any]: """An array of this consoles raw tile data. @@ -276,7 +276,7 @@ def buffer(self) -> NDArray[Any]: return self.rgba @property - @deprecate("This attribute has been renamed to `rgb`.", category=FutureWarning) + @deprecated("This attribute has been renamed to `rgb`.", category=FutureWarning) def tiles_rgb(self) -> NDArray[Any]: """An array of this consoles data without the alpha channel. @@ -288,7 +288,7 @@ def tiles_rgb(self) -> NDArray[Any]: return self.rgb @property - @deprecate("This attribute has been renamed to `rgb`.", category=FutureWarning) + @deprecated("This attribute has been renamed to `rgb`.", category=FutureWarning) def tiles2(self) -> NDArray[Any]: """This name is deprecated in favour of :any:`rgb`. @@ -352,7 +352,7 @@ def default_bg(self) -> tuple[int, int, int]: return color.r, color.g, color.b @default_bg.setter - @deprecate("Console defaults have been deprecated.", category=FutureWarning) + @deprecated("Console defaults have been deprecated.", category=FutureWarning) def default_bg(self, color: tuple[int, int, int]) -> None: self._console_data.back = color @@ -363,7 +363,7 @@ def default_fg(self) -> tuple[int, int, int]: return color.r, color.g, color.b @default_fg.setter - @deprecate("Console defaults have been deprecated.", category=FutureWarning) + @deprecated("Console defaults have been deprecated.", category=FutureWarning) def default_fg(self, color: tuple[int, int, int]) -> None: self._console_data.fore = color @@ -373,7 +373,7 @@ def default_bg_blend(self) -> int: return self._console_data.bkgnd_flag # type: ignore @default_bg_blend.setter - @deprecate("Console defaults have been deprecated.", category=FutureWarning) + @deprecated("Console defaults have been deprecated.", category=FutureWarning) def default_bg_blend(self, value: int) -> None: self._console_data.bkgnd_flag = value @@ -383,7 +383,7 @@ def default_alignment(self) -> int: return self._console_data.alignment # type: ignore @default_alignment.setter - @deprecate("Console defaults have been deprecated.", category=FutureWarning) + @deprecated("Console defaults have been deprecated.", category=FutureWarning) def default_alignment(self, value: int) -> None: self._console_data.alignment = value @@ -830,7 +830,7 @@ def blit( # noqa: PLR0913 bg_alpha, ) - @deprecate("Pass the key color to Console.blit instead of calling this function.") + @deprecated("Pass the key color to Console.blit instead of calling this function.") def set_key_color(self, color: tuple[int, int, int] | None) -> None: """Set a consoles blit transparent color. @@ -1234,7 +1234,7 @@ def get_height_rect(width: int, string: str) -> int: return int(lib.TCOD_console_get_height_rect_wn(width, len(string_), string_)) -@deprecate("This function does not support contexts.", category=FutureWarning) +@deprecated("This function does not support contexts.", category=FutureWarning) def recommended_size() -> tuple[int, int]: """Return the recommended size of a console for the current active window. diff --git a/tcod/context.py b/tcod/context.py index 3b3e8e0a..1b646e61 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -32,7 +32,7 @@ from pathlib import Path from typing import Any, Iterable, NoReturn, TypeVar -from typing_extensions import Literal +from typing_extensions import Literal, deprecated import tcod.console import tcod.event @@ -40,7 +40,7 @@ import tcod.sdl.render import tcod.sdl.video import tcod.tileset -from tcod._internal import _check, _check_warn, pending_deprecate +from tcod._internal import _check, _check_warn from tcod.cffi import ffi, lib __all__ = ( @@ -574,7 +574,7 @@ def new( # noqa: PLR0913 return Context._claim(context_pp[0]) -@pending_deprecate("Call tcod.context.new with width and height as keyword parameters.") +@deprecated("Call tcod.context.new with width and height as keyword parameters.") def new_window( # noqa: PLR0913 width: int, height: int, @@ -601,7 +601,7 @@ def new_window( # noqa: PLR0913 ) -@pending_deprecate("Call tcod.context.new with columns and rows as keyword parameters.") +@deprecated("Call tcod.context.new with columns and rows as keyword parameters.") def new_terminal( # noqa: PLR0913 columns: int, rows: int, diff --git a/tcod/image.py b/tcod/image.py index 7b2ee13d..dca67314 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -17,9 +17,10 @@ import numpy as np from numpy.typing import ArrayLike, NDArray +from typing_extensions import deprecated import tcod.console -from tcod._internal import _console, _path_encode, deprecate +from tcod._internal import _console, _path_encode from tcod.cffi import ffi, lib @@ -357,10 +358,9 @@ def _get_format_name(format: int) -> str: return str(format) -@deprecate( +@deprecated( "This function may be removed in the future." " It's recommended to load images with a more complete image library such as python-Pillow or python-imageio.", - category=PendingDeprecationWarning, ) def load(filename: str | PathLike[str]) -> NDArray[np.uint8]: """Load a PNG file as an RGBA array. diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index feaa0d52..f18be0bf 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -12,7 +12,7 @@ import numpy as np from numpy.typing import NDArray -from typing_extensions import Literal +from typing_extensions import Literal, deprecated import tcod.bsp import tcod.console @@ -77,6 +77,7 @@ def BKGND_ADDALPHA(a: int) -> int: return BKGND_ADDA | (int(a * 255) << 8) +@deprecated("Console array attributes perform better than this class.") class ConsoleBuffer: """Simple console that allows direct (fast) access to cells. Simplifies use of the "fill" functions. @@ -111,11 +112,6 @@ def __init__( Values to fill the buffer are optional, defaults to black with no characters. """ - warnings.warn( - "Console array attributes perform better than this class.", - DeprecationWarning, - stacklevel=2, - ) self.width = width self.height = height self.clear(back_r, back_g, back_b, fore_r, fore_g, fore_b, char) @@ -271,6 +267,7 @@ def blit( dest.ch.ravel()[:] = self.char +@deprecated("Using this class is not recommended.") class Dice(_CDataWrapper): """A libtcod dice object. @@ -286,11 +283,6 @@ class Dice(_CDataWrapper): """ def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn( - "Using this class is not recommended.", - DeprecationWarning, - stacklevel=2, - ) super().__init__(*args, **kwargs) if self.cdata == ffi.NULL: self._init(*args, **kwargs) diff --git a/tcod/random.py b/tcod/random.py index 73121578..fe1944db 100644 --- a/tcod/random.py +++ b/tcod/random.py @@ -14,8 +14,9 @@ import warnings from typing import Any, Hashable +from typing_extensions import deprecated + import tcod.constants -from tcod._internal import deprecate from tcod.cffi import ffi, lib MERSENNE_TWISTER = tcod.constants.RNG_MT @@ -127,7 +128,7 @@ def gauss(self, mu: float, sigma: float) -> float: """ return float(lib.TCOD_random_get_gaussian_double(self.random_c, mu, sigma)) - @deprecate("This is a typo, rename this to 'gauss'", category=FutureWarning) + @deprecated("This is a typo, rename this to 'gauss'", category=FutureWarning) def guass(self, mu: float, sigma: float) -> float: # noqa: D102 return self.gauss(mu, sigma) @@ -146,7 +147,7 @@ def inverse_gauss(self, mu: float, sigma: float) -> float: """ return float(lib.TCOD_random_get_gaussian_double_inv(self.random_c, mu, sigma)) - @deprecate("This is a typo, rename this to 'inverse_gauss'", category=FutureWarning) + @deprecated("This is a typo, rename this to 'inverse_gauss'", category=FutureWarning) def inverse_guass(self, mu: float, sigma: float) -> float: # noqa: D102 return self.inverse_gauss(mu, sigma) diff --git a/tcod/tileset.py b/tcod/tileset.py index 022c57ac..dad40f18 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -20,9 +20,10 @@ import numpy as np from numpy.typing import ArrayLike, NDArray +from typing_extensions import deprecated import tcod.console -from tcod._internal import _check, _check_p, _console, _path_encode, _raise_tcod_error, deprecate +from tcod._internal import _check, _check_p, _console, _path_encode, _raise_tcod_error from tcod.cffi import ffi, lib @@ -235,7 +236,7 @@ def remap(self, codepoint: int, x: int, y: int = 0) -> None: ) -@deprecate("Using the default tileset is deprecated.") +@deprecated("Using the default tileset is deprecated.") def get_default() -> Tileset: """Return a reference to the default Tileset. @@ -248,7 +249,7 @@ def get_default() -> Tileset: return Tileset._claim(lib.TCOD_get_default_tileset()) -@deprecate("Using the default tileset is deprecated.") +@deprecated("Using the default tileset is deprecated.") def set_default(tileset: Tileset) -> None: """Set the default tileset. @@ -278,7 +279,7 @@ def load_truetype_font(path: str | PathLike[str], tile_width: int, tile_height: return Tileset._claim(cdata) -@deprecate("Accessing the default tileset is deprecated.") +@deprecated("Accessing the default tileset is deprecated.") def set_truetype_font(path: str | PathLike[str], tile_width: int, tile_height: int) -> None: """Set the default tileset from a `.ttf` or `.otf` file. From 82d3582b904991cacc7a920a765aaafb9d8f9958 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 7 Aug 2024 00:37:09 -0700 Subject: [PATCH 095/134] Mark KMOD_ names as deprecated --- CHANGELOG.md | 4 ++++ examples/samples_tcod.py | 11 +++++++---- tcod/event.py | 26 ++++++++------------------ tcod/event_constants.py | 17 ----------------- tests/test_deprecated.py | 7 +++++++ 5 files changed, 26 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b5a2551..5b779612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +### Deprecated + +- Keyboard bitmask modifiers `tcod.event.KMOD_*` have been replaced by `tcod.event.Modifier`. + ## [16.2.3] - 2024-07-16 ### Fixed diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index d2a8cb86..c52f4be4 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -27,6 +27,7 @@ import tcod.sdl.mouse import tcod.sdl.render from tcod import libtcodpy +from tcod.sdl.video import WindowFlags # ruff: noqa: S311 @@ -82,11 +83,13 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: cur_sample = (cur_sample - 1) % len(SAMPLES) SAMPLES[cur_sample].on_enter() draw_samples_menu() - elif event.sym == tcod.event.KeySym.RETURN and event.mod & tcod.event.KMOD_LALT: - libtcodpy.console_set_fullscreen(not libtcodpy.console_is_fullscreen()) - elif event.sym == tcod.event.KeySym.PRINTSCREEN or event.sym == tcod.event.KeySym.p: + elif event.sym == tcod.event.KeySym.RETURN and event.mod & tcod.event.Modifier.ALT: + sdl_window = context.sdl_window + if sdl_window: + sdl_window.fullscreen = False if sdl_window.fullscreen else WindowFlags.FULLSCREEN_DESKTOP + elif event.sym in (tcod.event.KeySym.PRINTSCREEN, tcod.event.KeySym.p): print("screenshot") - if event.mod & tcod.event.KMOD_LALT: + if event.mod & tcod.event.Modifier.ALT: libtcodpy.console_save_apf(root_console, "samples.apf") print("apf") else: diff --git a/tcod/event.py b/tcod/event.py index 5d7e9aef..d1a2dc99 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -95,7 +95,6 @@ import tcod.sdl.sys from tcod.cffi import ffi, lib from tcod.event_constants import * # noqa: F403 -from tcod.event_constants import KMOD_ALT, KMOD_CTRL, KMOD_GUI, KMOD_SHIFT from tcod.sdl.joystick import _HAT_DIRECTIONS T = TypeVar("T") @@ -2807,6 +2806,14 @@ def __getattr__(name: str) -> int: FutureWarning, stacklevel=2, ) + elif name.startswith("KMOD_"): + modifier = name[5:] + warnings.warn( + "Key modifiers have been replaced with the Modifier IntFlag.\n" + f"`tcod.event.{modifier}` should be replaced with `tcod.event.Modifier.{modifier}`", + FutureWarning, + stacklevel=2, + ) return value @@ -2859,23 +2866,6 @@ def __getattr__(name: str) -> int: "Scancode", "KeySym", # --- From event_constants.py --- - "KMOD_NONE", - "KMOD_LSHIFT", - "KMOD_RSHIFT", - "KMOD_SHIFT", - "KMOD_LCTRL", - "KMOD_RCTRL", - "KMOD_CTRL", - "KMOD_LALT", - "KMOD_RALT", - "KMOD_ALT", - "KMOD_LGUI", - "KMOD_RGUI", - "KMOD_GUI", - "KMOD_NUM", - "KMOD_CAPS", - "KMOD_MODE", - "KMOD_RESERVED", "MOUSEWHEEL_NORMAL", "MOUSEWHEEL_FLIPPED", "MOUSEWHEEL", diff --git a/tcod/event_constants.py b/tcod/event_constants.py index d10eeb74..0f55d545 100644 --- a/tcod/event_constants.py +++ b/tcod/event_constants.py @@ -540,23 +540,6 @@ } __all__ = [ - "KMOD_NONE", - "KMOD_LSHIFT", - "KMOD_RSHIFT", - "KMOD_SHIFT", - "KMOD_LCTRL", - "KMOD_RCTRL", - "KMOD_CTRL", - "KMOD_LALT", - "KMOD_RALT", - "KMOD_ALT", - "KMOD_LGUI", - "KMOD_RGUI", - "KMOD_GUI", - "KMOD_NUM", - "KMOD_CAPS", - "KMOD_MODE", - "KMOD_SCROLL", "MOUSEWHEEL_NORMAL", "MOUSEWHEEL_FLIPPED", "MOUSEWHEEL", diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index 869c2b36..82001d47 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -51,6 +51,13 @@ def test_deprecate_mouse_constants() -> None: _ = tcod.event.BUTTON_LMASK +def test_deprecate_kmod_constants() -> None: + with pytest.warns(FutureWarning, match=r"Modifier.LSHIFT"): + _ = tcod.event.KMOD_LSHIFT + with pytest.warns(FutureWarning, match=r"Modifier.GUI"): + _ = tcod.event.KMOD_GUI + + def test_line_where() -> None: with pytest.warns(): where = tcod.libtcodpy.line_where(1, 0, 3, 4) From f187e9cb008fedd1bfe680ff040477b40f7403c5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 7 Aug 2024 01:19:52 -0700 Subject: [PATCH 096/134] Update pre-commit --- .pre-commit-config.yaml | 2 +- tcod/console.py | 4 ++-- tcod/context.py | 2 +- tcod/map.py | 2 +- tcod/sdl/render.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a312e948..e137cbe9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.2 + rev: v0.5.6 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] diff --git a/tcod/console.py b/tcod/console.py index 8dc7309b..a4f0a950 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -519,7 +519,7 @@ def __deprecate_defaults( # noqa: C901, PLR0912 stacklevel=3, ) - def print_( # noqa: PLR0913 + def print_( self, x: int, y: int, @@ -596,7 +596,7 @@ def print_rect( # noqa: PLR0913 ) ) - def get_height_rect(self, x: int, y: int, width: int, height: int, string: str) -> int: # noqa: PLR0913 + def get_height_rect(self, x: int, y: int, width: int, height: int, string: str) -> int: """Return the height of this text word-wrapped into this rectangle. Args: diff --git a/tcod/context.py b/tcod/context.py index 1b646e61..2132200f 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -181,7 +181,7 @@ def __exit__(self, *_: object) -> None: """Automatically close on the context on exit.""" self.close() - def present( # noqa: PLR0913 + def present( self, console: tcod.console.Console, *, diff --git a/tcod/map.py b/tcod/map.py index 488c4878..e8d31306 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -115,7 +115,7 @@ def fov(self) -> NDArray[np.bool_]: buffer: np.ndarray[Any, np.dtype[np.bool_]] = self.__buffer[:, :, 2] return buffer.T if self._order == "F" else buffer - def compute_fov( # noqa: PLR0913 + def compute_fov( self, x: int, y: int, diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index c57d2242..1def4c72 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -655,7 +655,7 @@ def draw_lines(self, points: NDArray[np.intc | np.float32]) -> None: raise TypeError(msg) @_required_version((2, 0, 18)) - def geometry( # noqa: PLR0913 + def geometry( self, texture: Texture | None, xy: NDArray[np.float32], From b692d789ff7ff99a6679b53f3722242e6343c837 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 13 Aug 2024 23:08:53 -0700 Subject: [PATCH 097/134] Make EventDispatch on event methods positional only --- CHANGELOG.md | 4 +++ tcod/event.py | 77 ++++++++++++++++++++++++++------------------------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b779612..efa8f927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +### Changed + +- `EventDispatch`'s on event methods are now defined as positional parameters, so renaming the `event` parameter is now valid in subclasses. + ### Deprecated - Keyboard bitmask modifiers `tcod.event.KMOD_*` have been replaced by `tcod.event.Modifier`. diff --git a/tcod/event.py b/tcod/event.py index d1a2dc99..29594d7b 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -90,6 +90,7 @@ from numpy.typing import NDArray from typing_extensions import Literal +import tcod.event import tcod.event_constants import tcod.sdl.joystick import tcod.sdl.sys @@ -1377,160 +1378,160 @@ def event_wait(self, timeout: float | None) -> None: wait(timeout) self.event_get() - def ev_quit(self, event: tcod.event.Quit) -> T | None: + def ev_quit(self, event: tcod.event.Quit, /) -> T | None: """Called when the termination of the program is requested.""" - def ev_keydown(self, event: tcod.event.KeyDown) -> T | None: + def ev_keydown(self, event: tcod.event.KeyDown, /) -> T | None: """Called when a keyboard key is pressed or repeated.""" - def ev_keyup(self, event: tcod.event.KeyUp) -> T | None: + def ev_keyup(self, event: tcod.event.KeyUp, /) -> T | None: """Called when a keyboard key is released.""" - def ev_mousemotion(self, event: tcod.event.MouseMotion) -> T | None: + def ev_mousemotion(self, event: tcod.event.MouseMotion, /) -> T | None: """Called when the mouse is moved.""" - def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> T | None: + def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown, /) -> T | None: """Called when a mouse button is pressed.""" - def ev_mousebuttonup(self, event: tcod.event.MouseButtonUp) -> T | None: + def ev_mousebuttonup(self, event: tcod.event.MouseButtonUp, /) -> T | None: """Called when a mouse button is released.""" - def ev_mousewheel(self, event: tcod.event.MouseWheel) -> T | None: + def ev_mousewheel(self, event: tcod.event.MouseWheel, /) -> T | None: """Called when the mouse wheel is scrolled.""" - def ev_textinput(self, event: tcod.event.TextInput) -> T | None: + def ev_textinput(self, event: tcod.event.TextInput, /) -> T | None: """Called to handle Unicode input.""" - def ev_windowshown(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowshown(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window is shown.""" - def ev_windowhidden(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowhidden(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window is hidden.""" - def ev_windowexposed(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowexposed(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when a window is exposed, and needs to be refreshed. This usually means a call to :any:`libtcodpy.console_flush` is necessary. """ - def ev_windowmoved(self, event: tcod.event.WindowMoved) -> T | None: + def ev_windowmoved(self, event: tcod.event.WindowMoved, /) -> T | None: """Called when the window is moved.""" - def ev_windowresized(self, event: tcod.event.WindowResized) -> T | None: + def ev_windowresized(self, event: tcod.event.WindowResized, /) -> T | None: """Called when the window is resized.""" - def ev_windowsizechanged(self, event: tcod.event.WindowResized) -> T | None: + def ev_windowsizechanged(self, event: tcod.event.WindowResized, /) -> T | None: """Called when the system or user changes the size of the window.""" - def ev_windowminimized(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowminimized(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window is minimized.""" - def ev_windowmaximized(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowmaximized(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window is maximized.""" - def ev_windowrestored(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowrestored(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window is restored.""" - def ev_windowenter(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowenter(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window gains mouse focus.""" - def ev_windowleave(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowleave(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window loses mouse focus.""" - def ev_windowfocusgained(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowfocusgained(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window gains keyboard focus.""" - def ev_windowfocuslost(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowfocuslost(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window loses keyboard focus.""" - def ev_windowclose(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowclose(self, event: tcod.event.WindowEvent, /) -> T | None: """Called when the window manager requests the window to be closed.""" - def ev_windowtakefocus(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowtakefocus(self, event: tcod.event.WindowEvent, /) -> T | None: pass - def ev_windowhittest(self, event: tcod.event.WindowEvent) -> T | None: + def ev_windowhittest(self, event: tcod.event.WindowEvent, /) -> T | None: pass - def ev_joyaxismotion(self, event: tcod.event.JoystickAxis) -> T | None: + def ev_joyaxismotion(self, event: tcod.event.JoystickAxis, /) -> T | None: """Called when a joystick analog is moved. .. versionadded:: 13.8 """ - def ev_joyballmotion(self, event: tcod.event.JoystickBall) -> T | None: + def ev_joyballmotion(self, event: tcod.event.JoystickBall, /) -> T | None: """Called when a joystick ball is moved. .. versionadded:: 13.8 """ - def ev_joyhatmotion(self, event: tcod.event.JoystickHat) -> T | None: + def ev_joyhatmotion(self, event: tcod.event.JoystickHat, /) -> T | None: """Called when a joystick hat is moved. .. versionadded:: 13.8 """ - def ev_joybuttondown(self, event: tcod.event.JoystickButton) -> T | None: + def ev_joybuttondown(self, event: tcod.event.JoystickButton, /) -> T | None: """Called when a joystick button is pressed. .. versionadded:: 13.8 """ - def ev_joybuttonup(self, event: tcod.event.JoystickButton) -> T | None: + def ev_joybuttonup(self, event: tcod.event.JoystickButton, /) -> T | None: """Called when a joystick button is released. .. versionadded:: 13.8 """ - def ev_joydeviceadded(self, event: tcod.event.JoystickDevice) -> T | None: + def ev_joydeviceadded(self, event: tcod.event.JoystickDevice, /) -> T | None: """Called when a joystick is added. .. versionadded:: 13.8 """ - def ev_joydeviceremoved(self, event: tcod.event.JoystickDevice) -> T | None: + def ev_joydeviceremoved(self, event: tcod.event.JoystickDevice, /) -> T | None: """Called when a joystick is removed. .. versionadded:: 13.8 """ - def ev_controlleraxismotion(self, event: tcod.event.ControllerAxis) -> T | None: + def ev_controlleraxismotion(self, event: tcod.event.ControllerAxis, /) -> T | None: """Called when a controller analog is moved. .. versionadded:: 13.8 """ - def ev_controllerbuttondown(self, event: tcod.event.ControllerButton) -> T | None: + def ev_controllerbuttondown(self, event: tcod.event.ControllerButton, /) -> T | None: """Called when a controller button is pressed. .. versionadded:: 13.8 """ - def ev_controllerbuttonup(self, event: tcod.event.ControllerButton) -> T | None: + def ev_controllerbuttonup(self, event: tcod.event.ControllerButton, /) -> T | None: """Called when a controller button is released. .. versionadded:: 13.8 """ - def ev_controllerdeviceadded(self, event: tcod.event.ControllerDevice) -> T | None: + def ev_controllerdeviceadded(self, event: tcod.event.ControllerDevice, /) -> T | None: """Called when a standard controller is added. .. versionadded:: 13.8 """ - def ev_controllerdeviceremoved(self, event: tcod.event.ControllerDevice) -> T | None: + def ev_controllerdeviceremoved(self, event: tcod.event.ControllerDevice, /) -> T | None: """Called when a standard controller is removed. .. versionadded:: 13.8 """ - def ev_controllerdeviceremapped(self, event: tcod.event.ControllerDevice) -> T | None: + def ev_controllerdeviceremapped(self, event: tcod.event.ControllerDevice, /) -> T | None: """Called when a standard controller is remapped. .. versionadded:: 13.8 """ - def ev_(self, event: Any) -> T | None: + def ev_(self, event: Any, /) -> T | None: pass From a8f66fcaba57f15901897496a4ccdcb10c4363fb Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 14 Aug 2024 14:34:43 -0700 Subject: [PATCH 098/134] Suppress internal mouse.tile_motion deprecation warning --- CHANGELOG.md | 4 ++++ tcod/context.py | 2 +- tcod/event.py | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efa8f927..2b87ebb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - Keyboard bitmask modifiers `tcod.event.KMOD_*` have been replaced by `tcod.event.Modifier`. +### Fixed + +- Suppressed internal `mouse.tile_motion` deprecation warning. + ## [16.2.3] - 2024-07-16 ### Fixed diff --git a/tcod/context.py b/tcod/context.py index 2132200f..1a2ea21c 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -265,7 +265,7 @@ def convert_event(self, event: _Event) -> _Event: event.position[0] - event.motion[0], event.position[1] - event.motion[1], ) - event_copy.motion = event.tile_motion = tcod.event.Point( + event_copy.motion = event._tile_motion = tcod.event.Point( event._tile[0] - prev_tile[0], event._tile[1] - prev_tile[1] ) return event_copy diff --git a/tcod/event.py b/tcod/event.py index 29594d7b..48cf9c37 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -486,7 +486,7 @@ def __init__( ) -> None: super().__init__(position, tile, state) self.motion = Point(*motion) - self.__tile_motion = Point(*tile_motion) if tile_motion is not None else None + self._tile_motion = Point(*tile_motion) if tile_motion is not None else None @property def pixel_motion(self) -> Point: @@ -514,7 +514,7 @@ def tile_motion(self) -> Point: DeprecationWarning, stacklevel=2, ) - return _verify_tile_coordinates(self.__tile_motion) + return _verify_tile_coordinates(self._tile_motion) @tile_motion.setter def tile_motion(self, xy: tuple[int, int]) -> None: @@ -524,7 +524,7 @@ def tile_motion(self, xy: tuple[int, int]) -> None: DeprecationWarning, stacklevel=2, ) - self.__tile_motion = Point(*xy) + self._tile_motion = Point(*xy) @classmethod def from_sdl_event(cls, sdl_event: Any) -> MouseMotion: From b033bf265275449ab6ff159362ab27521c5c71ea Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 15 Aug 2024 20:21:01 -0700 Subject: [PATCH 099/134] pre-commit update --- .pre-commit-config.yaml | 2 +- examples/samples_tcod.py | 3 +-- tests/conftest.py | 10 +++++----- tests/test_libtcodpy.py | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e137cbe9..f19dd6aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.6.0 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index c52f4be4..942c1d69 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -726,8 +726,7 @@ def on_draw(self) -> None: for y in range(SAMPLE_SCREEN_HEIGHT): for x in range(SAMPLE_SCREEN_WIDTH): d = libtcodpy.dijkstra_get_distance(self.dijkstra, x, y) - if d > self.dijkstra_dist: - self.dijkstra_dist = d + self.dijkstra_dist = max(d, self.dijkstra_dist) # compute path from px,py to dx,dy libtcodpy.dijkstra_path_set(self.dijkstra, self.dx, self.dy) self.recalculate = False diff --git a/tests/conftest.py b/tests/conftest.py index 2998b4ae..0efc716d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption("--no-window", action="store_true", help="Skip tests which need a rendering context.") -@pytest.fixture() +@pytest.fixture def uses_window(request: pytest.FixtureRequest) -> Iterator[None]: """Marks tests which require a rendering context.""" if request.config.getoption("--no-window"): @@ -40,7 +40,7 @@ def session_console(request: pytest.FixtureRequest) -> Iterator[tcod.console.Con yield con -@pytest.fixture() +@pytest.fixture def console(session_console: tcod.console.Console) -> tcod.console.Console: console = session_console tcod.console_flush() @@ -54,18 +54,18 @@ def console(session_console: tcod.console.Console) -> tcod.console.Console: return console -@pytest.fixture() +@pytest.fixture def offscreen(console: tcod.console.Console) -> tcod.console.Console: """Return an off-screen console with the same size as the root console.""" return tcod.console.Console(console.width, console.height) -@pytest.fixture() +@pytest.fixture def fg() -> tcod.Color: return tcod.Color(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) -@pytest.fixture() +@pytest.fixture def bg() -> tcod.Color: return tcod.Color(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py index 76762c8f..7b0bfcc1 100644 --- a/tests/test_libtcodpy.py +++ b/tests/test_libtcodpy.py @@ -660,7 +660,7 @@ def test_heightmap() -> None: POINTS_AC = POINT_A + POINT_C # invalid path -@pytest.fixture() +@pytest.fixture def map_() -> Iterator[tcod.map.Map]: map_ = tcod.map.Map(MAP_WIDTH, MAP_HEIGHT) map_.walkable[...] = map_.transparent[...] = MAP[...] == " " @@ -668,7 +668,7 @@ def map_() -> Iterator[tcod.map.Map]: libtcodpy.map_delete(map_) -@pytest.fixture() +@pytest.fixture def path_callback(map_: tcod.map.Map) -> Callable[[int, int, int, int, None], bool]: def callback(ox: int, oy: int, dx: int, dy: int, user_data: None) -> bool: return bool(map_.walkable[dy, dx]) From b24d53974ad0f9e4d43658fcb70076cd2b33766e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 18 Oct 2024 19:19:35 -0700 Subject: [PATCH 100/134] Update pre-commit --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f19dd6aa..19933511 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: autoupdate_schedule: quarterly repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.0 + rev: v0.7.0 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From c36dd03979b83ec1625c6419cb1b504b614cd429 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 18 Oct 2024 19:16:54 -0700 Subject: [PATCH 101/134] Clean up type errors, use standard deprecation functions Fix minor typos Improve deprecation docs Set lower bound for typing_extensions --- pyproject.toml | 2 +- tcod/_internal.py | 30 ++++------ tcod/cffi.py | 2 +- tcod/color.py | 14 ++--- tcod/console.py | 61 +++++++++++++++---- tcod/libtcodpy.py | 128 ++++++++++++++++++++-------------------- tcod/map.py | 2 +- tcod/path.py | 2 +- tests/test_console.py | 4 +- tests/test_libtcodpy.py | 6 +- 10 files changed, 143 insertions(+), 108 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 97ee6190..6c68edb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ license = { text = "Simplified BSD License" } dependencies = [ "cffi>=1.15", 'numpy>=1.21.4; implementation_name != "pypy"', - "typing_extensions", + "typing_extensions>=4.12.2", ] keywords = [ "roguelike", diff --git a/tcod/_internal.py b/tcod/_internal.py index cebe249e..85410cb2 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -2,17 +2,16 @@ from __future__ import annotations -import functools import locale import sys import warnings from pathlib import Path from types import TracebackType -from typing import TYPE_CHECKING, Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar, cast +from typing import TYPE_CHECKING, Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar import numpy as np from numpy.typing import ArrayLike, NDArray -from typing_extensions import Literal +from typing_extensions import Literal, LiteralString, deprecated from tcod.cffi import ffi, lib @@ -24,31 +23,28 @@ T = TypeVar("T") -def deprecate(message: str, category: type[Warning] = DeprecationWarning, stacklevel: int = 0) -> Callable[[F], F]: - """Return a decorator which adds a warning to functions.""" +def _deprecate_passthrough( + message: str, /, *, category: type[Warning] = DeprecationWarning, stacklevel: int = 0 +) -> Callable[[F], F]: + """Return a decorator which skips wrapping a warning onto functions. This is used for non-debug runs.""" def decorator(func: F) -> F: - if not __debug__: - return func + return func - @functools.wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 - warnings.warn(message, category, stacklevel=stacklevel + 2) - return func(*args, **kwargs) + return decorator - return cast(F, wrapper) - return decorator +deprecate = deprecated if __debug__ or TYPE_CHECKING else _deprecate_passthrough def pending_deprecate( - message: str = "This function may be deprecated in the future." + message: LiteralString = "This function may be deprecated in the future." " Consider raising an issue on GitHub if you need this feature.", category: type[Warning] = PendingDeprecationWarning, stacklevel: int = 0, ) -> Callable[[F], F]: """Like deprecate, but the default parameters are filled out for a generic pending deprecation warning.""" - return deprecate(message, category, stacklevel) + return deprecate(message, category=category, stacklevel=stacklevel) def verify_order(order: Literal["C", "F"]) -> Literal["C", "F"]: @@ -119,7 +115,7 @@ def _unicode(string: AnyStr, stacklevel: int = 2) -> str: stacklevel=stacklevel + 1, ) return string.decode("latin-1") - return string + return str(string) def _fmt(string: str, stacklevel: int = 2) -> bytes: @@ -271,5 +267,5 @@ def __init__(self, array: ArrayLike) -> None: def _as_image(image: ArrayLike | tcod.image.Image) -> TempImage | tcod.image.Image: """Convert this input into an Image-like object.""" if hasattr(image, "image_c"): - return image # type: ignore + return image return TempImage(image) diff --git a/tcod/cffi.py b/tcod/cffi.py index 19a597b6..2325519d 100644 --- a/tcod/cffi.py +++ b/tcod/cffi.py @@ -59,7 +59,7 @@ def get_sdl_version() -> str: verify_dependencies() -from tcod._libtcod import ffi, lib # noqa +from tcod._libtcod import ffi, lib # noqa: E402 __sdl_version__ = get_sdl_version() diff --git a/tcod/color.py b/tcod/color.py index ef75bfb8..49b98ad7 100644 --- a/tcod/color.py +++ b/tcod/color.py @@ -31,7 +31,7 @@ def r(self) -> int: return int(self[0]) @r.setter - @deprecate("Setting color attributes has been deprecated.", FutureWarning) + @deprecate("Setting color attributes has been deprecated.", category=FutureWarning) def r(self, value: int) -> None: self[0] = value & 0xFF @@ -45,7 +45,7 @@ def g(self) -> int: return int(self[1]) @g.setter - @deprecate("Setting color attributes has been deprecated.", FutureWarning) + @deprecate("Setting color attributes has been deprecated.", category=FutureWarning) def g(self, value: int) -> None: self[1] = value & 0xFF @@ -59,7 +59,7 @@ def b(self) -> int: return int(self[2]) @b.setter - @deprecate("Setting color attributes has been deprecated.", FutureWarning) + @deprecate("Setting color attributes has been deprecated.", category=FutureWarning) def b(self, value: int) -> None: self[2] = value & 0xFF @@ -82,7 +82,7 @@ def __getitem__(self, index: Any) -> Any: # noqa: ANN401 return super().__getitem__("rgb".index(index)) return super().__getitem__(index) - @deprecate("This class will not be mutable in the future.", FutureWarning) + @deprecate("This class will not be mutable in the future.", category=FutureWarning) def __setitem__(self, index: Any, value: Any) -> None: # noqa: ANN401, D105 if isinstance(index, str): super().__setitem__("rgb".index(index), value) @@ -99,7 +99,7 @@ def __eq__(self, other: object) -> bool: except TypeError: return False - @deprecate("Use NumPy instead for color math operations.", FutureWarning) + @deprecate("Use NumPy instead for color math operations.", category=FutureWarning) def __add__(self, other: object) -> Color: # type: ignore[override] """Add two colors together. @@ -108,7 +108,7 @@ def __add__(self, other: object) -> Color: # type: ignore[override] """ return Color._new_from_cdata(lib.TCOD_color_add(self, other)) - @deprecate("Use NumPy instead for color math operations.", FutureWarning) + @deprecate("Use NumPy instead for color math operations.", category=FutureWarning) def __sub__(self, other: object) -> Color: """Subtract one color from another. @@ -117,7 +117,7 @@ def __sub__(self, other: object) -> Color: """ return Color._new_from_cdata(lib.TCOD_color_subtract(self, other)) - @deprecate("Use NumPy instead for color math operations.", FutureWarning) + @deprecate("Use NumPy instead for color math operations.", category=FutureWarning) def __mul__(self, other: object) -> Color: """Multiply with a scaler or another color. diff --git a/tcod/console.py b/tcod/console.py index a4f0a950..04845a94 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -122,7 +122,7 @@ def __init__( ) -> None: """Initialize the console.""" self._key_color: tuple[int, int, int] | None = None - self._order = tcod._internal.verify_order(order) + self._order: Literal["C", "F"] = tcod._internal.verify_order(order) if buffer is not None: if self._order == "F": buffer = buffer.transpose() @@ -345,45 +345,82 @@ def rgb(self) -> NDArray[Any]: """ return self.rgba.view(self._DTYPE_RGB) + _DEPRECATE_CONSOLE_DEFAULTS_MSG = """Console defaults have been deprecated. +Consider one of the following: + + # Set parameters once then pass them as kwargs + DEFAULT_COLOR = {"bg": (0, 0, 127), "fg": (127, 127, 255)} + console.print(x, y, string, **DEFAULT_COLOR) + + # Clear the console to a color and then skip setting colors on printing/drawing + console.clear(fg=(127, 127, 255), bg=(0, 0, 127)) + console.print(x, y, string, fg=None) +""" + @property + @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_bg(self) -> tuple[int, int, int]: - """Tuple[int, int, int]: The default background color.""" + """Tuple[int, int, int]: The default background color. + + .. deprecated:: 8.5 + These should not be used. Prefer passing defaults as kwargs. + + .. code-block:: + + DEFAULT_COLOR = {"bg": (0, 0, 127), "fg": (127, 127, 255)} + console.print(x, y, string, **DEFAULT_COLOR) + """ color = self._console_data.back return color.r, color.g, color.b @default_bg.setter - @deprecated("Console defaults have been deprecated.", category=FutureWarning) + @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_bg(self, color: tuple[int, int, int]) -> None: self._console_data.back = color @property + @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_fg(self) -> tuple[int, int, int]: - """Tuple[int, int, int]: The default foreground color.""" + """Tuple[int, int, int]: The default foreground color. + + .. deprecated:: 8.5 + These should not be used. Prefer passing defaults as kwargs. + """ color = self._console_data.fore return color.r, color.g, color.b @default_fg.setter - @deprecated("Console defaults have been deprecated.", category=FutureWarning) + @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_fg(self, color: tuple[int, int, int]) -> None: self._console_data.fore = color @property + @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_bg_blend(self) -> int: - """int: The default blending mode.""" + """int: The default blending mode. + + .. deprecated:: 8.5 + These should not be used. Prefer passing defaults as kwargs. + """ return self._console_data.bkgnd_flag # type: ignore @default_bg_blend.setter - @deprecated("Console defaults have been deprecated.", category=FutureWarning) + @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_bg_blend(self, value: int) -> None: self._console_data.bkgnd_flag = value @property + @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_alignment(self) -> int: - """int: The default text alignment.""" + """int: The default text alignment. + + .. deprecated:: 8.5 + These should not be used. Prefer passing defaults as kwargs. + """ return self._console_data.alignment # type: ignore @default_alignment.setter - @deprecated("Console defaults have been deprecated.", category=FutureWarning) + @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_alignment(self, value: int) -> None: self._console_data.alignment = value @@ -391,7 +428,7 @@ def __clear_warning(self, name: str, value: tuple[int, int, int]) -> None: """Raise a warning for bad default values during calls to clear.""" warnings.warn( f"Clearing with the console default values is deprecated.\nAdd {name}={value!r} to this call.", - DeprecationWarning, + FutureWarning, stacklevel=3, ) @@ -737,8 +774,8 @@ def print_frame( # noqa: PLR0913 explicit. """ self.__deprecate_defaults("draw_frame", bg_blend) - string = _fmt(string) if string else ffi.NULL - _check(lib.TCOD_console_printf_frame(self.console_c, x, y, width, height, clear, bg_blend, string)) + string_: Any = _fmt(string) if string else ffi.NULL + _check(lib.TCOD_console_printf_frame(self.console_c, x, y, width, height, clear, bg_blend, string_)) def blit( # noqa: PLR0913 self, diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index f18be0bf..aa9cc51b 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -347,6 +347,8 @@ class Key(_CDataWrapper): Use events from the :any:`tcod.event` module instead. """ + cdata: Any + _BOOL_ATTRIBUTES = ( "lalt", "lctrl", @@ -509,7 +511,7 @@ def mouse_p(self) -> Any: return self.cdata -@deprecate("Call tcod.bsp.BSP(x, y, width, height) instead.", FutureWarning) +@deprecate("Call tcod.bsp.BSP(x, y, width, height) instead.", category=FutureWarning) def bsp_new_with_size(x: int, y: int, w: int, h: int) -> tcod.bsp.BSP: """Create a new BSP instance with the given rectangle. @@ -528,7 +530,7 @@ def bsp_new_with_size(x: int, y: int, w: int, h: int) -> tcod.bsp.BSP: return Bsp(x, y, w, h) -@deprecate("Call node.split_once instead.", FutureWarning) +@deprecate("Call node.split_once instead.", category=FutureWarning) def bsp_split_once(node: tcod.bsp.BSP, horizontal: bool, position: int) -> None: """Deprecated function. @@ -538,7 +540,7 @@ def bsp_split_once(node: tcod.bsp.BSP, horizontal: bool, position: int) -> None: node.split_once(horizontal, position) -@deprecate("Call node.split_recursive instead.", FutureWarning) +@deprecate("Call node.split_recursive instead.", category=FutureWarning) def bsp_split_recursive( node: tcod.bsp.BSP, randomizer: tcod.random.Random | None, @@ -556,7 +558,7 @@ def bsp_split_recursive( node.split_recursive(nb, minHSize, minVSize, maxHRatio, maxVRatio, randomizer) -@deprecate("Assign values via attribute instead.", FutureWarning) +@deprecate("Assign values via attribute instead.", category=FutureWarning) def bsp_resize(node: tcod.bsp.BSP, x: int, y: int, w: int, h: int) -> None: """Deprecated function. @@ -589,7 +591,7 @@ def bsp_right(node: tcod.bsp.BSP) -> tcod.bsp.BSP | None: return None if not node.children else node.children[1] -@deprecate("Get the parent with 'node.parent' instead.", FutureWarning) +@deprecate("Get the parent with 'node.parent' instead.", category=FutureWarning) def bsp_father(node: tcod.bsp.BSP) -> tcod.bsp.BSP | None: """Deprecated function. @@ -599,7 +601,7 @@ def bsp_father(node: tcod.bsp.BSP) -> tcod.bsp.BSP | None: return node.parent -@deprecate("Check for children with 'bool(node.children)' instead.", FutureWarning) +@deprecate("Check for children with 'bool(node.children)' instead.", category=FutureWarning) def bsp_is_leaf(node: tcod.bsp.BSP) -> bool: """Deprecated function. @@ -609,7 +611,7 @@ def bsp_is_leaf(node: tcod.bsp.BSP) -> bool: return not node.children -@deprecate("Use 'node.contains' instead.", FutureWarning) +@deprecate("Use 'node.contains' instead.", category=FutureWarning) def bsp_contains(node: tcod.bsp.BSP, cx: int, cy: int) -> bool: """Deprecated function. @@ -619,7 +621,7 @@ def bsp_contains(node: tcod.bsp.BSP, cx: int, cy: int) -> bool: return node.contains(cx, cy) -@deprecate("Use 'node.find_node' instead.", FutureWarning) +@deprecate("Use 'node.find_node' instead.", category=FutureWarning) def bsp_find_node(node: tcod.bsp.BSP, cx: int, cy: int) -> tcod.bsp.BSP | None: """Deprecated function. @@ -723,7 +725,7 @@ def bsp_remove_sons(node: tcod.bsp.BSP) -> None: node.children = () -@deprecate("libtcod objects are deleted automatically.", FutureWarning) +@deprecate("libtcod objects are deleted automatically.", category=FutureWarning) def bsp_delete(node: tcod.bsp.BSP) -> None: """Exists for backward compatibility. Does nothing. @@ -1609,8 +1611,8 @@ def console_print_frame( .. deprecated:: 8.5 Use :any:`Console.print_frame` instead. """ - fmt = _fmt(fmt) if fmt else ffi.NULL - _check(lib.TCOD_console_printf_frame(_console(con), x, y, w, h, clear, flag, fmt)) + fmt_: Any = _fmt(fmt) if fmt else ffi.NULL + _check(lib.TCOD_console_printf_frame(_console(con), x, y, w, h, clear, flag, fmt_)) @pending_deprecate() @@ -1680,7 +1682,7 @@ def console_get_char(con: tcod.console.Console, x: int, y: int) -> int: return lib.TCOD_console_get_char(_console(con), x, y) # type: ignore -@deprecate("This function is not supported if contexts are being used.", FutureWarning) +@deprecate("This function is not supported if contexts are being used.", category=FutureWarning) def console_set_fade(fade: int, fadingColor: tuple[int, int, int]) -> None: """Deprecated function. @@ -1690,7 +1692,7 @@ def console_set_fade(fade: int, fadingColor: tuple[int, int, int]) -> None: lib.TCOD_console_set_fade(fade, fadingColor) -@deprecate("This function is not supported if contexts are being used.", FutureWarning) +@deprecate("This function is not supported if contexts are being used.", category=FutureWarning) def console_get_fade() -> int: """Deprecated function. @@ -1700,7 +1702,7 @@ def console_get_fade() -> int: return int(lib.TCOD_console_get_fade()) -@deprecate("This function is not supported if contexts are being used.", FutureWarning) +@deprecate("This function is not supported if contexts are being used.", category=FutureWarning) def console_get_fading_color() -> Color: """Deprecated function. @@ -1753,7 +1755,7 @@ def console_check_for_keypress(flags: int = KEY_RELEASED) -> Key: return key -@deprecate("Use tcod.event.get_keyboard_state to see if a key is held.", FutureWarning) +@deprecate("Use tcod.event.get_keyboard_state to see if a key is held.", category=FutureWarning) def console_is_key_pressed(key: int) -> bool: """Return True if a key is held. @@ -2972,52 +2974,52 @@ def heightmap_delete(hm: Any) -> None: """ -@deprecate("Use `tcod.image.Image(width, height)` instead.", FutureWarning) +@deprecate("Use `tcod.image.Image(width, height)` instead.", category=FutureWarning) def image_new(width: int, height: int) -> tcod.image.Image: return tcod.image.Image(width, height) -@deprecate("Use the `image.clear()` method instead.", FutureWarning) +@deprecate("Use the `image.clear()` method instead.", category=FutureWarning) def image_clear(image: tcod.image.Image, col: tuple[int, int, int]) -> None: image.clear(col) -@deprecate("Use the `image.invert()` method instead.", FutureWarning) +@deprecate("Use the `image.invert()` method instead.", category=FutureWarning) def image_invert(image: tcod.image.Image) -> None: image.invert() -@deprecate("Use the `image.hflip()` method instead.", FutureWarning) +@deprecate("Use the `image.hflip()` method instead.", category=FutureWarning) def image_hflip(image: tcod.image.Image) -> None: image.hflip() -@deprecate("Use the `image.rotate90(n)` method instead.", FutureWarning) +@deprecate("Use the `image.rotate90(n)` method instead.", category=FutureWarning) def image_rotate90(image: tcod.image.Image, num: int = 1) -> None: image.rotate90(num) -@deprecate("Use the `image.vflip()` method instead.", FutureWarning) +@deprecate("Use the `image.vflip()` method instead.", category=FutureWarning) def image_vflip(image: tcod.image.Image) -> None: image.vflip() -@deprecate("Use the `image.scale(new_width, new_height)` method instead.", FutureWarning) +@deprecate("Use the `image.scale(new_width, new_height)` method instead.", category=FutureWarning) def image_scale(image: tcod.image.Image, neww: int, newh: int) -> None: image.scale(neww, newh) -@deprecate("Use the `image.image_set_key_color(rgb)` method instead.", FutureWarning) +@deprecate("Use the `image.image_set_key_color(rgb)` method instead.", category=FutureWarning) def image_set_key_color(image: tcod.image.Image, col: tuple[int, int, int]) -> None: image.set_key_color(col) -@deprecate("Use `np.asarray(image)[y, x, 3]` instead.", FutureWarning) +@deprecate("Use `np.asarray(image)[y, x, 3]` instead.", category=FutureWarning) def image_get_alpha(image: tcod.image.Image, x: int, y: int) -> int: return image.get_alpha(x, y) -@deprecate("Use the Numpy array interface to check alpha or color keys.", FutureWarning) +@deprecate("Use the Numpy array interface to check alpha or color keys.", category=FutureWarning) def image_is_pixel_transparent(image: tcod.image.Image, x: int, y: int) -> bool: return bool(lib.TCOD_image_is_pixel_transparent(image.image_c, x, y)) @@ -3025,7 +3027,7 @@ def image_is_pixel_transparent(image: tcod.image.Image, x: int, y: int) -> bool: @deprecate( "Call the classmethod `tcod.image.Image.from_file` instead to load images." "\nIt's recommended to load images with a more complete image library such as python-Pillow or python-imageio.", - FutureWarning, + category=FutureWarning, ) def image_load(filename: str | PathLike[str]) -> tcod.image.Image: """Load an image file into an Image instance and return it. @@ -3042,7 +3044,7 @@ def image_load(filename: str | PathLike[str]) -> tcod.image.Image: return tcod.image.Image.from_file(filename) -@deprecate("Use `Tileset.render` instead of this function.", FutureWarning) +@deprecate("Use `Tileset.render` instead of this function.", category=FutureWarning) def image_from_console(console: tcod.console.Console) -> tcod.image.Image: """Return an Image with a Consoles pixel data. @@ -3062,7 +3064,7 @@ def image_from_console(console: tcod.console.Console) -> tcod.image.Image: ) -@deprecate("Use `Tileset.render` instead of this function.", FutureWarning) +@deprecate("Use `Tileset.render` instead of this function.", category=FutureWarning) def image_refresh_console(image: tcod.image.Image, console: tcod.console.Console) -> None: """Update an image made with :any:`image_from_console`. @@ -3072,27 +3074,27 @@ def image_refresh_console(image: tcod.image.Image, console: tcod.console.Console image.refresh_console(console) -@deprecate("Access an images size with `image.width` or `image.height`.", FutureWarning) +@deprecate("Access an images size with `image.width` or `image.height`.", category=FutureWarning) def image_get_size(image: tcod.image.Image) -> tuple[int, int]: return image.width, image.height -@deprecate("Use `np.asarray(image)[y, x, :3]` instead.", FutureWarning) +@deprecate("Use `np.asarray(image)[y, x, :3]` instead.", category=FutureWarning) def image_get_pixel(image: tcod.image.Image, x: int, y: int) -> tuple[int, int, int]: return image.get_pixel(x, y) -@deprecate("Use the `image.get_mipmap_pixel(...)` method instead.", FutureWarning) +@deprecate("Use the `image.get_mipmap_pixel(...)` method instead.", category=FutureWarning) def image_get_mipmap_pixel(image: tcod.image.Image, x0: float, y0: float, x1: float, y1: float) -> tuple[int, int, int]: return image.get_mipmap_pixel(x0, y0, x1, y1) -@deprecate("Use `np.asarray(image)[y, x, :3] = rgb` instead.", FutureWarning) +@deprecate("Use `np.asarray(image)[y, x, :3] = rgb` instead.", category=FutureWarning) def image_put_pixel(image: tcod.image.Image, x: int, y: int, col: tuple[int, int, int]) -> None: image.put_pixel(x, y, col) -@deprecate("Use the `image.blit(...)` method instead.", FutureWarning) +@deprecate("Use the `image.blit(...)` method instead.", category=FutureWarning) def image_blit( image: tcod.image.Image, console: tcod.console.Console, @@ -3106,7 +3108,7 @@ def image_blit( image.blit(console, x, y, bkgnd_flag, scalex, scaley, angle) -@deprecate("Use the `image.blit_rect(...)` method instead.", FutureWarning) +@deprecate("Use the `image.blit_rect(...)` method instead.", category=FutureWarning) def image_blit_rect( image: tcod.image.Image, console: tcod.console.Console, @@ -3119,7 +3121,7 @@ def image_blit_rect( image.blit_rect(console, x, y, w, h, bkgnd_flag) -@deprecate("Use `Console.draw_semigraphics(image, ...)` instead.", FutureWarning) +@deprecate("Use `Console.draw_semigraphics(image, ...)` instead.", category=FutureWarning) def image_blit_2x( image: tcod.image.Image, console: tcod.console.Console, @@ -3133,12 +3135,12 @@ def image_blit_2x( image.blit_2x(console, dx, dy, sx, sy, w, h) -@deprecate("Use the `image.save_as` method instead.", FutureWarning) +@deprecate("Use the `image.save_as` method instead.", category=FutureWarning) def image_save(image: tcod.image.Image, filename: str | PathLike[str]) -> None: image.save_as(filename) -@deprecate("libtcod objects are deleted automatically.", FutureWarning) +@deprecate("libtcod objects are deleted automatically.", category=FutureWarning) def image_delete(image: tcod.image.Image) -> None: """Does nothing. libtcod objects are managed by Python's garbage collector. @@ -3146,7 +3148,7 @@ def image_delete(image: tcod.image.Image) -> None: """ -@deprecate("Use tcod.los.bresenham instead.", FutureWarning) +@deprecate("Use tcod.los.bresenham instead.", category=FutureWarning) def line_init(xo: int, yo: int, xd: int, yd: int) -> None: """Initialize a line whose points will be returned by `line_step`. @@ -3166,7 +3168,7 @@ def line_init(xo: int, yo: int, xd: int, yd: int) -> None: lib.TCOD_line_init(xo, yo, xd, yd) -@deprecate("Use tcod.los.bresenham instead.", FutureWarning) +@deprecate("Use tcod.los.bresenham instead.", category=FutureWarning) def line_step() -> tuple[int, int] | tuple[None, None]: """After calling line_init returns (x, y) points of the line. @@ -3188,7 +3190,7 @@ def line_step() -> tuple[int, int] | tuple[None, None]: return None, None -@deprecate("Use tcod.los.bresenham instead.", FutureWarning) +@deprecate("Use tcod.los.bresenham instead.", category=FutureWarning) def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], bool]) -> bool: """Iterate over a line using a callback function. @@ -3220,7 +3222,7 @@ def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], b return False -@deprecate("This function has been replaced by tcod.los.bresenham.", FutureWarning) +@deprecate("This function has been replaced by tcod.los.bresenham.", category=FutureWarning) def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[tuple[int, int]]: """Returns an Iterable over a Bresenham line. @@ -3247,7 +3249,7 @@ def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[tuple[int, int]]: yield (x[0], y[0]) -@deprecate("This function has been replaced by tcod.los.bresenham.", FutureWarning) +@deprecate("This function has been replaced by tcod.los.bresenham.", category=FutureWarning) def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> tuple[NDArray[np.intc], NDArray[np.intc]]: """Return a NumPy index array following a Bresenham line. @@ -3265,7 +3267,7 @@ def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> tu return i, j -@deprecate("Call tcod.map.Map(width, height) instead.", FutureWarning) +@deprecate("Call tcod.map.Map(width, height) instead.", category=FutureWarning) def map_new(w: int, h: int) -> tcod.map.Map: """Return a :any:`tcod.map.Map` with a width and height. @@ -3276,7 +3278,7 @@ def map_new(w: int, h: int) -> tcod.map.Map: return tcod.map.Map(w, h) -@deprecate("Use Python's standard copy module instead.", FutureWarning) +@deprecate("Use Python's standard copy module instead.", category=FutureWarning) def map_copy(source: tcod.map.Map, dest: tcod.map.Map) -> None: """Copy map data from `source` to `dest`. @@ -3289,7 +3291,7 @@ def map_copy(source: tcod.map.Map, dest: tcod.map.Map) -> None: dest._Map__buffer[:] = source._Map__buffer[:] # type: ignore -@deprecate("Set properties using the m.transparent and m.walkable arrays.", FutureWarning) +@deprecate("Set properties using the m.transparent and m.walkable arrays.", category=FutureWarning) def map_set_properties(m: tcod.map.Map, x: int, y: int, isTrans: bool, isWalk: bool) -> None: """Set the properties of a single cell. @@ -3302,7 +3304,7 @@ def map_set_properties(m: tcod.map.Map, x: int, y: int, isTrans: bool, isWalk: b lib.TCOD_map_set_properties(m.map_c, x, y, isTrans, isWalk) -@deprecate("Clear maps using NumPy broadcast rules instead.", FutureWarning) +@deprecate("Clear maps using NumPy broadcast rules instead.", category=FutureWarning) def map_clear(m: tcod.map.Map, transparent: bool = False, walkable: bool = False) -> None: """Change all map cells to a specific value. @@ -3314,7 +3316,7 @@ def map_clear(m: tcod.map.Map, transparent: bool = False, walkable: bool = False m.walkable[:] = walkable -@deprecate("Call the map.compute_fov method instead.", FutureWarning) +@deprecate("Call the map.compute_fov method instead.", category=FutureWarning) def map_compute_fov( m: tcod.map.Map, x: int, @@ -3331,7 +3333,7 @@ def map_compute_fov( m.compute_fov(x, y, radius, light_walls, algo) -@deprecate("Use map.fov to check for this property.", FutureWarning) +@deprecate("Use map.fov to check for this property.", category=FutureWarning) def map_is_in_fov(m: tcod.map.Map, x: int, y: int) -> bool: """Return True if the cell at x,y is lit by the last field-of-view algorithm. @@ -3343,7 +3345,7 @@ def map_is_in_fov(m: tcod.map.Map, x: int, y: int) -> bool: return bool(lib.TCOD_map_is_in_fov(m.map_c, x, y)) -@deprecate("Use map.transparent to check for this property.", FutureWarning) +@deprecate("Use map.transparent to check for this property.", category=FutureWarning) def map_is_transparent(m: tcod.map.Map, x: int, y: int) -> bool: """Return True is a map cell is transparent. @@ -3355,7 +3357,7 @@ def map_is_transparent(m: tcod.map.Map, x: int, y: int) -> bool: return bool(lib.TCOD_map_is_transparent(m.map_c, x, y)) -@deprecate("Use map.walkable to check for this property.", FutureWarning) +@deprecate("Use map.walkable to check for this property.", category=FutureWarning) def map_is_walkable(m: tcod.map.Map, x: int, y: int) -> bool: """Return True is a map cell is walkable. @@ -3367,7 +3369,7 @@ def map_is_walkable(m: tcod.map.Map, x: int, y: int) -> bool: return bool(lib.TCOD_map_is_walkable(m.map_c, x, y)) -@deprecate("libtcod objects are deleted automatically.", FutureWarning) +@deprecate("libtcod objects are deleted automatically.", category=FutureWarning) def map_delete(m: tcod.map.Map) -> None: """Does nothing. libtcod objects are managed by Python's garbage collector. @@ -3375,7 +3377,7 @@ def map_delete(m: tcod.map.Map) -> None: """ -@deprecate("Check the map.width attribute instead.", FutureWarning) +@deprecate("Check the map.width attribute instead.", category=FutureWarning) def map_get_width(map: tcod.map.Map) -> int: """Return the width of a map. @@ -3385,7 +3387,7 @@ def map_get_width(map: tcod.map.Map) -> int: return map.width -@deprecate("Check the map.height attribute instead.", FutureWarning) +@deprecate("Check the map.height attribute instead.", category=FutureWarning) def map_get_height(map: tcod.map.Map) -> int: """Return the height of a map. @@ -3395,7 +3397,7 @@ def map_get_height(map: tcod.map.Map) -> int: return map.height -@deprecate("Use `tcod.sdl.mouse.show(visible)` instead.", FutureWarning) +@deprecate("Use `tcod.sdl.mouse.show(visible)` instead.", category=FutureWarning) def mouse_show_cursor(visible: bool) -> None: """Change the visibility of the mouse cursor. @@ -3405,7 +3407,7 @@ def mouse_show_cursor(visible: bool) -> None: lib.TCOD_mouse_show_cursor(visible) -@deprecate("Use `is_visible = tcod.sdl.mouse.show()` instead.", FutureWarning) +@deprecate("Use `is_visible = tcod.sdl.mouse.show()` instead.", category=FutureWarning) def mouse_is_cursor_visible() -> bool: """Return True if the mouse cursor is visible. @@ -3415,12 +3417,12 @@ def mouse_is_cursor_visible() -> bool: return bool(lib.TCOD_mouse_is_cursor_visible()) -@deprecate("Use `tcod.sdl.mouse.warp_in_window` instead.", FutureWarning) +@deprecate("Use `tcod.sdl.mouse.warp_in_window` instead.", category=FutureWarning) def mouse_move(x: int, y: int) -> None: lib.TCOD_mouse_move(x, y) -@deprecate("Use tcod.event.get_mouse_state() instead.", FutureWarning) +@deprecate("Use tcod.event.get_mouse_state() instead.", category=FutureWarning) def mouse_get_status() -> Mouse: return Mouse(lib.TCOD_mouse_get_status()) @@ -3457,7 +3459,7 @@ def namegen_destroy() -> None: lib.TCOD_namegen_destroy() -@deprecate("Use `tcod.noise.Noise(dimensions, hurst=, lacunarity=)` instead.", FutureWarning) +@deprecate("Use `tcod.noise.Noise(dimensions, hurst=, lacunarity=)` instead.", category=FutureWarning) def noise_new( dim: int, h: float = NOISE_DEFAULT_HURST, @@ -3478,7 +3480,7 @@ def noise_new( return tcod.noise.Noise(dim, hurst=h, lacunarity=l, seed=random) -@deprecate("Use `noise.algorithm = x` instead.", FutureWarning) +@deprecate("Use `noise.algorithm = x` instead.", category=FutureWarning) def noise_set_type(n: tcod.noise.Noise, typ: int) -> None: """Set a Noise objects default noise algorithm. @@ -3489,7 +3491,7 @@ def noise_set_type(n: tcod.noise.Noise, typ: int) -> None: n.algorithm = typ -@deprecate("Use `value = noise[x]` instead.", FutureWarning) +@deprecate("Use `value = noise[x]` instead.", category=FutureWarning) def noise_get(n: tcod.noise.Noise, f: Sequence[float], typ: int = NOISE_DEFAULT) -> float: """Return the noise value sampled from the ``f`` coordinate. @@ -3509,7 +3511,7 @@ def noise_get(n: tcod.noise.Noise, f: Sequence[float], typ: int = NOISE_DEFAULT) return float(lib.TCOD_noise_get_ex(n.noise_c, ffi.new("float[4]", f), typ)) -@deprecate("Configure a Noise instance for FBM and then sample it like normal.", FutureWarning) +@deprecate("Configure a Noise instance for FBM and then sample it like normal.", category=FutureWarning) def noise_get_fbm( n: tcod.noise.Noise, f: Sequence[float], @@ -3530,7 +3532,7 @@ def noise_get_fbm( return float(lib.TCOD_noise_get_fbm_ex(n.noise_c, ffi.new("float[4]", f), oc, typ)) -@deprecate("Configure a Noise instance for FBM and then sample it like normal.", FutureWarning) +@deprecate("Configure a Noise instance for FBM and then sample it like normal.", category=FutureWarning) def noise_get_turbulence( n: tcod.noise.Noise, f: Sequence[float], @@ -3551,7 +3553,7 @@ def noise_get_turbulence( return float(lib.TCOD_noise_get_turbulence_ex(n.noise_c, ffi.new("float[4]", f), oc, typ)) -@deprecate("libtcod objects are deleted automatically.", FutureWarning) +@deprecate("libtcod objects are deleted automatically.", category=FutureWarning) def noise_delete(n: tcod.noise.Noise) -> None: # type (Any) -> None """Does nothing. libtcod objects are managed by Python's garbage collector. diff --git a/tcod/map.py b/tcod/map.py index e8d31306..17aaf0b4 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -262,4 +262,4 @@ def compute_fov( ) map_buffer["transparent"] = transparency lib.TCOD_map_compute_fov(map_cdata, pov[1], pov[0], radius, light_walls, algorithm) - return map_buffer["fov"] # type: ignore + return map_buffer["fov"] diff --git a/tcod/path.py b/tcod/path.py index 8950505f..ffbc1dab 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -131,7 +131,7 @@ class NodeCostArray(np.ndarray): # type: ignore def __new__(cls, array: ArrayLike) -> NodeCostArray: """Validate a numpy array and setup a C callback.""" - return np.asarray(array).view(cls) + return np.asarray(array).view(cls) # type: ignore[no-any-return] def __repr__(self) -> str: return f"{self.__class__.__name__}({repr(self.view(np.ndarray))!r})" diff --git a/tests/test_console.py b/tests/test_console.py index e329fe6f..b0be600f 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -65,7 +65,7 @@ def test_console_defaults() -> None: @pytest.mark.filterwarnings("ignore:Parameter names have been moved around,") -@pytest.mark.filterwarnings("ignore:Pass the key color to Console.blit instea") +@pytest.mark.filterwarnings("ignore:Pass the key color to Console.blit instead") @pytest.mark.filterwarnings("ignore:.*default values have been deprecated") def test_console_methods() -> None: console = tcod.console.Console(width=12, height=10) @@ -150,7 +150,7 @@ def test_rexpaint(tmp_path: Path) -> None: assert consoles[0].rgb.shape == loaded[0].rgb.shape assert consoles[1].rgb.shape == loaded[1].rgb.shape with pytest.raises(FileNotFoundError): - tcod.console.load_xp(tmp_path / "non_existant") + tcod.console.load_xp(tmp_path / "non_existent") def test_draw_frame() -> None: diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py index 7b0bfcc1..b82392a1 100644 --- a/tests/test_libtcodpy.py +++ b/tests/test_libtcodpy.py @@ -260,9 +260,9 @@ def test_console_fill_numpy(console: tcod.console.Console) -> None: for y in range(height): fill[y, :] = y % 256 - libtcodpy.console_fill_background(console, fill, fill, fill) # type: ignore - libtcodpy.console_fill_foreground(console, fill, fill, fill) # type: ignore - libtcodpy.console_fill_char(console, fill) # type: ignore + libtcodpy.console_fill_background(console, fill, fill, fill) + libtcodpy.console_fill_foreground(console, fill, fill, fill) + libtcodpy.console_fill_char(console, fill) # verify fill bg: NDArray[np.intc] = np.zeros((height, width), dtype=np.intc) From 3d19e29d8b5a6925ba4b287c272635699b99dd42 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 13 Nov 2024 12:56:58 -0800 Subject: [PATCH 102/134] Remove getter deprecation warnings These are used internally, but only the setters need to be deprecated --- tcod/console.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tcod/console.py b/tcod/console.py index 04845a94..0760ec7d 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -357,8 +357,7 @@ def rgb(self) -> NDArray[Any]: console.print(x, y, string, fg=None) """ - @property - @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) + @property # Getters used internally, so only deprecate the setters. def default_bg(self) -> tuple[int, int, int]: """Tuple[int, int, int]: The default background color. @@ -379,7 +378,6 @@ def default_bg(self, color: tuple[int, int, int]) -> None: self._console_data.back = color @property - @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_fg(self) -> tuple[int, int, int]: """Tuple[int, int, int]: The default foreground color. @@ -395,7 +393,6 @@ def default_fg(self, color: tuple[int, int, int]) -> None: self._console_data.fore = color @property - @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_bg_blend(self) -> int: """int: The default blending mode. @@ -410,7 +407,6 @@ def default_bg_blend(self, value: int) -> None: self._console_data.bkgnd_flag = value @property - @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) def default_alignment(self) -> int: """int: The default text alignment. From 10661eabe1809a38cb5f107c66840644486a483c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 24 Nov 2024 19:34:28 -0800 Subject: [PATCH 103/134] Pre-commit update Apply to files, update scripts for new formatting and ignore generated and manually aligned code --- .pre-commit-config.yaml | 2 +- build_libtcod.py | 7 ++++--- pyproject.toml | 26 ++++++++++++-------------- tcod/__init__.py | 10 +++++----- tcod/cffi.py | 2 +- tcod/constants.py | 2 +- tcod/context.py | 26 +++++++++++++------------- tcod/event.py | 2 +- tcod/event_constants.py | 2 +- tcod/libtcodpy.py | 2 +- tcod/sdl/video.py | 4 ++-- tcod/tcod.py | 2 +- 12 files changed, 43 insertions(+), 44 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19933511..d3d62742 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.0 + rev: v0.8.0 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] diff --git a/build_libtcod.py b/build_libtcod.py index 2ffe6aad..1e824088 100755 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -233,6 +233,7 @@ def walk_sources(directory: str) -> Iterator[str]: This module is auto-generated by `build_libtcod.py`. """ + from tcod.color import Color ''' @@ -366,7 +367,7 @@ def write_library_constants() -> None: f.write(f"{name[5:]} = {color!r}\n") all_names_merged = ",\n ".join(f'"{name}"' for name in all_names) - f.write(f"\n__all__ = [\n {all_names_merged},\n]\n") + f.write(f"\n__all__ = [ # noqa: RUF022\n {all_names_merged},\n]\n") update_module_all(Path("tcod/libtcodpy.py"), all_names_merged) with Path("tcod/event_constants.py").open("w", encoding="utf-8") as f: @@ -379,12 +380,12 @@ def write_library_constants() -> None: f.write(f"""{parse_sdl_attrs("SDLK", None)[0]}\n""") f.write("\n# --- SDL keyboard modifiers ---\n") - f.write("{}\n_REVERSE_MOD_TABLE = {}\n".format(*parse_sdl_attrs("KMOD", all_names))) + f.write("{}\n_REVERSE_MOD_TABLE = {}\n".format(*parse_sdl_attrs("KMOD", None))) f.write("\n# --- SDL wheel ---\n") f.write("{}\n_REVERSE_WHEEL_TABLE = {}\n".format(*parse_sdl_attrs("SDL_MOUSEWHEEL", all_names))) all_names_merged = ",\n ".join(f'"{name}"' for name in all_names) - f.write(f"\n__all__ = [\n {all_names_merged},\n]\n") + f.write(f"\n__all__ = [ # noqa: RUF022\n {all_names_merged},\n]\n") event_py = Path("tcod/event.py").read_text(encoding="utf-8") diff --git a/pyproject.toml b/pyproject.toml index 6c68edb6..78e88f2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,20 +178,18 @@ select = [ "D", # pydocstyle ] ignore = [ - "E501", # line-too-long - "S101", # assert - "S301", # suspicious-pickle-usage - "S311", # suspicious-non-cryptographic-random-usage - "ANN101", # missing-type-self - "ANN102", # missing-type-cls - "D203", # one-blank-line-before-class - "D204", # one-blank-line-after-class - "D213", # multi-line-summary-second-line - "D407", # dashed-underline-after-section - "D408", # section-underline-after-name - "D409", # section-underline-matches-section-length - "D206", # indent-with-spaces - "W191", # tab-indentation + "E501", # line-too-long + "S101", # assert + "S301", # suspicious-pickle-usage + "S311", # suspicious-non-cryptographic-random-usage + "D203", # one-blank-line-before-class + "D204", # one-blank-line-after-class + "D213", # multi-line-summary-second-line + "D407", # dashed-underline-after-section + "D408", # section-underline-after-name + "D409", # section-underline-matches-section-length + "D206", # indent-with-spaces + "W191", # tab-indentation ] [tool.ruff.lint.pydocstyle] # https://docs.astral.sh/ruff/settings/#lintpydocstyle diff --git a/tcod/__init__.py b/tcod/__init__.py index 63335ebc..b155be01 100644 --- a/tcod/__init__.py +++ b/tcod/__init__.py @@ -19,22 +19,22 @@ from tcod.version import __version__ __all__ = [ - "__version__", + "Console", "__sdl_version__", - "lib", - "ffi", + "__version__", "bsp", "color", "console", "context", "event", - "tileset", + "ffi", "image", + "lib", "los", "map", "noise", "path", "random", "tileset", - "Console", + "tileset", ] diff --git a/tcod/cffi.py b/tcod/cffi.py index 2325519d..eb94d5e1 100644 --- a/tcod/cffi.py +++ b/tcod/cffi.py @@ -76,4 +76,4 @@ def _libtcod_log_watcher(message: Any, userdata: None) -> None: # noqa: ANN401 lib.TCOD_set_log_callback(lib._libtcod_log_watcher, ffi.NULL) lib.TCOD_set_log_level(0) -__all__ = ["ffi", "lib", "__sdl_version__"] +__all__ = ["__sdl_version__", "ffi", "lib"] diff --git a/tcod/constants.py b/tcod/constants.py index 98f1ef1f..ebaa4f4b 100644 --- a/tcod/constants.py +++ b/tcod/constants.py @@ -503,7 +503,7 @@ white = Color(255, 255, 255) yellow = Color(255, 255, 0) -__all__ = [ +__all__ = [ # noqa: RUF022 "FOV_BASIC", "FOV_DIAMOND", "FOV_PERMISSIVE_0", diff --git a/tcod/context.py b/tcod/context.py index 1a2ea21c..78d18de2 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -44,24 +44,24 @@ from tcod.cffi import ffi, lib __all__ = ( - "Context", - "new", - "new_window", - "new_terminal", - "SDL_WINDOW_FULLSCREEN", - "SDL_WINDOW_FULLSCREEN_DESKTOP", - "SDL_WINDOW_HIDDEN", - "SDL_WINDOW_BORDERLESS", - "SDL_WINDOW_RESIZABLE", - "SDL_WINDOW_MINIMIZED", - "SDL_WINDOW_MAXIMIZED", - "SDL_WINDOW_INPUT_GRABBED", - "SDL_WINDOW_ALLOW_HIGHDPI", "RENDERER_OPENGL", "RENDERER_OPENGL2", "RENDERER_SDL", "RENDERER_SDL2", "RENDERER_XTERM", + "SDL_WINDOW_ALLOW_HIGHDPI", + "SDL_WINDOW_BORDERLESS", + "SDL_WINDOW_FULLSCREEN", + "SDL_WINDOW_FULLSCREEN_DESKTOP", + "SDL_WINDOW_HIDDEN", + "SDL_WINDOW_INPUT_GRABBED", + "SDL_WINDOW_MAXIMIZED", + "SDL_WINDOW_MINIMIZED", + "SDL_WINDOW_RESIZABLE", + "Context", + "new", + "new_terminal", + "new_window", ) _Event = TypeVar("_Event", bound=tcod.event.Event) diff --git a/tcod/event.py b/tcod/event.py index 48cf9c37..2f77c679 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -2818,7 +2818,7 @@ def __getattr__(name: str) -> int: return value -__all__ = [ # noqa: F405 +__all__ = [ # noqa: F405 RUF022 "Modifier", "Point", "BUTTON_LEFT", diff --git a/tcod/event_constants.py b/tcod/event_constants.py index 0f55d545..6f012293 100644 --- a/tcod/event_constants.py +++ b/tcod/event_constants.py @@ -539,7 +539,7 @@ 1027: "MOUSEWHEEL", } -__all__ = [ +__all__ = [ # noqa: RUF022 "MOUSEWHEEL_NORMAL", "MOUSEWHEEL_FLIPPED", "MOUSEWHEEL", diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index aa9cc51b..578d2610 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -4281,7 +4281,7 @@ def __getattr__(name: str) -> Color: raise AttributeError(msg) from None -__all__ = [ # noqa: F405 +__all__ = [ # noqa: F405 RUF022 "Color", "Bsp", "NB_FOV_ALGORITHMS", diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 53a8e2db..f6dc922f 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -20,11 +20,11 @@ from tcod.sdl._internal import _check, _check_p, _required_version, _version_at_least __all__ = ( - "WindowFlags", "FlashOperation", "Window", - "new_window", + "WindowFlags", "get_grabbed_window", + "new_window", "screen_saver_allowed", ) diff --git a/tcod/tcod.py b/tcod/tcod.py index 8181da79..cf12dceb 100644 --- a/tcod/tcod.py +++ b/tcod/tcod.py @@ -79,7 +79,6 @@ def __getattr__(name: str, stacklevel: int = 1) -> Any: # noqa: ANN401 "console", "context", "event", - "tileset", "image", "los", "map", @@ -87,4 +86,5 @@ def __getattr__(name: str, stacklevel: int = 1) -> Any: # noqa: ANN401 "path", "random", "tileset", + "tileset", ] From e66b22124ff086633c98e68b29091a1171e24483 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 24 Nov 2024 20:11:18 -0800 Subject: [PATCH 104/134] Update PyInstaller example and docs for loading tilesets --- examples/distribution/PyInstaller/main.py | 5 ++--- .../distribution/PyInstaller/requirements.txt | 6 +++--- tcod/tileset.py | 20 +++++++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/examples/distribution/PyInstaller/main.py b/examples/distribution/PyInstaller/main.py index b2767fed..5e034afd 100755 --- a/examples/distribution/PyInstaller/main.py +++ b/examples/distribution/PyInstaller/main.py @@ -5,7 +5,6 @@ # https://creativecommons.org/publicdomain/zero/1.0/ """PyInstaller main script example.""" -import sys from pathlib import Path import tcod.console @@ -15,8 +14,8 @@ WIDTH, HEIGHT = 80, 60 -# The base directory, this is sys._MEIPASS when in one-file mode. -BASE_DIR = Path(getattr(sys, "_MEIPASS", ".")) +BASE_DIR = Path(__file__).parent +"""The directory of this script.""" FONT_PATH = BASE_DIR / "data/terminal8x8_gs_ro.png" diff --git a/examples/distribution/PyInstaller/requirements.txt b/examples/distribution/PyInstaller/requirements.txt index 390bb9dd..477fbbdc 100644 --- a/examples/distribution/PyInstaller/requirements.txt +++ b/examples/distribution/PyInstaller/requirements.txt @@ -1,3 +1,3 @@ -tcod==12.2.0 -pyinstaller==5.13.1 -pypiwin32; sys_platform=="win32" +tcod==16.2.3 +pyinstaller==6.9.0 +pypiwin32; sys_platform=="win32" diff --git a/tcod/tileset.py b/tcod/tileset.py index dad40f18..f57304da 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -1,5 +1,7 @@ """Tileset and font related functions. +If you want to load a tileset from a common tileset image then you only need :any:`tcod.tileset.load_tilesheet`. + Tilesets can be loaded as a whole from tile-sheets or True-Type fonts, or they can be put together from multiple tile images by loading them separately using :any:`Tileset.set_tile`. @@ -342,6 +344,24 @@ def load_tilesheet(path: str | PathLike[str], columns: int, rows: int, charmap: If `None` is used then no tiles will be mapped, you will need to use :any:`Tileset.remap` to assign codepoints to this Tileset. + Image alpha and key colors are handled automatically. + For example any tileset from the `Dwarf Fortress tileset repository `_ + will load correctly with the following example: + + Example:: + + from pathlib import Path + + import tcod.tileset + + THIS_DIR = Path(__file__, "..") # Directory of this script file + FONT = THIS_DIR / "assets/tileset.png" # Replace with any tileset from the DF tileset repository + + # Will raise FileNotFoundError if the font is missing! + tileset = tcod.tileset.load_tilesheet(FONT, 16, 16, tcod.tileset.CHARMAP_CP437) + + The tileset return value is usually passed to the `tileset` parameter of :any:`tcod.context.new`. + .. versionadded:: 11.12 """ path = Path(path).resolve(strict=True) From 1ba890c2aeae7e28d7a7de5b5a3764301aca9030 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 24 Nov 2024 20:15:10 -0800 Subject: [PATCH 105/134] Normalize newlines in requirements.txt Noticed unusual newlines in a previous commit, this was the only other file with incorrect newlines. --- requirements.txt | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/requirements.txt b/requirements.txt index 03ad70a0..d0c9c27e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -attrs>=23.1.0 -cffi>=1.15 -numpy>=1.21.4 -pycparser>=2.14 -requests>=2.28.1 -setuptools==70.0.0 -types-cffi -types-requests -types-Pillow -types-setuptools -types-tabulate -typing_extensions -pcpp==1.30 +attrs>=23.1.0 +cffi>=1.15 +numpy>=1.21.4 +pycparser>=2.14 +requests>=2.28.1 +setuptools==70.0.0 +types-cffi +types-requests +types-Pillow +types-setuptools +types-tabulate +typing_extensions +pcpp==1.30 From 2b3d6fdc3c6d343e6859d4d90455a6770c9b3146 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 27 Mar 2025 23:53:38 -0700 Subject: [PATCH 106/134] Rewrite SDL renderer primitive drawing methods and add test coverage Fixes #159 --- CHANGELOG.md | 1 + tcod/sdl/render.py | 62 ++++++++++++++++++++++------------------------ tests/test_sdl.py | 17 +++++++++++++ 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b87ebb8..1e94088c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ### Fixed - Suppressed internal `mouse.tile_motion` deprecation warning. +- Fixed SDL renderer primitive drawing methods. #159 ## [16.2.3] - 2024-07-16 diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 1def4c72..5dcf65f4 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -581,62 +581,63 @@ def draw_point(self, xy: tuple[float, float]) -> None: .. versionadded:: 13.5 """ - _check(lib.SDL_RenderDrawPointF(self.p, (xy,))) + x, y = xy + _check(lib.SDL_RenderDrawPointF(self.p, x, y)) def draw_line(self, start: tuple[float, float], end: tuple[float, float]) -> None: """Draw a single line. .. versionadded:: 13.5 """ - _check(lib.SDL_RenderDrawLineF(self.p, *start, *end)) - - def fill_rects(self, rects: NDArray[np.intc | np.float32]) -> None: + x1, y1 = start + x2, y2 = end + _check(lib.SDL_RenderDrawLineF(self.p, x1, y1, x2, y2)) + + @staticmethod + def _convert_array(array: NDArray[np.number]) -> NDArray[np.intc] | NDArray[np.float32]: + """Convert ndarray for a SDL function expecting a C contiguous array of either intc or float32.""" + if array.dtype in (np.intc, np.int8, np.int16, np.int32, np.uint8, np.uint16): + return np.ascontiguousarray(array, np.intc) + return np.ascontiguousarray(array, np.float32) + + def fill_rects(self, rects: NDArray[np.number]) -> None: """Fill multiple rectangles from an array. .. versionadded:: 13.5 """ assert len(rects.shape) == 2 # noqa: PLR2004 assert rects.shape[1] == 4 # noqa: PLR2004 - rects = np.ascontiguousarray(rects) + rects = self._convert_array(rects) if rects.dtype == np.intc: _check(lib.SDL_RenderFillRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0])) - elif rects.dtype == np.float32: - _check(lib.SDL_RenderFillRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) - else: - msg = f"Array must be an np.intc or np.float32 type, got {rects.dtype}." - raise TypeError(msg) + return + _check(lib.SDL_RenderFillRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) - def draw_rects(self, rects: NDArray[np.intc | np.float32]) -> None: + def draw_rects(self, rects: NDArray[np.number]) -> None: """Draw multiple outlined rectangles from an array. .. versionadded:: 13.5 """ assert len(rects.shape) == 2 # noqa: PLR2004 assert rects.shape[1] == 4 # noqa: PLR2004 - rects = np.ascontiguousarray(rects) + rects = self._convert_array(rects) if rects.dtype == np.intc: _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0])) - elif rects.dtype == np.float32: - _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) - else: - msg = f"Array must be an np.intc or np.float32 type, got {rects.dtype}." - raise TypeError(msg) + return + _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) - def draw_points(self, points: NDArray[np.intc | np.float32]) -> None: + def draw_points(self, points: NDArray[np.number]) -> None: """Draw an array of points. .. versionadded:: 13.5 """ assert len(points.shape) == 2 # noqa: PLR2004 assert points.shape[1] == 2 # noqa: PLR2004 - points = np.ascontiguousarray(points) + points = self._convert_array(points) if points.dtype == np.intc: - _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0])) - elif points.dtype == np.float32: - _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0])) - else: - msg = f"Array must be an np.intc or np.float32 type, got {points.dtype}." - raise TypeError(msg) + _check(lib.SDL_RenderDrawPoints(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0])) + return + _check(lib.SDL_RenderDrawPointsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0])) def draw_lines(self, points: NDArray[np.intc | np.float32]) -> None: """Draw a connected series of lines from an array. @@ -645,14 +646,11 @@ def draw_lines(self, points: NDArray[np.intc | np.float32]) -> None: """ assert len(points.shape) == 2 # noqa: PLR2004 assert points.shape[1] == 2 # noqa: PLR2004 - points = np.ascontiguousarray(points) + points = self._convert_array(points) if points.dtype == np.intc: - _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0] - 1)) - elif points.dtype == np.float32: - _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0] - 1)) - else: - msg = f"Array must be an np.intc or np.float32 type, got {points.dtype}." - raise TypeError(msg) + _check(lib.SDL_RenderDrawLines(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0] - 1)) + return + _check(lib.SDL_RenderDrawLinesF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0] - 1)) @_required_version((2, 0, 18)) def geometry( diff --git a/tests/test_sdl.py b/tests/test_sdl.py index f22b7a72..d41c4152 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -76,6 +76,23 @@ def test_sdl_render(uses_window: None) -> None: assert (render.read_pixels(format="RGB") == (0, 0, 0)).all() assert render.read_pixels(rect=(1, 2, 3, 4)).shape == (4, 3, 4) + render.clear() + + render.draw_point((0, 0)) + render.draw_points(np.ones((3, 2), dtype=np.float32)) + render.draw_points(np.ones((3, 2), dtype=np.intc)) + render.draw_points(np.ones((3, 2), dtype=np.float16)) + render.draw_points(np.ones((3, 2), dtype=np.int8)) + render.draw_line((0, 0), (1, 1)) + render.draw_lines(np.ones((3, 2), dtype=np.float32)) + render.draw_lines(np.ones((3, 2), dtype=np.intc)) + render.draw_rect((0, 0, 1, 1)) + render.draw_rects(np.ones((3, 4), dtype=np.float32)) + render.draw_rects(np.ones((3, 4), dtype=np.intc)) + render.fill_rect((0, 0, 1, 1)) + render.fill_rects(np.ones((3, 4), dtype=np.float32)) + render.fill_rects(np.ones((3, 4), dtype=np.intc)) + def test_sdl_render_bad_types() -> None: with pytest.raises(TypeError): From aa7a0e7e913c5e5a6c61a5e0381e6c26ceb33a90 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Mar 2025 00:04:06 -0700 Subject: [PATCH 107/134] Update pre-commit, fix Ruff and Mypy errors --- .pre-commit-config.yaml | 2 +- examples/samples_libtcodpy.py | 7 ++----- examples/samples_tcod.py | 4 ++-- examples/thread_jobs.py | 9 +++------ tcod/console.py | 8 ++++---- tcod/event.py | 10 ++++------ tcod/libtcodpy.py | 8 ++++---- tcod/noise.py | 2 +- tcod/path.py | 2 +- tests/test_console.py | 2 +- 10 files changed, 23 insertions(+), 31 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3d62742..8960f446 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 + rev: v0.11.2 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] diff --git a/examples/samples_libtcodpy.py b/examples/samples_libtcodpy.py index ab68eb91..9e0cb41d 100755 --- a/examples/samples_libtcodpy.py +++ b/examples/samples_libtcodpy.py @@ -220,7 +220,7 @@ def render_colors(first, key, mouse): SAMPLE_SCREEN_HEIGHT - 1, libtcod.BKGND_MULTIPLY, libtcod.CENTER, - "The Doryen library uses 24 bits " "colors, for both background and " "foreground.", + "The Doryen library uses 24 bits colors, for both background and foreground.", ) if key.c == ord("f"): @@ -270,10 +270,7 @@ def render_offscreen(first, key, mouse): SAMPLE_SCREEN_HEIGHT // 2, libtcod.BKGND_NONE, libtcod.CENTER, - b"You can render to an offscreen " - b"console and blit in on another " - b"one, simulating alpha " - b"transparency.", + b"You can render to an offscreen console and blit in on another one, simulating alpha transparency.", ) if first: libtcod.sys_set_fps(30) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 942c1d69..5f8ea1be 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -163,7 +163,7 @@ def print_banner(self) -> None: y=5, width=sample_console.width - 2, height=sample_console.height - 1, - string="The Doryen library uses 24 bits colors, for both " "background and foreground.", + string="The Doryen library uses 24 bits colors, for both background and foreground.", fg=WHITE, bg=GREY, bg_blend=libtcodpy.BKGND_MULTIPLY, @@ -198,7 +198,7 @@ def __init__(self) -> None: 2, sample_console.width // 2 - 2, sample_console.height // 2, - "You can render to an offscreen console and blit in on another " "one, simulating alpha transparency.", + "You can render to an offscreen console and blit in on another one, simulating alpha transparency.", fg=WHITE, bg=None, alignment=libtcodpy.CENTER, diff --git a/examples/thread_jobs.py b/examples/thread_jobs.py index 0c616bba..fa8ce9dc 100755 --- a/examples/thread_jobs.py +++ b/examples/thread_jobs.py @@ -77,7 +77,7 @@ def run_test( for i in range(1, THREADS + 1): executor = concurrent.futures.ThreadPoolExecutor(i) multi_time = min(timeit.repeat(lambda: multi_func(executor, maps), number=1, repeat=REPEAT)) - print(f"{i} threads: {multi_time * 1000:.2f}ms, " f"{single_time / (multi_time * i) * 100:.2f}% efficiency") + print(f"{i} threads: {multi_time * 1000:.2f}ms, {single_time / (multi_time * i) * 100:.2f}% efficiency") def main() -> None: @@ -89,13 +89,10 @@ def main() -> None: print(f"Python {sys.version}\n{platform.platform()}\n{platform.processor()}") - print(f"\nComputing field-of-view for " f"{len(maps)} empty {MAP_WIDTH}x{MAP_HEIGHT} maps.") + print(f"\nComputing field-of-view for {len(maps)} empty {MAP_WIDTH}x{MAP_HEIGHT} maps.") run_test(maps, test_fov_single, test_fov_threads) - print( - f"\nComputing AStar from corner to corner {len(maps)} times " - f"on separate empty {MAP_WIDTH}x{MAP_HEIGHT} maps." - ) + print(f"\nComputing AStar from corner to corner {len(maps)} times on separate empty {MAP_WIDTH}x{MAP_HEIGHT} maps.") run_test(maps, test_astar_single, test_astar_threads) diff --git a/tcod/console.py b/tcod/console.py index 0760ec7d..80144ab4 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -29,7 +29,7 @@ def _fmt(string: str) -> bytes: _root_console = None -rgba_graphic = np.dtype([("ch", np.intc), ("fg", "4B"), ("bg", "4B")]) +rgba_graphic: np.dtype[Any] = np.dtype([("ch", np.intc), ("fg", "4B"), ("bg", "4B")]) """A NumPy :any:`dtype` compatible with :any:`Console.rgba`. This dtype is: ``np.dtype([("ch", np.intc), ("fg", "4B"), ("bg", "4B")])`` @@ -37,7 +37,7 @@ def _fmt(string: str) -> bytes: .. versionadded:: 12.3 """ -rgb_graphic = np.dtype([("ch", np.intc), ("fg", "3B"), ("bg", "3B")]) +rgb_graphic: np.dtype[Any] = np.dtype([("ch", np.intc), ("fg", "3B"), ("bg", "3B")]) """A NumPy :any:`dtype` compatible with :any:`Console.rgb`. This dtype is: ``np.dtype([("ch", np.intc), ("fg", "3B"), ("bg", "3B")])`` @@ -104,7 +104,7 @@ class Console: DTYPE = rgba_graphic # A structured array type with the added "fg_rgb" and "bg_rgb" fields. - _DTYPE_RGB = np.dtype( + _DTYPE_RGB: np.dtype[Any] = np.dtype( { "names": ["ch", "fg", "bg"], "formats": [np.int32, "3u1", "3u1"], @@ -955,7 +955,7 @@ def __setstate__(self, state: dict[str, Any]) -> None: def __repr__(self) -> str: """Return a string representation of this console.""" - return "tcod.console.Console(width=%i, height=%i, " "order=%r,buffer=\n%r)" % ( + return "tcod.console.Console(width=%i, height=%i, order=%r,buffer=\n%r)" % ( self.width, self.height, self._order, diff --git a/tcod/event.py b/tcod/event.py index 2f77c679..d225ae37 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -831,7 +831,7 @@ def joystick(self) -> tcod.sdl.joystick.Joystick: return tcod.sdl.joystick.Joystick._from_instance_id(self.which) def __repr__(self) -> str: - return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which})" + return f"tcod.event.{self.__class__.__name__}(type={self.type!r}, which={self.which})" def __str__(self) -> str: prefix = super().__str__().strip("<>") @@ -934,9 +934,7 @@ def from_sdl_event(cls, sdl_event: Any) -> JoystickHat: return cls("JOYHATMOTION", sdl_event.jhat.which, *_HAT_DIRECTIONS[sdl_event.jhat.hat]) def __repr__(self) -> str: - return ( - f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which}, x={self.x}, y={self.y})" - ) + return f"tcod.event.{self.__class__.__name__}(type={self.type!r}, which={self.which}, x={self.x}, y={self.y})" def __str__(self) -> str: prefix = super().__str__().strip("<>") @@ -977,7 +975,7 @@ def from_sdl_event(cls, sdl_event: Any) -> JoystickButton: return cls(type, sdl_event.jbutton.which, sdl_event.jbutton.button) def __repr__(self) -> str: - return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which}, button={self.button})" + return f"tcod.event.{self.__class__.__name__}(type={self.type!r}, which={self.which}, button={self.button})" def __str__(self) -> str: prefix = super().__str__().strip("<>") @@ -1032,7 +1030,7 @@ def controller(self) -> tcod.sdl.joystick.GameController: return tcod.sdl.joystick.GameController._from_instance_id(self.which) def __repr__(self) -> str: - return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which})" + return f"tcod.event.{self.__class__.__name__}(type={self.type!r}, which={self.which})" def __str__(self) -> str: prefix = super().__str__().strip("<>") diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 578d2610..568f04d3 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -697,7 +697,7 @@ def bsp_traverse_level_order( _bsp_traverse(node.level_order(), callback, userData) -@deprecate("Iterate over nodes using " "'for n in node.inverted_level_order():' instead.") +@deprecate("Iterate over nodes using 'for n in node.inverted_level_order():' instead.") def bsp_traverse_inverted_level_order( node: tcod.bsp.BSP, callback: Callable[[tcod.bsp.BSP, Any], None], @@ -1863,7 +1863,7 @@ def console_delete(con: tcod.console.Console) -> None: ) else: warnings.warn( - "You no longer need to make this call, " "Console's are deleted when they go out of scope.", + "You no longer need to make this call, Console's are deleted when they go out of scope.", DeprecationWarning, stacklevel=2, ) @@ -2398,7 +2398,7 @@ def heightmap_set_value(hm: NDArray[np.float32], x: int, y: int, value: float) - """ if hm.flags["C_CONTIGUOUS"]: warnings.warn( - "Assign to this heightmap with hm[i,j] = value\n" "consider using order='F'", + "Assign to this heightmap with hm[i,j] = value\nconsider using order='F'", DeprecationWarning, stacklevel=2, ) @@ -2847,7 +2847,7 @@ def heightmap_get_value(hm: NDArray[np.float32], x: int, y: int) -> float: """ if hm.flags["C_CONTIGUOUS"]: warnings.warn( - "Get a value from this heightmap with hm[i,j]\n" "consider using order='F'", + "Get a value from this heightmap with hm[i,j]\nconsider using order='F'", DeprecationWarning, stacklevel=2, ) diff --git a/tcod/noise.py b/tcod/noise.py index 108fa2c0..b91c90b6 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -91,7 +91,7 @@ def __repr__(self) -> str: def __getattr__(name: str) -> Implementation: if name in Implementation.__members__: warnings.warn( - f"'tcod.noise.{name}' is deprecated," f" use 'tcod.noise.Implementation.{name}' instead.", + f"'tcod.noise.{name}' is deprecated, use 'tcod.noise.Implementation.{name}' instead.", FutureWarning, stacklevel=2, ) diff --git a/tcod/path.py b/tcod/path.py index ffbc1dab..e55223d7 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -171,7 +171,7 @@ def __init__(self, cost: Any, diagonal: float = 1.41) -> None: if not hasattr(self.cost, "get_tcod_path_ffi"): assert not callable(self.cost), ( - "Any callback alone is missing shape information. " "Wrap your callback in tcod.path.EdgeCostCallback" + "Any callback alone is missing shape information. Wrap your callback in tcod.path.EdgeCostCallback" ) self.cost = NodeCostArray(self.cost) diff --git a/tests/test_console.py b/tests/test_console.py index b0be600f..0681368f 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -112,7 +112,7 @@ def test_console_str() -> None: console.ch[:] = ord(".") with pytest.warns(): console.print_(0, 0, "Test") - assert str(console) == ("") + assert str(console) == ("") def test_console_fortran_buffer() -> None: From bbf8b62045a3dcee4ad04bec184d49e7d9f3535c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Mar 2025 00:20:59 -0700 Subject: [PATCH 108/134] Catch sneaky whitespace when culling va_list functions --- .github/workflows/python-package.yml | 2 +- build_sdl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0417777c..5c61ff6e 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -71,7 +71,7 @@ jobs: strategy: matrix: os: ["windows-latest", "macos-latest"] - sdl-version: ["2.0.14", "2.0.16"] + sdl-version: ["2.0.14", "2.0.16", "2.30.0"] fail-fast: true steps: - uses: actions/checkout@v4 diff --git a/build_sdl.py b/build_sdl.py index 629c98f4..99c08d25 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -33,7 +33,7 @@ # Used to remove excessive newlines in debug outputs. RE_NEWLINES = re.compile(r"\n\n+") # Functions using va_list need to be culled. -RE_VAFUNC = re.compile(r"^.*?\([^()]*va_list[^()]*\);$", re.MULTILINE) +RE_VAFUNC = re.compile(r"^.*?\([^()]*va_list[^()]*\)\s*;\s*$", re.MULTILINE) # Static inline functions need to be culled. RE_INLINE = re.compile(r"^static inline.*?^}$", re.MULTILINE | re.DOTALL) # Most SDL_PIXELFORMAT names need their values scrubbed. From 87dc70067a2daba5e1539b9a686fb2de898db290 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Mar 2025 00:26:48 -0700 Subject: [PATCH 109/134] Add more ignored defines --- build_sdl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build_sdl.py b/build_sdl.py index 99c08d25..41f91b22 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -60,6 +60,9 @@ "SDL_INLINE", "SDL_FORCE_INLINE", "SDL_FALLTHROUGH", + "SDL_HAS_FALLTHROUGH", + "SDL_NO_THREAD_SAFETY_ANALYSIS", + "SDL_SCOPED_CAPABILITY", # Might show up in parsing and not in source. "SDL_ANDROID_EXTERNAL_STORAGE_READ", "SDL_ANDROID_EXTERNAL_STORAGE_WRITE", From fabb04676cd222eb2d96993d6ff685333c831d22 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Mar 2025 00:48:36 -0700 Subject: [PATCH 110/134] Fix SDL renderer test Clear first to ensure the renderer is blank for reading --- tests/test_sdl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_sdl.py b/tests/test_sdl.py index d41c4152..5dc2bf40 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -58,7 +58,9 @@ def test_sdl_screen_saver(uses_window: None) -> None: def test_sdl_render(uses_window: None) -> None: window = tcod.sdl.video.new_window(1, 1) render = tcod.sdl.render.new_renderer(window, software=True, vsync=False, target_textures=True) + render.clear() render.present() + render.clear() rgb = render.upload_texture(np.zeros((8, 8, 3), np.uint8)) assert (rgb.width, rgb.height) == (8, 8) assert rgb.access == tcod.sdl.render.TextureAccess.STATIC @@ -76,8 +78,6 @@ def test_sdl_render(uses_window: None) -> None: assert (render.read_pixels(format="RGB") == (0, 0, 0)).all() assert render.read_pixels(rect=(1, 2, 3, 4)).shape == (4, 3, 4) - render.clear() - render.draw_point((0, 0)) render.draw_points(np.ones((3, 2), dtype=np.float32)) render.draw_points(np.ones((3, 2), dtype=np.intc)) From 048d1805a851046fb88bcffcdc66c3bb1a2813ff Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Mar 2025 01:11:13 -0700 Subject: [PATCH 111/134] Prepare 17.0.0 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e94088c..fe453402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +## [17.0.0] - 2025-03-28 + ### Changed - `EventDispatch`'s on event methods are now defined as positional parameters, so renaming the `event` parameter is now valid in subclasses. From ea7c3b0fa43c22b1d83fa3dd4b0951372c0cafaa Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Mar 2025 16:49:56 -0700 Subject: [PATCH 112/134] Refactor type support for SDL primitive drawing Now raises TypeError for incorrect shapes --- CHANGELOG.md | 8 +++++++ tcod/sdl/render.py | 59 ++++++++++++++++++++++++++++++---------------- tests/test_sdl.py | 26 ++++++++++++-------- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe453402..7ad01870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +### Added + +- SDL renderer primitive drawing methods now support sequences of tuples. + +### Fixed + +- `tcod.sdl.Renderer.draw_lines` type hint was too narrow. + ## [17.0.0] - 2025-03-28 ### Changed diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 5dcf65f4..ed93fb6b 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -6,7 +6,7 @@ from __future__ import annotations import enum -from typing import Any, Final +from typing import Any, Final, Sequence import numpy as np from numpy.typing import NDArray @@ -594,59 +594,78 @@ def draw_line(self, start: tuple[float, float], end: tuple[float, float]) -> Non _check(lib.SDL_RenderDrawLineF(self.p, x1, y1, x2, y2)) @staticmethod - def _convert_array(array: NDArray[np.number]) -> NDArray[np.intc] | NDArray[np.float32]: - """Convert ndarray for a SDL function expecting a C contiguous array of either intc or float32.""" - if array.dtype in (np.intc, np.int8, np.int16, np.int32, np.uint8, np.uint16): - return np.ascontiguousarray(array, np.intc) - return np.ascontiguousarray(array, np.float32) + def _convert_array( + array: NDArray[np.number] | Sequence[Sequence[float]], item_length: int + ) -> NDArray[np.intc] | NDArray[np.float32]: + """Convert ndarray for a SDL function expecting a C contiguous array of either intc or float32. - def fill_rects(self, rects: NDArray[np.number]) -> None: + Array shape is enforced to be (n, item_length) + """ + if getattr(array, "dtype", None) in (np.intc, np.int8, np.int16, np.int32, np.uint8, np.uint16): + out = np.ascontiguousarray(array, np.intc) + else: + out = np.ascontiguousarray(array, np.float32) + if len(out.shape) != 2: # noqa: PLR2004 + msg = f"Array must have 2 axes, but shape is {out.shape!r}" + raise TypeError(msg) + if out.shape[1] != item_length: + msg = f"Array shape[1] must be {item_length}, but shape is {out.shape!r}" + raise TypeError(msg) + return out + + def fill_rects(self, rects: NDArray[np.number] | Sequence[tuple[float, float, float, float]]) -> None: """Fill multiple rectangles from an array. + Args: + rects: A sequence or array of (x, y, width, height) rectangles. + .. versionadded:: 13.5 """ - assert len(rects.shape) == 2 # noqa: PLR2004 - assert rects.shape[1] == 4 # noqa: PLR2004 - rects = self._convert_array(rects) + rects = self._convert_array(rects, item_length=4) if rects.dtype == np.intc: _check(lib.SDL_RenderFillRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0])) return _check(lib.SDL_RenderFillRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) - def draw_rects(self, rects: NDArray[np.number]) -> None: + def draw_rects(self, rects: NDArray[np.number] | Sequence[tuple[float, float, float, float]]) -> None: """Draw multiple outlined rectangles from an array. + Args: + rects: A sequence or array of (x, y, width, height) rectangles. + .. versionadded:: 13.5 """ + rects = self._convert_array(rects, item_length=4) assert len(rects.shape) == 2 # noqa: PLR2004 assert rects.shape[1] == 4 # noqa: PLR2004 - rects = self._convert_array(rects) if rects.dtype == np.intc: _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0])) return _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) - def draw_points(self, points: NDArray[np.number]) -> None: + def draw_points(self, points: NDArray[np.number] | Sequence[tuple[float, float]]) -> None: """Draw an array of points. + Args: + points: A sequence or array of (x, y) points. + .. versionadded:: 13.5 """ - assert len(points.shape) == 2 # noqa: PLR2004 - assert points.shape[1] == 2 # noqa: PLR2004 - points = self._convert_array(points) + points = self._convert_array(points, item_length=2) if points.dtype == np.intc: _check(lib.SDL_RenderDrawPoints(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0])) return _check(lib.SDL_RenderDrawPointsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0])) - def draw_lines(self, points: NDArray[np.intc | np.float32]) -> None: + def draw_lines(self, points: NDArray[np.number] | Sequence[tuple[float, float]]) -> None: """Draw a connected series of lines from an array. + Args: + points: A sequence or array of (x, y) points. + .. versionadded:: 13.5 """ - assert len(points.shape) == 2 # noqa: PLR2004 - assert points.shape[1] == 2 # noqa: PLR2004 - points = self._convert_array(points) + points = self._convert_array(points, item_length=2) if points.dtype == np.intc: _check(lib.SDL_RenderDrawLines(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0] - 1)) return diff --git a/tests/test_sdl.py b/tests/test_sdl.py index 5dc2bf40..f67049a6 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -79,19 +79,25 @@ def test_sdl_render(uses_window: None) -> None: assert render.read_pixels(rect=(1, 2, 3, 4)).shape == (4, 3, 4) render.draw_point((0, 0)) - render.draw_points(np.ones((3, 2), dtype=np.float32)) - render.draw_points(np.ones((3, 2), dtype=np.intc)) - render.draw_points(np.ones((3, 2), dtype=np.float16)) - render.draw_points(np.ones((3, 2), dtype=np.int8)) render.draw_line((0, 0), (1, 1)) - render.draw_lines(np.ones((3, 2), dtype=np.float32)) - render.draw_lines(np.ones((3, 2), dtype=np.intc)) render.draw_rect((0, 0, 1, 1)) - render.draw_rects(np.ones((3, 4), dtype=np.float32)) - render.draw_rects(np.ones((3, 4), dtype=np.intc)) render.fill_rect((0, 0, 1, 1)) - render.fill_rects(np.ones((3, 4), dtype=np.float32)) - render.fill_rects(np.ones((3, 4), dtype=np.intc)) + + render.draw_points([(0, 0)]) + render.draw_lines([(0, 0), (1, 1)]) + render.draw_rects([(0, 0, 1, 1)]) + render.fill_rects([(0, 0, 1, 1)]) + + for dtype in (np.intc, np.int8, np.uint8, np.float32, np.float16): + render.draw_points(np.ones((3, 2), dtype=dtype)) + render.draw_lines(np.ones((3, 2), dtype=dtype)) + render.draw_rects(np.ones((3, 4), dtype=dtype)) + render.fill_rects(np.ones((3, 4), dtype=dtype)) + + with pytest.raises(TypeError, match=r"shape\[1\] must be 2"): + render.draw_points(np.ones((3, 1), dtype=dtype)) + with pytest.raises(TypeError, match=r"must have 2 axes"): + render.draw_points(np.ones((3,), dtype=dtype)) def test_sdl_render_bad_types() -> None: From 9c7bfc0848030173075c28226fcec34349208cff Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Mar 2025 16:59:58 -0700 Subject: [PATCH 113/134] Test and fix Renderer.geometry method --- CHANGELOG.md | 1 + tcod/sdl/render.py | 24 ++++++++++++++---------- tests/test_sdl.py | 8 ++++++++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad01870..2d169a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ### Fixed - `tcod.sdl.Renderer.draw_lines` type hint was too narrow. +- Fixed crash in `tcod.sdl.Renderer.geometry`. ## [17.0.0] - 2025-03-28 diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index ed93fb6b..ae2b542d 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -675,29 +675,33 @@ def draw_lines(self, points: NDArray[np.number] | Sequence[tuple[float, float]]) def geometry( self, texture: Texture | None, - xy: NDArray[np.float32], - color: NDArray[np.uint8], - uv: NDArray[np.float32], + xy: NDArray[np.float32] | Sequence[tuple[float, float]], + color: NDArray[np.uint8] | Sequence[tuple[int, int, int, int]], + uv: NDArray[np.float32] | Sequence[tuple[float, float]], indices: NDArray[np.uint8 | np.uint16 | np.uint32] | None = None, ) -> None: """Render triangles from texture and vertex data. + Args: + texture: The SDL texture to render from. + xy: A sequence of (x, y) points to buffer. + color: A sequence of (r, g, b, a) colors to buffer. + uv: A sequence of (x, y) coordinates to buffer. + indices: A sequence of indexes referring to the buffered data, every 3 indexes is a triangle to render. + .. versionadded:: 13.5 """ - assert xy.dtype == np.float32 + xy = np.ascontiguousarray(xy, np.float32) assert len(xy.shape) == 2 # noqa: PLR2004 assert xy.shape[1] == 2 # noqa: PLR2004 - assert xy[0].flags.c_contiguous - assert color.dtype == np.uint8 + color = np.ascontiguousarray(color, np.uint8) assert len(color.shape) == 2 # noqa: PLR2004 assert color.shape[1] == 4 # noqa: PLR2004 - assert color[0].flags.c_contiguous - assert uv.dtype == np.float32 + uv = np.ascontiguousarray(uv, np.float32) assert len(uv.shape) == 2 # noqa: PLR2004 assert uv.shape[1] == 2 # noqa: PLR2004 - assert uv[0].flags.c_contiguous if indices is not None: assert indices.dtype.type in (np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32) indices = np.ascontiguousarray(indices) @@ -709,7 +713,7 @@ def geometry( texture.p if texture else ffi.NULL, ffi.cast("float*", xy.ctypes.data), xy.strides[0], - ffi.cast("uint8_t*", color.ctypes.data), + ffi.cast("SDL_Color*", color.ctypes.data), color.strides[0], ffi.cast("float*", uv.ctypes.data), uv.strides[0], diff --git a/tests/test_sdl.py b/tests/test_sdl.py index f67049a6..e29554df 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -99,6 +99,14 @@ def test_sdl_render(uses_window: None) -> None: with pytest.raises(TypeError, match=r"must have 2 axes"): render.draw_points(np.ones((3,), dtype=dtype)) + render.geometry( + None, + np.zeros((1, 2), np.float32), + np.zeros((1, 4), np.uint8), + np.zeros((1, 2), np.float32), + np.zeros((3,), np.uint8), + ) + def test_sdl_render_bad_types() -> None: with pytest.raises(TypeError): From 250d0ad0d593e2df2ff87b2c3e7ef9a50e329088 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Mar 2025 21:45:56 -0700 Subject: [PATCH 114/134] Prepare 17.1.0 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d169a30..c36415ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +## [17.1.0] - 2025-03-29 + ### Added - SDL renderer primitive drawing methods now support sequences of tuples. From 07ea8ac7d03ebf08704b5a89f602a618e4b69830 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 3 Apr 2025 15:49:06 -0700 Subject: [PATCH 115/134] Decorate already deprecated functions Clean up deprecation warnings in tests --- pyproject.toml | 3 +-- tcod/console.py | 16 ++++++++++++---- tcod/libtcodpy.py | 13 +++++++------ tests/conftest.py | 25 ++++++++++++++----------- tests/test_console.py | 12 ++++++++---- tests/test_random.py | 8 ++++++-- tests/test_tcod.py | 19 ++++++++++--------- 7 files changed, 58 insertions(+), 38 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 78e88f2d..2590d920 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,8 +99,7 @@ addopts = [ log_file_level = "DEBUG" faulthandler_timeout = 5 filterwarnings = [ - "ignore::DeprecationWarning:tcod.libtcodpy", - "ignore::PendingDeprecationWarning:tcod.libtcodpy", + "ignore:This function may be deprecated in the future:PendingDeprecationWarning", "ignore:This class may perform poorly and is no longer needed.::tcod.map", "ignore:'import tcod as libtcodpy' is preferred.", ] diff --git a/tcod/console.py b/tcod/console.py index 80144ab4..a1461965 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -645,6 +645,7 @@ def get_height_rect(self, x: int, y: int, width: int, height: int, string: str) string_ = string.encode("utf-8") return int(lib.TCOD_console_get_height_rect_n(self.console_c, x, y, width, height, len(string_), string_)) + @deprecated("""Replace with 'console.draw_rect(x, y, width, height, ch=..., fg=..., bg=..., bg_blend=...)'""") def rect( # noqa: PLR0913 self, x: int, @@ -677,6 +678,9 @@ def rect( # noqa: PLR0913 self.__deprecate_defaults("draw_rect", bg_blend, clear=bool(clear)) lib.TCOD_console_rect(self.console_c, x, y, width, height, clear, bg_blend) + @deprecated( + """Replace with 'console.draw_rect(x, y, width=width, height=1, ch=ord("─"), fg=..., bg=..., bg_blend=...)'""" + ) def hline( self, x: int, @@ -686,7 +690,7 @@ def hline( ) -> None: """Draw a horizontal line on the console. - This always uses ord('─'), the horizontal line character. + This always uses ord("─"), the horizontal line character. Args: x (int): The x coordinate from the left. @@ -704,6 +708,9 @@ def hline( self.__deprecate_defaults("draw_rect", bg_blend) lib.TCOD_console_hline(self.console_c, x, y, width, bg_blend) + @deprecated( + """Replace with 'console.draw_rect(x, y, width=1, height=height, ch=ord("│"), fg=..., bg=..., bg_blend=...)'""" + ) def vline( self, x: int, @@ -713,7 +720,7 @@ def vline( ) -> None: """Draw a vertical line on the console. - This always uses ord('│'), the vertical line character. + This always uses ord("│"), the vertical line character. Args: x (int): The x coordinate from the left. @@ -1130,15 +1137,16 @@ def draw_frame( # noqa: PLR0913 Example:: + >>> from tcod import libtcodpy >>> console = tcod.console.Console(12, 6) >>> console.draw_frame(x=0, y=0, width=3, height=3) >>> console.draw_frame(x=3, y=0, width=3, height=3, decoration="╔═╗║ ║╚═╝") >>> console.draw_frame(x=6, y=0, width=3, height=3, decoration="123456789") >>> console.draw_frame(x=9, y=0, width=3, height=3, decoration="/-\\| |\\-/") >>> console.draw_frame(x=0, y=3, width=12, height=3) - >>> console.print_box(x=0, y=3, width=12, height=1, string=" Title ", alignment=tcod.CENTER) + >>> console.print_box(x=0, y=3, width=12, height=1, string=" Title ", alignment=libtcodpy.CENTER) 1 - >>> console.print_box(x=0, y=5, width=12, height=1, string="┤Lower├", alignment=tcod.CENTER) + >>> console.print_box(x=0, y=5, width=12, height=1, string="┤Lower├", alignment=libtcodpy.CENTER) 1 >>> print(console) <┌─┐╔═╗123/-\ diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 568f04d3..27c0032d 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -410,7 +410,7 @@ def __setattr__(self, attr: str, value: Any) -> None: def __repr__(self) -> str: """Return a representation of this Key object.""" params = [] - params.append(f"pressed={self.pressed!r}, vk=tcod.{_LOOKUP_VK[self.vk]}") + params.append(f"pressed={self.pressed!r}, vk=libtcodpy.{_LOOKUP_VK[self.vk]}") if self.c: params.append(f"c=ord({chr(self.c)!r})") if self.text: @@ -426,7 +426,7 @@ def __repr__(self) -> str: ]: if getattr(self, attr): params.append(f"{attr}={getattr(self, attr)!r}") - return "tcod.Key({})".format(", ".join(params)) + return "libtcodpy.Key({})".format(", ".join(params)) @property def key_p(self) -> Any: @@ -504,7 +504,7 @@ def __repr__(self) -> str: ]: if getattr(self, attr): params.append(f"{attr}={getattr(self, attr)!r}") - return "tcod.Mouse({})".format(", ".join(params)) + return "libtcodpy.Mouse({})".format(", ".join(params)) @property def mouse_p(self) -> Any: @@ -828,7 +828,8 @@ def color_gen_map(colors: Iterable[tuple[int, int, int]], indexes: Iterable[int] List[Color]: A list of Color instances. Example: - >>> tcod.color_gen_map([(0, 0, 0), (255, 128, 0)], [0, 5]) + >>> from tcod import libtcodpy + >>> libtcodpy.color_gen_map([(0, 0, 0), (255, 128, 0)], [0, 5]) [Color(0, 0, 0), Color(51, 25, 0), Color(102, 51, 0), \ Color(153, 76, 0), Color(204, 102, 0), Color(255, 128, 0)] """ @@ -2667,13 +2668,13 @@ def heightmap_kernel_transform( Example: >>> import numpy as np + >>> from tcod import libtcodpy >>> heightmap = np.zeros((3, 3), dtype=np.float32) >>> heightmap[:,1] = 1 >>> dx = [-1, 1, 0] >>> dy = [0, 0, 0] >>> weight = [0.33, 0.33, 0.33] - >>> tcod.heightmap_kernel_transform(heightmap, 3, dx, dy, weight, - ... 0.0, 1.0) + >>> libtcodpy.heightmap_kernel_transform(heightmap, 3, dx, dy, weight, 0.0, 1.0) """ c_dx = ffi.new("int[]", dx) c_dy = ffi.new("int[]", dy) diff --git a/tests/conftest.py b/tests/conftest.py index 0efc716d..0a04cf39 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ """Test directory configuration.""" +from __future__ import annotations + import random import warnings from typing import Callable, Iterator, Union @@ -7,6 +9,7 @@ import pytest import tcod +from tcod import libtcodpy # ruff: noqa: D103 @@ -33,23 +36,23 @@ def session_console(request: pytest.FixtureRequest) -> Iterator[tcod.console.Con HEIGHT = 10 TITLE = "libtcod-cffi tests" FULLSCREEN = False - RENDERER = getattr(tcod, "RENDERER_" + request.param) + RENDERER = getattr(libtcodpy, "RENDERER_" + request.param) - tcod.console_set_custom_font(FONT_FILE) - with tcod.console_init_root(WIDTH, HEIGHT, TITLE, FULLSCREEN, RENDERER, vsync=False) as con: + libtcodpy.console_set_custom_font(FONT_FILE) + with libtcodpy.console_init_root(WIDTH, HEIGHT, TITLE, FULLSCREEN, RENDERER, vsync=False) as con: yield con @pytest.fixture def console(session_console: tcod.console.Console) -> tcod.console.Console: console = session_console - tcod.console_flush() + libtcodpy.console_flush() with warnings.catch_warnings(): warnings.simplefilter("ignore") console.default_fg = (255, 255, 255) console.default_bg = (0, 0, 0) - console.default_bg_blend = tcod.BKGND_SET - console.default_alignment = tcod.LEFT + console.default_bg_blend = libtcodpy.BKGND_SET + console.default_alignment = libtcodpy.LEFT console.clear() return console @@ -61,13 +64,13 @@ def offscreen(console: tcod.console.Console) -> tcod.console.Console: @pytest.fixture -def fg() -> tcod.Color: - return tcod.Color(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) +def fg() -> libtcodpy.Color: + return libtcodpy.Color(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) @pytest.fixture -def bg() -> tcod.Color: - return tcod.Color(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) +def bg() -> libtcodpy.Color: + return libtcodpy.Color(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) def ch_ascii_int() -> int: @@ -96,4 +99,4 @@ def ch_latin1_str() -> str: ) def ch(request: pytest.FixtureRequest) -> Callable[[], Union[int, str]]: """Test with multiple types of ascii/latin1 characters.""" - return globals()["ch_%s" % request.param]() # type: ignore + return globals()[f"ch_{request.param}"]() # type: ignore diff --git a/tests/test_console.py b/tests/test_console.py index 0681368f..3aad0c69 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -73,13 +73,17 @@ def test_console_methods() -> None: console.print_(0, 0, "Test") console.print_rect(0, 0, 2, 8, "a b c d e f") console.get_height_rect(0, 0, 2, 8, "a b c d e f") - console.rect(0, 0, 2, 2, True) - console.hline(0, 1, 10) - console.vline(1, 0, 10) + with pytest.deprecated_call(): + console.rect(0, 0, 2, 2, True) + with pytest.deprecated_call(): + console.hline(0, 1, 10) + with pytest.deprecated_call(): + console.vline(1, 0, 10) console.print_frame(0, 0, 8, 8, "Frame") console.blit(0, 0, 0, 0, console, 0, 0) # type: ignore console.blit(0, 0, 0, 0, console, 0, 0, key_color=(0, 0, 0)) # type: ignore - console.set_key_color((254, 0, 254)) + with pytest.deprecated_call(): + console.set_key_color((254, 0, 254)) def test_console_pickle() -> None: diff --git a/tests/test_random.py b/tests/test_random.py index 5bed9d95..d20045cc 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -4,6 +4,8 @@ import pickle from pathlib import Path +import pytest + import tcod.random # ruff: noqa: D103 @@ -15,8 +17,10 @@ def test_tcod_random() -> None: rand = tcod.random.Random(tcod.random.COMPLEMENTARY_MULTIPLY_WITH_CARRY) assert 0 <= rand.randint(0, 100) <= 100 # noqa: PLR2004 assert 0 <= rand.uniform(0, 100) <= 100 # noqa: PLR2004 - rand.guass(0, 1) - rand.inverse_guass(0, 1) + with pytest.warns(FutureWarning, match=r"typo"): + rand.guass(0, 1) + with pytest.warns(FutureWarning, match=r"typo"): + rand.inverse_guass(0, 1) def test_tcod_random_copy() -> None: diff --git a/tests/test_tcod.py b/tests/test_tcod.py index d15b145d..990aafe0 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -10,6 +10,7 @@ import tcod import tcod.console +from tcod import libtcodpy # ruff: noqa: D103 @@ -35,7 +36,7 @@ def test_tcod_bsp() -> None: assert not bsp.children with pytest.raises(RuntimeError): - tcod.bsp_traverse_pre_order(bsp, raise_Exception) + libtcodpy.bsp_traverse_pre_order(bsp, raise_Exception) bsp.split_recursive(3, 4, 4, 1, 1) for node in bsp.walk(): @@ -66,11 +67,11 @@ def test_tcod_map_set_bits() -> None: assert not map_.fov[:].any() map_.transparent[1, 0] = True - assert tcod.map_is_transparent(map_, 0, 1) + assert libtcodpy.map_is_transparent(map_, 0, 1) map_.walkable[1, 0] = True - assert tcod.map_is_walkable(map_, 0, 1) + assert libtcodpy.map_is_walkable(map_, 0, 1) map_.fov[1, 0] = True - assert tcod.map_is_in_fov(map_, 0, 1) + assert libtcodpy.map_is_in_fov(map_, 0, 1) @pytest.mark.filterwarnings("ignore:This class may perform poorly") @@ -115,7 +116,7 @@ def test_color_class() -> None: assert tcod.white - tcod.white == tcod.black assert tcod.black + (2, 2, 2) - (1, 1, 1) == (1, 1, 1) # noqa: RUF005 - color = tcod.Color() + color = libtcodpy.Color() color.r = 1 color.g = 2 color.b = 3 @@ -156,7 +157,7 @@ def test_path_callback() -> None: def test_key_repr() -> None: - Key = tcod.Key + Key = libtcodpy.Key key = Key(vk=1, c=2, shift=True) assert key.vk == 1 assert key.c == 2 # noqa: PLR2004 @@ -168,7 +169,7 @@ def test_key_repr() -> None: def test_mouse_repr() -> None: - Mouse = tcod.Mouse + Mouse = libtcodpy.Mouse mouse = Mouse(x=1, lbutton=True) mouse_copy = eval(repr(mouse)) # noqa: S307 assert mouse.x == mouse_copy.x @@ -188,10 +189,10 @@ def test_recommended_size(console: tcod.console.Console) -> None: @pytest.mark.filterwarnings("ignore") def test_context(uses_window: None) -> None: - with tcod.context.new_window(32, 32, renderer=tcod.RENDERER_SDL2): + with tcod.context.new_window(32, 32, renderer=libtcodpy.RENDERER_SDL2): pass WIDTH, HEIGHT = 16, 4 - with tcod.context.new_terminal(columns=WIDTH, rows=HEIGHT, renderer=tcod.RENDERER_SDL2) as context: + with tcod.context.new_terminal(columns=WIDTH, rows=HEIGHT, renderer=libtcodpy.RENDERER_SDL2) as context: console = tcod.console.Console(*context.recommended_console_size()) context.present(console) assert context.sdl_window_p is not None From 2e8efdd90fe75bab85b486b15984c238601a8149 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 3 Apr 2025 23:31:57 -0700 Subject: [PATCH 116/134] Merge Console.print_box into Console.print method and modernize Changes are mostly backwards compatible expect for a slight change in text placement. Also update some old docs which were not using correct directives. --- CHANGELOG.md | 11 +++ docs/tcod/getting-started.rst | 26 +++--- tcod/console.py | 162 ++++++++++++++++++++++++++++------ tests/test_console.py | 6 +- 4 files changed, 163 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c36415ce..2cb837e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +### Changed + +- `Console.print` now accepts `height` and `width` keywords and has renamed `string` to `text`. +- Text printed with `Console.print` using right-alignment has been shifted to the left by 1-tile. + +### Deprecated + +- Using `Console.print` without keywords for only the `x`, `y`, and `text` parameters has been deprecated. + The `string` parameter has been renamed to `text`. +- `Console.print_box` has been replaced by `Console.print`. + ## [17.1.0] - 2025-03-29 ### Added diff --git a/docs/tcod/getting-started.rst b/docs/tcod/getting-started.rst index 7d5ec175..6a275637 100644 --- a/docs/tcod/getting-started.rst +++ b/docs/tcod/getting-started.rst @@ -19,7 +19,7 @@ console will be stretched to fit the window. You can add arguments to :any:`Context.present` to fix the aspect ratio or only scale the console by integer increments. -Example:: +.. code-block:: python #!/usr/bin/env python # Make sure 'dejavu10x10_gs_tc.png' is in the same directory as this script. @@ -38,14 +38,14 @@ Example:: "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD, ) # Create the main console. - console = tcod.console.Console(WIDTH, HEIGHT, order="F") + console = tcod.console.Console(WIDTH, HEIGHT) # Create a window based on this console and tileset. with tcod.context.new( # New window for a console of size columns×rows. columns=console.width, rows=console.height, tileset=tileset, ) as context: while True: # Main loop, runs until SystemExit is raised. console.clear() - console.print(x=0, y=0, string="Hello World!") + console.print(x=0, y=0, text="Hello World!") context.present(console) # Show the console. # This event loop will wait until at least one event is processed before exiting. @@ -53,8 +53,9 @@ Example:: for event in tcod.event.wait(): context.convert_event(event) # Sets tile coordinates for mouse events. print(event) # Print event names and attributes. - if isinstance(event, tcod.event.Quit): - raise SystemExit() + match event: + case tcod.event.Quit(): + raise SystemExit() # The window will be closed after the above with-block exits. @@ -87,7 +88,7 @@ You can call :any:`Context.new_console` every frame or only when the window is resized. This example creates a new console every frame instead of clearing the console every frame and replacing it only on resizing the window. -Example:: +.. code-block:: python #!/usr/bin/env python import tcod.context @@ -103,17 +104,18 @@ Example:: width=WIDTH, height=HEIGHT, sdl_window_flags=FLAGS ) as context: while True: - console = context.new_console(order="F") # Console size based on window resolution and tile size. + console = context.new_console() # Console size based on window resolution and tile size. console.print(0, 0, "Hello World") context.present(console, integer_scaling=True) for event in tcod.event.wait(): - context.convert_event(event) # Sets tile coordinates for mouse events. + event = context.convert_event(event) # Sets tile coordinates for mouse events. print(event) # Print event names and attributes. - if isinstance(event, tcod.event.Quit): - raise SystemExit() - elif isinstance(event, tcod.event.WindowResized) and event.type == "WindowSizeChanged": - pass # The next call to context.new_console may return a different size. + match event: + case tcod.event.Quit(): + raise SystemExit() + case tcod.event.WindowResized(type="WindowSizeChanged"): + pass # The next call to context.new_console may return a different size. if __name__ == "__main__": diff --git a/tcod/console.py b/tcod/console.py index a1461965..1b7f30e0 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -10,7 +10,7 @@ import warnings from os import PathLike from pathlib import Path -from typing import Any, Iterable +from typing import Any, Iterable, overload import numpy as np from numpy.typing import ArrayLike, NDArray @@ -552,6 +552,7 @@ def __deprecate_defaults( # noqa: C901, PLR0912 stacklevel=3, ) + @deprecated("Switch to using keywords and then replace with 'console.print(...)'") def print_( self, x: int, @@ -579,6 +580,7 @@ def print_( alignment = self.default_alignment if alignment is None else alignment lib.TCOD_console_printf_ex(self.console_c, x, y, bg_blend, alignment, _fmt(string)) + @deprecated("Switch to using keywords and then replace with 'console.print(...)'") def print_rect( # noqa: PLR0913 self, x: int, @@ -973,55 +975,159 @@ def __str__(self) -> str: """Return a simplified representation of this consoles contents.""" return "<{}>".format("\n ".join("".join(chr(c) for c in line) for line in self._tiles["ch"])) + @overload + @deprecated( + "Switch all parameters to keywords such as 'console.print(x=..., y=..., text=..., width=..., height=..., fg=..., bg=..., bg_blend=..., alignment=...)'" + "\n'string' keyword should be renamed to `text`" + ) + def print( + self, + x: int, + y: int, + text: str, + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, + bg_blend: int = tcod.constants.BKGND_SET, + alignment: int = tcod.constants.LEFT, + *, + string: str = "", + ) -> int: ... + + @overload + def print( + self, + x: int, + y: int, + text: str, + *, + width: int | None = None, + height: int | None = None, + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, + bg_blend: int = tcod.constants.BKGND_SET, + alignment: int = tcod.constants.LEFT, + ) -> int: ... + def print( # noqa: PLR0913 self, x: int, y: int, - string: str, + text: str = "", fg: tuple[int, int, int] | None = None, bg: tuple[int, int, int] | None = None, bg_blend: int = tcod.constants.BKGND_SET, alignment: int = tcod.constants.LEFT, - ) -> None: - r"""Print a string on a console with manual line breaks. + *, + width: int | None = None, + height: int | None = None, + string: str = "", + ) -> int: + r"""Print a string of Unicode text on this console. - `x` and `y` are the starting tile, with ``0,0`` as the upper-left - corner of the console. + Prefer using keywords for this method call to avoid ambiguous parameters. - `string` is a Unicode string which may include color control - characters. Strings which are too long will be truncated until the - next newline character ``"\n"``. + Args: + x: Starting X coordinate, with the left-most tile as zero. + y: Starting Y coordinate, with the top-most tile as zero. + text: A Unicode string which may include color control characters. + width: Width in tiles to constrain the printing region. + If a `width` is given then `text` will have automatic word wrapping applied to it. + A `width` of `None` means `text` will only have manual line breaks. + height: Height in tiles to constrain the printing region. + fg: RGB tuple to use as the foreground color, or `None` to leave the foreground unchanged. + Tuple values should be 0-255. + bg: RGB tuple to use as the background color, or `None` to leave the foreground unchanged. + Tuple values should be 0-255. + bg_blend: Background blend type used by libtcod. + Typically starts with `libtcodpy.BKGND_*`. + alignment: One of `libtcodpy.LEFT`, `libtcodpy.CENTER`, or `libtcodpy.RIGHT` + string: Older deprecated name of the `text` parameter. - `fg` and `bg` are the foreground text color and background tile color - respectfully. This is a 3-item tuple with (r, g, b) color values from - 0 to 255. These parameters can also be set to `None` to leave the - colors unchanged. + Returns: + The height of `text` in lines via word wrapping and line breaks. - `bg_blend` is the blend type used by libtcod. + Example:: - `alignment` can be `tcod.LEFT`, `tcod.CENTER`, or `tcod.RIGHT`. + >>> from tcod import libtcodpy + >>> console = tcod.console.Console(20, 1) + >>> console.clear(ch=ord('·')) + >>> console.print(x=0, y=0, text="left") + 1 + >>> console.print(x=console.width, y=0, text="right", alignment=libtcodpy.RIGHT) + 1 + >>> console.print(x=10, y=0, text="center", alignment=libtcodpy.CENTER) + 1 + >>> print(console) + + + >>> console = tcod.console.Console(20, 4) + >>> console.clear(ch=ord('·')) + >>> console.print(x=1, y=1, text="words within bounds", width=8) + 3 + >>> print(console) + <···················· + ·words·············· + ·within············· + ·bounds·············> + >>> WHITE = (255, 255, 255) + >>> BLACK = (0, 0, 0) + >>> console.print(x=0, y=0, text="Black text on white background", fg=BLACK, bg=WHITE) + 1 .. versionadded:: 8.5 .. versionchanged:: 9.0 + `fg` and `bg` now default to `None` instead of white-on-black. .. versionchanged:: 13.0 + `x` and `y` are now always used as an absolute position for negative values. + + .. versionchanged:: Unreleased + + Added `text` parameter to replace `string`. + + Added `width` and `height` keyword parameters to bind text to a rectangle and replace other print functions. + Right-aligned text with `width=None` now treats the `x` coordinate as a past-the-end index, this will shift + the text of older calls to the left by 1 tile. + + Now returns the number of lines printed via word wrapping. """ - string_ = string.encode("utf-8") - lib.TCOD_console_printn( - self.console_c, - x, - y, - len(string_), - string_, - (fg,) if fg is not None else ffi.NULL, - (bg,) if bg is not None else ffi.NULL, - bg_blend, - alignment, + if width is not None and width <= 0: + return 0 + if width is None and alignment == tcod.constants.LEFT: # Fix alignment + width = 0x100000 + if width is None and alignment == tcod.constants.CENTER: # Fix center alignment + x -= 0x100000 + width = 0x200000 + if width is None and alignment == tcod.constants.RIGHT: # Fix right alignment + x -= 0x100000 + width = 0x100000 + rgb_fg = ffi.new("TCOD_ColorRGB*", fg) if fg is not None else ffi.NULL + rgb_bg = ffi.new("TCOD_ColorRGB*", bg) if bg is not None else ffi.NULL + utf8 = (string or text).encode("utf-8") + return _check( + int( + lib.TCOD_printn_rgb( + self.console_c, + { + "x": x, + "y": y, + "width": width or 0, + "height": height or 0, + "fg": rgb_fg, + "bg": rgb_bg, + "flag": bg_blend, + "alignment": alignment, + }, + len(utf8), + utf8, + ) + ) ) + @deprecated("Switch to using keywords and then replace with 'console.print(...)'") def print_box( # noqa: PLR0913 self, x: int, @@ -1144,9 +1250,9 @@ def draw_frame( # noqa: PLR0913 >>> console.draw_frame(x=6, y=0, width=3, height=3, decoration="123456789") >>> console.draw_frame(x=9, y=0, width=3, height=3, decoration="/-\\| |\\-/") >>> console.draw_frame(x=0, y=3, width=12, height=3) - >>> console.print_box(x=0, y=3, width=12, height=1, string=" Title ", alignment=libtcodpy.CENTER) + >>> console.print(x=0, y=3, width=12, height=1, string=" Title ", alignment=libtcodpy.CENTER) 1 - >>> console.print_box(x=0, y=5, width=12, height=1, string="┤Lower├", alignment=libtcodpy.CENTER) + >>> console.print(x=0, y=5, width=12, height=1, string="┤Lower├", alignment=libtcodpy.CENTER) 1 >>> print(console) <┌─┐╔═╗123/-\ diff --git a/tests/test_console.py b/tests/test_console.py index 3aad0c69..04bae6cb 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -70,8 +70,10 @@ def test_console_defaults() -> None: def test_console_methods() -> None: console = tcod.console.Console(width=12, height=10) console.put_char(0, 0, ord("@")) - console.print_(0, 0, "Test") - console.print_rect(0, 0, 2, 8, "a b c d e f") + with pytest.deprecated_call(): + console.print_(0, 0, "Test") + with pytest.deprecated_call(): + console.print_rect(0, 0, 2, 8, "a b c d e f") console.get_height_rect(0, 0, 2, 8, "a b c d e f") with pytest.deprecated_call(): console.rect(0, 0, 2, 2, True) From 6e57885116cfee194d0f780fa01f658c903c19f4 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 4 Apr 2025 00:36:00 -0700 Subject: [PATCH 117/134] Resolve several Ruff and Mypy warnings Mostly applying and checking unsafe fixes --- build_libtcod.py | 8 +-- build_sdl.py | 12 ++--- docs/conf.py | 4 +- docs/tcod/getting-started.rst | 4 +- examples/distribution/PyInstaller/main.py | 2 +- examples/distribution/cx_Freeze/main.py | 2 +- examples/eventget.py | 2 +- examples/framerate.py | 2 +- examples/samples_tcod.py | 32 ++++++------ examples/sdl-hello-world.py | 4 +- examples/thread_jobs.py | 12 ++--- examples/ttf.py | 8 +-- pyproject.toml | 63 +++++++++-------------- scripts/tag_release.py | 2 +- setup.py | 6 ++- tcod/_internal.py | 8 +-- tcod/bsp.py | 18 ++++--- tcod/console.py | 13 +++-- tcod/context.py | 4 +- tcod/event.py | 12 +++-- tcod/image.py | 12 +++-- tcod/libtcodpy.py | 9 ++-- tcod/los.py | 6 ++- tcod/map.py | 6 ++- tcod/noise.py | 9 ++-- tcod/path.py | 10 ++-- tcod/sdl/_internal.py | 6 ++- tcod/sdl/audio.py | 9 ++-- tcod/sdl/mouse.py | 8 +-- tcod/sdl/render.py | 6 ++- tcod/sdl/sys.py | 4 +- tcod/sdl/video.py | 10 ++-- tcod/tileset.py | 13 +++-- tests/conftest.py | 6 +-- tests/test_libtcodpy.py | 19 ++++--- tests/test_parser.py | 2 +- tests/test_tcod.py | 2 +- 37 files changed, 197 insertions(+), 158 deletions(-) diff --git a/build_libtcod.py b/build_libtcod.py index 1e824088..a5bca5ac 100755 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -14,6 +14,8 @@ from cffi import FFI +# ruff: noqa: T201 + sys.path.append(str(Path(__file__).parent)) # Allow importing local modules. import build_sdl @@ -50,14 +52,14 @@ class ParsedHeader: def __init__(self, path: Path) -> None: """Initialize and organize a header file.""" - self.path = path = path.resolve(True) + self.path = path = path.resolve(strict=True) directory = path.parent depends = set() header = self.path.read_text(encoding="utf-8") header = RE_COMMENT.sub("", header) header = RE_CPLUSPLUS.sub("", header) for dependency in RE_INCLUDE.findall(header): - depends.add((directory / str(dependency)).resolve(True)) + depends.add((directory / str(dependency)).resolve(strict=True)) header = RE_PREPROCESSOR.sub("", header) header = RE_TAGS.sub("", header) header = RE_VAFUNC.sub("", header) @@ -91,7 +93,7 @@ def walk_includes(directory: str) -> Iterator[ParsedHeader]: if file in HEADER_PARSE_EXCLUDES: continue if file.endswith(".h"): - yield ParsedHeader(Path(path, file).resolve(True)) + yield ParsedHeader(Path(path, file).resolve(strict=True)) def resolve_dependencies( diff --git a/build_sdl.py b/build_sdl.py index 41f91b22..e395c4d4 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -14,11 +14,11 @@ from pathlib import Path from typing import Any -import pcpp # type: ignore +import pcpp # type: ignore[import-untyped] import requests # This script calls a lot of programs. -# ruff: noqa: S603, S607 +# ruff: noqa: S603, S607, T201 BIT_SIZE, LINKAGE = platform.architecture() @@ -141,7 +141,7 @@ def unpack_sdl2(version: str) -> Path: return sdl2_path -class SDLParser(pcpp.Preprocessor): # type: ignore +class SDLParser(pcpp.Preprocessor): # type: ignore[misc] """A modified preprocessor to output code in a format for CFFI.""" def __init__(self) -> None: @@ -159,7 +159,7 @@ def get_output(self) -> str: buffer.write(f"#define {name} ...\n") return buffer.getvalue() - def on_include_not_found(self, is_malformed: bool, is_system_include: bool, curdir: str, includepath: str) -> None: + def on_include_not_found(self, is_malformed: bool, is_system_include: bool, curdir: str, includepath: str) -> None: # noqa: ARG002, FBT001 """Remove bad includes such as stddef.h and stdarg.h.""" raise pcpp.OutputDirective(pcpp.Action.IgnoreAndRemove) @@ -190,7 +190,7 @@ def on_directive_handle( self, directive: Any, # noqa: ANN401 tokens: list[Any], - if_passthru: bool, + if_passthru: bool, # noqa: FBT001 preceding_tokens: list[Any], ) -> Any: # noqa: ANN401 """Catch and store definitions.""" @@ -204,7 +204,7 @@ def on_directive_handle( check_sdl_version() -if sys.platform in ["win32", "darwin"]: +if sys.platform == "win32" or sys.platform == "darwin": SDL2_PARSE_PATH = unpack_sdl2(SDL2_PARSE_VERSION) SDL2_BUNDLE_PATH = unpack_sdl2(SDL2_BUNDLE_VERSION) diff --git a/docs/conf.py b/docs/conf.py index d4edf824..81c2d205 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,6 +27,8 @@ THIS_DIR = Path(__file__).parent +# ruff: noqa: ERA001 + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -79,7 +81,7 @@ ) release = git_describe.stdout.strip() assert release -print("release version: %r" % release) +print(f"release version: {release!r}") # The short X.Y version. match_version = re.match(r"([0-9]+\.[0-9]+).*?", release) diff --git a/docs/tcod/getting-started.rst b/docs/tcod/getting-started.rst index 6a275637..add5d9f0 100644 --- a/docs/tcod/getting-started.rst +++ b/docs/tcod/getting-started.rst @@ -55,7 +55,7 @@ integer increments. print(event) # Print event names and attributes. match event: case tcod.event.Quit(): - raise SystemExit() + raise SystemExit # The window will be closed after the above with-block exits. @@ -113,7 +113,7 @@ clearing the console every frame and replacing it only on resizing the window. print(event) # Print event names and attributes. match event: case tcod.event.Quit(): - raise SystemExit() + raise SystemExit case tcod.event.WindowResized(type="WindowSizeChanged"): pass # The next call to context.new_console may return a different size. diff --git a/examples/distribution/PyInstaller/main.py b/examples/distribution/PyInstaller/main.py index 5e034afd..ce500635 100755 --- a/examples/distribution/PyInstaller/main.py +++ b/examples/distribution/PyInstaller/main.py @@ -30,7 +30,7 @@ def main() -> None: context.present(console) for event in tcod.event.wait(): if isinstance(event, tcod.event.Quit): - raise SystemExit() + raise SystemExit if __name__ == "__main__": diff --git a/examples/distribution/cx_Freeze/main.py b/examples/distribution/cx_Freeze/main.py index 79846377..569dddad 100755 --- a/examples/distribution/cx_Freeze/main.py +++ b/examples/distribution/cx_Freeze/main.py @@ -20,7 +20,7 @@ def main() -> None: context.present(console) for event in tcod.event.wait(): if isinstance(event, tcod.event.Quit): - raise SystemExit() + raise SystemExit if __name__ == "__main__": diff --git a/examples/eventget.py b/examples/eventget.py index ada61668..9049203f 100755 --- a/examples/eventget.py +++ b/examples/eventget.py @@ -41,7 +41,7 @@ def main() -> None: context.convert_event(event) # Set tile coordinates for event. print(repr(event)) if isinstance(event, tcod.event.Quit): - raise SystemExit() + raise SystemExit if isinstance(event, tcod.event.WindowResized) and event.type == "WindowSizeChanged": console = context.new_console() if isinstance(event, tcod.event.ControllerDevice): diff --git a/examples/framerate.py b/examples/framerate.py index 25a1dd3f..d7f4a08d 100755 --- a/examples/framerate.py +++ b/examples/framerate.py @@ -138,7 +138,7 @@ def main() -> None: for event in tcod.event.get(): context.convert_event(event) # Set tile coordinates for event. if isinstance(event, tcod.event.Quit): - raise SystemExit() + raise SystemExit if isinstance(event, tcod.event.MouseWheel): desired_fps = max(1, desired_fps + event.y) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 5f8ea1be..adcf9656 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -14,10 +14,9 @@ import time import warnings from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np -from numpy.typing import NDArray import tcod.cffi import tcod.context @@ -29,6 +28,9 @@ from tcod import libtcodpy from tcod.sdl.video import WindowFlags +if TYPE_CHECKING: + from numpy.typing import NDArray + # ruff: noqa: S311 if not sys.warnoptions: @@ -96,13 +98,13 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: libtcodpy.sys_save_screenshot() print("png") elif event.sym == tcod.event.KeySym.ESCAPE: - raise SystemExit() + raise SystemExit elif event.sym in RENDERER_KEYS: # Swap the active context for one with a different renderer. init_context(RENDERER_KEYS[event.sym]) def ev_quit(self, event: tcod.event.Quit) -> None: - raise SystemExit() + raise SystemExit class TrueColorSample(Sample): @@ -307,7 +309,7 @@ def on_draw(self) -> None: sample_console.print( 2, 2, - "%s (ENTER to change)" % self.FLAG_NAMES[self.bk_flag & 0xFF], + f"{self.FLAG_NAMES[self.bk_flag & 0xFF]} (ENTER to change)", fg=WHITE, bg=None, ) @@ -431,26 +433,26 @@ def on_draw(self) -> None: sample_console.print(2, 2 + cur_func, text, fg=WHITE, bg=LIGHT_BLUE) else: sample_console.print(2, 2 + cur_func, text, fg=GREY, bg=None) - sample_console.print(2, 11, "Y/H : zoom (%2.1f)" % self.zoom, fg=WHITE, bg=None) + sample_console.print(2, 11, f"Y/H : zoom ({self.zoom:2.1f})", fg=WHITE, bg=None) if self.implementation != tcod.noise.Implementation.SIMPLE: sample_console.print( 2, 12, - "E/D : hurst (%2.1f)" % self.hurst, + f"E/D : hurst ({self.hurst:2.1f})", fg=WHITE, bg=None, ) sample_console.print( 2, 13, - "R/F : lacunarity (%2.1f)" % self.lacunarity, + f"R/F : lacunarity ({self.lacunarity:2.1f})", fg=WHITE, bg=None, ) sample_console.print( 2, 14, - "T/G : octaves (%2.1f)" % self.octaves, + f"T/G : octaves ({self.octaves:2.1f})", fg=WHITE, bg=None, ) @@ -991,7 +993,7 @@ def on_draw(self) -> None: walls = "OFF" if bsp_room_walls: walls = "ON" - sample_console.print(1, 6, "2 : room walls %s" % walls, fg=WHITE, bg=None) + sample_console.print(1, 6, f"2 : room walls {walls}", fg=WHITE, bg=None) # render the level for y in range(SAMPLE_SCREEN_HEIGHT): for x in range(SAMPLE_SCREEN_WIDTH): @@ -1161,7 +1163,7 @@ def on_draw(self) -> None: sample_console.print( 1, 1, - "%s\n\n+ : next generator\n- : prev generator" % self.sets[self.current_set], + f"{self.sets[self.current_set]}\n\n+ : next generator\n- : prev generator", fg=WHITE, bg=None, ) @@ -1214,8 +1216,8 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: # the coordinates of all tiles in the screen, as numpy arrays. # example: (4x3 pixels screen) -# xc = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]] -# yc = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]] +# xc = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]] # noqa: ERA001 +# yc = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]] # noqa: ERA001 if numpy_available: (xc, yc) = np.meshgrid(range(SCREEN_W), range(SCREEN_H)) # translate coordinates of all pixels to center @@ -1504,7 +1506,7 @@ def handle_events() -> None: SAMPLES[cur_sample].dispatch(event) if isinstance(event, tcod.event.Quit): - raise SystemExit() + raise SystemExit def draw_samples_menu() -> None: @@ -1518,7 +1520,7 @@ def draw_samples_menu() -> None: root_console.print( 2, 46 - (len(SAMPLES) - i), - " %s" % sample.name.ljust(19), + f" {sample.name.ljust(19)}", fg, bg, alignment=libtcodpy.LEFT, diff --git a/examples/sdl-hello-world.py b/examples/sdl-hello-world.py index 6afd02ab..67b0fa01 100644 --- a/examples/sdl-hello-world.py +++ b/examples/sdl-hello-world.py @@ -17,7 +17,7 @@ def render_text(renderer: tcod.sdl.render.Renderer, text: str) -> tcod.sdl.rende """Render text, upload it to VRAM, then return it as an SDL Texture.""" # Use Pillow to render the font. _left, _top, right, bottom = font.getbbox(text) - width, height = right, bottom + width, height = int(right), int(bottom) image = Image.new("RGBA", (width, height)) draw = ImageDraw.Draw(image) draw.text((0, 0), text, font=font) @@ -43,7 +43,7 @@ def main() -> None: renderer.present() for event in tcod.event.get(): if isinstance(event, tcod.event.Quit): - raise SystemExit() + raise SystemExit if __name__ == "__main__": diff --git a/examples/thread_jobs.py b/examples/thread_jobs.py index fa8ce9dc..a7212c4f 100755 --- a/examples/thread_jobs.py +++ b/examples/thread_jobs.py @@ -32,32 +32,32 @@ REPEAT = 10 # Number to times to run a test. Only the fastest result is shown. -def test_fov(map_: tcod.map.Map) -> tcod.map.Map: # noqa: D103 +def test_fov(map_: tcod.map.Map) -> tcod.map.Map: map_.compute_fov(MAP_WIDTH // 2, MAP_HEIGHT // 2) return map_ -def test_fov_single(maps: List[tcod.map.Map]) -> None: # noqa: D103 +def test_fov_single(maps: List[tcod.map.Map]) -> None: for map_ in maps: test_fov(map_) -def test_fov_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: # noqa: D103 +def test_fov_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: for _result in executor.map(test_fov, maps): pass -def test_astar(map_: tcod.map.Map) -> List[Tuple[int, int]]: # noqa: D103 +def test_astar(map_: tcod.map.Map) -> List[Tuple[int, int]]: astar = tcod.path.AStar(map_) return astar.get_path(0, 0, MAP_WIDTH - 1, MAP_HEIGHT - 1) -def test_astar_single(maps: List[tcod.map.Map]) -> None: # noqa: D103 +def test_astar_single(maps: List[tcod.map.Map]) -> None: for map_ in maps: test_astar(map_) -def test_astar_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: # noqa: D103 +def test_astar_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: for _result in executor.map(test_astar, maps): pass diff --git a/examples/ttf.py b/examples/ttf.py index 3e13a837..d6504bd8 100755 --- a/examples/ttf.py +++ b/examples/ttf.py @@ -9,17 +9,19 @@ # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights to this example script. # https://creativecommons.org/publicdomain/zero/1.0/ -from typing import Tuple +from typing import TYPE_CHECKING, Tuple import freetype # type: ignore # pip install freetype-py import numpy as np -from numpy.typing import NDArray import tcod.console import tcod.context import tcod.event import tcod.tileset +if TYPE_CHECKING: + from numpy.typing import NDArray + FONT = "VeraMono.ttf" @@ -81,7 +83,7 @@ def main() -> None: context.present(console, integer_scaling=True) for event in tcod.event.wait(): if isinstance(event, tcod.event.Quit): - raise SystemExit() + raise SystemExit if isinstance(event, tcod.event.WindowResized) and event.type == "WindowSizeChanged": # Resize the Tileset to match the new screen size. context.change_tileset( diff --git a/pyproject.toml b/pyproject.toml index 2590d920..f6a26c12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,46 +149,31 @@ extend-exclude = ["libtcod"] # Ignore submodule line-length = 120 [tool.ruff.lint] # https://docs.astral.sh/ruff/rules/ -select = [ - "C90", # mccabe - "E", # pycodestyle - "W", # pycodestyle - "F", # Pyflakes - "I", # isort - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "S", # flake8-bandit - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "EM", # flake8-errmsg - "EXE", # flake8-executable - "RET", # flake8-return - "ICN", # flake8-import-conventions - "PIE", # flake8-pie - "PT", # flake8-pytest-style - "SIM", # flake8-simplify - "PTH", # flake8-use-pathlib - "PL", # Pylint - "TRY", # tryceratops - "RUF", # NumPy-specific rules - "G", # flake8-logging-format - "D", # pydocstyle -] +select = ["ALL"] ignore = [ - "E501", # line-too-long - "S101", # assert - "S301", # suspicious-pickle-usage - "S311", # suspicious-non-cryptographic-random-usage - "D203", # one-blank-line-before-class - "D204", # one-blank-line-after-class - "D213", # multi-line-summary-second-line - "D407", # dashed-underline-after-section - "D408", # section-underline-after-name - "D409", # section-underline-matches-section-length - "D206", # indent-with-spaces - "W191", # tab-indentation + "COM", # flake8-commas + "D203", # one-blank-line-before-class + "D204", # one-blank-line-after-class + "D213", # multi-line-summary-second-line + "D407", # dashed-underline-after-section + "D408", # section-underline-after-name + "D409", # section-underline-matches-section-length + "D206", # indent-with-spaces + "E501", # line-too-long + "PYI064", # redundant-final-literal + "S101", # assert + "S301", # suspicious-pickle-usage + "S311", # suspicious-non-cryptographic-random-usage + "SLF001", # private-member-access + "W191", # tab-indentation +] +[tool.ruff.lint.per-file-ignores] +"**/{tests}/*" = [ + "D103", # undocumented-public-function +] +"**/{tests,docs,examples,scripts}/*" = [ + "D103", # undocumented-public-function + "T201", # print ] [tool.ruff.lint.pydocstyle] # https://docs.astral.sh/ruff/settings/#lintpydocstyle diff --git a/scripts/tag_release.py b/scripts/tag_release.py index 2155aaae..066eaeda 100755 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -42,7 +42,7 @@ def parse_changelog(args: argparse.Namespace) -> tuple[str, str]: print("--- Tagged section:") print(tagged) - return "".join((header, tagged, tail)), changes + return f"{header}{tagged}{tail}", changes def replace_unreleased_tags(tag: str, dry_run: bool) -> None: diff --git a/setup.py b/setup.py index 29f9cb3d..a98b64b3 100755 --- a/setup.py +++ b/setup.py @@ -10,6 +10,8 @@ from setuptools import setup +# ruff: noqa: T201 + SDL_VERSION_NEEDED = (2, 0, 5) SETUP_DIR = Path(__file__).parent # setup.py current directory @@ -17,7 +19,7 @@ def get_package_data() -> list[str]: """Get data files which will be included in the main tcod/ directory.""" - BIT_SIZE, _ = platform.architecture() + bit_size, _ = platform.architecture() files = [ "py.typed", "lib/LIBTCOD-CREDITS.txt", @@ -25,7 +27,7 @@ def get_package_data() -> list[str]: "lib/README-SDL.txt", ] if "win32" in sys.platform: - if BIT_SIZE == "32bit": + if bit_size == "32bit": files += ["x86/SDL2.dll"] else: files += ["x64/SDL2.dll"] diff --git a/tcod/_internal.py b/tcod/_internal.py index 85410cb2..01136c5e 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -5,17 +5,19 @@ import locale import sys import warnings -from pathlib import Path -from types import TracebackType from typing import TYPE_CHECKING, Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar import numpy as np -from numpy.typing import ArrayLike, NDArray from typing_extensions import Literal, LiteralString, deprecated from tcod.cffi import ffi, lib if TYPE_CHECKING: + from pathlib import Path + from types import TracebackType + + from numpy.typing import ArrayLike, NDArray + import tcod.image FuncType = Callable[..., Any] diff --git a/tcod/bsp.py b/tcod/bsp.py index 98783685..0f8caeaa 100644 --- a/tcod/bsp.py +++ b/tcod/bsp.py @@ -27,13 +27,15 @@ from __future__ import annotations -from typing import Any, Iterator +from typing import TYPE_CHECKING, Any, Iterator from typing_extensions import deprecated -import tcod.random from tcod.cffi import ffi, lib +if TYPE_CHECKING: + import tcod.random + class BSP: """A binary space partitioning tree which can be used for simple dungeon generation. @@ -236,13 +238,13 @@ def inverted_level_order(self) -> Iterator[BSP]: .. versionadded:: 8.3 """ levels: list[list[BSP]] = [] - next = [self] - while next: - levels.append(next) - level = next - next = [] + next_: list[BSP] = [self] + while next_: + levels.append(next_) + level = next_ + next_ = [] for node in level: - next.extend(node.children) + next_.extend(node.children) while levels: yield from levels.pop() diff --git a/tcod/console.py b/tcod/console.py index 1b7f30e0..0f108d95 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -8,19 +8,22 @@ from __future__ import annotations import warnings -from os import PathLike from pathlib import Path -from typing import Any, Iterable, overload +from typing import TYPE_CHECKING, Any, Iterable, overload import numpy as np -from numpy.typing import ArrayLike, NDArray -from typing_extensions import Literal, deprecated +from typing_extensions import Literal, Self, deprecated import tcod._internal import tcod.constants from tcod._internal import _check, _path_encode from tcod.cffi import ffi, lib +if TYPE_CHECKING: + from os import PathLike + + from numpy.typing import ArrayLike, NDArray + def _fmt(string: str) -> bytes: """Return a string that escapes 'C printf' side effects.""" @@ -884,7 +887,7 @@ def set_key_color(self, color: tuple[int, int, int] | None) -> None: """ self._key_color = color - def __enter__(self) -> Console: + def __enter__(self) -> Self: """Return this console in a managed context. When the root console is used as a context, the graphical window will diff --git a/tcod/context.py b/tcod/context.py index 78d18de2..826307dc 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -32,7 +32,7 @@ from pathlib import Path from typing import Any, Iterable, NoReturn, TypeVar -from typing_extensions import Literal, deprecated +from typing_extensions import Literal, Self, deprecated import tcod.console import tcod.event @@ -164,7 +164,7 @@ def _p(self) -> Any: # noqa: ANN401 msg = "This context has been closed can no longer be used." raise RuntimeError(msg) from None - def __enter__(self) -> Context: + def __enter__(self) -> Self: """Enter this context which will close on exiting.""" return self diff --git a/tcod/event.py b/tcod/event.py index d225ae37..80c4acce 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -84,10 +84,9 @@ import enum import warnings -from typing import Any, Callable, Final, Generic, Iterator, Mapping, NamedTuple, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Final, Generic, Iterator, Mapping, NamedTuple, TypeVar import numpy as np -from numpy.typing import NDArray from typing_extensions import Literal import tcod.event @@ -98,6 +97,9 @@ from tcod.event_constants import * # noqa: F403 from tcod.sdl.joystick import _HAT_DIRECTIONS +if TYPE_CHECKING: + from numpy.typing import NDArray + T = TypeVar("T") @@ -299,7 +301,7 @@ def __init__(self, type: str | None = None) -> None: @classmethod def from_sdl_event(cls, sdl_event: Any) -> Event: """Return a class instance from a python-cffi 'SDL_Event*' pointer.""" - raise NotImplementedError() + raise NotImplementedError def __str__(self) -> str: return f"" @@ -2189,7 +2191,7 @@ def _missing_(cls, value: object) -> Scancode | None: result._value_ = value return result - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if isinstance(other, KeySym): msg = "Scancode and KeySym enums can not be compared directly. Convert one or the other to the same type." raise TypeError(msg) @@ -2743,7 +2745,7 @@ def _missing_(cls, value: object) -> KeySym | None: result._value_ = value return result - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if isinstance(other, Scancode): msg = "Scancode and KeySym enums can not be compared directly. Convert one or the other to the same type." raise TypeError(msg) diff --git a/tcod/image.py b/tcod/image.py index dca67314..219916cf 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -11,18 +11,22 @@ from __future__ import annotations -from os import PathLike from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np -from numpy.typing import ArrayLike, NDArray from typing_extensions import deprecated -import tcod.console from tcod._internal import _console, _path_encode from tcod.cffi import ffi, lib +if TYPE_CHECKING: + from os import PathLike + + from numpy.typing import ArrayLike, NDArray + + import tcod.console + class Image: """A libtcod image. diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 27c0032d..52cba83e 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -6,12 +6,10 @@ import sys import threading import warnings -from os import PathLike from pathlib import Path -from typing import Any, Callable, Hashable, Iterable, Iterator, Sequence +from typing import TYPE_CHECKING, Any, Callable, Hashable, Iterable, Iterator, Sequence import numpy as np -from numpy.typing import NDArray from typing_extensions import Literal, deprecated import tcod.bsp @@ -54,6 +52,11 @@ NOISE_DEFAULT, ) +if TYPE_CHECKING: + from os import PathLike + + from numpy.typing import NDArray + # Functions are too deprecated to make changes. # ruff: noqa: ANN401 PLR0913 D102 D103 D105 D107 diff --git a/tcod/los.py b/tcod/los.py index 6d30d0b8..f09b073c 100644 --- a/tcod/los.py +++ b/tcod/los.py @@ -2,13 +2,15 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np -from numpy.typing import NDArray from tcod.cffi import ffi, lib +if TYPE_CHECKING: + from numpy.typing import NDArray + def bresenham(start: tuple[int, int], end: tuple[int, int]) -> NDArray[np.intc]: """Return a thin Bresenham line as a NumPy array of shape (length, 2). diff --git a/tcod/map.py b/tcod/map.py index 17aaf0b4..32e42d1e 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -3,16 +3,18 @@ from __future__ import annotations import warnings -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np -from numpy.typing import ArrayLike, NDArray from typing_extensions import Literal import tcod._internal import tcod.constants from tcod.cffi import ffi, lib +if TYPE_CHECKING: + from numpy.typing import ArrayLike, NDArray + class Map: """A map containing libtcod attributes. diff --git a/tcod/noise.py b/tcod/noise.py index b91c90b6..82920a1c 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -36,16 +36,18 @@ import enum import warnings -from typing import Any, Sequence +from typing import TYPE_CHECKING, Any, Sequence import numpy as np -from numpy.typing import ArrayLike, NDArray from typing_extensions import Literal import tcod.constants import tcod.random from tcod.cffi import ffi, lib +if TYPE_CHECKING: + from numpy.typing import ArrayLike, NDArray + class Algorithm(enum.IntEnum): """Libtcod noise algorithms. @@ -274,7 +276,8 @@ def __getitem__(self, indexes: Any) -> NDArray[np.float32]: ffi.from_buffer("float*", out), ) else: - raise TypeError("Unexpected %r" % self.implementation) + msg = f"Unexpected {self.implementation!r}" + raise TypeError(msg) return out diff --git a/tcod/path.py b/tcod/path.py index e55223d7..8ce9aaa2 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -21,15 +21,17 @@ import functools import itertools import warnings -from typing import Any, Callable, Final +from typing import TYPE_CHECKING, Any, Callable, Final import numpy as np -from numpy.typing import ArrayLike, DTypeLike, NDArray -from typing_extensions import Literal +from typing_extensions import Literal, Self from tcod._internal import _check from tcod.cffi import ffi, lib +if TYPE_CHECKING: + from numpy.typing import ArrayLike, DTypeLike, NDArray + @ffi.def_extern() # type: ignore def _pycall_path_old(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 @@ -129,7 +131,7 @@ class NodeCostArray(np.ndarray): # type: ignore np.uint32: ("uint32_t*", _get_path_cost_func("PathCostArrayUInt32")), } - def __new__(cls, array: ArrayLike) -> NodeCostArray: + def __new__(cls, array: ArrayLike) -> Self: """Validate a numpy array and setup a C callback.""" return np.asarray(array).view(cls) # type: ignore[no-any-return] diff --git a/tcod/sdl/_internal.py b/tcod/sdl/_internal.py index 8ea5e13f..8904e318 100644 --- a/tcod/sdl/_internal.py +++ b/tcod/sdl/_internal.py @@ -5,11 +5,13 @@ import logging import sys as _sys from dataclasses import dataclass -from types import TracebackType -from typing import Any, Callable, NoReturn, TypeVar +from typing import TYPE_CHECKING, Any, Callable, NoReturn, TypeVar from tcod.cffi import ffi, lib +if TYPE_CHECKING: + from types import TracebackType + T = TypeVar("T") logger = logging.getLogger("tcod.sdl") diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 33356132..a5da2f5b 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -48,17 +48,20 @@ import sys import threading import time -from types import TracebackType -from typing import Any, Callable, Final, Hashable, Iterator +from typing import TYPE_CHECKING, Any, Callable, Final, Hashable, Iterator import numpy as np -from numpy.typing import ArrayLike, DTypeLike, NDArray from typing_extensions import Literal, Self import tcod.sdl.sys from tcod.cffi import ffi, lib from tcod.sdl._internal import _check, _get_error, _ProtectedContext +if TYPE_CHECKING: + from types import TracebackType + + from numpy.typing import ArrayLike, DTypeLike, NDArray + def _get_format(format: DTypeLike) -> int: """Return a SDL_AudioFormat bit-field from a NumPy dtype.""" diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index fefd71d7..de481427 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -10,16 +10,18 @@ from __future__ import annotations import enum -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np -from numpy.typing import ArrayLike, NDArray import tcod.event import tcod.sdl.video from tcod.cffi import ffi, lib from tcod.sdl._internal import _check, _check_p +if TYPE_CHECKING: + from numpy.typing import ArrayLike, NDArray + class Cursor: """A cursor icon for use with :any:`set_cursor`.""" @@ -33,7 +35,7 @@ def __init__(self, sdl_cursor_p: Any) -> None: # noqa: ANN401 raise TypeError(msg) self.p = sdl_cursor_p - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: return bool(self.p == getattr(other, "p", None)) @classmethod diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index ae2b542d..22d3911d 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -6,16 +6,18 @@ from __future__ import annotations import enum -from typing import Any, Final, Sequence +from typing import TYPE_CHECKING, Any, Final, Sequence import numpy as np -from numpy.typing import NDArray from typing_extensions import Literal import tcod.sdl.video from tcod.cffi import ffi, lib from tcod.sdl._internal import _check, _check_p, _required_version +if TYPE_CHECKING: + from numpy.typing import NDArray + class TextureAccess(enum.IntEnum): """Determines how a texture is expected to be used.""" diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index dff4d4c0..7e7b2d88 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -3,6 +3,8 @@ import enum import warnings +from typing_extensions import Self + from tcod.cffi import ffi, lib from tcod.sdl._internal import _check, _get_error @@ -40,7 +42,7 @@ def close(self) -> None: def __del__(self) -> None: self.close() - def __enter__(self) -> _ScopeInit: + def __enter__(self) -> Self: return self def __exit__(self, *_: object) -> None: diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index f6dc922f..b140aa17 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -11,14 +11,16 @@ import enum import sys -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np -from numpy.typing import ArrayLike, NDArray from tcod.cffi import ffi, lib from tcod.sdl._internal import _check, _check_p, _required_version, _version_at_least +if TYPE_CHECKING: + from numpy.typing import ArrayLike, NDArray + __all__ = ( "FlashOperation", "Window", @@ -134,7 +136,9 @@ def __init__(self, sdl_window_p: Any) -> None: # noqa: ANN401 raise TypeError(msg) self.p = sdl_window_p - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: + if not isinstance(other, Window): + return NotImplemented return bool(self.p == other.p) def set_icon(self, pixels: ArrayLike) -> None: diff --git a/tcod/tileset.py b/tcod/tileset.py index f57304da..5f5a7511 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -16,18 +16,22 @@ from __future__ import annotations import itertools -from os import PathLike from pathlib import Path -from typing import Any, Iterable +from typing import TYPE_CHECKING, Any, Iterable import numpy as np -from numpy.typing import ArrayLike, NDArray from typing_extensions import deprecated -import tcod.console from tcod._internal import _check, _check_p, _console, _path_encode, _raise_tcod_error from tcod.cffi import ffi, lib +if TYPE_CHECKING: + from os import PathLike + + from numpy.typing import ArrayLike, NDArray + + import tcod.console + class Tileset: """A collection of graphical tiles. @@ -390,6 +394,7 @@ def procedural_block_elements(*, tileset: Tileset) -> None: Example:: + >>> import tcod.tileset >>> tileset = tcod.tileset.Tileset(8, 8) >>> tcod.tileset.procedural_block_elements(tileset=tileset) >>> tileset.get_tile(0x259E)[:, :, 3] # "▞" Quadrant upper right and lower left. diff --git a/tests/conftest.py b/tests/conftest.py index 0a04cf39..f7a42fd5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import random import warnings -from typing import Callable, Iterator, Union +from typing import Callable, Iterator import pytest @@ -97,6 +97,6 @@ def ch_latin1_str() -> str: "latin1_str", ] ) -def ch(request: pytest.FixtureRequest) -> Callable[[], Union[int, str]]: +def ch(request: pytest.FixtureRequest) -> Callable[[], int | str]: """Test with multiple types of ascii/latin1 characters.""" - return globals()[f"ch_{request.param}"]() # type: ignore + return globals()[f"ch_{request.param}"]() # type: ignore[no-any-return] diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py index b82392a1..f459f27f 100644 --- a/tests/test_libtcodpy.py +++ b/tests/test_libtcodpy.py @@ -433,7 +433,6 @@ def test_line_iter() -> None: @pytest.mark.filterwarnings("ignore") def test_bsp() -> None: - """Commented out statements work in libtcod-cffi.""" bsp = libtcodpy.bsp_new_with_size(0, 0, 64, 64) repr(bsp) # test __repr__ on leaf libtcodpy.bsp_resize(bsp, 0, 0, 32, 32) @@ -449,15 +448,15 @@ def test_bsp() -> None: bsp.level = bsp.level # cover functions on leaf - # self.assertFalse(libtcodpy.bsp_left(bsp)) - # self.assertFalse(libtcodpy.bsp_right(bsp)) - # self.assertFalse(libtcodpy.bsp_father(bsp)) + assert not libtcodpy.bsp_left(bsp) + assert not libtcodpy.bsp_right(bsp) + assert not libtcodpy.bsp_father(bsp) assert libtcodpy.bsp_is_leaf(bsp) assert libtcodpy.bsp_contains(bsp, 1, 1) - # self.assertFalse(libtcodpy.bsp_contains(bsp, -1, -1)) - # self.assertEqual(libtcodpy.bsp_find_node(bsp, 1, 1), bsp) - # self.assertFalse(libtcodpy.bsp_find_node(bsp, -1, -1)) + assert not libtcodpy.bsp_contains(bsp, -1, -1) + assert libtcodpy.bsp_find_node(bsp, 1, 1) == bsp + assert not libtcodpy.bsp_find_node(bsp, -1, -1) libtcodpy.bsp_split_once(bsp, False, 4) repr(bsp) # test __repr__ with parent @@ -467,10 +466,10 @@ def test_bsp() -> None: # cover functions on parent assert libtcodpy.bsp_left(bsp) assert libtcodpy.bsp_right(bsp) - # self.assertFalse(libtcodpy.bsp_father(bsp)) + assert not libtcodpy.bsp_father(bsp) assert not libtcodpy.bsp_is_leaf(bsp) - # self.assertEqual(libtcodpy.bsp_father(libtcodpy.bsp_left(bsp)), bsp) - # self.assertEqual(libtcodpy.bsp_father(libtcodpy.bsp_right(bsp)), bsp) + assert libtcodpy.bsp_father(libtcodpy.bsp_left(bsp)) == bsp # type: ignore[arg-type] + assert libtcodpy.bsp_father(libtcodpy.bsp_right(bsp)) == bsp # type: ignore[arg-type] libtcodpy.bsp_split_recursive(bsp, None, 4, 2, 2, 1.0, 1.0) diff --git a/tests/test_parser.py b/tests/test_parser.py index 07f736a1..5494b786 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -62,7 +62,7 @@ def new_property(self, name: str, typ: int, value: Any) -> bool: # noqa: ANN401 type_names = ["NONE", "BOOL", "CHAR", "INT", "FLOAT", "STRING", "COLOR", "DICE"] type_name = type_names[typ & 0xFF] if typ & libtcod.TYPE_LIST: - type_name = "LIST<%s>" % type_name + type_name = f"LIST<{type_name}>" print("new property named ", name, " type ", type_name, " value ", value) return True diff --git a/tests/test_tcod.py b/tests/test_tcod.py index 990aafe0..8b910c3a 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -15,7 +15,7 @@ # ruff: noqa: D103 -def raise_Exception(*args: object) -> NoReturn: +def raise_Exception(*_args: object) -> NoReturn: raise RuntimeError("testing exception") # noqa: TRY003, EM101 From 56d0e605630e3d4bc84bca5dade92a1fe6aa7f2a Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 4 Apr 2025 00:42:32 -0700 Subject: [PATCH 118/134] Adjust right-aligned printing in samples --- examples/samples_tcod.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index adcf9656..8abba781 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -1169,7 +1169,7 @@ def on_draw(self) -> None: ) for i in range(len(self.names)): sample_console.print( - SAMPLE_SCREEN_WIDTH - 2, + SAMPLE_SCREEN_WIDTH - 1, 2 + i, self.names[i], fg=WHITE, @@ -1533,14 +1533,14 @@ def draw_stats() -> None: except ZeroDivisionError: fps = 0 root_console.print( - 79, + root_console.width, 46, "last frame :%5.1f ms (%4d fps)" % (frame_length[-1] * 1000.0, fps), fg=GREY, alignment=libtcodpy.RIGHT, ) root_console.print( - 79, + root_console.width, 47, "elapsed : %8d ms %5.2fs" % (time.perf_counter() * 1000, time.perf_counter()), fg=GREY, From 3a21c6d33e7f001f3d396df2a7806f478b952e67 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 4 Apr 2025 01:14:57 -0700 Subject: [PATCH 119/134] Cleanup tcod samples --- examples/samples_tcod.py | 89 +++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 8abba781..e069c2ea 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -190,7 +190,7 @@ def __init__(self) -> None: sample_console.width // 2, sample_console.height // 2, "Offscreen console", - False, + clear=False, fg=WHITE, bg=BLACK, ) @@ -239,7 +239,7 @@ def on_draw(self) -> None: class LineDrawingSample(Sample): - FLAG_NAMES = [ + FLAG_NAMES = ( "BKGND_NONE", "BKGND_SET", "BKGND_MULTIPLY", @@ -253,7 +253,7 @@ class LineDrawingSample(Sample): "BKGND_BURN", "BKGND_OVERLAY", "BKGND_ALPHA", - ] + ) def __init__(self) -> None: self.name = "Line drawing" @@ -316,7 +316,7 @@ def on_draw(self) -> None: class NoiseSample(Sample): - NOISE_OPTIONS = [ # (name, algorithm, implementation) + NOISE_OPTIONS = ( # (name, algorithm, implementation) ( "perlin noise", tcod.noise.Algorithm.PERLIN, @@ -362,7 +362,7 @@ class NoiseSample(Sample): tcod.noise.Algorithm.WAVELET, tcod.noise.Implementation.TURBULENCE, ), - ] + ) def __init__(self) -> None: self.name = "Noise" @@ -428,7 +428,7 @@ def on_draw(self) -> None: ) for cur_func in range(len(self.NOISE_OPTIONS)): - text = "%i : %s" % (cur_func + 1, self.NOISE_OPTIONS[cur_func][0]) + text = f"{cur_func + 1} : {self.NOISE_OPTIONS[cur_func][0]}" if cur_func == self.func: sample_console.print(2, 2 + cur_func, text, fg=WHITE, bg=LIGHT_BLUE) else: @@ -495,7 +495,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: DARK_GROUND = (50, 50, 150) LIGHT_GROUND = (200, 180, 50) -SAMPLE_MAP_ = [ +SAMPLE_MAP_ = ( "##############################################", "####################### #################", "##################### # ###############", @@ -516,11 +516,11 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: "######## # #### ##### #####", "######## ##### ####################", "##############################################", -] +) SAMPLE_MAP: NDArray[Any] = np.array([list(line) for line in SAMPLE_MAP_]).transpose() -FOV_ALGO_NAMES = [ +FOV_ALGO_NAMES = ( "BASIC ", "DIAMOND ", "SHADOW ", @@ -535,7 +535,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: "PERMISSIVE8", "RESTRICTIVE", "SYMMETRIC_SHADOWCAST", -] +) TORCH_RADIUS = 10 SQUARED_TORCH_RADIUS = TORCH_RADIUS * TORCH_RADIUS @@ -635,13 +635,13 @@ def on_draw(self) -> None: sample_console.bg[...] = np.where(fov[:, :, np.newaxis], self.light_map_bg, self.dark_map_bg) def ev_keydown(self, event: tcod.event.KeyDown) -> None: - MOVE_KEYS = { + MOVE_KEYS = { # noqa: N806 tcod.event.KeySym.i: (0, -1), tcod.event.KeySym.j: (-1, 0), tcod.event.KeySym.k: (0, 1), tcod.event.KeySym.l: (1, 0), } - FOV_SELECT_KEYS = { + FOV_SELECT_KEYS = { # noqa: N806 tcod.event.KeySym.MINUS: -1, tcod.event.KeySym.EQUALS: 1, tcod.event.KeySym.KP_MINUS: -1, @@ -753,7 +753,7 @@ def on_draw(self) -> None: sample_console, x, y, - libtcodpy.color_lerp( # type: ignore + libtcodpy.color_lerp( # type: ignore[arg-type] LIGHT_GROUND, DARK_GROUND, 0.9 * libtcodpy.dijkstra_get_distance(self.dijkstra, x, y) / self.dijkstra_dist, @@ -771,11 +771,11 @@ def on_draw(self) -> None: if self.using_astar: if not libtcodpy.path_is_empty(self.path): libtcodpy.console_put_char(sample_console, self.px, self.py, " ", libtcodpy.BKGND_NONE) - self.px, self.py = libtcodpy.path_walk(self.path, True) # type: ignore + self.px, self.py = libtcodpy.path_walk(self.path, True) # type: ignore[assignment] libtcodpy.console_put_char(sample_console, self.px, self.py, "@", libtcodpy.BKGND_NONE) elif not libtcodpy.dijkstra_is_empty(self.dijkstra): libtcodpy.console_put_char(sample_console, self.px, self.py, " ", libtcodpy.BKGND_NONE) - self.px, self.py = libtcodpy.dijkstra_path_walk(self.dijkstra) # type: ignore + self.px, self.py = libtcodpy.dijkstra_path_walk(self.dijkstra) # type: ignore[assignment] libtcodpy.console_put_char(sample_console, self.px, self.py, "@", libtcodpy.BKGND_NONE) self.recalculate = True @@ -983,9 +983,9 @@ def on_draw(self) -> None: 1, "ENTER : rebuild bsp\n" "SPACE : rebuild dungeon\n" - "+-: bsp depth %d\n" - "*/: room size %d\n" - "1 : random room size %s" % (bsp_depth, bsp_min_room_size, rooms), + f"+-: bsp depth {bsp_depth}\n" + f"*/: room size {bsp_min_room_size}\n" + f"1 : random room size {rooms}", fg=WHITE, bg=None, ) @@ -1102,23 +1102,12 @@ def on_draw(self) -> None: sample_console.print( 1, 1, - "Pixel position : %4dx%4d\n" - "Tile position : %4dx%4d\n" - "Tile movement : %4dx%4d\n" - "Left button : %s\n" - "Right button : %s\n" - "Middle button : %s\n" - % ( - self.motion.position.x, - self.motion.position.y, - self.motion.tile.x, - self.motion.tile.y, - self.motion.tile_motion.x, - self.motion.tile_motion.y, - ("OFF", "ON")[self.mouse_left], - ("OFF", "ON")[self.mouse_right], - ("OFF", "ON")[self.mouse_middle], - ), + f"Pixel position : {self.motion.position.x:4d}x{self.motion.position.y:4d}\n" + f"Tile position : {self.motion.tile.x:4d}x{self.motion.tile.y:4d}\n" + f"Tile movement : {self.motion.tile_motion.x:4d}x{self.motion.tile_motion.y:4d}\n" + f"Left button : {'ON' if self.mouse_left else 'OFF'}\n" + f"Right button : {'ON' if self.mouse_right else 'OFF'}\n" + f"Middle button : {'ON' if self.mouse_middle else 'OFF'}\n", fg=LIGHT_YELLOW, bg=None, ) @@ -1318,9 +1307,9 @@ def on_draw(self) -> None: brightness = texture[uu.astype(int), vv.astype(int)] / 4.0 + 0.5 # use the brightness map to compose the final color of the tunnel - R = brightness * self.tex_r - G = brightness * self.tex_g - B = brightness * self.tex_b + rr = brightness * self.tex_r + gg = brightness * self.tex_g + bb = brightness * self.tex_b # create new light source if random.random() <= time_delta * LIGHTS_CHANCE and len(self.lights) < MAX_LIGHTS: @@ -1350,17 +1339,17 @@ def on_draw(self) -> None: brightness = light_brightness / ((xc - xl) ** 2 + (yc - yl) ** 2) # make all pixels shine around this light - R += brightness * light.r - G += brightness * light.g - B += brightness * light.b + rr += brightness * light.r + gg += brightness * light.g + bb += brightness * light.b # truncate values - R = R.clip(0, 255) - G = G.clip(0, 255) - B = B.clip(0, 255) + rr = rr.clip(0, 255) + gg = gg.clip(0, 255) + bb = bb.clip(0, 255) # fill the screen with these background colors - sample_console.bg.transpose(2, 1, 0)[...] = (R, G, B) + sample_console.bg.transpose(2, 1, 0)[...] = (rr, gg, bb) ############################################# @@ -1406,10 +1395,8 @@ def init_context(renderer: int) -> None: global context, console_render, sample_minimap if "context" in globals(): context.close() - libtcod_version = "%i.%i.%i" % ( - tcod.cffi.lib.TCOD_MAJOR_VERSION, - tcod.cffi.lib.TCOD_MINOR_VERSION, - tcod.cffi.lib.TCOD_PATCHLEVEL, + libtcod_version = ( + f"{tcod.cffi.lib.TCOD_MAJOR_VERSION}.{tcod.cffi.lib.TCOD_MINOR_VERSION}.{tcod.cffi.lib.TCOD_PATCHLEVEL}" ) context = tcod.context.new( columns=root_console.width, @@ -1535,14 +1522,14 @@ def draw_stats() -> None: root_console.print( root_console.width, 46, - "last frame :%5.1f ms (%4d fps)" % (frame_length[-1] * 1000.0, fps), + f"last frame :{frame_length[-1] * 1000.0:5.1f} ms ({int(fps):4d} fps)", fg=GREY, alignment=libtcodpy.RIGHT, ) root_console.print( root_console.width, 47, - "elapsed : %8d ms %5.2fs" % (time.perf_counter() * 1000, time.perf_counter()), + f"elapsed : {int(time.perf_counter() * 1000):8d} ms {time.perf_counter():5.2f}s", fg=GREY, alignment=libtcodpy.RIGHT, ) From c8103fdc5261148fe2599fe67c21b57b3ccc5063 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 4 Apr 2025 16:53:58 -0700 Subject: [PATCH 120/134] Resolve remaining warnings in console module --- tcod/console.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/tcod/console.py b/tcod/console.py index 0f108d95..013ea84e 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -193,7 +193,7 @@ def _init_setup_console_data(self, order: Literal["C", "F"] = "C") -> None: else: self._console_data = ffi.cast("struct TCOD_Console*", self.console_c) - self._tiles: NDArray[Any] = np.frombuffer( # type: ignore + self._tiles = np.frombuffer( ffi.buffer(self._console_data.tiles[0 : self.width * self.height]), dtype=self.DTYPE, ).reshape((self.height, self.width)) @@ -203,12 +203,12 @@ def _init_setup_console_data(self, order: Literal["C", "F"] = "C") -> None: @property def width(self) -> int: """The width of this Console.""" - return lib.TCOD_console_get_width(self.console_c) # type: ignore + return int(lib.TCOD_console_get_width(self.console_c)) @property def height(self) -> int: """The height of this Console.""" - return lib.TCOD_console_get_height(self.console_c) # type: ignore + return int(lib.TCOD_console_get_height(self.console_c)) @property def bg(self) -> NDArray[np.uint8]: @@ -402,7 +402,7 @@ def default_bg_blend(self) -> int: .. deprecated:: 8.5 These should not be used. Prefer passing defaults as kwargs. """ - return self._console_data.bkgnd_flag # type: ignore + return int(self._console_data.bkgnd_flag) @default_bg_blend.setter @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) @@ -416,7 +416,7 @@ def default_alignment(self) -> int: .. deprecated:: 8.5 These should not be used. Prefer passing defaults as kwargs. """ - return self._console_data.alignment # type: ignore + return int(self._console_data.alignment) @default_alignment.setter @deprecated(_DEPRECATE_CONSOLE_DEFAULTS_MSG, category=FutureWarning) @@ -434,8 +434,8 @@ def __clear_warning(self, name: str, value: tuple[int, int, int]) -> None: def clear( self, ch: int = 0x20, - fg: tuple[int, int, int] = ..., # type: ignore - bg: tuple[int, int, int] = ..., # type: ignore + fg: tuple[int, int, int] = ..., # type: ignore[assignment] + bg: tuple[int, int, int] = ..., # type: ignore[assignment] ) -> None: """Reset all values in this console to a single value. @@ -454,11 +454,11 @@ def clear( Added the `ch`, `fg`, and `bg` parameters. Non-white-on-black default values are deprecated. """ - if fg is ...: # type: ignore + if fg is ...: # type: ignore[comparison-overlap] fg = self.default_fg if fg != (255, 255, 255): self.__clear_warning("fg", fg) - if bg is ...: # type: ignore + if bg is ...: # type: ignore[comparison-overlap] bg = self.default_bg if bg != (0, 0, 0): self.__clear_warning("bg", bg) @@ -657,7 +657,7 @@ def rect( # noqa: PLR0913 y: int, width: int, height: int, - clear: bool, + clear: bool, # noqa: FBT001 bg_blend: int = tcod.constants.BKGND_DEFAULT, ) -> None: """Draw a the background color on a rect optionally clearing the text. @@ -750,7 +750,7 @@ def print_frame( # noqa: PLR0913 width: int, height: int, string: str = "", - clear: bool = True, + clear: bool = True, # noqa: FBT001, FBT002 bg_blend: int = tcod.constants.BKGND_DEFAULT, ) -> None: """Draw a framed rectangle with optional text. @@ -831,11 +831,11 @@ def blit( # noqa: PLR0913 # The old syntax is easy to detect and correct. if hasattr(src_y, "console_c"): (src_x, src_y, width, height, dest, dest_x, dest_y) = ( - dest, # type: ignore + dest, # type: ignore[assignment] dest_x, dest_y, src_x, - src_y, # type: ignore + src_y, # type: ignore[assignment] width, height, ) @@ -933,6 +933,7 @@ def __bool__(self) -> bool: return bool(self.console_c != ffi.NULL) def __getstate__(self) -> dict[str, Any]: + """Support serialization via :mod:`pickle`.""" state = self.__dict__.copy() del state["console_c"] state["_console_data"] = { @@ -948,6 +949,7 @@ def __getstate__(self) -> dict[str, Any]: return state def __setstate__(self, state: dict[str, Any]) -> None: + """Support serialization via :mod:`pickle`.""" self._key_color = None if "_tiles" not in state: tiles: NDArray[Any] = np.ndarray((self.height, self.width), dtype=self.DTYPE) @@ -967,12 +969,7 @@ def __setstate__(self, state: dict[str, Any]) -> None: def __repr__(self) -> str: """Return a string representation of this console.""" - return "tcod.console.Console(width=%i, height=%i, order=%r,buffer=\n%r)" % ( - self.width, - self.height, - self._order, - self.rgba, - ) + return f"tcod.console.Console(width={self.width}, height={self.height}, order={self._order!r},buffer=\n{self.rgba!r})" def __str__(self) -> str: """Return a simplified representation of this consoles contents.""" @@ -1197,7 +1194,7 @@ def draw_frame( # noqa: PLR0913 width: int, height: int, title: str = "", - clear: bool = True, + clear: bool = True, # noqa: FBT001, FBT002 fg: tuple[int, int, int] | None = None, bg: tuple[int, int, int] | None = None, bg_blend: int = tcod.constants.BKGND_SET, From 079dd62cd726d80c62bc82142fe305cb5c7d0e23 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 5 Apr 2025 12:44:22 -0700 Subject: [PATCH 121/134] Deprecate fg, bg, bg_blend keywords in Console methods --- CHANGELOG.md | 5 +- tcod/console.py | 112 ++++++++++++++++++++++++++++++++++++------ tests/test_console.py | 7 +-- 3 files changed, 104 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb837e1..e6390394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,12 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ### Deprecated -- Using `Console.print` without keywords for only the `x`, `y`, and `text` parameters has been deprecated. +- In general the `fg`, `bg`, and `bg_blend` keywords are too hard to keep track of as positional arguments so they must be replaced with keyword arguments instead. +- `Console.print`: deprecated `string`, `fg`, `bg`, and `bg_blend` being given as positional arguments. The `string` parameter has been renamed to `text`. - `Console.print_box` has been replaced by `Console.print`. +- `Console.draw_frame`: deprecated `clear`, `fg`, `bg`, and `bg_blend` being given as positional arguments. +- `Console.draw_rect`: deprecated `fg`, `bg`, and `bg_blend` being given as positional arguments. ## [17.1.0] - 2025-03-29 diff --git a/tcod/console.py b/tcod/console.py index 013ea84e..a0b08633 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -743,6 +743,7 @@ def vline( self.__deprecate_defaults("draw_rect", bg_blend) lib.TCOD_console_vline(self.console_c, x, y, height, bg_blend) + @deprecated("Replace with 'console.draw_frame(...)', use a separate print call for frame titles") def print_frame( # noqa: PLR0913 self, x: int, @@ -976,36 +977,36 @@ def __str__(self) -> str: return "<{}>".format("\n ".join("".join(chr(c) for c in line) for line in self._tiles["ch"])) @overload - @deprecated( - "Switch all parameters to keywords such as 'console.print(x=..., y=..., text=..., width=..., height=..., fg=..., bg=..., bg_blend=..., alignment=...)'" - "\n'string' keyword should be renamed to `text`" - ) def print( self, x: int, y: int, text: str, + *, + width: int | None = None, + height: int | None = None, fg: tuple[int, int, int] | None = None, bg: tuple[int, int, int] | None = None, bg_blend: int = tcod.constants.BKGND_SET, alignment: int = tcod.constants.LEFT, - *, - string: str = "", ) -> int: ... @overload + @deprecated( + "Replace text, fg, bg, bg_blend, and alignment with keyword arguments." + "\n'string' keyword should be renamed to `text`" + ) def print( self, x: int, y: int, text: str, - *, - width: int | None = None, - height: int | None = None, fg: tuple[int, int, int] | None = None, bg: tuple[int, int, int] | None = None, bg_blend: int = tcod.constants.BKGND_SET, alignment: int = tcod.constants.LEFT, + *, + string: str = "", ) -> int: ... def print( # noqa: PLR0913 @@ -1036,11 +1037,15 @@ def print( # noqa: PLR0913 height: Height in tiles to constrain the printing region. fg: RGB tuple to use as the foreground color, or `None` to leave the foreground unchanged. Tuple values should be 0-255. + Must be given as a keyword argument. bg: RGB tuple to use as the background color, or `None` to leave the foreground unchanged. Tuple values should be 0-255. + Must be given as a keyword argument. bg_blend: Background blend type used by libtcod. Typically starts with `libtcodpy.BKGND_*`. + Must be given as a keyword argument. alignment: One of `libtcodpy.LEFT`, `libtcodpy.CENTER`, or `libtcodpy.RIGHT` + Must be given as a keyword argument. string: Older deprecated name of the `text` parameter. Returns: @@ -1086,13 +1091,16 @@ def print( # noqa: PLR0913 .. versionchanged:: Unreleased + Deprecated giving `string`, `fg`, `bg`, and `bg_blend` as positional arguments. + Added `text` parameter to replace `string`. Added `width` and `height` keyword parameters to bind text to a rectangle and replace other print functions. Right-aligned text with `width=None` now treats the `x` coordinate as a past-the-end index, this will shift the text of older calls to the left by 1 tile. - Now returns the number of lines printed via word wrapping. + Now returns the number of lines printed via word wrapping, + same as previous print functions bound to rectangles. """ if width is not None and width <= 0: return 0 @@ -1187,6 +1195,38 @@ def print_box( # noqa: PLR0913 ) ) + @overload + def draw_frame( + self, + x: int, + y: int, + width: int, + height: int, + *, + clear: bool = True, + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, + bg_blend: int = tcod.constants.BKGND_SET, + decoration: str | tuple[int, int, int, int, int, int, int, int, int] = "┌─┐│ │└─┘", + ) -> None: ... + + @overload + @deprecated("Parameters clear, fg, bg, bg_blend should be keyword arguments. Remove title parameter") + def draw_frame( + self, + x: int, + y: int, + width: int, + height: int, + title: str = "", + clear: bool = True, # noqa: FBT001, FBT002 + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, + bg_blend: int = tcod.constants.BKGND_SET, + *, + decoration: str | tuple[int, int, int, int, int, int, int, int, int] = "┌─┐│ │└─┘", + ) -> None: ... + def draw_frame( # noqa: PLR0913 self, x: int, @@ -1215,13 +1255,16 @@ def draw_frame( # noqa: PLR0913 border with :any:`Console.print_box` using your own style. If `clear` is True than the region inside of the frame will be cleared. + Must be given as a keyword argument. `fg` and `bg` are the foreground and background colors for the frame border. This is a 3-item tuple with (r, g, b) color values from 0 to 255. These parameters can also be set to `None` to leave the colors unchanged. + Must be given as a keyword argument. `bg_blend` is the blend type used by libtcod. + Must be given as a keyword argument. `decoration` is a sequence of glyphs to use for rendering the borders. This a str or tuple of int's with 9 items with the items arranged in @@ -1241,6 +1284,10 @@ def draw_frame( # noqa: PLR0913 .. versionchanged:: 13.0 `x` and `y` are now always used as an absolute position for negative values. + .. versionchanged:: Unreleased + Deprecated `clear`, `fg`, `bg`, and `bg_blend` being given as positional arguments. + These should be keyword arguments only. + Example:: >>> from tcod import libtcodpy @@ -1305,6 +1352,34 @@ def draw_frame( # noqa: PLR0913 ) ) + @overload + def draw_rect( + self, + x: int, + y: int, + width: int, + height: int, + *, + ch: int, + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, + bg_blend: int = tcod.constants.BKGND_SET, + ) -> None: ... + + @overload + @deprecated("Parameters cg, fg, bg, bg_blend should be keyword arguments") + def draw_rect( + self, + x: int, + y: int, + width: int, + height: int, + ch: int, + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, + bg_blend: int = tcod.constants.BKGND_SET, + ) -> None: ... + def draw_rect( # noqa: PLR0913 self, x: int, @@ -1323,15 +1398,16 @@ def draw_rect( # noqa: PLR0913 `width` and `height` determine the size of the rectangle. - `ch` is a Unicode integer. You can use 0 to leave the current - characters unchanged. + `ch` is a Unicode integer. You can use 0 to leave the current characters unchanged. + Must be given as a keyword argument. - `fg` and `bg` are the foreground text color and background tile color - respectfully. This is a 3-item tuple with (r, g, b) color values from - 0 to 255. These parameters can also be set to `None` to leave the - colors unchanged. + `fg` and `bg` are the foreground text color and background tile color respectfully. + This is a 3-item tuple with (r, g, b) color values from 0 to 255. + These parameters can also be set to `None` to leave the colors unchanged. + Must be given as a keyword argument. `bg_blend` is the blend type used by libtcod. + Must be given as a keyword argument. .. versionadded:: 8.5 @@ -1340,6 +1416,10 @@ def draw_rect( # noqa: PLR0913 .. versionchanged:: 13.0 `x` and `y` are now always used as an absolute position for negative values. + + .. versionchanged:: Unreleased + Deprecated `ch`, `fg`, `bg`, and `bg_blend` being given as positional arguments. + These should be keyword arguments only. """ lib.TCOD_console_draw_rect_rgb( self.console_c, diff --git a/tests/test_console.py b/tests/test_console.py index 04bae6cb..4b8f8435 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -81,9 +81,10 @@ def test_console_methods() -> None: console.hline(0, 1, 10) with pytest.deprecated_call(): console.vline(1, 0, 10) - console.print_frame(0, 0, 8, 8, "Frame") - console.blit(0, 0, 0, 0, console, 0, 0) # type: ignore - console.blit(0, 0, 0, 0, console, 0, 0, key_color=(0, 0, 0)) # type: ignore + with pytest.deprecated_call(): + console.print_frame(0, 0, 8, 8, "Frame") + console.blit(0, 0, 0, 0, console, 0, 0) # type: ignore[arg-type] + console.blit(0, 0, 0, 0, console, 0, 0, key_color=(0, 0, 0)) # type: ignore[arg-type] with pytest.deprecated_call(): console.set_key_color((254, 0, 254)) From 3a04143a6b94b159f5ecb666185ed4355736b773 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 5 Apr 2025 20:24:28 -0700 Subject: [PATCH 122/134] Refactor FOV and pathfinding samples --- examples/samples_tcod.py | 241 ++++++++++++++------------------------- 1 file changed, 83 insertions(+), 158 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index e069c2ea..672ce4f6 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -63,6 +63,12 @@ cur_sample = 0 # Current selected sample. frame_times = [time.perf_counter()] frame_length = [0.0] +START_TIME = time.perf_counter() + + +def _get_elapsed_time() -> float: + """Return time passed since the start of the program.""" + return time.perf_counter() - START_TIME class Sample(tcod.event.EventDispatch[None]): @@ -207,13 +213,13 @@ def __init__(self) -> None: ) def on_enter(self) -> None: - self.counter = time.perf_counter() + self.counter = _get_elapsed_time() # get a "screenshot" of the current sample screen sample_console.blit(dest=self.screenshot) def on_draw(self) -> None: - if time.perf_counter() - self.counter >= 1: - self.counter = time.perf_counter() + if _get_elapsed_time() - self.counter >= 1: + self.counter = _get_elapsed_time() self.x += self.x_dir self.y += self.y_dir if self.x == sample_console.width / 2 + 5: @@ -396,8 +402,8 @@ def get_noise(self) -> tcod.noise.Noise: ) def on_draw(self) -> None: - self.dx = time.perf_counter() * 0.25 - self.dy = time.perf_counter() * 0.25 + self.dx = _get_elapsed_time() * 0.25 + self.dy = _get_elapsed_time() * 0.25 for y in range(2 * sample_console.height): for x in range(2 * sample_console.width): f = [ @@ -518,7 +524,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: "##############################################", ) -SAMPLE_MAP: NDArray[Any] = np.array([list(line) for line in SAMPLE_MAP_]).transpose() +SAMPLE_MAP: NDArray[Any] = np.array([[ord(c) for c in line] for line in SAMPLE_MAP_]).transpose() FOV_ALGO_NAMES = ( "BASIC ", @@ -555,17 +561,17 @@ def __init__(self) -> None: map_shape = (SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) self.walkable: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F") - self.walkable[:] = SAMPLE_MAP[:] == " " + self.walkable[:] = SAMPLE_MAP[:] == ord(" ") self.transparent: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F") - self.transparent[:] = self.walkable[:] | (SAMPLE_MAP == "=") + self.transparent[:] = self.walkable[:] | (SAMPLE_MAP[:] == ord("=")) # Lit background colors for the map. self.light_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, LIGHT_GROUND, dtype="3B") - self.light_map_bg[SAMPLE_MAP[:] == "#"] = LIGHT_WALL + self.light_map_bg[SAMPLE_MAP[:] == ord("#")] = LIGHT_WALL # Dark background colors for the map. self.dark_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, DARK_GROUND, dtype="3B") - self.dark_map_bg[SAMPLE_MAP[:] == "#"] = DARK_WALL + self.dark_map_bg[SAMPLE_MAP[:] == ord("#")] = DARK_WALL def draw_ui(self) -> None: sample_console.print( @@ -586,8 +592,8 @@ def on_draw(self) -> None: self.draw_ui() sample_console.print(self.player_x, self.player_y, "@") # Draw windows. - sample_console.rgb["ch"][SAMPLE_MAP == "="] = 0x2550 # BOX DRAWINGS DOUBLE HORIZONTAL - sample_console.rgb["fg"][SAMPLE_MAP == "="] = BLACK + sample_console.rgb["ch"][SAMPLE_MAP[:] == ord("=")] = 0x2550 # BOX DRAWINGS DOUBLE HORIZONTAL + sample_console.rgb["fg"][SAMPLE_MAP[:] == ord("=")] = BLACK # Get a 2D boolean array of visible cells. fov = tcod.map.compute_fov( @@ -600,7 +606,7 @@ def on_draw(self) -> None: if self.torch: # Derive the touch from noise based on the current time. - torch_t = time.perf_counter() * 5 + torch_t = _get_elapsed_time() * 5 # Randomize the light position between -1.5 and 1.5 torch_x = self.player_x + self.noise.get_point(torch_t) * 1.5 torch_y = self.player_y + self.noise.get_point(torch_t + 11) * 1.5 @@ -632,7 +638,11 @@ def on_draw(self) -> None: # Linear interpolation between colors. sample_console.rgb["bg"] = dark_bg + (light_bg - dark_bg) * light[..., np.newaxis] else: - sample_console.bg[...] = np.where(fov[:, :, np.newaxis], self.light_map_bg, self.dark_map_bg) + sample_console.bg[...] = np.select( + condlist=[fov[:, :, np.newaxis]], + choicelist=[self.light_map_bg], + default=self.dark_map_bg, + ) def ev_keydown(self, event: tcod.event.KeyDown) -> None: MOVE_KEYS = { # noqa: N806 @@ -665,174 +675,89 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: class PathfindingSample(Sample): def __init__(self) -> None: + """Initialize this sample.""" self.name = "Path finding" - self.px = 20 - self.py = 10 - self.dx = 24 - self.dy = 1 - self.dijkstra_dist = 0.0 + self.player_x = 20 + self.player_y = 10 + self.dest_x = 24 + self.dest_y = 1 self.using_astar = True - self.recalculate = False self.busy = 0.0 - self.old_char = " " + self.cost = SAMPLE_MAP.T[:] == ord(" ") + self.graph = tcod.path.SimpleGraph(cost=self.cost, cardinal=70, diagonal=99) + self.pathfinder = tcod.path.Pathfinder(graph=self.graph) - self.map = tcod.map.Map(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) - for y in range(SAMPLE_SCREEN_HEIGHT): - for x in range(SAMPLE_SCREEN_WIDTH): - if SAMPLE_MAP[x, y] == " ": - # ground - self.map.walkable[y, x] = True - self.map.transparent[y, x] = True - elif SAMPLE_MAP[x, y] == "=": - # window - self.map.walkable[y, x] = False - self.map.transparent[y, x] = True - self.path = tcod.path.AStar(self.map) - self.dijkstra = tcod.path.Dijkstra(self.map) + self.background_console = tcod.console.Console(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) + + # draw the dungeon + self.background_console.rgb["fg"] = BLACK + self.background_console.rgb["bg"] = DARK_GROUND + self.background_console.rgb["bg"][SAMPLE_MAP.T[:] == ord("#")] = DARK_WALL + self.background_console.rgb["ch"][SAMPLE_MAP.T[:] == ord("=")] = ord("═") def on_enter(self) -> None: - # we draw the foreground only the first time. - # during the player movement, only the @ is redrawn. - # the rest impacts only the background color - # draw the help text & player @ - sample_console.clear() - sample_console.ch[self.dx, self.dy] = ord("+") - sample_console.fg[self.dx, self.dy] = WHITE - sample_console.ch[self.px, self.py] = ord("@") - sample_console.fg[self.px, self.py] = WHITE - sample_console.print( - 1, - 1, - "IJKL / mouse :\nmove destination\nTAB : A*/dijkstra", - fg=WHITE, - bg=None, - ) - sample_console.print(1, 4, "Using : A*", fg=WHITE, bg=None) - # draw windows - for y in range(SAMPLE_SCREEN_HEIGHT): - for x in range(SAMPLE_SCREEN_WIDTH): - if SAMPLE_MAP[x, y] == "=": - libtcodpy.console_put_char(sample_console, x, y, libtcodpy.CHAR_DHLINE, libtcodpy.BKGND_NONE) - self.recalculate = True + """Do nothing.""" def on_draw(self) -> None: - if self.recalculate: - if self.using_astar: - libtcodpy.path_compute(self.path, self.px, self.py, self.dx, self.dy) - else: - self.dijkstra_dist = 0.0 - # compute dijkstra grid (distance from px,py) - libtcodpy.dijkstra_compute(self.dijkstra, self.px, self.py) - # get the maximum distance (needed for rendering) - for y in range(SAMPLE_SCREEN_HEIGHT): - for x in range(SAMPLE_SCREEN_WIDTH): - d = libtcodpy.dijkstra_get_distance(self.dijkstra, x, y) - self.dijkstra_dist = max(d, self.dijkstra_dist) - # compute path from px,py to dx,dy - libtcodpy.dijkstra_path_set(self.dijkstra, self.dx, self.dy) - self.recalculate = False - self.busy = 0.2 + """Recompute and render pathfinding.""" + self.pathfinder = tcod.path.Pathfinder(graph=self.graph) + # self.pathfinder.clear() # Known issues, needs fixing # noqa: ERA001 + self.pathfinder.add_root((self.player_y, self.player_x)) + # draw the dungeon - for y in range(SAMPLE_SCREEN_HEIGHT): - for x in range(SAMPLE_SCREEN_WIDTH): - if SAMPLE_MAP[x, y] == "#": - libtcodpy.console_set_char_background(sample_console, x, y, DARK_WALL, libtcodpy.BKGND_SET) - else: - libtcodpy.console_set_char_background(sample_console, x, y, DARK_GROUND, libtcodpy.BKGND_SET) + self.background_console.blit(dest=sample_console) + + sample_console.print(self.dest_x, self.dest_y, "+", fg=WHITE) + sample_console.print(self.player_x, self.player_y, "@", fg=WHITE) + sample_console.print(1, 1, "IJKL / mouse :\nmove destination\nTAB : A*/dijkstra", fg=WHITE, bg=None) + sample_console.print(1, 4, "Using : A*", fg=WHITE, bg=None) + + if not self.using_astar: + self.pathfinder.resolve(goal=None) + reachable = self.pathfinder.distance != np.iinfo(self.pathfinder.distance.dtype).max + + # draw distance from player + dijkstra_max_dist = float(self.pathfinder.distance[reachable].max()) + np.array(self.pathfinder.distance, copy=True, dtype=np.float32) + interpolate = self.pathfinder.distance[reachable] * 0.9 / dijkstra_max_dist + color_delta = (np.array(DARK_GROUND) - np.array(LIGHT_GROUND)).astype(np.float32) + sample_console.rgb.T["bg"][reachable] = np.array(LIGHT_GROUND) + interpolate[:, np.newaxis] * color_delta + # draw the path - if self.using_astar: - for i in range(libtcodpy.path_size(self.path)): - x, y = libtcodpy.path_get(self.path, i) - libtcodpy.console_set_char_background(sample_console, x, y, LIGHT_GROUND, libtcodpy.BKGND_SET) - else: - for y in range(SAMPLE_SCREEN_HEIGHT): - for x in range(SAMPLE_SCREEN_WIDTH): - if SAMPLE_MAP[x, y] != "#": - libtcodpy.console_set_char_background( - sample_console, - x, - y, - libtcodpy.color_lerp( # type: ignore[arg-type] - LIGHT_GROUND, - DARK_GROUND, - 0.9 * libtcodpy.dijkstra_get_distance(self.dijkstra, x, y) / self.dijkstra_dist, - ), - libtcodpy.BKGND_SET, - ) - for i in range(libtcodpy.dijkstra_size(self.dijkstra)): - x, y = libtcodpy.dijkstra_get(self.dijkstra, i) - libtcodpy.console_set_char_background(sample_console, x, y, LIGHT_GROUND, libtcodpy.BKGND_SET) + path = self.pathfinder.path_to((self.dest_y, self.dest_x))[1:, ::-1] + sample_console.rgb["bg"][tuple(path.T)] = LIGHT_GROUND # move the creature self.busy -= frame_length[-1] if self.busy <= 0.0: self.busy = 0.2 - if self.using_astar: - if not libtcodpy.path_is_empty(self.path): - libtcodpy.console_put_char(sample_console, self.px, self.py, " ", libtcodpy.BKGND_NONE) - self.px, self.py = libtcodpy.path_walk(self.path, True) # type: ignore[assignment] - libtcodpy.console_put_char(sample_console, self.px, self.py, "@", libtcodpy.BKGND_NONE) - elif not libtcodpy.dijkstra_is_empty(self.dijkstra): - libtcodpy.console_put_char(sample_console, self.px, self.py, " ", libtcodpy.BKGND_NONE) - self.px, self.py = libtcodpy.dijkstra_path_walk(self.dijkstra) # type: ignore[assignment] - libtcodpy.console_put_char(sample_console, self.px, self.py, "@", libtcodpy.BKGND_NONE) - self.recalculate = True + if len(path): + self.player_x = int(path.item(0, 0)) + self.player_y = int(path.item(0, 1)) def ev_keydown(self, event: tcod.event.KeyDown) -> None: - if event.sym == tcod.event.KeySym.i and self.dy > 0: - # destination move north - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) - self.dy -= 1 - self.old_char = sample_console.ch[self.dx, self.dy] - libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) - if SAMPLE_MAP[self.dx, self.dy] == " ": - self.recalculate = True - elif event.sym == tcod.event.KeySym.k and self.dy < SAMPLE_SCREEN_HEIGHT - 1: - # destination move south - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) - self.dy += 1 - self.old_char = sample_console.ch[self.dx, self.dy] - libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) - if SAMPLE_MAP[self.dx, self.dy] == " ": - self.recalculate = True - elif event.sym == tcod.event.KeySym.j and self.dx > 0: - # destination move west - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) - self.dx -= 1 - self.old_char = sample_console.ch[self.dx, self.dy] - libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) - if SAMPLE_MAP[self.dx, self.dy] == " ": - self.recalculate = True - elif event.sym == tcod.event.KeySym.l and self.dx < SAMPLE_SCREEN_WIDTH - 1: - # destination move east - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) - self.dx += 1 - self.old_char = sample_console.ch[self.dx, self.dy] - libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) - if SAMPLE_MAP[self.dx, self.dy] == " ": - self.recalculate = True + """Handle movement and UI.""" + if event.sym == tcod.event.KeySym.i and self.dest_y > 0: # destination move north + self.dest_y -= 1 + elif event.sym == tcod.event.KeySym.k and self.dest_y < SAMPLE_SCREEN_HEIGHT - 1: # destination move south + self.dest_y += 1 + elif event.sym == tcod.event.KeySym.j and self.dest_x > 0: # destination move west + self.dest_x -= 1 + elif event.sym == tcod.event.KeySym.l and self.dest_x < SAMPLE_SCREEN_WIDTH - 1: # destination move east + self.dest_x += 1 elif event.sym == tcod.event.KeySym.TAB: self.using_astar = not self.using_astar - if self.using_astar: - libtcodpy.console_print(sample_console, 1, 4, "Using : A* ") - else: - libtcodpy.console_print(sample_console, 1, 4, "Using : Dijkstra") - self.recalculate = True else: super().ev_keydown(event) def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None: + """Move destination via mouseover.""" mx = event.tile.x - SAMPLE_SCREEN_X my = event.tile.y - SAMPLE_SCREEN_Y - if 0 <= mx < SAMPLE_SCREEN_WIDTH and 0 <= my < SAMPLE_SCREEN_HEIGHT and (self.dx != mx or self.dy != my): - libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE) - self.dx = mx - self.dy = my - self.old_char = sample_console.ch[self.dx, self.dy] - libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE) - if SAMPLE_MAP[self.dx, self.dy] == " ": - self.recalculate = True + if 0 <= mx < SAMPLE_SCREEN_WIDTH and 0 <= my < SAMPLE_SCREEN_HEIGHT: + self.dest_x = mx + self.dest_y = my ############################################# @@ -1044,7 +969,7 @@ def on_draw(self) -> None: y = sample_console.height / 2 scalex = 0.2 + 1.8 * (1.0 + math.cos(time.time() / 2)) / 2.0 scaley = scalex - angle = time.perf_counter() + angle = _get_elapsed_time() if int(time.time()) % 2: # split the color channels of circle.png # the red channel @@ -1529,7 +1454,7 @@ def draw_stats() -> None: root_console.print( root_console.width, 47, - f"elapsed : {int(time.perf_counter() * 1000):8d} ms {time.perf_counter():5.2f}s", + f"elapsed : {int(_get_elapsed_time() * 1000):8d} ms {_get_elapsed_time():5.2f}s", fg=GREY, alignment=libtcodpy.RIGHT, ) From c7b6bc55d7395f17fe78fdfc645dcb0695adfafc Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 6 Apr 2025 16:29:11 -0700 Subject: [PATCH 123/134] Clean up warnings Specify specific warnings for Mypy ignores Narrow type hints Move image internals to image.py to avoid cyclic imports --- tcod/__init__.py | 2 +- tcod/_internal.py | 55 ++++++-------------------------------- tcod/bsp.py | 17 +++--------- tcod/cffi.py | 4 +-- tcod/console.py | 3 ++- tcod/context.py | 2 +- tcod/event.py | 2 +- tcod/image.py | 52 +++++++++++++++++++++++++++--------- tcod/libtcodpy.py | 24 +++++++---------- tcod/noise.py | 5 ++-- tcod/path.py | 61 ++++++++++++++++++++++++------------------- tcod/sdl/_internal.py | 4 +-- tcod/sdl/audio.py | 2 +- tcod/tcod.py | 2 +- tcod/tileset.py | 7 ++--- 15 files changed, 112 insertions(+), 130 deletions(-) diff --git a/tcod/__init__.py b/tcod/__init__.py index b155be01..2db18f54 100644 --- a/tcod/__init__.py +++ b/tcod/__init__.py @@ -13,7 +13,7 @@ __path__ = extend_path(__path__, __name__) -from tcod import bsp, color, console, context, event, image, los, map, noise, path, random, tileset +from tcod import bsp, color, console, context, event, image, los, map, noise, path, random, tileset # noqa: A004 from tcod.cffi import __sdl_version__, ffi, lib from tcod.tcod import __getattr__ # noqa: F401 from tcod.version import __version__ diff --git a/tcod/_internal.py b/tcod/_internal.py index 01136c5e..049dd37d 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -7,7 +7,6 @@ import warnings from typing import TYPE_CHECKING, Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar -import numpy as np from typing_extensions import Literal, LiteralString, deprecated from tcod.cffi import ffi, lib @@ -16,9 +15,6 @@ from pathlib import Path from types import TracebackType - from numpy.typing import ArrayLike, NDArray - - import tcod.image FuncType = Callable[..., Any] F = TypeVar("F", bound=FuncType) @@ -26,7 +22,11 @@ def _deprecate_passthrough( - message: str, /, *, category: type[Warning] = DeprecationWarning, stacklevel: int = 0 + message: str, # noqa: ARG001 + /, + *, + category: type[Warning] = DeprecationWarning, # noqa: ARG001 + stacklevel: int = 0, # noqa: ARG001 ) -> Callable[[F], F]: """Return a decorator which skips wrapping a warning onto functions. This is used for non-debug runs.""" @@ -51,7 +51,7 @@ def pending_deprecate( def verify_order(order: Literal["C", "F"]) -> Literal["C", "F"]: """Verify and return a Numpy order string.""" - order = order.upper() # type: ignore + order = order.upper() # type: ignore[assignment] if order not in ("C", "F"): msg = f"order must be 'C' or 'F', not {order!r}" raise TypeError(msg) @@ -91,7 +91,7 @@ def _check_warn(error: int, stacklevel: int = 2) -> int: def _unpack_char_p(char_p: Any) -> str: # noqa: ANN401 if char_p == ffi.NULL: return "" - return ffi.string(char_p).decode() # type: ignore + return str(ffi.string(char_p), encoding="utf-8") def _int(int_or_str: SupportsInt | str | bytes) -> int: @@ -171,7 +171,7 @@ def __enter__(self) -> Callable[[Any], None]: return self.propagate def __exit__( - self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + self, _type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None ) -> None: """If we're holding on to an exception, raise it now. @@ -232,42 +232,3 @@ def _console(console: Any) -> Any: # noqa: ANN401 stacklevel=3, ) return ffi.NULL - - -class TempImage: - """An Image-like container for NumPy arrays.""" - - def __init__(self, array: ArrayLike) -> None: - """Initialize an image from the given array. May copy or reference the array.""" - self._array: NDArray[np.uint8] = np.ascontiguousarray(array, dtype=np.uint8) - height, width, depth = self._array.shape - if depth != 3: # noqa: PLR2004 - msg = f"Array must have RGB channels. Shape is: {self._array.shape!r}" - raise TypeError(msg) - self._buffer = ffi.from_buffer("TCOD_color_t[]", self._array) - self._mipmaps = ffi.new( - "struct TCOD_mipmap_*", - { - "width": width, - "height": height, - "fwidth": width, - "fheight": height, - "buf": self._buffer, - "dirty": True, - }, - ) - self.image_c = ffi.new( - "TCOD_Image*", - { - "nb_mipmaps": 1, - "mipmaps": self._mipmaps, - "has_key_color": False, - }, - ) - - -def _as_image(image: ArrayLike | tcod.image.Image) -> TempImage | tcod.image.Image: - """Convert this input into an Image-like object.""" - if hasattr(image, "image_c"): - return image - return TempImage(image) diff --git a/tcod/bsp.py b/tcod/bsp.py index 0f8caeaa..a2959b16 100644 --- a/tcod/bsp.py +++ b/tcod/bsp.py @@ -104,20 +104,9 @@ def __repr__(self) -> str: """Provide a useful readout when printed.""" status = "leaf" if self.children: - status = "split at position=%i,horizontal=%r" % ( - self.position, - self.horizontal, - ) - - return "<%s(x=%i,y=%i,width=%i,height=%i) level=%i %s>" % ( - self.__class__.__name__, - self.x, - self.y, - self.width, - self.height, - self.level, - status, - ) + status = f"split at position={self.position},horizontal={self.horizontal!r}" + + return f"<{self.__class__.__name__}(x={self.x},y={self.y},width={self.width},height={self.height}) level={self.level} {status}>" def _unpack_bsp_tree(self, cdata: Any) -> None: # noqa: ANN401 self.x = cdata.x diff --git a/tcod/cffi.py b/tcod/cffi.py index eb94d5e1..d1c93025 100644 --- a/tcod/cffi.py +++ b/tcod/cffi.py @@ -64,8 +64,8 @@ def get_sdl_version() -> str: __sdl_version__ = get_sdl_version() -@ffi.def_extern() # type: ignore -def _libtcod_log_watcher(message: Any, userdata: None) -> None: # noqa: ANN401 +@ffi.def_extern() # type: ignore[misc] +def _libtcod_log_watcher(message: Any, _userdata: None) -> None: # noqa: ANN401 text = str(ffi.string(message.message), encoding="utf-8") source = str(ffi.string(message.source), encoding="utf-8") level = int(message.level) diff --git a/tcod/console.py b/tcod/console.py index a0b08633..b1c7c24e 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -16,6 +16,7 @@ import tcod._internal import tcod.constants +import tcod.image from tcod._internal import _check, _path_encode from tcod.cffi import ffi, lib @@ -1444,7 +1445,7 @@ def draw_semigraphics(self, pixels: ArrayLike | tcod.image.Image, x: int = 0, y: .. versionadded:: 11.4 """ - image = tcod._internal._as_image(pixels) + image = tcod.image._as_image(pixels) lib.TCOD_image_blit_2x(image.image_c, self.console_c, x, y, 0, 0, -1, -1) diff --git a/tcod/context.py b/tcod/context.py index 826307dc..386b7324 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -447,7 +447,7 @@ def __reduce__(self) -> NoReturn: raise pickle.PicklingError(msg) -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _pycall_cli_output(catch_reference: Any, output: Any) -> None: # noqa: ANN401 """Callback for the libtcod context CLI. diff --git a/tcod/event.py b/tcod/event.py index 80c4acce..85bbead8 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1548,7 +1548,7 @@ def get_mouse_state() -> MouseState: return MouseState((xy[0], xy[1]), (int(tile[0]), int(tile[1])), buttons) -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _sdl_event_watcher(userdata: Any, sdl_event: Any) -> int: callback: Callable[[Event], None] = ffi.from_handle(userdata) callback(_parse_event(sdl_event)) diff --git a/tcod/image.py b/tcod/image.py index 219916cf..6b4a1083 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -145,7 +145,7 @@ def get_alpha(self, x: int, y: int) -> int: int: The alpha value of the pixel. With 0 being fully transparent and 255 being fully opaque. """ - return lib.TCOD_image_get_alpha(self.image_c, x, y) # type: ignore + return int(lib.TCOD_image_get_alpha(self.image_c, x, y)) def refresh_console(self, console: tcod.console.Console) -> None: """Update an Image created with :any:`libtcodpy.image_from_console`. @@ -351,17 +351,6 @@ def __array_interface__(self) -> dict[str, Any]: } -def _get_format_name(format: int) -> str: - """Return the SDL_PIXELFORMAT_X name for this format, if possible.""" - for attr in dir(lib): - if not attr.startswith("SDL_PIXELFORMAT"): - continue - if getattr(lib, attr) != format: - continue - return attr - return str(format) - - @deprecated( "This function may be removed in the future." " It's recommended to load images with a more complete image library such as python-Pillow or python-imageio.", @@ -388,3 +377,42 @@ def load(filename: str | PathLike[str]) -> NDArray[np.uint8]: axis=2, ) return array + + +class _TempImage: + """An Image-like container for NumPy arrays.""" + + def __init__(self, array: ArrayLike) -> None: + """Initialize an image from the given array. May copy or reference the array.""" + self._array: NDArray[np.uint8] = np.ascontiguousarray(array, dtype=np.uint8) + height, width, depth = self._array.shape + if depth != 3: # noqa: PLR2004 + msg = f"Array must have RGB channels. Shape is: {self._array.shape!r}" + raise TypeError(msg) + self._buffer = ffi.from_buffer("TCOD_color_t[]", self._array) + self._mipmaps = ffi.new( + "struct TCOD_mipmap_*", + { + "width": width, + "height": height, + "fwidth": width, + "fheight": height, + "buf": self._buffer, + "dirty": True, + }, + ) + self.image_c = ffi.new( + "TCOD_Image*", + { + "nb_mipmaps": 1, + "mipmaps": self._mipmaps, + "has_key_color": False, + }, + ) + + +def _as_image(image: ArrayLike | Image | _TempImage) -> _TempImage | Image: + """Convert this input into an Image-like object.""" + if isinstance(image, (Image, _TempImage)): + return image + return _TempImage(image) diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 52cba83e..0018745d 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -238,8 +238,8 @@ def set( def blit( self, dest: tcod.console.Console, - fill_fore: bool = True, - fill_back: bool = True, + fill_fore: bool = True, # noqa: FBT001, FBT002 + fill_back: bool = True, # noqa: FBT001, FBT002 ) -> None: """Use libtcod's "fill" functions to write the buffer to a console. @@ -313,12 +313,7 @@ def nb_dices(self, value: int) -> None: def __str__(self) -> str: add = f"+({self.addsub})" if self.addsub != 0 else "" - return "%id%ix%s%s" % ( - self.nb_dices, - self.nb_faces, - self.multiplier, - add, - ) + return f"{self.nb_dices}d{self.nb_faces}x{self.multiplier}{add}" def __repr__(self) -> str: return f"{self.__class__.__name__}(nb_dices={self.nb_dices!r},nb_faces={self.nb_faces!r},multiplier={self.multiplier!r},addsub={self.addsub!r})" @@ -3584,7 +3579,8 @@ def _unpack_union(type_: int, union: Any) -> Any: # noqa: PLR0911 return Dice(union.dice) if type_ & lib.TCOD_TYPE_LIST: return _convert_TCODList(union.list, type_ & 0xFF) - raise RuntimeError("Unknown libtcod type: %i" % type_) + msg = f"Unknown libtcod type: {type_}" + raise RuntimeError(msg) def _convert_TCODList(c_list: Any, type_: int) -> Any: @@ -3607,27 +3603,27 @@ def parser_new_struct(parser: Any, name: str) -> Any: _parser_listener: Any = None -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _pycall_parser_new_struct(struct: Any, name: str) -> Any: return _parser_listener.new_struct(struct, _unpack_char_p(name)) -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _pycall_parser_new_flag(name: str) -> Any: return _parser_listener.new_flag(_unpack_char_p(name)) -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _pycall_parser_new_property(propname: Any, type: Any, value: Any) -> Any: return _parser_listener.new_property(_unpack_char_p(propname), type, _unpack_union(type, value)) -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _pycall_parser_end_struct(struct: Any, name: Any) -> Any: return _parser_listener.end_struct(struct, _unpack_char_p(name)) -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _pycall_parser_error(msg: Any) -> None: _parser_listener.error(_unpack_char_p(msg)) diff --git a/tcod/noise.py b/tcod/noise.py index 82920a1c..da14b3f2 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -236,9 +236,8 @@ def __getitem__(self, indexes: Any) -> NDArray[np.float32]: if not isinstance(indexes, tuple): indexes = (indexes,) if len(indexes) > self.dimensions: - raise IndexError( - "This noise generator has %i dimensions, but was indexed with %i." % (self.dimensions, len(indexes)) - ) + msg = f"This noise generator has {self.dimensions} dimensions, but was indexed with {len(indexes)}." + raise IndexError(msg) indexes = list(np.broadcast_arrays(*indexes)) c_input = [ffi.NULL, ffi.NULL, ffi.NULL, ffi.NULL] for i, index in enumerate(indexes): diff --git a/tcod/path.py b/tcod/path.py index 8ce9aaa2..a58cb218 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -26,6 +26,7 @@ import numpy as np from typing_extensions import Literal, Self +import tcod.map from tcod._internal import _check from tcod.cffi import ffi, lib @@ -33,29 +34,29 @@ from numpy.typing import ArrayLike, DTypeLike, NDArray -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _pycall_path_old(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 - """Libtcodpy style callback, needs to preserve the old userData issue.""" - func, userData = ffi.from_handle(handle) - return func(x1, y1, x2, y2, userData) # type: ignore + """Libtcodpy style callback, needs to preserve the old userdata issue.""" + func, userdata = ffi.from_handle(handle) + return func(x1, y1, x2, y2, userdata) # type: ignore[no-any-return] -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _pycall_path_simple(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 """Does less and should run faster, just calls the handle function.""" - return ffi.from_handle(handle)(x1, y1, x2, y2) # type: ignore + return ffi.from_handle(handle)(x1, y1, x2, y2) # type: ignore[no-any-return] -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _pycall_path_swap_src_dest(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 """A TDL function dest comes first to match up with a dest only call.""" - return ffi.from_handle(handle)(x2, y2, x1, y1) # type: ignore + return ffi.from_handle(handle)(x2, y2, x1, y1) # type: ignore[no-any-return] -@ffi.def_extern() # type: ignore -def _pycall_path_dest_only(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 +@ffi.def_extern() # type: ignore[misc] +def _pycall_path_dest_only(_x1: int, _y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401 """A TDL function which samples the dest coordinate only.""" - return ffi.from_handle(handle)(x2, y2) # type: ignore + return ffi.from_handle(handle)(x2, y2) # type: ignore[no-any-return] def _get_path_cost_func( @@ -63,8 +64,8 @@ def _get_path_cost_func( ) -> Callable[[int, int, int, int, Any], float]: """Return a properly cast PathCostArray callback.""" if not ffi: - return lambda x1, y1, x2, y2, _: 0 - return ffi.cast("TCOD_path_func_t", ffi.addressof(lib, name)) # type: ignore + return lambda _x1, _y1, _x2, _y2, _: 0 + return ffi.cast("TCOD_path_func_t", ffi.addressof(lib, name)) # type: ignore[no-any-return] class _EdgeCostFunc: @@ -113,7 +114,7 @@ def __init__( super().__init__(callback, shape) -class NodeCostArray(np.ndarray): # type: ignore +class NodeCostArray(np.ndarray): # type: ignore[type-arg] """Calculate cost from a numpy array of nodes. `array` is a NumPy array holding the path-cost of each node. @@ -157,13 +158,13 @@ def get_tcod_path_ffi(self) -> tuple[Any, Any, tuple[int, int]]: class _PathFinder: """A class sharing methods used by AStar and Dijkstra.""" - def __init__(self, cost: Any, diagonal: float = 1.41) -> None: + def __init__(self, cost: tcod.map.Map | ArrayLike | _EdgeCostFunc, diagonal: float = 1.41) -> None: self.cost = cost self.diagonal = diagonal self._path_c: Any = None self._callback = self._userdata = None - if hasattr(self.cost, "map_c"): + if isinstance(self.cost, tcod.map.Map): self.shape = self.cost.width, self.cost.height self._path_c = ffi.gc( self._path_new_using_map(self.cost.map_c, diagonal), @@ -171,7 +172,7 @@ def __init__(self, cost: Any, diagonal: float = 1.41) -> None: ) return - if not hasattr(self.cost, "get_tcod_path_ffi"): + if not isinstance(self.cost, _EdgeCostFunc): assert not callable(self.cost), ( "Any callback alone is missing shape information. Wrap your callback in tcod.path.EdgeCostCallback" ) @@ -206,7 +207,7 @@ def __getstate__(self) -> dict[str, Any]: def __setstate__(self, state: dict[str, Any]) -> None: self.__dict__.update(state) - self.__init__(self.cost, self.diagonal) # type: ignore + _PathFinder.__init__(self, self.cost, self.diagonal) _path_new_using_map = lib.TCOD_path_new_using_map _path_new_using_function = lib.TCOD_path_new_using_function @@ -239,7 +240,7 @@ def get_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> list path = [] x = ffi.new("int[2]") y = x + 1 - while lib.TCOD_path_walk(self._path_c, x, y, False): + while lib.TCOD_path_walk(self._path_c, x, y, False): # noqa: FBT003 path.append((x[0], y[0])) return path @@ -323,7 +324,7 @@ def _export_dict(array: NDArray[Any]) -> dict[str, Any]: } -def _export(array: NDArray[Any]) -> Any: # noqa: ANN401 +def _export(array: NDArray[np.number]) -> Any: # noqa: ANN401 """Convert a NumPy array into a cffi object.""" return ffi.new("struct NArray*", _export_dict(array)) @@ -332,7 +333,8 @@ def _compile_cost_edges(edge_map: ArrayLike) -> tuple[NDArray[np.intc], int]: """Return an edge_cost array using an integer map.""" edge_map = np.array(edge_map, copy=True) if edge_map.ndim != 2: # noqa: PLR2004 - raise ValueError("edge_map must be 2 dimensional. (Got %i)" % edge_map.ndim) + msg = f"edge_map must be 2 dimensional. (Got {edge_map.ndim})" + raise ValueError(msg) edge_center = edge_map.shape[0] // 2, edge_map.shape[1] // 2 edge_map[edge_center] = 0 edge_map[edge_map < 0] = 0 @@ -353,7 +355,7 @@ def dijkstra2d( # noqa: PLR0913 diagonal: int | None = None, *, edge_map: ArrayLike | None = None, - out: np.ndarray | None = ..., # type: ignore + out: NDArray[np.number] | None = ..., # type: ignore[assignment, unused-ignore] ) -> NDArray[Any]: """Return the computed distance of all nodes on a 2D Dijkstra grid. @@ -750,7 +752,8 @@ def add_edge( edge_dir = tuple(edge_dir) cost = np.asarray(cost) if len(edge_dir) != self._ndim: - raise TypeError("edge_dir must have exactly %i items, got %r" % (self._ndim, edge_dir)) + msg = f"edge_dir must have exactly {self._ndim} items, got {edge_dir!r}" + raise TypeError(msg) if edge_cost <= 0: msg = f"edge_cost must be greater than zero, got {edge_cost!r}" raise ValueError(msg) @@ -884,7 +887,8 @@ def add_edges( if edge_map.ndim < self._ndim: edge_map = np.asarray(edge_map[(np.newaxis,) * (self._ndim - edge_map.ndim)]) if edge_map.ndim != self._ndim: - raise TypeError("edge_map must must match graph dimensions (%i). (Got %i)" % (self.ndim, edge_map.ndim)) + msg = f"edge_map must must match graph dimensions ({self.ndim}). (Got {edge_map.ndim})" + raise TypeError(msg) if self._order == "F": # edge_map needs to be converted into C. # The other parameters are converted by the add_edge method. @@ -1186,7 +1190,8 @@ def add_root(self, index: tuple[int, ...], value: int = 0) -> None: if self._order == "F": # Convert to ij indexing order. index = index[::-1] if len(index) != self._distance.ndim: - raise TypeError("Index must be %i items, got %r" % (self._distance.ndim, index)) + msg = f"Index must be {self._distance.ndim} items, got {index!r}" + raise TypeError(msg) self._distance[index] = value self._update_heuristic(None) lib.TCOD_frontier_push(self._frontier_p, index, value, value) @@ -1273,7 +1278,8 @@ def resolve(self, goal: tuple[int, ...] | None = None) -> None: if goal is not None: goal = tuple(goal) # Check for bad input. if len(goal) != self._distance.ndim: - raise TypeError("Goal must be %i items, got %r" % (self._distance.ndim, goal)) + msg = f"Goal must be {self._distance.ndim} items, got {goal!r}" + raise TypeError(msg) if self._order == "F": # Goal is now ij indexed for the rest of this function. goal = goal[::-1] @@ -1320,7 +1326,8 @@ def path_from(self, index: tuple[int, ...]) -> NDArray[Any]: """ index = tuple(index) # Check for bad input. if len(index) != self._graph._ndim: - raise TypeError("Index must be %i items, got %r" % (self._distance.ndim, index)) + msg = f"Index must be {self._distance.ndim} items, got {index!r}" + raise TypeError(msg) self.resolve(index) if self._order == "F": # Convert to ij indexing order. index = index[::-1] diff --git a/tcod/sdl/_internal.py b/tcod/sdl/_internal.py index 8904e318..e7f5aa8c 100644 --- a/tcod/sdl/_internal.py +++ b/tcod/sdl/_internal.py @@ -64,7 +64,7 @@ def __exit__( return True -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _sdl_log_output_function(_userdata: None, category: int, priority: int, message_p: Any) -> None: # noqa: ANN401 """Pass logs sent by SDL to Python's logging system.""" message = str(ffi.string(message_p), encoding="utf-8") @@ -118,7 +118,7 @@ def replacement(*_args: object, **_kwargs: object) -> NoReturn: msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" raise RuntimeError(msg) - return lambda x: replacement # type: ignore[return-value] + return lambda _: replacement # type: ignore[return-value] lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index a5da2f5b..8627bdeb 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -564,7 +564,7 @@ class _AudioCallbackUserdata: device: AudioDevice -@ffi.def_extern() # type: ignore +@ffi.def_extern() # type: ignore[misc] def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None: # noqa: ANN401 """Handle audio device callbacks.""" data: _AudioCallbackUserdata = ffi.from_handle(userdata) diff --git a/tcod/tcod.py b/tcod/tcod.py index cf12dceb..39240194 100644 --- a/tcod/tcod.py +++ b/tcod/tcod.py @@ -22,7 +22,7 @@ image, libtcodpy, los, - map, + map, # noqa: A004 noise, path, random, diff --git a/tcod/tileset.py b/tcod/tileset.py index 5f5a7511..56213fe7 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -229,10 +229,11 @@ def remap(self, codepoint: int, x: int, y: int = 0) -> None: """ tile_i = x + y * self._tileset_p.virtual_columns if not (0 <= tile_i < self._tileset_p.tiles_count): - raise IndexError( - "Tile %i is non-existent and can't be assigned." - " (Tileset has %i tiles.)" % (tile_i, self._tileset_p.tiles_count) + msg = ( + f"Tile {tile_i} is non-existent and can't be assigned." + f" (Tileset has {self._tileset_p.tiles_count} tiles.)" ) + raise IndexError(msg) _check( lib.TCOD_tileset_assign_tile( self._tileset_p, From b4545249dfa1948b52b2c751d61eb7d179041f5b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 6 Apr 2025 18:33:58 -0700 Subject: [PATCH 124/134] Deprecate EventDispatch class This class has faded into irrelevance due to modern Python language features. I need to discourage its usage in new programs. --- CHANGELOG.md | 3 +++ tcod/event.py | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6390394..3678f4b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - `Console.print_box` has been replaced by `Console.print`. - `Console.draw_frame`: deprecated `clear`, `fg`, `bg`, and `bg_blend` being given as positional arguments. - `Console.draw_rect`: deprecated `fg`, `bg`, and `bg_blend` being given as positional arguments. +- The `EventDispatch` class is now deprecated. + This class was made before Python supported protocols and structural pattern matching, + now the class serves little purpose and its usage can create a minor technical burden. ## [17.1.0] - 2025-03-29 diff --git a/tcod/event.py b/tcod/event.py index 85bbead8..69bc8575 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -87,7 +87,7 @@ from typing import TYPE_CHECKING, Any, Callable, Final, Generic, Iterator, Mapping, NamedTuple, TypeVar import numpy as np -from typing_extensions import Literal +from typing_extensions import Literal, deprecated import tcod.event import tcod.event_constants @@ -1238,6 +1238,10 @@ def wait(timeout: float | None = None) -> Iterator[Any]: return get() +@deprecated( + "Event dispatch should be handled via a single custom method in a Protocol instead of this class.", + category=DeprecationWarning, +) class EventDispatch(Generic[T]): '''Dispatches events to methods depending on the events type attribute. @@ -1248,6 +1252,10 @@ class EventDispatch(Generic[T]): This is now a generic class. The type hints at the return value of :any:`dispatch` and the `ev_*` methods. + .. deprecated:: Unreleased + Event dispatch should be handled via a single custom method in a Protocol instead of this class. + Note that events can and should be handled using Python's `match` statement. + Example:: import tcod From c176a6f4d0f0e20d36b0b029c8224d30ef8b8c5d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 7 Apr 2025 13:34:59 -0700 Subject: [PATCH 125/134] Update pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8960f446..8f044c36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.2 + rev: v0.11.4 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From de5d8456a05c49a2d78450611661329aa47d90af Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 7 Apr 2025 22:51:32 -0700 Subject: [PATCH 126/134] Prepare 18.0.0 release. --- CHANGELOG.md | 2 ++ tcod/console.py | 6 +++--- tcod/event.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3678f4b8..3dba327d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +## [18.0.0] - 2025-04-08 + ### Changed - `Console.print` now accepts `height` and `width` keywords and has renamed `string` to `text`. diff --git a/tcod/console.py b/tcod/console.py index b1c7c24e..bec669a3 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -1090,7 +1090,7 @@ def print( # noqa: PLR0913 `x` and `y` are now always used as an absolute position for negative values. - .. versionchanged:: Unreleased + .. versionchanged:: 18.0 Deprecated giving `string`, `fg`, `bg`, and `bg_blend` as positional arguments. @@ -1285,7 +1285,7 @@ def draw_frame( # noqa: PLR0913 .. versionchanged:: 13.0 `x` and `y` are now always used as an absolute position for negative values. - .. versionchanged:: Unreleased + .. versionchanged:: 18.0 Deprecated `clear`, `fg`, `bg`, and `bg_blend` being given as positional arguments. These should be keyword arguments only. @@ -1418,7 +1418,7 @@ def draw_rect( # noqa: PLR0913 .. versionchanged:: 13.0 `x` and `y` are now always used as an absolute position for negative values. - .. versionchanged:: Unreleased + .. versionchanged:: 18.0 Deprecated `ch`, `fg`, `bg`, and `bg_blend` being given as positional arguments. These should be keyword arguments only. """ diff --git a/tcod/event.py b/tcod/event.py index 69bc8575..e8220285 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1252,7 +1252,7 @@ class EventDispatch(Generic[T]): This is now a generic class. The type hints at the return value of :any:`dispatch` and the `ev_*` methods. - .. deprecated:: Unreleased + .. deprecated:: 18.0 Event dispatch should be handled via a single custom method in a Protocol instead of this class. Note that events can and should be handled using Python's `match` statement. From f4c1d77b7357463551ca5abf92cbddcab2fa0e80 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 12 Apr 2025 01:51:46 -0700 Subject: [PATCH 127/134] Fix minor typo in deprecation string --- tcod/console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/console.py b/tcod/console.py index bec669a3..7e46ed7d 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -1368,7 +1368,7 @@ def draw_rect( ) -> None: ... @overload - @deprecated("Parameters cg, fg, bg, bg_blend should be keyword arguments") + @deprecated("Parameters ch, fg, bg, bg_blend should be keyword arguments") def draw_rect( self, x: int, From cae1dc8c0a04042bf60668c3ce723749806107bb Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 20 Apr 2025 21:58:50 -0700 Subject: [PATCH 128/134] Add more realistic Console.rgb example --- tcod/console.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tcod/console.py b/tcod/console.py index 7e46ed7d..e84ba372 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -335,6 +335,33 @@ def rgb(self) -> NDArray[Any]: The :any:`rgb_graphic` dtype can be used to make arrays compatible with this attribute that are independent of a :any:`Console`. + Example: + >>> tile_graphics = np.array( # Tile graphics lookup table + ... [ # (Unicode, foreground, background) + ... (ord("."), (255, 255, 255), (0, 0, 0)), # Tile 0 + ... (ord("#"), (255, 255, 255), (0, 0, 0)), # Tile 1 + ... (ord("^"), (255, 255, 255), (0, 0, 0)), # Tile 2 + ... (ord("~"), (255, 255, 255), (0, 0, 0)), # Tile 3 + ... ], + ... dtype=tcod.console.rgb_graphic, + ... ) + >>> console = tcod.console.Console(6, 5) + >>> console.rgb[:] = tile_graphics[ # Convert 2D array of indexes to tile graphics + ... [ + ... [1, 1, 1, 1, 1, 1], + ... [1, 0, 2, 0, 0, 1], + ... [1, 0, 0, 3, 3, 1], + ... [1, 0, 0, 3, 3, 1], + ... [1, 1, 1, 1, 1, 1], + ... ], + ... ] + >>> print(console) + <###### + #.^..# + #..~~# + #..~~# + ######> + Example: >>> con = tcod.console.Console(10, 2) >>> BLUE, YELLOW, BLACK = (0, 0, 255), (255, 255, 0), (0, 0, 0) From 76f8731be4e29164daa04e58ecaa20415432dade Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 23 Apr 2025 04:05:47 -0700 Subject: [PATCH 129/134] Fix edge case in tcod.noise.grid Number check needed to be more broad --- CHANGELOG.md | 4 ++++ tcod/noise.py | 4 ++-- tests/test_noise.py | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dba327d..91ea30a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +### Fixed + +- `tcod.noise.grid` would raise `TypeError` when given a plain integer for scale. + ## [18.0.0] - 2025-04-08 ### Changed diff --git a/tcod/noise.py b/tcod/noise.py index da14b3f2..808ac818 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -411,7 +411,7 @@ def grid( scale: tuple[float, ...] | float, origin: tuple[int, ...] | None = None, indexing: Literal["ij", "xy"] = "xy", -) -> tuple[NDArray[Any], ...]: +) -> tuple[NDArray[np.number], ...]: """Generate a mesh-grid of sample points to use with noise sampling. Args: @@ -449,7 +449,7 @@ def grid( .. versionadded:: 12.2 """ - if isinstance(scale, float): + if isinstance(scale, (int, float)): scale = (scale,) * len(shape) if origin is None: origin = (0,) * len(shape) diff --git a/tests/test_noise.py b/tests/test_noise.py index 72149062..80023f5f 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -7,6 +7,7 @@ import pytest import tcod.noise +import tcod.random # ruff: noqa: D103 @@ -100,3 +101,7 @@ def test_noise_copy() -> None: assert repr(noise3) == repr(pickle.loads(pickle.dumps(noise3))) noise4 = tcod.noise.Noise(2, seed=42) assert repr(noise4) == repr(pickle.loads(pickle.dumps(noise4))) + + +def test_noise_grid() -> None: + tcod.noise.grid((2, 2), scale=2) # Check int scale From ccdd225eeadeec3ef0c7e2f99bbfd775ef949f1e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 4 May 2025 05:30:47 -0700 Subject: [PATCH 130/134] Add tcod.path.path2d function The library really needed a simple A to B pathfinding function --- CHANGELOG.md | 4 ++ tcod/path.py | 157 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 154 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91ea30a9..6e589d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +### Added + +- `tcod.path.path2d` computes a path for the most basic cases. + ### Fixed - `tcod.noise.grid` would raise `TypeError` when given a plain integer for scale. diff --git a/tcod/path.py b/tcod/path.py index a58cb218..603a0d3e 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -21,7 +21,7 @@ import functools import itertools import warnings -from typing import TYPE_CHECKING, Any, Callable, Final +from typing import TYPE_CHECKING, Any, Callable, Final, Sequence import numpy as np from typing_extensions import Literal, Self @@ -324,7 +324,7 @@ def _export_dict(array: NDArray[Any]) -> dict[str, Any]: } -def _export(array: NDArray[np.number]) -> Any: # noqa: ANN401 +def _export(array: NDArray[np.integer]) -> Any: # noqa: ANN401 """Convert a NumPy array into a cffi object.""" return ffi.new("struct NArray*", _export_dict(array)) @@ -355,8 +355,8 @@ def dijkstra2d( # noqa: PLR0913 diagonal: int | None = None, *, edge_map: ArrayLike | None = None, - out: NDArray[np.number] | None = ..., # type: ignore[assignment, unused-ignore] -) -> NDArray[Any]: + out: NDArray[np.integer] | None = ..., # type: ignore[assignment, unused-ignore] +) -> NDArray[np.integer]: """Return the computed distance of all nodes on a 2D Dijkstra grid. `distance` is an input array of node distances. Is this often an @@ -528,7 +528,7 @@ def hillclimb2d( diagonal: bool | None = None, *, edge_map: ArrayLike | None = None, -) -> NDArray[Any]: +) -> NDArray[np.intc]: """Return a path on a grid from `start` to the lowest point. `distance` should be a fully computed distance array. This kind of array @@ -1289,7 +1289,7 @@ def resolve(self, goal: tuple[int, ...] | None = None) -> None: self._update_heuristic(goal) self._graph._resolve(self) - def path_from(self, index: tuple[int, ...]) -> NDArray[Any]: + def path_from(self, index: tuple[int, ...]) -> NDArray[np.intc]: """Return the shortest path from `index` to the nearest root. The returned array is of shape `(length, ndim)` where `length` is the @@ -1343,7 +1343,7 @@ def path_from(self, index: tuple[int, ...]) -> NDArray[Any]: ) return path[:, ::-1] if self._order == "F" else path - def path_to(self, index: tuple[int, ...]) -> NDArray[Any]: + def path_to(self, index: tuple[int, ...]) -> NDArray[np.intc]: """Return the shortest path from the nearest root to `index`. See :any:`path_from`. @@ -1370,3 +1370,146 @@ def path_to(self, index: tuple[int, ...]) -> NDArray[Any]: [] """ return self.path_from(index)[::-1] + + +def path2d( # noqa: C901, PLR0912, PLR0913 + cost: ArrayLike, + *, + start_points: Sequence[tuple[int, int]], + end_points: Sequence[tuple[int, int]], + cardinal: int, + diagonal: int | None = None, + check_bounds: bool = True, +) -> NDArray[np.intc]: + """Return a path between `start_points` and `end_points`. + + If `start_points` or `end_points` has only one item then this is equivalent to A*. + Otherwise it is equivalent to Dijkstra. + + If multiple `start_points` or `end_points` are given then the single shortest path between them is returned. + + Points placed on nodes with a cost of 0 are treated as always reachable from adjacent nodes. + + Args: + cost: A 2D array of integers with the cost of each node. + start_points: A sequence of one or more starting points indexing `cost`. + end_points: A sequence of one or more ending points indexing `cost`. + cardinal: The relative cost to move a cardinal direction. + diagonal: The relative cost to move a diagonal direction. + `None` or `0` will disable diagonal movement. + check_bounds: If `False` then out-of-bounds points are silently ignored. + If `True` (default) then out-of-bounds points raise :any:`IndexError`. + + Returns: + A `(length, 2)` array of indexes of the path including the start and end points. + If there is no path then an array with zero items will be returned. + + Example:: + + # Note: coordinates in this example are (i, j), or (y, x) + >>> cost = np.array([ + ... [1, 0, 1, 1, 1, 0, 1], + ... [1, 0, 1, 1, 1, 0, 1], + ... [1, 0, 1, 0, 1, 0, 1], + ... [1, 1, 1, 1, 1, 0, 1], + ... ]) + + # Endpoints are reachable even when endpoints are on blocked nodes + >>> tcod.path.path2d(cost, start_points=[(0, 0)], end_points=[(2, 3)], cardinal=70, diagonal=99) + array([[0, 0], + [1, 0], + [2, 0], + [3, 1], + [2, 2], + [2, 3]], dtype=int...) + + # Unreachable endpoints return a zero length array + >>> tcod.path.path2d(cost, start_points=[(0, 0)], end_points=[(3, 6)], cardinal=70, diagonal=99) + array([], shape=(0, 2), dtype=int...) + >>> tcod.path.path2d(cost, start_points=[(0, 0), (3, 0)], end_points=[(0, 6), (3, 6)], cardinal=70, diagonal=99) + array([], shape=(0, 2), dtype=int...) + >>> tcod.path.path2d(cost, start_points=[], end_points=[], cardinal=70, diagonal=99) + array([], shape=(0, 2), dtype=int...) + + # Overlapping endpoints return a single step + >>> tcod.path.path2d(cost, start_points=[(0, 0)], end_points=[(0, 0)], cardinal=70, diagonal=99) + array([[0, 0]], dtype=int32) + + # Multiple endpoints return the shortest path + >>> tcod.path.path2d( + ... cost, start_points=[(0, 0)], end_points=[(1, 3), (3, 3), (2, 2), (2, 4)], cardinal=70, diagonal=99) + array([[0, 0], + [1, 0], + [2, 0], + [3, 1], + [2, 2]], dtype=int...) + >>> tcod.path.path2d( + ... cost, start_points=[(0, 0), (0, 2)], end_points=[(1, 3), (3, 3), (2, 2), (2, 4)], cardinal=70, diagonal=99) + array([[0, 2], + [1, 3]], dtype=int...) + >>> tcod.path.path2d(cost, start_points=[(0, 0), (0, 2)], end_points=[(3, 2)], cardinal=1) + array([[0, 2], + [1, 2], + [2, 2], + [3, 2]], dtype=int...) + + # Checking for out-of-bounds points may be toggled + >>> tcod.path.path2d(cost, start_points=[(0, 0)], end_points=[(-1, -1), (3, 1)], cardinal=1) + Traceback (most recent call last): + ... + IndexError: End point (-1, -1) is out-of-bounds of cost shape (4, 7) + >>> tcod.path.path2d(cost, start_points=[(0, 0)], end_points=[(-1, -1), (3, 1)], cardinal=1, check_bounds=False) + array([[0, 0], + [1, 0], + [2, 0], + [3, 0], + [3, 1]], dtype=int...) + + .. versionadded:: Unreleased + """ + cost = np.copy(cost) # Copy array to later modify nodes to be always reachable + + # Check bounds of endpoints + if check_bounds: + for points, name in [(start_points, "start"), (end_points, "end")]: + for i, j in points: + if not (0 <= i < cost.shape[0] and 0 <= j < cost.shape[1]): + msg = f"{name.capitalize()} point {(i, j)!r} is out-of-bounds of cost shape {cost.shape!r}" + raise IndexError(msg) + else: + start_points = [(i, j) for i, j in start_points if 0 <= i < cost.shape[0] and 0 <= j < cost.shape[1]] + end_points = [(i, j) for i, j in end_points if 0 <= i < cost.shape[0] and 0 <= j < cost.shape[1]] + + if not start_points or not end_points: + return np.zeros((0, 2), dtype=np.intc) # Missing endpoints + + # Check if endpoints can be manipulated to use A* for a one-to-many computation + reversed_path = False + if len(end_points) == 1 and len(start_points) > 1: + # Swap endpoints to ensure single start point as the A* goal + reversed_path = True + start_points, end_points = end_points, start_points + + for ij in start_points: + cost[ij] = 1 # Enforce reachability of endpoint + + graph = SimpleGraph(cost=cost, cardinal=cardinal, diagonal=diagonal or 0) + pf = Pathfinder(graph) + for ij in end_points: + pf.add_root(ij) + + if len(start_points) == 1: # Compute A* from possibly multiple roots to one goal + out = pf.path_from(start_points[0]) + if pf.distance[start_points[0]] == np.iinfo(pf.distance.dtype).max: + return np.zeros((0, 2), dtype=np.intc) # Unreachable endpoint + if reversed_path: + out = out[::-1] + return out + + # Crude Dijkstra implementation until issues with Pathfinder are fixed + pf.resolve(None) + best_distance, best_ij = min((pf.distance[ij], ij) for ij in start_points) + if best_distance == np.iinfo(pf.distance.dtype).max: + return np.zeros((0, 2), dtype=np.intc) # All endpoints unreachable + + return hillclimb2d(pf.distance, best_ij, cardinal=bool(cardinal), diagonal=bool(diagonal)) From f553f45e8153572822569eac1e55e1d86e6a1cec Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 4 May 2025 17:59:00 -0700 Subject: [PATCH 131/134] Prepare 18.1.0 release. --- CHANGELOG.md | 4 +++- tcod/path.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e589d9b..5ab3b06b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +## [18.1.0] - 2025-05-05 + ### Added -- `tcod.path.path2d` computes a path for the most basic cases. +- `tcod.path.path2d` to compute paths for the most basic cases. ### Fixed diff --git a/tcod/path.py b/tcod/path.py index 603a0d3e..9ab634f3 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -1465,7 +1465,7 @@ def path2d( # noqa: C901, PLR0912, PLR0913 [3, 0], [3, 1]], dtype=int...) - .. versionadded:: Unreleased + .. versionadded:: 18.1 """ cost = np.copy(cost) # Copy array to later modify nodes to be always reachable From 4b5e044214ba3897a3056fa4edb7ada7504bfa36 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 6 May 2025 01:24:30 -0700 Subject: [PATCH 132/134] Fix incorrect imageio calls in examples --- .vscode/settings.json | 1 + tcod/tileset.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a6f96553..78327ad2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -189,6 +189,7 @@ "iinfo", "IJKL", "imageio", + "imread", "INCOL", "INROW", "interactable", diff --git a/tcod/tileset.py b/tcod/tileset.py index 56213fe7..f2a5ac33 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -126,20 +126,20 @@ def set_tile(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8]) -> None: # Normal usage when a tile already has its own alpha channel. # The loaded tile must be the correct shape for the tileset you assign it to. # The tile is assigned to a private use area and will not conflict with any exiting codepoint. - tileset.set_tile(0x100000, imageio.load("rgba_tile.png")) + tileset.set_tile(0x100000, imageio.imread("rgba_tile.png")) # Load a greyscale tile. - tileset.set_tile(0x100001, imageio.load("greyscale_tile.png"), pilmode="L") + tileset.set_tile(0x100001, imageio.imread("greyscale_tile.png"), mode="L") # If you are stuck with an RGB array then you can use the red channel as the input: `rgb[:, :, 0]` # Loads an RGB sprite without a background. - tileset.set_tile(0x100002, imageio.load("rgb_no_background.png", pilmode="RGBA")) + tileset.set_tile(0x100002, imageio.imread("rgb_no_background.png", mode="RGBA")) # If you're stuck with an RGB array then you can pad the channel axis with an alpha of 255: # rgba = np.pad(rgb, pad_width=((0, 0), (0, 0), (0, 1)), constant_values=255) # Loads an RGB sprite with a key color background. KEY_COLOR = np.asarray((255, 0, 255), dtype=np.uint8) - sprite_rgb = imageio.load("rgb_tile.png") + sprite_rgb = imageio.imread("rgb_tile.png") # Compare the RGB colors to KEY_COLOR, compress full matches to a 2D mask. sprite_mask = (sprite_rgb != KEY_COLOR).all(axis=2) # Generate the alpha array, with 255 as the foreground and 0 as the background. From 484b6a250aaeb17f7b1dc4268a8e4ee4c560a107 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 6 May 2025 01:25:54 -0700 Subject: [PATCH 133/134] Pre-commit update --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f044c36..00cc1d41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.4 + rev: v0.11.8 hooks: - id: ruff args: [--fix-only, --exit-non-zero-on-fix] From 01ddb08ada310b0bbc3b65db5ac29ced5e6c3dc3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 24 May 2025 15:10:04 -0700 Subject: [PATCH 134/134] Fix samples script Properly clear the renderer per frame. Use explicit namespace imports for tcod Ignore spelling of Numpy parameters Refactor redundant quit events --- .vscode/settings.json | 2 ++ examples/samples_tcod.py | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 78327ad2..e3434adc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -79,6 +79,7 @@ "CFLAGS", "CHARMAP", "Chebyshev", + "choicelist", "cibuildwheel", "CIBW", "CLEARAGAIN", @@ -92,6 +93,7 @@ "Coef", "COLCTRL", "COMPILEDVERSION", + "condlist", "consolas", "contextdata", "CONTROLLERAXISMOTION", diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 672ce4f6..593ef002 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -18,13 +18,20 @@ import numpy as np +import tcod.bsp import tcod.cffi +import tcod.console import tcod.context import tcod.event +import tcod.image +import tcod.los +import tcod.map import tcod.noise +import tcod.path import tcod.render import tcod.sdl.mouse import tcod.sdl.render +import tcod.tileset from tcod import libtcodpy from tcod.sdl.video import WindowFlags @@ -103,15 +110,10 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: else: libtcodpy.sys_save_screenshot() print("png") - elif event.sym == tcod.event.KeySym.ESCAPE: - raise SystemExit elif event.sym in RENDERER_KEYS: # Swap the active context for one with a different renderer. init_context(RENDERER_KEYS[event.sym]) - def ev_quit(self, event: tcod.event.Quit) -> None: - raise SystemExit - class TrueColorSample(Sample): def __init__(self) -> None: @@ -1364,6 +1366,9 @@ def main() -> None: sample_console.blit(root_console, SAMPLE_SCREEN_X, SAMPLE_SCREEN_Y) draw_stats() if context.sdl_renderer: + # Clear the screen to ensure no garbage data outside of the logical area is displayed + context.sdl_renderer.draw_color = (0, 0, 0, 255) + context.sdl_renderer.clear() # SDL renderer support, upload the sample console background to a minimap texture. sample_minimap.update(sample_console.rgb.T["bg"]) # Render the root_console normally, this is the drawing step of context.present without presenting. @@ -1419,6 +1424,8 @@ def handle_events() -> None: SAMPLES[cur_sample].dispatch(event) if isinstance(event, tcod.event.Quit): raise SystemExit + if isinstance(event, tcod.event.KeyDown) and event.sym == tcod.event.KeySym.ESCAPE: + raise SystemExit def draw_samples_menu() -> None: