From 6aa4caeb659c887e2242520777aacdd0e3922981 Mon Sep 17 00:00:00 2001 From: yxshee <84263676+yxshee@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:03:48 +0530 Subject: [PATCH 1/2] Fix PyInstaller version data collision --- CHANGELOG.md | 3 +++ MANIFEST.in | 2 +- RELEASE_CHECKLIST.md | 2 +- arcade/{VERSION => _VERSION} | 0 arcade/__pyinstaller/hook-arcade.py | 4 ++-- arcade/version.py | 8 ++++---- doc/conf.py | 2 +- tests/unit/test_pyinstaller_hook.py | 28 +++++++++++++++++++++++++ tests/unit/test_version.py | 32 ++++++++++++++++++++++++++++- webplayground/server.py | 2 +- 10 files changed, 72 insertions(+), 11 deletions(-) rename arcade/{VERSION => _VERSION} (100%) create mode 100644 tests/unit/test_pyinstaller_hook.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f55b46ed7e..8ecb02cc09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. ### Fixes +- Fixed PyInstaller builds on case-insensitive file systems by preventing the bundled + version data file from colliding with the `arcade.version` module. See + [#2837](https://github.com/pythonarcade/arcade/issues/2837) - Fixed an issue where pixel scaling for high-dpi displays did not work correctly in web browsers via Pyodide. See [#2846](https://github.com/pythonarcade/arcade/pull/2846) - Fixed issues with update/draw rate handling that changes with Pyglet 3, rates are now handled properly between desktop and browser. See [#2845](https://github.com/pythonarcade/arcade/pull/2845) - Fixed caret behavior not responding appropriately when activating an input field. See [#2850](https://github.com/pythonarcade/arcade/pull/2850) diff --git a/MANIFEST.in b/MANIFEST.in index 63a2558163..f6f161da18 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,7 @@ # one-liner to get extensions in package: set([l.split(",")[0].split('.')[-1] for l in open('files.txt').readlines()]) recursive-exclude arcade *.pyc -recursive-include arcade VERSION +recursive-include arcade _VERSION recursive-include arcade *.typed recursive-include arcade/resources *.txt *.md *.url recursive-include arcade/resources *.mp3 *.wav *.ogg diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index 2d51f16792..166dae3c5d 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -8,7 +8,7 @@ 6. Run `tests/test_examples/run_all_examples.py` 7. Make sure `arcade/examples/asteroid_smasher.py` is playable. 8. Make sure `arcade/examples/platform_tutorial/17_views.py` is playable. -9. Update version number in `arcade/version.py` +9. Update version number in `arcade/_VERSION` 10. Update :ref:`release_notes` with release dates and any additional info needed. 11. Make sure last check-in ran clean on GitHub actions, viewable on Discord diff --git a/arcade/VERSION b/arcade/_VERSION similarity index 100% rename from arcade/VERSION rename to arcade/_VERSION diff --git a/arcade/__pyinstaller/hook-arcade.py b/arcade/__pyinstaller/hook-arcade.py index ef5ab231f1..43a97e3017 100644 --- a/arcade/__pyinstaller/hook-arcade.py +++ b/arcade/__pyinstaller/hook-arcade.py @@ -40,8 +40,8 @@ "./arcade/resources/system", ), ( - arcade_path / "VERSION", - "./arcade/VERSION", + arcade_path / "_VERSION", + "./arcade", ), ] ) diff --git a/arcade/version.py b/arcade/version.py index bcf99536fb..8fbd17f482 100644 --- a/arcade/version.py +++ b/arcade/version.py @@ -102,9 +102,9 @@ def _parse_python_friendly_version( def _parse_py_version_from_file( - version_path: str | Path = _HERE / "VERSION", write_errors_to=sys.stderr + version_path: str | Path = _HERE / "_VERSION", write_errors_to=sys.stderr ) -> str: - """Read & validate the VERSION file as from a limited subset. + """Read & validate the _VERSION file as from a limited subset. On failure, it will: @@ -114,7 +114,7 @@ def _parse_py_version_from_file( Args: version_path: - The VERSION file's path, defaulting to the same directory as + The _VERSION file's path, defaulting to the same directory as this file. write_errors_to: Makes CI simpler by allowing a stream mock to be passed easily. @@ -137,6 +137,6 @@ def _parse_py_version_from_file( VERSION: Final[str] = _parse_py_version_from_file() """A Python-friendly version string. -This value is converted from the GitHub-style ``VERSION`` file at the +This value is converted from the GitHub-style ``_VERSION`` file at the top-level of the arcade module. """ diff --git a/doc/conf.py b/doc/conf.py index 11eae1b569..6731d3be47 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -71,7 +71,7 @@ for k, v in ENV.items(): log.info(f"Env variable {k:{col_width}} : {v!r}") -# _temp_version = (REPO_LOCAL_ROOT / "arcade" / "VERSION").read_text().replace("-",'') +# _temp_version = (REPO_LOCAL_ROOT / "arcade" / "_VERSION").read_text().replace("-",'') # Don't change to # from arcade.version import VERSION diff --git a/tests/unit/test_pyinstaller_hook.py b/tests/unit/test_pyinstaller_hook.py new file mode 100644 index 0000000000..6a77f179ad --- /dev/null +++ b/tests/unit/test_pyinstaller_hook.py @@ -0,0 +1,28 @@ +import runpy +import sys +from pathlib import Path +from types import ModuleType + +import pytest + +import arcade + + +def test_version_data_is_bundled_at_package_root(monkeypatch: pytest.MonkeyPatch): + pyinstaller = ModuleType("PyInstaller") + compat = ModuleType("PyInstaller.compat") + compat.is_darwin = False + compat.is_unix = True + compat.is_win = False + monkeypatch.setitem(sys.modules, "PyInstaller", pyinstaller) + monkeypatch.setitem(sys.modules, "PyInstaller.compat", compat) + + package_dir = Path(arcade.__file__).parent + hook_globals = runpy.run_path(package_dir / "__pyinstaller" / "hook-arcade.py") + bundled_version_paths = [ + Path(destination) / Path(source).name + for source, destination in hook_globals["datas"] + if Path(source).name == "_VERSION" + ] + + assert bundled_version_paths == [Path("arcade/_VERSION")] diff --git a/tests/unit/test_version.py b/tests/unit/test_version.py index 617ce28040..c907a94025 100644 --- a/tests/unit/test_version.py +++ b/tests/unit/test_version.py @@ -1,9 +1,39 @@ import sys import tempfile +from pathlib import Path from unittest import mock import pytest -from arcade.version import _parse_python_friendly_version, _parse_py_version_from_file + +import arcade.version as version_module +from arcade.version import _parse_py_version_from_file, _parse_python_friendly_version + + +def test_default_version_file_is_loaded(): + version_path = Path(version_module.__file__).with_name("_VERSION") + + assert version_path.is_file() + assert _parse_py_version_from_file(version_path) == version_module.VERSION + + +def test_package_data_names_do_not_collide_with_modules_when_casefolded(): + package_dir = Path(version_module.__file__).parent + importable_names = { + path.stem.casefold() for path in package_dir.iterdir() if path.suffix == ".py" + } + importable_names.update( + path.name.casefold() + for path in package_dir.iterdir() + if path.is_dir() and (path / "__init__.py").is_file() + ) + data_names = { + path.name.casefold() + for path in package_dir.iterdir() + if path.is_file() and path.suffix != ".py" + } + + collisions = importable_names & data_names + assert not collisions, f"Case-insensitive package path collisions: {sorted(collisions)}" @pytest.mark.parametrize( diff --git a/webplayground/server.py b/webplayground/server.py index f79fe36fce..3883acbbdc 100644 --- a/webplayground/server.py +++ b/webplayground/server.py @@ -20,7 +20,7 @@ here = Path(__file__).parent.resolve() path_arcade = here.parent -version_file_path = path_arcade / "arcade" / "VERSION" +version_file_path = path_arcade / "arcade" / "_VERSION" VERSION = "unknown" with open(version_file_path, "r") as version_file: VERSION = version_file.read().rstrip() From 8f1404e48f59e65ed04158f98686726c71ddc05c Mon Sep 17 00:00:00 2001 From: yxshee <84263676+yxshee@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:30:43 +0530 Subject: [PATCH 2/2] Address PyInstaller review feedback --- arcade/version.py | 2 +- tests/unit/test_pyinstaller_hook.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/arcade/version.py b/arcade/version.py index 8fbd17f482..da75597eb7 100644 --- a/arcade/version.py +++ b/arcade/version.py @@ -104,7 +104,7 @@ def _parse_python_friendly_version( def _parse_py_version_from_file( version_path: str | Path = _HERE / "_VERSION", write_errors_to=sys.stderr ) -> str: - """Read & validate the _VERSION file as from a limited subset. + """Read and validate the _VERSION file using a limited version syntax. On failure, it will: diff --git a/tests/unit/test_pyinstaller_hook.py b/tests/unit/test_pyinstaller_hook.py index 6a77f179ad..c2102a2898 100644 --- a/tests/unit/test_pyinstaller_hook.py +++ b/tests/unit/test_pyinstaller_hook.py @@ -11,9 +11,11 @@ def test_version_data_is_bundled_at_package_root(monkeypatch: pytest.MonkeyPatch): pyinstaller = ModuleType("PyInstaller") compat = ModuleType("PyInstaller.compat") - compat.is_darwin = False - compat.is_unix = True - compat.is_win = False + setattr(pyinstaller, "__path__", []) + setattr(pyinstaller, "compat", compat) + setattr(compat, "is_darwin", False) + setattr(compat, "is_unix", True) + setattr(compat, "is_win", False) monkeypatch.setitem(sys.modules, "PyInstaller", pyinstaller) monkeypatch.setitem(sys.modules, "PyInstaller.compat", compat)