diff --git a/.gitignore b/.gitignore index 2becc04..d882066 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __pycache__/ .idea *.dist-info/ +*.egg-info/ build/ logs/ .DS_Store diff --git a/pth-tester/README.md b/pth-tester/README.md new file mode 100644 index 0000000..15beb0a --- /dev/null +++ b/pth-tester/README.md @@ -0,0 +1,18 @@ +# pth-tester + +This is a package that tests whether `.pth` files are correctly processed on +import. It is not designed to be published; it is only useful in the context of +the testbed app. + +When installed, it includes a `.pth` file that invokes the `pth_tester.init()` method. +This sets the `initialized` attribute of the module to `True`. In this way, it is +possible to tell if `.pth` handling has occurred on app startup. + +This project has been compiled into a wheel, stored in the `wheels` directory +of the top-level directory. The wheel can be rebuilt using: + + $ pip install build + $ python -m build --wheel --outdir ../wheels + +If you make any modifications to the code for this project, you will need to +rebuild the wheel. diff --git a/pth-tester/pth_tester.py b/pth-tester/pth_tester.py new file mode 100644 index 0000000..a052e9a --- /dev/null +++ b/pth-tester/pth_tester.py @@ -0,0 +1,21 @@ +initialized = False +has_socket = False + + +# The pth_tester module should be initalized by processing the `.pth` file +# created on installation. +def init(): + global initialized + global has_socket + + initialized = True + + # At the time that the module is initialized, it *should* have access + # to all of the standard library. This might not be true, depending on + # the initialization order of the site module and sys.path. + try: + import socket # NOQA: F401 + + has_socket = True + except ImportError: + pass diff --git a/pth-tester/pyproject.toml b/pth-tester/pyproject.toml new file mode 100644 index 0000000..66559c2 --- /dev/null +++ b/pth-tester/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = ["setuptools==78.0.2", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "x-pth-tester" +version = "2025.3.26" +classifiers = ["Private :: Do Not Upload"] diff --git a/pth-tester/setup.py b/pth-tester/setup.py new file mode 100644 index 0000000..b1ace4a --- /dev/null +++ b/pth-tester/setup.py @@ -0,0 +1,44 @@ +import os + +import setuptools +from setuptools.command.install import install + + +# Copied from setuptools: +# (https://github.com/pypa/setuptools/blob/7c859e017368360ba66c8cc591279d8964c031bc/setup.py#L40C6-L82) +class install_with_pth(install): + """ + Custom install command to install a .pth file. + + This hack is necessary because there's no standard way to install behavior + on startup (and it's debatable if there should be one). This hack (ab)uses + the `extra_path` behavior in Setuptools to install a `.pth` file with + implicit behavior on startup. + + The original source strongly recommends against using this behavior. + """ + + _pth_name = "_pth_tester" + _pth_contents = "import pth_tester; pth_tester.init()" + + def initialize_options(self): + install.initialize_options(self) + self.extra_path = self._pth_name, self._pth_contents + + def finalize_options(self): + install.finalize_options(self) + self._restore_install_lib() + + def _restore_install_lib(self): + """ + Undo secondary effect of `extra_path` adding to `install_lib` + """ + suffix = os.path.relpath(self.install_lib, self.install_libbase) + + if suffix.strip() == self._pth_contents.strip(): + self.install_lib = self.install_libbase + + +setuptools.setup( + cmdclass={"install": install_with_pth}, +) diff --git a/pyproject.toml b/pyproject.toml index 8fda42a..47dcfcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,8 @@ sources = ["src/testbed"] test_sources = ["tests"] requires = [ + "x-pth-tester", + # Cryptography provides an ABI3 wheel for all desktop platforms, but requires cffi which doesn't. """cryptography; \ (platform_system != 'iOS' and platform_system != 'Android' and python_version < '3.14') \ diff --git a/tests/test_common.py b/tests/test_common.py index a987c7a..ee6040a 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -444,3 +444,28 @@ def test_zoneinfo(): dt = datetime(2022, 5, 4, 13, 40, 42, tzinfo=ZoneInfo("Australia/Perth")) assert str(dt) == "2022-05-04 13:40:42+08:00" + + +def test_pth_handling(): + ".pth files installed by a package are processed" + import pth_tester + + # The pth_tester module should be "initialized" as a result of + # processing the .pth file created when the package is installed. + assert pth_tester.initialized + + # When the .pth file is processed, the full standard library should be + # available. Check if the initialization process could import socket. + if sys.platform == "android" or hasattr(sys, "getandroidapilevel"): + # Android is known to have an issue with .pth/sys.path ordering that + # causes this test to fail. For now, accept this as an XFAIL; if it + # passes, fail as an indicator that the bug has been resolved, and we + # can simplify the test. + if pth_tester.has_socket: + pytest.fail("Android .pth handling bug has been resolved.") + else: + pytest.xfail( + "On Android, .pth files are processed before sys.path is finalized." + ) + else: + assert pth_tester.has_socket diff --git a/wheels/x_pth_tester-2025.3.26-py3-none-any.whl b/wheels/x_pth_tester-2025.3.26-py3-none-any.whl new file mode 100644 index 0000000..6f95bdc Binary files /dev/null and b/wheels/x_pth_tester-2025.3.26-py3-none-any.whl differ