diff --git a/src/runez/logsetup.py b/src/runez/logsetup.py index 6654fef..72d9f65 100644 --- a/src/runez/logsetup.py +++ b/src/runez/logsetup.py @@ -967,8 +967,9 @@ def enable_faulthandler(cls, signum=UNSET): cls.faulthandler_signum = signum dump_file = cls.file_handler.stream - faulthandler.enable(file=dump_file, all_threads=True) - faulthandler.register(signum, file=dump_file, all_threads=True, chain=False) + if dump_file is not None: + faulthandler.enable(file=dump_file, all_threads=True) + faulthandler.register(signum, file=dump_file, all_threads=True, chain=False) @classmethod def override_spec(cls, **settings): diff --git a/src/runez/pyenv.py b/src/runez/pyenv.py index 3057134..ee96426 100644 --- a/src/runez/pyenv.py +++ b/src/runez/pyenv.py @@ -828,7 +828,7 @@ def mm(self): def mm_spec(self): """Major/minor spec, e.g: cpython:3.11""" if self.mm: - return PythonSpec(self.family, self.mm) + return PythonSpec(self.family, self.mm, freethreading=self.inspection.freethreading) @cached_property def machine(self): diff --git a/tests/test_pyenv.py b/tests/test_pyenv.py index 9ca11ab..870eee9 100644 --- a/tests/test_pyenv.py +++ b/tests/test_pyenv.py @@ -354,6 +354,44 @@ def test_spec(): assert freeth.freethreading +def test_freethreading_satisfies(temp_folder): + # A freethreaded installation must not satisfy a non-freethreaded spec (and vice versa) + arch = runez.SYS_INFO.platform_id.arch + mk_python("3.14.0", content={"version": "3.14.0", "machine": arch, "freethreading": True}) + from runez.pyenv import PythonInstallation + + path = runez.to_path(".pyenv/versions/3.14.0/bin/python3.14") + install = PythonInstallation(path) + assert install.inspection.freethreading + assert install.mm_spec.freethreading + assert not install.satisfies(PythonSpec.from_text("3.14")) + assert install.satisfies(PythonSpec.from_text("3.14t")) + + # Confirm a depot won't hand a freethreaded binary to a non-freethreaded spec + depot = PythonDepot(".pyenv/versions/**") + depot.invoker = None # Don't let the running interpreter satisfy specs from outside the depot + assert depot.find_python("3.14").problem # no non-freethreaded 3.14 available + found = depot.find_python("3.14t") + assert not found.problem + assert found.inspection.freethreading + + # Confirm the invoker fallback also respects the freethreading boundary + mk_python("3.14.1", content={"version": "3.14.1", "machine": arch, "freethreading": False}) + nft_path = runez.to_path(".pyenv/versions/3.14.1/bin/python3.14") + nft_install = PythonInstallation(nft_path) + assert not nft_install.inspection.freethreading + + ft_depot = PythonDepot() # no locations, only invoker + ft_depot.invoker = install # freethreaded invoker + assert not ft_depot.find_python("3.14t").problem # invoker satisfies 3.14t + assert ft_depot.find_python("3.14").problem # invoker does NOT satisfy non-freethreaded 3.14 + + nft_depot = PythonDepot() + nft_depot.invoker = nft_install # non-freethreaded invoker + assert not nft_depot.find_python("3.14").problem # invoker satisfies 3.14 + assert nft_depot.find_python("3.14t").problem # invoker does NOT satisfy 3.14t + + def test_spec_equivalent(): def check_equivalent_specs(*text): specs = [PythonSpec.from_text(x) for x in text]