From 0be0ba004378121d2119489b03c0af961f80716a Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Fri, 23 Oct 2026 20:08:30 -0400 Subject: [PATCH] fix(conda): read conda-forge's CFEP-27 Windows layout in the smoke check (#2126) - util/conda_vendor.py: built_for recognises lib/python/site-packages, the layout conda-forge's CPython 3.15 uses on Windows (CFEP-27), beside Lib/site-packages, and reads the version from the pyXY build string for either. It returned the bare `python` before, failing the win-64 3.15 smoke. - tests/project/test_conda_windows_leg_2060.py: TestTheWindowsLayout covers the CFEP-27 path for built_for and end to end through smoke(). tests/project/test_conda_windows_leg_2060.py: 22 passed; the 2 new tests fail without the fix. --- tests/project/test_conda_windows_leg_2060.py | 23 +++++++++++++++++--- util/conda_vendor.py | 13 ++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/tests/project/test_conda_windows_leg_2060.py b/tests/project/test_conda_windows_leg_2060.py index 1d42401437..03014580c9 100644 --- a/tests/project/test_conda_windows_leg_2060.py +++ b/tests/project/test_conda_windows_leg_2060.py @@ -18,7 +18,8 @@ previous ``FOR`` loop wrote ``%A``, a syntax error in a batch file. * :class:`TestPortableExecutable` and :class:`TestTheWindowsLayout`: :file:`util/conda_vendor.py` reads a ``.pyd``'s PE machine, and ``--smoke`` - takes a Windows package's ``Lib/site-packages``, whose path names no Python. + takes a Windows package's ``Lib/site-packages``, or from Python 3.15 its + ``lib/python/site-packages`` (conda-forge's CFEP-27), whose path names no Python. Nothing here runs on Windows; what a real Conda Update run shows is the rest. @@ -318,14 +319,25 @@ def test_the_python_is_read_from_the_build_string(self) -> None: posix = pathlib.Path('root') / 'lib' / 'python3.14' / 'site-packages' self.assertEqual(vendor.built_for(posix, {'build': 'py39_0'}), 'python3.14') - def smoke(self, build: 'str') -> 'tuple[int, str]': + def test_the_cfep_27_layout_is_read_from_the_build_string(self) -> None: + """conda-forge's Python 3.15 on Windows has ``lib/python/site-packages``, naming no version.""" + vendor = load('conda_vendor') + site = pathlib.Path('root') / 'lib' / 'python' / 'site-packages' + self.assertEqual(vendor.built_for(site, {'build': 'py315_0'}), 'python3.15') + self.assertEqual(vendor.built_for(site, {'build': 'h1a2b3c4_0'}), + "lib/python/site-packages with build string 'h1a2b3c4_0'") + # Only under lib: anything else named python is not the layout. + other = pathlib.Path('root') / 'opt' / 'python' / 'site-packages' + self.assertEqual(vendor.built_for(other, {'build': 'py315_0'}), 'python') + + def smoke(self, build: 'str', site: 'str' = 'Lib/site-packages') -> 'tuple[int, str]': vendor = load('conda_vendor') files = dict({'pcapkit/__init__.py': SHIM.encode()}, **DISTRIBUTION) files['pcapkit/_extern/native/_speedups.pyd'] = pe(0x8664) files['pcapkit/_extern/native-1.0.dist-info/RECORD'] = ( b'native/__init__.py,,\nnative/_speedups.pyd,,\nnative-1.0.dist-info/METADATA,,\n' b'native-1.0.dist-info/RECORD,,\n') - members = {f'Lib/site-packages/{name}': data for name, data in files.items()} + members = {f'{site}/{name}': data for name, data in files.items()} members['info/index.json'] = json.dumps({'name': 'pypcapkit', 'subdir': 'win-64', 'build': build}).encode() with tempfile.TemporaryDirectory() as temp: package = pathlib.Path(temp) / 'pypcapkit-0-py_0.tar.bz2' @@ -347,6 +359,11 @@ def test_a_windows_package_for_this_python_passes(self) -> None: self.assertEqual(status, 0, log) self.assertIn('win-64: 1 native binaries read, none failing', log) + def test_a_cfep_27_windows_package_for_this_python_passes(self) -> None: + status, log = self.smoke(f'py{self.PYTHON.replace(".", "")}h0_0', site='lib/python/site-packages') + self.assertEqual(status, 0, log) + self.assertIn('win-64: 1 native binaries read, none failing', log) + def test_a_windows_package_for_another_python_fails(self) -> None: status, log = self.smoke('py27_0') self.assertEqual(status, 1, log) diff --git a/util/conda_vendor.py b/util/conda_vendor.py index f243954a48..50f8cb2159 100644 --- a/util/conda_vendor.py +++ b/util/conda_vendor.py @@ -906,19 +906,26 @@ def runner_platform() -> 'Optional[str]': None) +#: The ``site-packages`` of a Windows package, which names no Python: conda-forge's +#: up to 3.14, and its CFEP-27 one from 3.15 on. +WINDOWS_SITES = (('Lib', 'site-packages'), ('lib', 'python', 'site-packages')) + + def built_for(site: 'pathlib.Path', index: 'dict[str, Any]') -> 'str': """The ``pythonX.Y`` a package's ``site-packages`` at ``site`` is for, or what it is if not that. A POSIX package names it in the path, ``lib/pythonX.Y/site-packages``. A - Windows one has ``Lib/site-packages`` for every version (#2060), so it is read + Windows one does not (#2060): ``Lib/site-packages`` up to 3.14, and from 3.15 + ``lib/python/site-packages``, conda-forge's CFEP-27 layout. So that is read from the ``pyXY`` that starts the build string in ``info/index.json``. """ - if site.parent.name != 'Lib': + layout = next((parts for parts in WINDOWS_SITES if site.parts[-len(parts):] == parts), None) + if layout is None: return site.parent.name match = re.match(r'py(\d)(\d+)(?![0-9])', str(index.get('build', ''))) if match is None: - return f'Lib/site-packages with build string {index.get("build")!r}' + return f'{"/".join(layout)} with build string {index.get("build")!r}' return f'python{match.group(1)}.{match.group(2)}'