Skip to content

Commit 2eb854d

Browse files
committed
Use the C locale when passing file paths to libtcod
Fixes encoding errors which prevented existing files from loading correctly.
1 parent 688fc66 commit 2eb854d

File tree

6 files changed

+44
-28
lines changed

6 files changed

+44
-28
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here.
44
This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`.
55

66
## [Unreleased]
7+
### Fixed
8+
- Fixed errors loading files where their paths are non-ASCII and the C locale is not UTF-8.
79

810
## [16.2.0] - 2023-09-20
911
### Changed

tcod/_internal.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
from __future__ import annotations
33

44
import functools
5+
import locale
6+
import sys
57
import warnings
8+
from pathlib import Path
69
from types import TracebackType
710
from typing import Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar, cast
811

@@ -126,6 +129,16 @@ def _fmt(string: str, stacklevel: int = 2) -> bytes:
126129
return string.encode("utf-8").replace(b"%", b"%%")
127130

128131

132+
def _path_encode(path: Path) -> bytes:
133+
"""Return a bytes file path for the current C locale."""
134+
try:
135+
return str(path).encode(locale.getlocale()[1] or "utf-8")
136+
except UnicodeEncodeError as exc:
137+
if sys.version_info >= (3, 11):
138+
exc.add_note("""Consider calling 'locale.setlocale(locale.LC_CTYPES, ".UTF8")' to support Unicode paths.""")
139+
raise
140+
141+
129142
class _PropagateException:
130143
"""Context manager designed to propagate exceptions outside of a cffi callback context.
131144

tcod/console.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import tcod._internal
1919
import tcod.constants
20-
from tcod._internal import _check, deprecate
20+
from tcod._internal import _check, _path_encode, deprecate
2121
from tcod.cffi import ffi, lib
2222

2323

@@ -1301,9 +1301,9 @@ def load_xp(path: str | PathLike[str], order: Literal["C", "F"] = "C") -> tuple[
13011301
console.rgba[is_transparent] = (ord(" "), (0,), (0,))
13021302
"""
13031303
path = Path(path).resolve(strict=True)
1304-
layers = _check(tcod.lib.TCOD_load_xp(bytes(path), 0, ffi.NULL))
1304+
layers = _check(tcod.lib.TCOD_load_xp(_path_encode(path), 0, ffi.NULL))
13051305
consoles = ffi.new("TCOD_Console*[]", layers)
1306-
_check(tcod.lib.TCOD_load_xp(bytes(path), layers, consoles))
1306+
_check(tcod.lib.TCOD_load_xp(_path_encode(path), layers, consoles))
13071307
return tuple(Console._from_cdata(console_p, order=order) for console_p in consoles)
13081308

13091309

@@ -1364,7 +1364,7 @@ def save_xp(
13641364
tcod.lib.TCOD_save_xp(
13651365
len(consoles_c),
13661366
consoles_c,
1367-
bytes(path),
1367+
_path_encode(path),
13681368
compress_level,
13691369
)
13701370
)

tcod/image.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from numpy.typing import ArrayLike, NDArray
1919

2020
import tcod.console
21-
from tcod._internal import _console, deprecate
21+
from tcod._internal import _console, _path_encode, deprecate
2222
from tcod.cffi import ffi, lib
2323

2424

@@ -72,7 +72,7 @@ def from_file(cls, path: str | PathLike[str]) -> Image:
7272
.. versionadded:: 16.0
7373
"""
7474
path = Path(path).resolve(strict=True)
75-
return cls._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(path)), lib.TCOD_image_delete))
75+
return cls._from_cdata(ffi.gc(lib.TCOD_image_load(_path_encode(path)), lib.TCOD_image_delete))
7676

7777
def clear(self, color: tuple[int, int, int]) -> None:
7878
"""Fill this entire Image with color.
@@ -306,7 +306,7 @@ def save_as(self, filename: str | PathLike[str]) -> None:
306306
.. versionchanged:: 16.0
307307
Added PathLike support.
308308
"""
309-
lib.TCOD_image_save(self.image_c, bytes(Path(filename)))
309+
lib.TCOD_image_save(self.image_c, _path_encode(Path(filename)))
310310

311311
@property
312312
def __array_interface__(self) -> dict[str, Any]:
@@ -364,7 +364,7 @@ def load(filename: str | PathLike[str]) -> NDArray[np.uint8]:
364364
365365
.. versionadded:: 11.4
366366
"""
367-
image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(Path(filename))), lib.TCOD_image_delete))
367+
image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(_path_encode(Path(filename))), lib.TCOD_image_delete))
368368
array: NDArray[np.uint8] = np.asarray(image, dtype=np.uint8)
369369
height, width, depth = array.shape
370370
if depth == 3:

tcod/libtcodpy.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
_console,
3232
_fmt,
3333
_int,
34+
_path_encode,
3435
_PropagateException,
3536
_unicode,
3637
_unpack_char_p,
@@ -991,7 +992,7 @@ def console_set_custom_font(
991992
Added PathLike support. `fontFile` no longer takes bytes.
992993
"""
993994
fontFile = Path(fontFile).resolve(strict=True)
994-
_check(lib.TCOD_console_set_custom_font(bytes(fontFile), flags, nb_char_horiz, nb_char_vertic))
995+
_check(lib.TCOD_console_set_custom_font(_path_encode(fontFile), flags, nb_char_horiz, nb_char_vertic))
995996

996997

997998
@deprecate("Check `con.width` instead.")
@@ -1806,7 +1807,7 @@ def console_from_file(filename: str | PathLike[str]) -> tcod.console.Console:
18061807
Added PathLike support.
18071808
"""
18081809
filename = Path(filename).resolve(strict=True)
1809-
return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(bytes(filename))))
1810+
return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(_path_encode(filename))))
18101811

18111812

18121813
@deprecate("Call the `Console.blit` method instead.")
@@ -1985,7 +1986,7 @@ def console_load_asc(con: tcod.console.Console, filename: str | PathLike[str]) -
19851986
Added PathLike support.
19861987
"""
19871988
filename = Path(filename).resolve(strict=True)
1988-
return bool(lib.TCOD_console_load_asc(_console(con), bytes(filename)))
1989+
return bool(lib.TCOD_console_load_asc(_console(con), _path_encode(filename)))
19891990

19901991

19911992
@deprecate("This format is not actively supported")
@@ -1998,7 +1999,7 @@ def console_save_asc(con: tcod.console.Console, filename: str | PathLike[str]) -
19981999
.. versionchanged:: 16.0
19992000
Added PathLike support.
20002001
"""
2001-
return bool(lib.TCOD_console_save_asc(_console(con), bytes(Path(filename))))
2002+
return bool(lib.TCOD_console_save_asc(_console(con), _path_encode(Path(filename))))
20022003

20032004

20042005
@deprecate("This format is not actively supported")
@@ -2012,7 +2013,7 @@ def console_load_apf(con: tcod.console.Console, filename: str | PathLike[str]) -
20122013
Added PathLike support.
20132014
"""
20142015
filename = Path(filename).resolve(strict=True)
2015-
return bool(lib.TCOD_console_load_apf(_console(con), bytes(filename)))
2016+
return bool(lib.TCOD_console_load_apf(_console(con), _path_encode(filename)))
20162017

20172018

20182019
@deprecate("This format is not actively supported")
@@ -2025,7 +2026,7 @@ def console_save_apf(con: tcod.console.Console, filename: str | PathLike[str]) -
20252026
.. versionchanged:: 16.0
20262027
Added PathLike support.
20272028
"""
2028-
return bool(lib.TCOD_console_save_apf(_console(con), bytes(Path(filename))))
2029+
return bool(lib.TCOD_console_save_apf(_console(con), _path_encode(Path(filename))))
20292030

20302031

20312032
@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]) ->
20402041
Added PathLike support.
20412042
"""
20422043
filename = Path(filename).resolve(strict=True)
2043-
return bool(lib.TCOD_console_load_xp(_console(con), bytes(filename)))
2044+
return bool(lib.TCOD_console_load_xp(_console(con), _path_encode(filename)))
20442045

20452046

20462047
@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
20502051
.. versionchanged:: 16.0
20512052
Added PathLike support.
20522053
"""
2053-
return bool(lib.TCOD_console_save_xp(_console(con), bytes(Path(filename)), compress_level))
2054+
return bool(lib.TCOD_console_save_xp(_console(con), _path_encode(Path(filename)), compress_level))
20542055

20552056

20562057
@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:
20612062
Added PathLike support.
20622063
"""
20632064
filename = Path(filename).resolve(strict=True)
2064-
return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(bytes(filename))))
2065+
return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(_path_encode(filename))))
20652066

20662067

20672068
@deprecate("Use tcod.console.load_xp to load this file.")
@@ -2074,7 +2075,7 @@ def console_list_load_xp(
20742075
Added PathLike support.
20752076
"""
20762077
filename = Path(filename).resolve(strict=True)
2077-
tcod_list = lib.TCOD_console_list_from_xp(bytes(filename))
2078+
tcod_list = lib.TCOD_console_list_from_xp(_path_encode(filename))
20782079
if tcod_list == ffi.NULL:
20792080
return None
20802081
try:
@@ -2102,7 +2103,7 @@ def console_list_save_xp(
21022103
try:
21032104
for console in console_list:
21042105
lib.TCOD_list_push(tcod_list, _console(console))
2105-
return bool(lib.TCOD_console_list_save_xp(tcod_list, bytes(Path(filename)), compress_level))
2106+
return bool(lib.TCOD_console_list_save_xp(tcod_list, _path_encode(Path(filename)), compress_level))
21062107
finally:
21072108
lib.TCOD_list_delete(tcod_list)
21082109

@@ -3436,7 +3437,7 @@ def mouse_get_status() -> Mouse:
34363437

34373438
@pending_deprecate()
34383439
def namegen_parse(filename: str | PathLike[str], random: tcod.random.Random | None = None) -> None:
3439-
lib.TCOD_namegen_parse(bytes(Path(filename)), random or ffi.NULL)
3440+
lib.TCOD_namegen_parse(_path_encode(Path(filename)), random or ffi.NULL)
34403441

34413442

34423443
@pending_deprecate()
@@ -3639,7 +3640,7 @@ def _pycall_parser_error(msg: Any) -> None:
36393640
def parser_run(parser: Any, filename: str | PathLike[str], listener: Any = None) -> None:
36403641
global _parser_listener
36413642
if not listener:
3642-
lib.TCOD_parser_run(parser, bytes(Path(filename)), ffi.NULL)
3643+
lib.TCOD_parser_run(parser, _path_encode(Path(filename)), ffi.NULL)
36433644
return
36443645

36453646
propagate_manager = _PropagateException()
@@ -3658,7 +3659,7 @@ def parser_run(parser: Any, filename: str | PathLike[str], listener: Any = None)
36583659
with _parser_callback_lock:
36593660
_parser_listener = listener
36603661
with propagate_manager:
3661-
lib.TCOD_parser_run(parser, bytes(Path(filename)), c_listener)
3662+
lib.TCOD_parser_run(parser, _path_encode(Path(filename)), c_listener)
36623663

36633664

36643665
@deprecate("libtcod objects are deleted automatically.")
@@ -4079,7 +4080,7 @@ def sys_save_screenshot(name: str | PathLike[str] | None = None) -> None:
40794080
.. versionchanged:: 16.0
40804081
Added PathLike support.
40814082
"""
4082-
lib.TCOD_sys_save_screenshot(bytes(Path(name)) if name is not None else ffi.NULL)
4083+
lib.TCOD_sys_save_screenshot(_path_encode(Path(name)) if name is not None else ffi.NULL)
40834084

40844085

40854086
# custom fullscreen resolution

tcod/tileset.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from numpy.typing import ArrayLike, NDArray
2222

2323
import tcod.console
24-
from tcod._internal import _check, _console, _raise_tcod_error, deprecate
24+
from tcod._internal import _check, _console, _path_encode, _raise_tcod_error, deprecate
2525
from tcod.cffi import ffi, lib
2626

2727

@@ -268,7 +268,7 @@ def load_truetype_font(path: str | PathLike[str], tile_width: int, tile_height:
268268
This function is provisional. The API may change.
269269
"""
270270
path = Path(path).resolve(strict=True)
271-
cdata = lib.TCOD_load_truetype_font_(bytes(path), tile_width, tile_height)
271+
cdata = lib.TCOD_load_truetype_font_(_path_encode(path), tile_width, tile_height)
272272
if not cdata:
273273
raise RuntimeError(ffi.string(lib.TCOD_get_error()))
274274
return Tileset._claim(cdata)
@@ -296,7 +296,7 @@ def set_truetype_font(path: str | PathLike[str], tile_width: int, tile_height: i
296296
Use :any:`load_truetype_font` instead.
297297
"""
298298
path = Path(path).resolve(strict=True)
299-
if lib.TCOD_tileset_load_truetype_(bytes(path), tile_width, tile_height):
299+
if lib.TCOD_tileset_load_truetype_(_path_encode(path), tile_width, tile_height):
300300
raise RuntimeError(ffi.string(lib.TCOD_get_error()))
301301

302302

@@ -314,7 +314,7 @@ def load_bdf(path: str | PathLike[str]) -> Tileset:
314314
.. versionadded:: 11.10
315315
"""
316316
path = Path(path).resolve(strict=True)
317-
cdata = lib.TCOD_load_bdf(bytes(path))
317+
cdata = lib.TCOD_load_bdf(_path_encode(path))
318318
if not cdata:
319319
raise RuntimeError(ffi.string(lib.TCOD_get_error()).decode())
320320
return Tileset._claim(cdata)
@@ -343,7 +343,7 @@ def load_tilesheet(path: str | PathLike[str], columns: int, rows: int, charmap:
343343
mapping = []
344344
if charmap is not None:
345345
mapping = list(itertools.islice(charmap, columns * rows))
346-
cdata = lib.TCOD_tileset_load(bytes(path), columns, rows, len(mapping), mapping)
346+
cdata = lib.TCOD_tileset_load(_path_encode(path), columns, rows, len(mapping), mapping)
347347
if not cdata:
348348
_raise_tcod_error()
349349
return Tileset._claim(cdata)

0 commit comments

Comments
 (0)