From 3a66ddf54f3de57ba67eba2a0cef4a0dc0f1e70d Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 17:45:52 +0200 Subject: [PATCH 01/15] chore: gitignore .python-version local pin --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e65f22ca..df4a0146 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ cover /stl/*.c uv.lock benchmarks/.cache/ +.python-version From 9af85a373f370773e937b80c046cdc2c523e91fa Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 17:49:22 +0200 Subject: [PATCH 02/15] chore: remove legacy MSVC build script for removed speedups --- build.cmd | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 build.cmd diff --git a/build.cmd b/build.cmd deleted file mode 100644 index 0e15329e..00000000 --- a/build.cmd +++ /dev/null @@ -1,21 +0,0 @@ -@echo off -:: To build extensions for 64 bit Python 3, we need to configure environment -:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 4 -:: -:: More details at: -:: https://github.com/cython/cython/wiki/CythonExtensionsOnWindows - -IF "%DISTUTILS_USE_SDK%"=="1" ( - ECHO Configuring environment to build with MSVC on a 64bit architecture - ECHO Using Windows SDK 7.1 - "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1 - CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release - SET MSSdk=1 - REM Need the following to allow tox to see the SDK compiler - SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB -) ELSE ( - ECHO Using default MSVC build environment -) - -CALL %* From 9b3858afb315c411696d403dddcf209c22d5476f Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 17:52:20 +0200 Subject: [PATCH 03/15] fix: resolve __version__ redeclaration without suppressions --- stl/__about__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stl/__about__.py b/stl/__about__.py index b05ae206..cf1dc28f 100644 --- a/stl/__about__.py +++ b/stl/__about__.py @@ -5,9 +5,11 @@ ) try: - __version__: typing.Final[str] = _version('numpy-stl') + _found_version: str = _version('numpy-stl') except PackageNotFoundError: - __version__: typing.Final[str] = '0.0.0' # type: ignore[misc] + _found_version = '0.0.0' + +__version__: typing.Final[str] = _found_version __package_name__: typing.Final[str] = 'numpy-stl' __import_name__: typing.Final[str] = 'stl' From 626c0524af90fbddfbeea96b52d1776c430e2c17 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 17:57:06 +0200 Subject: [PATCH 04/15] fix: replace malformed ndarray type-ignores with typing.cast --- stl/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stl/base.py b/stl/base.py index af1d46e8..97f97fed 100644 --- a/stl/base.py +++ b/stl/base.py @@ -301,7 +301,7 @@ def __init__( def attr(self) -> _u16_2d: """Per-triangle attribute field (uint16), shape (N, 1).""" # https://github.com/numpy/numpy/pull/30261 - return self.data['attr'] # type: ignore[return-value, ty:invalid-return-type] + return cast('_u16_2d', self.data['attr']) @attr.setter def attr(self, value: '_ArrayLikeInt_co', /) -> None: @@ -325,7 +325,7 @@ def normals(self) -> _f32_2d: [0.0, 0.0, 1.0] """ # https://github.com/numpy/numpy/pull/30261 - return self.data['normals'] # type: ignore[return-value, ty:invalid-return-type] + return cast('_f32_2d', self.data['normals']) @normals.setter def normals(self, value: '_ArrayLikeFloat_co', /) -> None: @@ -335,7 +335,7 @@ def normals(self, value: '_ArrayLikeFloat_co', /) -> None: def vectors(self) -> _f32_3d: """Triangle vertices as (N, 3, 3) array.""" # https://github.com/numpy/numpy/pull/30261 - return self.data['vectors'] # type: ignore[return-value, ty:invalid-return-type] + return cast('_f32_3d', self.data['vectors']) @vectors.setter def vectors(self, value: '_ArrayLikeFloat_co', /) -> None: @@ -474,7 +474,7 @@ def remove_empty_areas(data: _data_1d) -> _data_1d: triangles removed. """ # https://github.com/numpy/numpy/pull/30261 - vectors: _f32_3d = data['vectors'] # type: ignore[assignment, ty:invalid-assignment] + vectors: _f32_3d = cast('_f32_3d', data['vectors']) v0 = vectors[:, 0] v1 = vectors[:, 1] v2 = vectors[:, 2] From e74138767f0917bb898d3a5bda5f2c57a59a44b0 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 18:04:32 +0200 Subject: [PATCH 05/15] fix: compute logger name without name-mangled private access --- stl/base.py | 5 ++--- tests/test_mesh.py | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/stl/base.py b/stl/base.py index 97f97fed..9410bbf5 100644 --- a/stl/base.py +++ b/stl/base.py @@ -143,9 +143,8 @@ def logged(class_: type[_LoggedT]) -> type[_LoggedT]: Returns: The class with logger initialized. """ - logger_name = cast( - 'str', - logger.Logged._Logged__get_name(__name__, class_.__name__), # type: ignore[attr-defined, ty:unresolved-attribute] + logger_name: str = '.'.join( + part.strip() for part in (__name__, class_.__name__) if part.strip() ) class_.logger = logging.getLogger(logger_name) diff --git a/tests/test_mesh.py b/tests/test_mesh.py index de170784..6c8105a1 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -420,3 +420,10 @@ def test_is_closed_heuristic(caplog): assert 'not exact' in caplog.text assert not _single_triangle_mesh().check(exact=False) + + +def test_logged_decorator_logger_name(): + # logged() must produce the same dotted name python-utils generates + # Note: after other tests create Mesh instances, Logged.__new__ overwrites + # the logger with the instantiated class's module and name + assert Mesh.logger.name == 'stl.mesh.Mesh' From f954f2f676d92a2fb41f16c95935f26fec367f5c Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 19:22:22 +0200 Subject: [PATCH 06/15] test: make logged() characterization test order-independent --- tests/test_mesh.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_mesh.py b/tests/test_mesh.py index 6c8105a1..620f49d1 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -5,7 +5,8 @@ import numpy as np import pytest -from stl.base import BaseMesh, RemoveDuplicates +from python_utils.logger import Logged +from stl.base import BaseMesh, RemoveDuplicates, logged from stl.mesh import Mesh from . import utils @@ -423,7 +424,10 @@ def test_is_closed_heuristic(caplog): def test_logged_decorator_logger_name(): - # logged() must produce the same dotted name python-utils generates - # Note: after other tests create Mesh instances, Logged.__new__ overwrites - # the logger with the instantiated class's module and name - assert Mesh.logger.name == 'stl.mesh.Mesh' + # logged() must produce the same dotted name python-utils generates: + # '.'.join of the defining module and class name. + @logged + class _Sample(Logged): + pass + + assert _Sample.logger.name == 'stl.base._Sample' From c2b03941c5f7155ed65050936e01783dd1b126ea Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 19:26:04 +0200 Subject: [PATCH 07/15] fix: declare explicit identity __hash__ alongside custom __eq__ --- stl/base.py | 4 +++- tests/test_mesh.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/stl/base.py b/stl/base.py index 9410bbf5..c1f27e5e 100644 --- a/stl/base.py +++ b/stl/base.py @@ -1096,7 +1096,9 @@ def __eq__(self, other: object) -> bool: return True return NotImplemented - __hash__ = object.__hash__ + def __hash__(self) -> int: + # Identity hash, consistent with the identity-based __eq__ above. + return object.__hash__(self) def __repr__(self) -> str: return f'' diff --git a/tests/test_mesh.py b/tests/test_mesh.py index 620f49d1..de87558f 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -431,3 +431,13 @@ class _Sample(Logged): pass assert _Sample.logger.name == 'stl.base._Sample' + + +def test_mesh_hash_is_identity_based(): + data = np.zeros(1, dtype=BaseMesh.dtype) + a = Mesh(data.copy(), remove_empty_areas=False) + b = Mesh(data.copy(), remove_empty_areas=False) + + assert hash(a) == object.__hash__(a) + assert a == a # identity equality + assert (a == b) is False # equal content, different identity From 00dd717963dd0a7f9f81c76a423ecd55143d8585 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 19:28:47 +0200 Subject: [PATCH 08/15] fix: exhaustive bytes/str narrowing in save_ply name handling --- stl/stl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stl/stl.py b/stl/stl.py index 56d34e4f..ee25cf30 100644 --- a/stl/stl.py +++ b/stl/stl.py @@ -901,7 +901,7 @@ def save_ply( name = '' if isinstance(self.name, bytes): name = self.name.decode('ascii', errors='replace') - elif isinstance(self.name, str): + elif isinstance(self.name, str): # pyright: ignore[reportUnnecessaryIsInstance] name = self.name if fh: From 8fc64557147e1a13a4ffbf49de0abf9752a84b29 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 19:31:34 +0200 Subject: [PATCH 09/15] fix: drop unused type-ignores, parameterize PLY mesh dtypes --- stl/_compat.py | 4 ++-- stl/ply.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/stl/_compat.py b/stl/_compat.py index 92e9f1b2..8201c1a8 100644 --- a/stl/_compat.py +++ b/stl/_compat.py @@ -16,8 +16,8 @@ if _speedups_available: try: from speedups import ( # noqa: F401 - ascii_read, # type: ignore[assignment] - ascii_write, # type: ignore[assignment] + ascii_read, + ascii_write, ) except ImportError: _speedups_available = False diff --git a/stl/ply.py b/stl/ply.py index 893e3f54..cf818106 100644 --- a/stl/ply.py +++ b/stl/ply.py @@ -413,7 +413,7 @@ def _triangulate( def _build_mesh_data( vertices: np.ndarray, triangles: list[tuple[int, int, int]], - mesh_dtype: np.dtype, # type: ignore[type-arg] + mesh_dtype: np.dtype[np.void], ) -> np.ndarray: """Build the structured numpy array for the mesh. @@ -546,7 +546,7 @@ def write_ply( def read_ply( fh: IO[bytes], - mesh_dtype: np.dtype, # type: ignore[type-arg] + mesh_dtype: np.dtype[np.void], ) -> tuple[np.ndarray, str]: """Read a PLY file and return mesh data. From 60a035b7413b959496d456e4c8d2a062adafa93f Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 19:33:41 +0200 Subject: [PATCH 10/15] fix: mark speedups imports as explicit re-exports --- stl/_compat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stl/_compat.py b/stl/_compat.py index 8201c1a8..d628d2b8 100644 --- a/stl/_compat.py +++ b/stl/_compat.py @@ -16,8 +16,8 @@ if _speedups_available: try: from speedups import ( # noqa: F401 - ascii_read, - ascii_write, + ascii_read as ascii_read, + ascii_write as ascii_write, ) except ImportError: _speedups_available = False From e663f59c0e50d37e87a364fe14a3dc2cc343be47 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 19:42:48 +0200 Subject: [PATCH 11/15] fix: replace deprecated argparse.FileType with post-parse opens --- stl/main.py | 129 ++++++++++++++++++++++---------------- tests/test_commandline.py | 44 +++++++++++++ 2 files changed, 118 insertions(+), 55 deletions(-) diff --git a/stl/main.py b/stl/main.py index 104e9d76..f3d08b88 100644 --- a/stl/main.py +++ b/stl/main.py @@ -1,27 +1,27 @@ import argparse import random import sys +import typing from . import stl +if typing.TYPE_CHECKING: # pragma: no cover + from typing import IO + def _get_parser(description: str) -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=description) - # The std stream defaults use the underlying binary buffers: STL - # data is binary and the text wrappers would corrupt it (or raise). parser.add_argument( 'infile', nargs='?', - type=argparse.FileType('rb'), - default=sys.stdin.buffer, - help='STL file to read', + default='-', + help="STL file to read ('-' or omitted reads from stdin)", ) parser.add_argument( 'outfile', nargs='?', - type=argparse.FileType('wb'), - default=sys.stdout.buffer, - help='STL file to write', + default='-', + help="STL file to write ('-' or omitted writes to stdout)", ) parser.add_argument('--name', nargs='?', help='Name of the mesh') parser.add_argument( @@ -46,17 +46,27 @@ def _get_parser(description: str) -> argparse.ArgumentParser: return parser +# The std stream defaults use the underlying binary buffers: STL +# data is binary and the text wrappers would corrupt it (or raise). +def _open_infile(path: str) -> 'IO[bytes]': + if path == '-': + return sys.stdin.buffer + return open(path, 'rb') + + +def _open_outfile(path: str) -> 'IO[bytes]': + if path == '-': + return sys.stdout.buffer + return open(path, 'wb') + + def _get_name(args: argparse.Namespace) -> str: - names = [ - args.name, - getattr(args.outfile, 'name', None), - getattr(args.infile, 'name', None), - ] + names: list[str | None] = [args.name, args.outfile, args.infile] for name in names: if not isinstance(name, str): continue - elif name.startswith('<'): # pragma: no cover + elif name == '-': continue elif r'\AppData\Local\Temp' in name: # pragma: no cover # Windows temp file @@ -67,6 +77,52 @@ def _get_name(args: argparse.Namespace) -> str: return 'numpy-stl-%06d' % random.randint(0, 1_000_000) # noqa: UP031 +def _convert( + parser: argparse.ArgumentParser, + args: argparse.Namespace, + mode: 'stl.Mode', +) -> None: + """Load the input mesh and save it in the requested mode. + + The input is opened and fully loaded before the output is opened, + so a bad input path never truncates an existing output file. + """ + name: str = _get_name(args) + + try: + infile: IO[bytes] = _open_infile(args.infile) + except OSError as exc: + parser.error(f"can't open {args.infile!r}: {exc}") + + try: + stl_file = stl.StlMesh( + filename=name, + fh=infile, + calculate_normals=False, + remove_empty_areas=args.remove_empty_areas, + speedups=not args.disable_speedups, + ) + finally: + if infile is not sys.stdin.buffer: + infile.close() + + try: + outfile: IO[bytes] = _open_outfile(args.outfile) + except OSError as exc: + parser.error(f"can't open {args.outfile!r}: {exc}") + + try: + stl_file.save( + name, + outfile, + mode=mode, + update_normals=not args.use_file_normals, + ) + finally: + if outfile is not sys.stdout.buffer: + outfile.close() + + def main() -> None: """CLI entry point for the ``stl`` command. @@ -89,15 +145,8 @@ def main() -> None: ) args = parser.parse_args() - name = _get_name(args) - stl_file = stl.StlMesh( - filename=name, - fh=args.infile, - calculate_normals=False, - remove_empty_areas=args.remove_empty_areas, - speedups=not args.disable_speedups, - ) + mode: stl.Mode if args.binary: mode = stl.BINARY elif args.ascii: @@ -105,9 +154,7 @@ def main() -> None: else: mode = stl.AUTOMATIC - stl_file.save( - name, args.outfile, mode=mode, update_normals=not args.use_file_normals - ) + _convert(parser, args, mode) def to_ascii() -> None: @@ -117,21 +164,7 @@ def to_ascii() -> None: Supports ``-n`` (keep file normals) and ``-s`` (disable speedups). """ parser = _get_parser('Convert STL files to ASCII (text) format') - args = parser.parse_args() - name = _get_name(args) - stl_file = stl.StlMesh( - filename=name, - fh=args.infile, - calculate_normals=False, - remove_empty_areas=args.remove_empty_areas, - speedups=not args.disable_speedups, - ) - stl_file.save( - name, - args.outfile, - mode=stl.ASCII, - update_normals=not args.use_file_normals, - ) + _convert(parser, parser.parse_args(), stl.ASCII) def to_binary() -> None: @@ -141,18 +174,4 @@ def to_binary() -> None: Supports ``-n`` (keep file normals) and ``-s`` (disable speedups). """ parser = _get_parser('Convert STL files to binary format') - args = parser.parse_args() - name = _get_name(args) - stl_file = stl.StlMesh( - filename=name, - fh=args.infile, - calculate_normals=False, - remove_empty_areas=args.remove_empty_areas, - speedups=not args.disable_speedups, - ) - stl_file.save( - name, - args.outfile, - mode=stl.BINARY, - update_normals=not args.use_file_normals, - ) + _convert(parser, parser.parse_args(), stl.BINARY) diff --git a/tests/test_commandline.py b/tests/test_commandline.py index 901d0c70..ab3de432 100644 --- a/tests/test_commandline.py +++ b/tests/test_commandline.py @@ -2,6 +2,8 @@ import subprocess import sys +import pytest + from stl import main, mesh @@ -104,3 +106,45 @@ def test_to_ascii_stdin_stdout_pipes(binary_file): result = _run_pipe('to_ascii', binary_file) assert result.returncode == 0, result.stderr.decode() assert result.stdout.startswith(b'solid') + + +def test_open_helpers_std_streams(): + assert main._open_infile('-') is sys.stdin.buffer + assert main._open_outfile('-') is sys.stdout.buffer + + +def test_main_std_streams_in_process(binary_file, monkeypatch): + with open(binary_file, 'rb') as fh: + data = fh.read() + + stdin_buffer = io.BytesIO(data) + stdout_buffer = io.BytesIO() + monkeypatch.setattr(sys, 'stdin', io.TextIOWrapper(stdin_buffer)) + monkeypatch.setattr(sys, 'stdout', io.TextIOWrapper(stdout_buffer)) + monkeypatch.setattr(sys, 'argv', ['stl', '-b']) + + main.main() + + loaded = mesh.Mesh.from_file( + 'out.stl', fh=io.BytesIO(stdout_buffer.getvalue()) + ) + assert len(loaded.data) > 0 + + +def test_main_missing_infile(tmpdir, monkeypatch, capsys): + missing = str(tmpdir.join('does-not-exist.stl')) + out = str(tmpdir.join('out.stl')) + monkeypatch.setattr(sys, 'argv', ['stl', missing, out]) + + with pytest.raises(SystemExit): + main.main() + assert "can't open" in capsys.readouterr().err + + +def test_main_unopenable_outfile(binary_file, tmpdir, monkeypatch, capsys): + bad_out = str(tmpdir.join('missing-dir', 'out.stl')) + monkeypatch.setattr(sys, 'argv', ['stl', binary_file, bad_out]) + + with pytest.raises(SystemExit): + main.main() + assert "can't open" in capsys.readouterr().err From 24b85fab5d7adb3e9f8c62c18f5becc34c64d2d6 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 19:58:52 +0200 Subject: [PATCH 12/15] ci: enforce mypy, basedpyright and ty alongside pyrefly Add mypy and basedpyright tox envs (mirroring the existing pyrefly env) and wire pyrefly,mypy,basedpyright,ty into CI. Fresh envs expose three gaps the dev venv masked: mypy has no ignore-missing-imports override for the optional speedups module, numpy 2.5.1 stubs mistype an update_areas assignment as float64, and newer bundled typeshed makes BufferedWriter generic. Fixed with a mypy override, a cast to the declared alias, and a scoped pyright suppression respectively; stl/_compat.py also restructured to import speedups under private aliases to clear a residual mypy no-redef false positive. --- .github/workflows/ci.yml | 2 +- pyproject.toml | 5 +++++ stl/_compat.py | 9 ++++++--- stl/base.py | 3 ++- stl/stl.py | 2 +- tox.ini | 21 ++++++++++++++++++++- 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4662b4b..dbabd3a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: - name: Install tox run: uv pip install '.[tox]' - name: Type-check - run: tox -e pyrefly + run: tox -e pyrefly,mypy,basedpyright,ty docs: runs-on: ubuntu-latest diff --git a/pyproject.toml b/pyproject.toml index 73d9a911..8ad1d5be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dev = [ "mypy>=1.0", "basedpyright>=1.0", "pyrefly>=0.1", + "ty>=0.0.1", ] fast = ["speedups>=2.0.0"] tox = [ @@ -99,6 +100,10 @@ enable_error_code = [ ] warn_return_any = false +[[tool.mypy.overrides]] +module = "speedups" +ignore_missing_imports = true + [tool.pyright] pythonPlatform = "All" diff --git a/stl/_compat.py b/stl/_compat.py index d628d2b8..19794574 100644 --- a/stl/_compat.py +++ b/stl/_compat.py @@ -15,12 +15,15 @@ if _speedups_available: try: - from speedups import ( # noqa: F401 - ascii_read as ascii_read, - ascii_write as ascii_write, + from speedups import ( + ascii_read as _speedups_ascii_read, + ascii_write as _speedups_ascii_write, ) except ImportError: _speedups_available = False + else: + ascii_read = _speedups_ascii_read + ascii_write = _speedups_ascii_write def has_speedups() -> bool: diff --git a/stl/base.py b/stl/base.py index c1f27e5e..2d5bfc5a 100644 --- a/stl/base.py +++ b/stl/base.py @@ -553,7 +553,8 @@ def update_areas(self, normals: '_f32_2d | None' = None) -> None: normals = np.cross(self.v1 - self.v0, self.v2 - self.v0) # pyrefly: ignore areas = 0.5 * np.sqrt((normals**2).sum(axis=1)) # pyrefly: ignore - self._areas = areas.reshape((areas.size, 1)) + # https://github.com/numpy/numpy/pull/30261 + self._areas = cast('_f32_2d', areas.reshape((areas.size, 1))) def update_centroids(self) -> None: """Refresh the cached per-triangle centroids.""" diff --git a/stl/stl.py b/stl/stl.py index ee25cf30..b85c5ba9 100644 --- a/stl/stl.py +++ b/stl/stl.py @@ -539,7 +539,7 @@ def _write_binary( if isinstance(fh, io.BufferedWriter) and fh.seekable(): # Write to a true file. numpy's tofile() needs to query the # file position, which fails on pipes such as stdout. - self.data.tofile(fh) + self.data.tofile(fh) # pyright: ignore[reportUnknownArgumentType] else: # Write to a pseudo buffer (e.g. BytesIO or a pipe). cast('_StatefulWriter', fh).write(self.data.data) diff --git a/tox.ini b/tox.ini index f485116f..cf4a5a48 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,9 @@ env_list = speedups lint pyrefly + mypy + basedpyright + ty docs docs-examples coverage @@ -71,8 +74,24 @@ deps = commands = pyrefly check +[testenv:mypy] +description = Type-check with mypy +deps = + . + mypy>=1.0 +commands = + mypy + +[testenv:basedpyright] +description = Type-check with basedpyright +deps = + . + basedpyright>=1.0 +commands = + basedpyright + [testenv:ty] -description = Type-check with ty (run explicitly: tox -e ty) +description = Type-check with ty deps = . ty From 5491cdafabdc24b936c1d28003b26b84d363e5cc Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 20:06:15 +0200 Subject: [PATCH 13/15] docs: changelog for type-check enforcement and CLI FileType fix --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9697e9e..e6e33b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Resolved all mypy and basedpyright findings; malformed `# type: ignore[ty:...]` + suppressions replaced by `typing.cast` or restructured code. +- Replaced `argparse.FileType` (deprecated since Python 3.14) in the CLI with + path arguments opened after parsing; stdin/stdout `-` semantics unchanged. + This also fixes a latent bug where the output file was truncated at + argument-parsing time, before the input was validated. + +### Changed +- CI now enforces all four type checkers (pyrefly, mypy, basedpyright, ty). + +### Removed +- Legacy `build.cmd` MSVC build script for the speedups extension removed in 4.0.0. + ## [4.0.0] - 2026-06-17 ### Fixed From 60d039cbf87610fdb32a240436b3c433d9760b12 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 22:00:35 +0200 Subject: [PATCH 14/15] fix: hermetic basedpyright resolution and pinned checker versions Add a root speedups.pyi stub (matching the real package's signatures) so basedpyright resolves stl/_compat.py without the optional speedups extension installed, and force basedpyright's tox env to use its own interpreter (--pythonpath {envpython}) instead of auto-detecting the root .venv, which happens to have speedups installed and was masking the failure CI would hit. Also pin mypy/basedpyright/pyrefly/ty tox env versions to the currently resolved releases so an untouched repo can't be redenned by an upstream checker release, and tidy two CHANGELOG entries (wording + line wrap). --- CHANGELOG.md | 8 +++++--- speedups.pyi | 15 +++++++++++++++ tox.ini | 11 ++++++----- 3 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 speedups.pyi diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e33b29..1305b431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed -- Resolved all mypy and basedpyright findings; malformed `# type: ignore[ty:...]` - suppressions replaced by `typing.cast` or restructured code. +- Resolved all mypy and basedpyright findings; malformed + `# type: ignore[ty:...]` suppressions replaced by `typing.cast` or + restructured code. - Replaced `argparse.FileType` (deprecated since Python 3.14) in the CLI with path arguments opened after parsing; stdin/stdout `-` semantics unchanged. This also fixes a latent bug where the output file was truncated at @@ -19,7 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CI now enforces all four type checkers (pyrefly, mypy, basedpyright, ty). ### Removed -- Legacy `build.cmd` MSVC build script for the speedups extension removed in 4.0.0. +- Legacy `build.cmd` MSVC build script (the speedups extension it built was + removed in 4.0.0). ## [4.0.0] - 2026-06-17 diff --git a/speedups.pyi b/speedups.pyi new file mode 100644 index 00000000..f3c3fae9 --- /dev/null +++ b/speedups.pyi @@ -0,0 +1,15 @@ +# Local stub for the optional speedups package so type checkers resolve it without it installed. +from typing import IO + +import numpy as np +import numpy.typing as npt + +def ascii_read( + fh: IO[bytes], + buf: bytes, +) -> tuple[bytes, npt.NDArray[np.void]]: ... +def ascii_write( + fh: IO[bytes], + name: bytes, + arr: npt.NDArray[np.void], +) -> None: ... diff --git a/tox.ini b/tox.ini index cf4a5a48..9d77f280 100644 --- a/tox.ini +++ b/tox.ini @@ -66,11 +66,12 @@ commands = # ── Type checking ──────────────────────────────────────────── +# Checker versions are pinned for CI stability; bump deliberately. [testenv:pyrefly] description = Type-check with pyrefly deps = . - pyrefly>=0.1 + pyrefly==1.1.1 commands = pyrefly check @@ -78,7 +79,7 @@ commands = description = Type-check with mypy deps = . - mypy>=1.0 + mypy==2.1.0 commands = mypy @@ -86,15 +87,15 @@ commands = description = Type-check with basedpyright deps = . - basedpyright>=1.0 + basedpyright==1.39.9 commands = - basedpyright + basedpyright --pythonpath {envpython} [testenv:ty] description = Type-check with ty deps = . - ty + ty==0.0.56 commands = ty check stl From 947197d7f73c4cc7c7d740df170a20e5d115d78a Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 22:02:56 +0200 Subject: [PATCH 15/15] chore: wrap speedups stub comment to line-length convention --- speedups.pyi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/speedups.pyi b/speedups.pyi index f3c3fae9..3545c4a0 100644 --- a/speedups.pyi +++ b/speedups.pyi @@ -1,4 +1,5 @@ -# Local stub for the optional speedups package so type checkers resolve it without it installed. +# Local stub for the optional speedups package so type +# checkers resolve it without it installed. from typing import IO import numpy as np