From 91bf74ee9a23c2442516e9cfd4503b5f07c5fcda 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 | 1 + 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 | 29 +++++++++++++++++++++++++++++ webplayground/server.py | 2 +- 10 files changed, 68 insertions(+), 10 deletions(-) rename arcade/{VERSION => _VERSION} (100%) create mode 100644 tests/unit/test_pyinstaller_hook.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb4b62c0e..3d42aaa89e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. ## Unreleased ### Fixes +- Fixed the PyInstaller hook bundling Arcade's version file at `arcade/VERSION/VERSION`. Frozen apps printed an error on startup and reported `arcade.VERSION` as `0.0.0`, and builds using `collect_all("arcade")` failed because the hook and `collect_all` put a folder and a file at the same path. The version file is now `arcade/_VERSION`, so it can't be confused with the `arcade.version` module ([#2837](https://github.com/pythonarcade/arcade/issues/2837)). - Fixed drawing into a texture atlas losing new drawing when the atlas grows during `render_into()`. Nested rendering and exception exits now restore the previous camera and render region. - Fixed several `arcade.create_text_sprite()` bugs: - Sprites with the same text shared one image in the texture atlas, so creating a second one (for example in another color or size) changed the first one and drew the second stretched. Each sprite now gets its own image. 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 023a8808a4..4c9d814309 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -65,7 +65,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 e11fd798d2..c907a94025 100644 --- a/tests/unit/test_version.py +++ b/tests/unit/test_version.py @@ -1,12 +1,41 @@ import sys import tempfile +from pathlib import Path from unittest import mock import pytest +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( "value, expected", [ 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 6b1ed001914e675c6c9e7c7798dd975c0b1ce737 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)