1import copy
2import ntpath
3import pathlib
4import posixpath
5import sys
6import unittest
7
8from test.support import verbose
9
10try:
11    # If we are in a source tree, use the original source file for tests
12    SOURCE = (pathlib.Path(__file__).absolute().parent.parent.parent / "Modules/getpath.py").read_bytes()
13except FileNotFoundError:
14    # Try from _testcapimodule instead
15    from _testinternalcapi import get_getpath_codeobject
16    SOURCE = get_getpath_codeobject()
17
18
19class MockGetPathTests(unittest.TestCase):
20    def __init__(self, *a, **kw):
21        super().__init__(*a, **kw)
22        self.maxDiff = None
23
24    def test_normal_win32(self):
25        "Test a 'standard' install layout on Windows."
26        ns = MockNTNamespace(
27            argv0=r"C:\Python\python.exe",
28            real_executable=r"C:\Python\python.exe",
29        )
30        ns.add_known_xfile(r"C:\Python\python.exe")
31        ns.add_known_file(r"C:\Python\Lib\os.py")
32        ns.add_known_dir(r"C:\Python\DLLs")
33        expected = dict(
34            executable=r"C:\Python\python.exe",
35            base_executable=r"C:\Python\python.exe",
36            prefix=r"C:\Python",
37            exec_prefix=r"C:\Python",
38            module_search_paths_set=1,
39            module_search_paths=[
40                r"C:\Python\python98.zip",
41                r"C:\Python\DLLs",
42                r"C:\Python\Lib",
43                r"C:\Python",
44            ],
45        )
46        actual = getpath(ns, expected)
47        self.assertEqual(expected, actual)
48
49    def test_buildtree_win32(self):
50        "Test an in-build-tree layout on Windows."
51        ns = MockNTNamespace(
52            argv0=r"C:\CPython\PCbuild\amd64\python.exe",
53            real_executable=r"C:\CPython\PCbuild\amd64\python.exe",
54        )
55        ns.add_known_xfile(r"C:\CPython\PCbuild\amd64\python.exe")
56        ns.add_known_file(r"C:\CPython\Lib\os.py")
57        ns.add_known_file(r"C:\CPython\PCbuild\amd64\pybuilddir.txt", [""])
58        expected = dict(
59            executable=r"C:\CPython\PCbuild\amd64\python.exe",
60            base_executable=r"C:\CPython\PCbuild\amd64\python.exe",
61            prefix=r"C:\CPython",
62            exec_prefix=r"C:\CPython",
63            build_prefix=r"C:\CPython",
64            _is_python_build=1,
65            module_search_paths_set=1,
66            module_search_paths=[
67                r"C:\CPython\PCbuild\amd64\python98.zip",
68                r"C:\CPython\PCbuild\amd64",
69                r"C:\CPython\Lib",
70            ],
71        )
72        actual = getpath(ns, expected)
73        self.assertEqual(expected, actual)
74
75    def test_venv_win32(self):
76        """Test a venv layout on Windows.
77
78        This layout is discovered by the presence of %__PYVENV_LAUNCHER__%,
79        specifying the original launcher executable. site.py is responsible
80        for updating prefix and exec_prefix.
81        """
82        ns = MockNTNamespace(
83            argv0=r"C:\Python\python.exe",
84            ENV___PYVENV_LAUNCHER__=r"C:\venv\Scripts\python.exe",
85            real_executable=r"C:\Python\python.exe",
86        )
87        ns.add_known_xfile(r"C:\Python\python.exe")
88        ns.add_known_xfile(r"C:\venv\Scripts\python.exe")
89        ns.add_known_file(r"C:\Python\Lib\os.py")
90        ns.add_known_dir(r"C:\Python\DLLs")
91        ns.add_known_file(r"C:\venv\pyvenv.cfg", [
92            r"home = C:\Python"
93        ])
94        expected = dict(
95            executable=r"C:\venv\Scripts\python.exe",
96            prefix=r"C:\Python",
97            exec_prefix=r"C:\Python",
98            base_executable=r"C:\Python\python.exe",
99            base_prefix=r"C:\Python",
100            base_exec_prefix=r"C:\Python",
101            module_search_paths_set=1,
102            module_search_paths=[
103                r"C:\Python\python98.zip",
104                r"C:\Python\DLLs",
105                r"C:\Python\Lib",
106                r"C:\Python",
107            ],
108        )
109        actual = getpath(ns, expected)
110        self.assertEqual(expected, actual)
111
112    def test_registry_win32(self):
113        """Test registry lookup on Windows.
114
115        On Windows there are registry entries that are intended for other
116        applications to register search paths.
117        """
118        hkey = rf"HKLM\Software\Python\PythonCore\9.8-XY\PythonPath"
119        winreg = MockWinreg({
120            hkey: None,
121            f"{hkey}\\Path1": "path1-dir",
122            f"{hkey}\\Path1\\Subdir": "not-subdirs",
123        })
124        ns = MockNTNamespace(
125            argv0=r"C:\Python\python.exe",
126            real_executable=r"C:\Python\python.exe",
127            winreg=winreg,
128        )
129        ns.add_known_xfile(r"C:\Python\python.exe")
130        ns.add_known_file(r"C:\Python\Lib\os.py")
131        ns.add_known_dir(r"C:\Python\DLLs")
132        expected = dict(
133            module_search_paths_set=1,
134            module_search_paths=[
135                r"C:\Python\python98.zip",
136                "path1-dir",
137                # should not contain not-subdirs
138                r"C:\Python\DLLs",
139                r"C:\Python\Lib",
140                r"C:\Python",
141            ],
142        )
143        actual = getpath(ns, expected)
144        self.assertEqual(expected, actual)
145
146        ns["config"]["use_environment"] = 0
147        ns["config"]["module_search_paths_set"] = 0
148        ns["config"]["module_search_paths"] = None
149        expected = dict(
150            module_search_paths_set=1,
151            module_search_paths=[
152                r"C:\Python\python98.zip",
153                r"C:\Python\DLLs",
154                r"C:\Python\Lib",
155                r"C:\Python",
156            ],
157        )
158        actual = getpath(ns, expected)
159        self.assertEqual(expected, actual)
160
161    def test_symlink_normal_win32(self):
162        "Test a 'standard' install layout via symlink on Windows."
163        ns = MockNTNamespace(
164            argv0=r"C:\LinkedFrom\python.exe",
165            real_executable=r"C:\Python\python.exe",
166        )
167        ns.add_known_xfile(r"C:\LinkedFrom\python.exe")
168        ns.add_known_xfile(r"C:\Python\python.exe")
169        ns.add_known_link(r"C:\LinkedFrom\python.exe", r"C:\Python\python.exe")
170        ns.add_known_file(r"C:\Python\Lib\os.py")
171        ns.add_known_dir(r"C:\Python\DLLs")
172        expected = dict(
173            executable=r"C:\LinkedFrom\python.exe",
174            base_executable=r"C:\LinkedFrom\python.exe",
175            prefix=r"C:\Python",
176            exec_prefix=r"C:\Python",
177            module_search_paths_set=1,
178            module_search_paths=[
179                r"C:\Python\python98.zip",
180                r"C:\Python\DLLs",
181                r"C:\Python\Lib",
182                r"C:\Python",
183            ],
184        )
185        actual = getpath(ns, expected)
186        self.assertEqual(expected, actual)
187
188    def test_symlink_buildtree_win32(self):
189        "Test an in-build-tree layout via symlink on Windows."
190        ns = MockNTNamespace(
191            argv0=r"C:\LinkedFrom\python.exe",
192            real_executable=r"C:\CPython\PCbuild\amd64\python.exe",
193        )
194        ns.add_known_xfile(r"C:\LinkedFrom\python.exe")
195        ns.add_known_xfile(r"C:\CPython\PCbuild\amd64\python.exe")
196        ns.add_known_link(r"C:\LinkedFrom\python.exe", r"C:\CPython\PCbuild\amd64\python.exe")
197        ns.add_known_file(r"C:\CPython\Lib\os.py")
198        ns.add_known_file(r"C:\CPython\PCbuild\amd64\pybuilddir.txt", [""])
199        expected = dict(
200            executable=r"C:\LinkedFrom\python.exe",
201            base_executable=r"C:\LinkedFrom\python.exe",
202            prefix=r"C:\CPython",
203            exec_prefix=r"C:\CPython",
204            build_prefix=r"C:\CPython",
205            _is_python_build=1,
206            module_search_paths_set=1,
207            module_search_paths=[
208                r"C:\CPython\PCbuild\amd64\python98.zip",
209                r"C:\CPython\PCbuild\amd64",
210                r"C:\CPython\Lib",
211            ],
212        )
213        actual = getpath(ns, expected)
214        self.assertEqual(expected, actual)
215
216    def test_buildtree_pythonhome_win32(self):
217        "Test an out-of-build-tree layout on Windows with PYTHONHOME override."
218        ns = MockNTNamespace(
219            argv0=r"C:\Out\python.exe",
220            real_executable=r"C:\Out\python.exe",
221            ENV_PYTHONHOME=r"C:\CPython",
222        )
223        ns.add_known_xfile(r"C:\Out\python.exe")
224        ns.add_known_file(r"C:\CPython\Lib\os.py")
225        ns.add_known_file(r"C:\Out\pybuilddir.txt", [""])
226        expected = dict(
227            executable=r"C:\Out\python.exe",
228            base_executable=r"C:\Out\python.exe",
229            prefix=r"C:\CPython",
230            exec_prefix=r"C:\CPython",
231            # This build_prefix is a miscalculation, because we have
232            # moved the output direction out of the prefix.
233            # Specify PYTHONHOME to get the correct prefix/exec_prefix
234            build_prefix="C:\\",
235            _is_python_build=1,
236            module_search_paths_set=1,
237            module_search_paths=[
238                r"C:\Out\python98.zip",
239                r"C:\Out",
240                r"C:\CPython\Lib",
241            ],
242        )
243        actual = getpath(ns, expected)
244        self.assertEqual(expected, actual)
245
246    def test_no_dlls_win32(self):
247        "Test a layout on Windows with no DLLs directory."
248        ns = MockNTNamespace(
249            argv0=r"C:\Python\python.exe",
250            real_executable=r"C:\Python\python.exe",
251        )
252        ns.add_known_xfile(r"C:\Python\python.exe")
253        ns.add_known_file(r"C:\Python\Lib\os.py")
254        expected = dict(
255            executable=r"C:\Python\python.exe",
256            base_executable=r"C:\Python\python.exe",
257            prefix=r"C:\Python",
258            exec_prefix=r"C:\Python",
259            module_search_paths_set=1,
260            module_search_paths=[
261                r"C:\Python\python98.zip",
262                r"C:\Python",
263                r"C:\Python\Lib",
264            ],
265        )
266        actual = getpath(ns, expected)
267        self.assertEqual(expected, actual)
268
269    def test_normal_posix(self):
270        "Test a 'standard' install layout on *nix"
271        ns = MockPosixNamespace(
272            PREFIX="/usr",
273            argv0="python",
274            ENV_PATH="/usr/bin",
275        )
276        ns.add_known_xfile("/usr/bin/python")
277        ns.add_known_file("/usr/lib/python9.8/os.py")
278        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
279        expected = dict(
280            executable="/usr/bin/python",
281            base_executable="/usr/bin/python",
282            prefix="/usr",
283            exec_prefix="/usr",
284            module_search_paths_set=1,
285            module_search_paths=[
286                "/usr/lib/python98.zip",
287                "/usr/lib/python9.8",
288                "/usr/lib/python9.8/lib-dynload",
289            ],
290        )
291        actual = getpath(ns, expected)
292        self.assertEqual(expected, actual)
293
294    def test_buildpath_posix(self):
295        """Test an in-build-tree layout on POSIX.
296
297        This layout is discovered from the presence of pybuilddir.txt, which
298        contains the relative path from the executable's directory to the
299        platstdlib path.
300        """
301        ns = MockPosixNamespace(
302            argv0=r"/home/cpython/python",
303            PREFIX="/usr/local",
304        )
305        ns.add_known_xfile("/home/cpython/python")
306        ns.add_known_xfile("/usr/local/bin/python")
307        ns.add_known_file("/home/cpython/pybuilddir.txt", ["build/lib.linux-x86_64-9.8"])
308        ns.add_known_file("/home/cpython/Lib/os.py")
309        ns.add_known_dir("/home/cpython/lib-dynload")
310        expected = dict(
311            executable="/home/cpython/python",
312            prefix="/usr/local",
313            exec_prefix="/usr/local",
314            base_executable="/home/cpython/python",
315            build_prefix="/home/cpython",
316            _is_python_build=1,
317            module_search_paths_set=1,
318            module_search_paths=[
319                "/usr/local/lib/python98.zip",
320                "/home/cpython/Lib",
321                "/home/cpython/build/lib.linux-x86_64-9.8",
322            ],
323        )
324        actual = getpath(ns, expected)
325        self.assertEqual(expected, actual)
326
327    def test_venv_posix(self):
328        "Test a venv layout on *nix."
329        ns = MockPosixNamespace(
330            argv0="python",
331            PREFIX="/usr",
332            ENV_PATH="/venv/bin:/usr/bin",
333        )
334        ns.add_known_xfile("/usr/bin/python")
335        ns.add_known_xfile("/venv/bin/python")
336        ns.add_known_file("/usr/lib/python9.8/os.py")
337        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
338        ns.add_known_file("/venv/pyvenv.cfg", [
339            r"home = /usr/bin"
340        ])
341        expected = dict(
342            executable="/venv/bin/python",
343            prefix="/usr",
344            exec_prefix="/usr",
345            base_executable="/usr/bin/python",
346            base_prefix="/usr",
347            base_exec_prefix="/usr",
348            module_search_paths_set=1,
349            module_search_paths=[
350                "/usr/lib/python98.zip",
351                "/usr/lib/python9.8",
352                "/usr/lib/python9.8/lib-dynload",
353            ],
354        )
355        actual = getpath(ns, expected)
356        self.assertEqual(expected, actual)
357
358    def test_venv_changed_name_posix(self):
359        "Test a venv layout on *nix."
360        ns = MockPosixNamespace(
361            argv0="python",
362            PREFIX="/usr",
363            ENV_PATH="/venv/bin:/usr/bin",
364        )
365        ns.add_known_xfile("/usr/bin/python3")
366        ns.add_known_xfile("/venv/bin/python")
367        ns.add_known_link("/venv/bin/python", "/usr/bin/python3")
368        ns.add_known_file("/usr/lib/python9.8/os.py")
369        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
370        ns.add_known_file("/venv/pyvenv.cfg", [
371            r"home = /usr/bin"
372        ])
373        expected = dict(
374            executable="/venv/bin/python",
375            prefix="/usr",
376            exec_prefix="/usr",
377            base_executable="/usr/bin/python3",
378            base_prefix="/usr",
379            base_exec_prefix="/usr",
380            module_search_paths_set=1,
381            module_search_paths=[
382                "/usr/lib/python98.zip",
383                "/usr/lib/python9.8",
384                "/usr/lib/python9.8/lib-dynload",
385            ],
386        )
387        actual = getpath(ns, expected)
388        self.assertEqual(expected, actual)
389
390    def test_venv_non_installed_zip_path_posix(self):
391        "Test a venv created from non-installed python has correct zip path."""
392        ns = MockPosixNamespace(
393            argv0="/venv/bin/python",
394            PREFIX="/usr",
395            ENV_PATH="/venv/bin:/usr/bin",
396        )
397        ns.add_known_xfile("/path/to/non-installed/bin/python")
398        ns.add_known_xfile("/venv/bin/python")
399        ns.add_known_link("/venv/bin/python",
400                          "/path/to/non-installed/bin/python")
401        ns.add_known_file("/path/to/non-installed/lib/python9.8/os.py")
402        ns.add_known_dir("/path/to/non-installed/lib/python9.8/lib-dynload")
403        ns.add_known_file("/venv/pyvenv.cfg", [
404            r"home = /path/to/non-installed"
405        ])
406        expected = dict(
407            executable="/venv/bin/python",
408            prefix="/path/to/non-installed",
409            exec_prefix="/path/to/non-installed",
410            base_executable="/path/to/non-installed/bin/python",
411            base_prefix="/path/to/non-installed",
412            base_exec_prefix="/path/to/non-installed",
413            module_search_paths_set=1,
414            module_search_paths=[
415                "/path/to/non-installed/lib/python98.zip",
416                "/path/to/non-installed/lib/python9.8",
417                "/path/to/non-installed/lib/python9.8/lib-dynload",
418            ],
419        )
420        actual = getpath(ns, expected)
421        self.assertEqual(expected, actual)
422
423    def test_venv_changed_name_copy_posix(self):
424        "Test a venv --copies layout on *nix that lacks a distributed 'python'"
425        ns = MockPosixNamespace(
426            argv0="python",
427            PREFIX="/usr",
428            ENV_PATH="/venv/bin:/usr/bin",
429        )
430        ns.add_known_xfile("/usr/bin/python9")
431        ns.add_known_xfile("/venv/bin/python")
432        ns.add_known_file("/usr/lib/python9.8/os.py")
433        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
434        ns.add_known_file("/venv/pyvenv.cfg", [
435            r"home = /usr/bin"
436        ])
437        expected = dict(
438            executable="/venv/bin/python",
439            prefix="/usr",
440            exec_prefix="/usr",
441            base_executable="/usr/bin/python9",
442            base_prefix="/usr",
443            base_exec_prefix="/usr",
444            module_search_paths_set=1,
445            module_search_paths=[
446                "/usr/lib/python98.zip",
447                "/usr/lib/python9.8",
448                "/usr/lib/python9.8/lib-dynload",
449            ],
450        )
451        actual = getpath(ns, expected)
452        self.assertEqual(expected, actual)
453
454    def test_symlink_normal_posix(self):
455        "Test a 'standard' install layout via symlink on *nix"
456        ns = MockPosixNamespace(
457            PREFIX="/usr",
458            argv0="/linkfrom/python",
459        )
460        ns.add_known_xfile("/linkfrom/python")
461        ns.add_known_xfile("/usr/bin/python")
462        ns.add_known_link("/linkfrom/python", "/usr/bin/python")
463        ns.add_known_file("/usr/lib/python9.8/os.py")
464        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
465        expected = dict(
466            executable="/linkfrom/python",
467            base_executable="/linkfrom/python",
468            prefix="/usr",
469            exec_prefix="/usr",
470            module_search_paths_set=1,
471            module_search_paths=[
472                "/usr/lib/python98.zip",
473                "/usr/lib/python9.8",
474                "/usr/lib/python9.8/lib-dynload",
475            ],
476        )
477        actual = getpath(ns, expected)
478        self.assertEqual(expected, actual)
479
480    def test_symlink_buildpath_posix(self):
481        """Test an in-build-tree layout on POSIX.
482
483        This layout is discovered from the presence of pybuilddir.txt, which
484        contains the relative path from the executable's directory to the
485        platstdlib path.
486        """
487        ns = MockPosixNamespace(
488            argv0=r"/linkfrom/python",
489            PREFIX="/usr/local",
490        )
491        ns.add_known_xfile("/linkfrom/python")
492        ns.add_known_xfile("/home/cpython/python")
493        ns.add_known_link("/linkfrom/python", "/home/cpython/python")
494        ns.add_known_xfile("/usr/local/bin/python")
495        ns.add_known_file("/home/cpython/pybuilddir.txt", ["build/lib.linux-x86_64-9.8"])
496        ns.add_known_file("/home/cpython/Lib/os.py")
497        ns.add_known_dir("/home/cpython/lib-dynload")
498        expected = dict(
499            executable="/linkfrom/python",
500            prefix="/usr/local",
501            exec_prefix="/usr/local",
502            base_executable="/linkfrom/python",
503            build_prefix="/home/cpython",
504            _is_python_build=1,
505            module_search_paths_set=1,
506            module_search_paths=[
507                "/usr/local/lib/python98.zip",
508                "/home/cpython/Lib",
509                "/home/cpython/build/lib.linux-x86_64-9.8",
510            ],
511        )
512        actual = getpath(ns, expected)
513        self.assertEqual(expected, actual)
514
515    def test_custom_platlibdir_posix(self):
516        "Test an install with custom platlibdir on *nix"
517        ns = MockPosixNamespace(
518            PREFIX="/usr",
519            argv0="/linkfrom/python",
520            PLATLIBDIR="lib64",
521        )
522        ns.add_known_xfile("/usr/bin/python")
523        ns.add_known_file("/usr/lib64/python9.8/os.py")
524        ns.add_known_dir("/usr/lib64/python9.8/lib-dynload")
525        expected = dict(
526            executable="/linkfrom/python",
527            base_executable="/linkfrom/python",
528            prefix="/usr",
529            exec_prefix="/usr",
530            module_search_paths_set=1,
531            module_search_paths=[
532                "/usr/lib64/python98.zip",
533                "/usr/lib64/python9.8",
534                "/usr/lib64/python9.8/lib-dynload",
535            ],
536        )
537        actual = getpath(ns, expected)
538        self.assertEqual(expected, actual)
539
540    def test_framework_macos(self):
541        """ Test framework layout on macOS
542
543        This layout is primarily detected using a compile-time option
544        (WITH_NEXT_FRAMEWORK).
545        """
546        ns = MockPosixNamespace(
547            os_name="darwin",
548            argv0="/Library/Frameworks/Python.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/Python",
549            WITH_NEXT_FRAMEWORK=1,
550            PREFIX="/Library/Frameworks/Python.framework/Versions/9.8",
551            EXEC_PREFIX="/Library/Frameworks/Python.framework/Versions/9.8",
552            ENV___PYVENV_LAUNCHER__="/Library/Frameworks/Python.framework/Versions/9.8/bin/python9.8",
553            real_executable="/Library/Frameworks/Python.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/Python",
554            library="/Library/Frameworks/Python.framework/Versions/9.8/Python",
555        )
556        ns.add_known_xfile("/Library/Frameworks/Python.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/Python")
557        ns.add_known_xfile("/Library/Frameworks/Python.framework/Versions/9.8/bin/python9.8")
558        ns.add_known_dir("/Library/Frameworks/Python.framework/Versions/9.8/lib/python9.8/lib-dynload")
559        ns.add_known_file("/Library/Frameworks/Python.framework/Versions/9.8/lib/python9.8/os.py")
560
561        # This is definitely not the stdlib (see discusion in bpo-46890)
562        #ns.add_known_file("/Library/Frameworks/lib/python98.zip")
563
564        expected = dict(
565            executable="/Library/Frameworks/Python.framework/Versions/9.8/bin/python9.8",
566            prefix="/Library/Frameworks/Python.framework/Versions/9.8",
567            exec_prefix="/Library/Frameworks/Python.framework/Versions/9.8",
568            base_executable="/Library/Frameworks/Python.framework/Versions/9.8/bin/python9.8",
569            base_prefix="/Library/Frameworks/Python.framework/Versions/9.8",
570            base_exec_prefix="/Library/Frameworks/Python.framework/Versions/9.8",
571            module_search_paths_set=1,
572            module_search_paths=[
573                "/Library/Frameworks/Python.framework/Versions/9.8/lib/python98.zip",
574                "/Library/Frameworks/Python.framework/Versions/9.8/lib/python9.8",
575                "/Library/Frameworks/Python.framework/Versions/9.8/lib/python9.8/lib-dynload",
576            ],
577        )
578        actual = getpath(ns, expected)
579        self.assertEqual(expected, actual)
580
581    def test_alt_framework_macos(self):
582        """ Test framework layout on macOS with alternate framework name
583
584        ``--with-framework-name=DebugPython``
585
586        This layout is primarily detected using a compile-time option
587        (WITH_NEXT_FRAMEWORK).
588        """
589        ns = MockPosixNamespace(
590            argv0="/Library/Frameworks/DebugPython.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/DebugPython",
591            os_name="darwin",
592            WITH_NEXT_FRAMEWORK=1,
593            PREFIX="/Library/Frameworks/DebugPython.framework/Versions/9.8",
594            EXEC_PREFIX="/Library/Frameworks/DebugPython.framework/Versions/9.8",
595            ENV___PYVENV_LAUNCHER__="/Library/Frameworks/DebugPython.framework/Versions/9.8/bin/python9.8",
596            real_executable="/Library/Frameworks/DebugPython.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/DebugPython",
597            library="/Library/Frameworks/DebugPython.framework/Versions/9.8/DebugPython",
598            PYTHONPATH=None,
599            ENV_PYTHONHOME=None,
600            ENV_PYTHONEXECUTABLE=None,
601            executable_dir=None,
602            py_setpath=None,
603        )
604        ns.add_known_xfile("/Library/Frameworks/DebugPython.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/DebugPython")
605        ns.add_known_xfile("/Library/Frameworks/DebugPython.framework/Versions/9.8/bin/python9.8")
606        ns.add_known_dir("/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python9.8/lib-dynload")
607        ns.add_known_xfile("/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python9.8/os.py")
608
609        # This is definitely not the stdlib (see discusion in bpo-46890)
610        #ns.add_known_xfile("/Library/lib/python98.zip")
611        expected = dict(
612            executable="/Library/Frameworks/DebugPython.framework/Versions/9.8/bin/python9.8",
613            prefix="/Library/Frameworks/DebugPython.framework/Versions/9.8",
614            exec_prefix="/Library/Frameworks/DebugPython.framework/Versions/9.8",
615            base_executable="/Library/Frameworks/DebugPython.framework/Versions/9.8/bin/python9.8",
616            base_prefix="/Library/Frameworks/DebugPython.framework/Versions/9.8",
617            base_exec_prefix="/Library/Frameworks/DebugPython.framework/Versions/9.8",
618            module_search_paths_set=1,
619            module_search_paths=[
620                "/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python98.zip",
621                "/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python9.8",
622                "/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python9.8/lib-dynload",
623            ],
624        )
625        actual = getpath(ns, expected)
626        self.assertEqual(expected, actual)
627
628    def test_venv_framework_macos(self):
629        """Test a venv layout on macOS using a framework build
630        """
631        venv_path = "/tmp/workdir/venv"
632        ns = MockPosixNamespace(
633            os_name="darwin",
634            argv0="/Library/Frameworks/Python.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/Python",
635            WITH_NEXT_FRAMEWORK=1,
636            PREFIX="/Library/Frameworks/Python.framework/Versions/9.8",
637            EXEC_PREFIX="/Library/Frameworks/Python.framework/Versions/9.8",
638            ENV___PYVENV_LAUNCHER__=f"{venv_path}/bin/python",
639            real_executable="/Library/Frameworks/Python.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/Python",
640            library="/Library/Frameworks/Python.framework/Versions/9.8/Python",
641        )
642        ns.add_known_dir(venv_path)
643        ns.add_known_dir(f"{venv_path}/bin")
644        ns.add_known_dir(f"{venv_path}/lib")
645        ns.add_known_dir(f"{venv_path}/lib/python9.8")
646        ns.add_known_xfile(f"{venv_path}/bin/python")
647        ns.add_known_xfile("/Library/Frameworks/Python.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/Python")
648        ns.add_known_xfile("/Library/Frameworks/Python.framework/Versions/9.8/bin/python9.8")
649        ns.add_known_dir("/Library/Frameworks/Python.framework/Versions/9.8/lib/python9.8/lib-dynload")
650        ns.add_known_xfile("/Library/Frameworks/Python.framework/Versions/9.8/lib/python9.8/os.py")
651        ns.add_known_file(f"{venv_path}/pyvenv.cfg", [
652            "home = /Library/Frameworks/Python.framework/Versions/9.8/bin"
653        ])
654        expected = dict(
655            executable=f"{venv_path}/bin/python",
656            prefix="/Library/Frameworks/Python.framework/Versions/9.8",
657            exec_prefix="/Library/Frameworks/Python.framework/Versions/9.8",
658            base_executable="/Library/Frameworks/Python.framework/Versions/9.8/bin/python9.8",
659            base_prefix="/Library/Frameworks/Python.framework/Versions/9.8",
660            base_exec_prefix="/Library/Frameworks/Python.framework/Versions/9.8",
661            module_search_paths_set=1,
662            module_search_paths=[
663                "/Library/Frameworks/Python.framework/Versions/9.8/lib/python98.zip",
664                "/Library/Frameworks/Python.framework/Versions/9.8/lib/python9.8",
665                "/Library/Frameworks/Python.framework/Versions/9.8/lib/python9.8/lib-dynload",
666            ],
667        )
668        actual = getpath(ns, expected)
669        self.assertEqual(expected, actual)
670
671    def test_venv_alt_framework_macos(self):
672        """Test a venv layout on macOS using a framework build
673
674        ``--with-framework-name=DebugPython``
675        """
676        venv_path = "/tmp/workdir/venv"
677        ns = MockPosixNamespace(
678            os_name="darwin",
679            argv0="/Library/Frameworks/DebugPython.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/DebugPython",
680            WITH_NEXT_FRAMEWORK=1,
681            PREFIX="/Library/Frameworks/DebugPython.framework/Versions/9.8",
682            EXEC_PREFIX="/Library/Frameworks/DebugPython.framework/Versions/9.8",
683            ENV___PYVENV_LAUNCHER__=f"{venv_path}/bin/python",
684            real_executable="/Library/Frameworks/DebugPython.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/DebugPython",
685            library="/Library/Frameworks/DebugPython.framework/Versions/9.8/DebugPython",
686        )
687        ns.add_known_dir(venv_path)
688        ns.add_known_dir(f"{venv_path}/bin")
689        ns.add_known_dir(f"{venv_path}/lib")
690        ns.add_known_dir(f"{venv_path}/lib/python9.8")
691        ns.add_known_xfile(f"{venv_path}/bin/python")
692        ns.add_known_xfile("/Library/Frameworks/DebugPython.framework/Versions/9.8/Resources/Python.app/Contents/MacOS/DebugPython")
693        ns.add_known_xfile("/Library/Frameworks/DebugPython.framework/Versions/9.8/bin/python9.8")
694        ns.add_known_dir("/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python9.8/lib-dynload")
695        ns.add_known_xfile("/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python9.8/os.py")
696        ns.add_known_file(f"{venv_path}/pyvenv.cfg", [
697            "home = /Library/Frameworks/DebugPython.framework/Versions/9.8/bin"
698        ])
699        expected = dict(
700            executable=f"{venv_path}/bin/python",
701            prefix="/Library/Frameworks/DebugPython.framework/Versions/9.8",
702            exec_prefix="/Library/Frameworks/DebugPython.framework/Versions/9.8",
703            base_executable="/Library/Frameworks/DebugPython.framework/Versions/9.8/bin/python9.8",
704            base_prefix="/Library/Frameworks/DebugPython.framework/Versions/9.8",
705            base_exec_prefix="/Library/Frameworks/DebugPython.framework/Versions/9.8",
706            module_search_paths_set=1,
707            module_search_paths=[
708                "/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python98.zip",
709                "/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python9.8",
710                "/Library/Frameworks/DebugPython.framework/Versions/9.8/lib/python9.8/lib-dynload",
711            ],
712        )
713        actual = getpath(ns, expected)
714        self.assertEqual(expected, actual)
715
716    def test_venv_macos(self):
717        """Test a venv layout on macOS.
718
719        This layout is discovered when 'executable' and 'real_executable' match,
720        but $__PYVENV_LAUNCHER__ has been set to the original process.
721        """
722        ns = MockPosixNamespace(
723            os_name="darwin",
724            argv0="/usr/bin/python",
725            PREFIX="/usr",
726            ENV___PYVENV_LAUNCHER__="/framework/Python9.8/python",
727            real_executable="/usr/bin/python",
728        )
729        ns.add_known_xfile("/usr/bin/python")
730        ns.add_known_xfile("/framework/Python9.8/python")
731        ns.add_known_file("/usr/lib/python9.8/os.py")
732        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
733        ns.add_known_file("/framework/Python9.8/pyvenv.cfg", [
734            "home = /usr/bin"
735        ])
736        expected = dict(
737            executable="/framework/Python9.8/python",
738            prefix="/usr",
739            exec_prefix="/usr",
740            base_executable="/usr/bin/python",
741            base_prefix="/usr",
742            base_exec_prefix="/usr",
743            module_search_paths_set=1,
744            module_search_paths=[
745                "/usr/lib/python98.zip",
746                "/usr/lib/python9.8",
747                "/usr/lib/python9.8/lib-dynload",
748            ],
749        )
750        actual = getpath(ns, expected)
751        self.assertEqual(expected, actual)
752
753    def test_symlink_normal_macos(self):
754        "Test a 'standard' install layout via symlink on macOS"
755        ns = MockPosixNamespace(
756            os_name="darwin",
757            PREFIX="/usr",
758            argv0="python",
759            ENV_PATH="/linkfrom:/usr/bin",
760            # real_executable on macOS matches the invocation path
761            real_executable="/linkfrom/python",
762        )
763        ns.add_known_xfile("/linkfrom/python")
764        ns.add_known_xfile("/usr/bin/python")
765        ns.add_known_link("/linkfrom/python", "/usr/bin/python")
766        ns.add_known_file("/usr/lib/python9.8/os.py")
767        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
768        expected = dict(
769            executable="/linkfrom/python",
770            base_executable="/linkfrom/python",
771            prefix="/usr",
772            exec_prefix="/usr",
773            module_search_paths_set=1,
774            module_search_paths=[
775                "/usr/lib/python98.zip",
776                "/usr/lib/python9.8",
777                "/usr/lib/python9.8/lib-dynload",
778            ],
779        )
780        actual = getpath(ns, expected)
781        self.assertEqual(expected, actual)
782
783    def test_symlink_buildpath_macos(self):
784        """Test an in-build-tree layout via symlink on macOS.
785
786        This layout is discovered from the presence of pybuilddir.txt, which
787        contains the relative path from the executable's directory to the
788        platstdlib path.
789        """
790        ns = MockPosixNamespace(
791            os_name="darwin",
792            argv0=r"python",
793            ENV_PATH="/linkfrom:/usr/bin",
794            PREFIX="/usr/local",
795            # real_executable on macOS matches the invocation path
796            real_executable="/linkfrom/python",
797        )
798        ns.add_known_xfile("/linkfrom/python")
799        ns.add_known_xfile("/home/cpython/python")
800        ns.add_known_link("/linkfrom/python", "/home/cpython/python")
801        ns.add_known_xfile("/usr/local/bin/python")
802        ns.add_known_file("/home/cpython/pybuilddir.txt", ["build/lib.macos-9.8"])
803        ns.add_known_file("/home/cpython/Lib/os.py")
804        ns.add_known_dir("/home/cpython/lib-dynload")
805        expected = dict(
806            executable="/linkfrom/python",
807            prefix="/usr/local",
808            exec_prefix="/usr/local",
809            base_executable="/linkfrom/python",
810            build_prefix="/home/cpython",
811            _is_python_build=1,
812            module_search_paths_set=1,
813            module_search_paths=[
814                "/usr/local/lib/python98.zip",
815                "/home/cpython/Lib",
816                "/home/cpython/build/lib.macos-9.8",
817            ],
818        )
819        actual = getpath(ns, expected)
820        self.assertEqual(expected, actual)
821
822
823# ******************************************************************************
824
825DEFAULT_NAMESPACE = dict(
826    PREFIX="",
827    EXEC_PREFIX="",
828    PYTHONPATH="",
829    VPATH="",
830    PLATLIBDIR="",
831    PYDEBUGEXT="",
832    VERSION_MAJOR=9,    # fixed version number for ease
833    VERSION_MINOR=8,    # of testing
834    PYWINVER=None,
835    EXE_SUFFIX=None,
836
837    ENV_PATH="",
838    ENV_PYTHONHOME="",
839    ENV_PYTHONEXECUTABLE="",
840    ENV___PYVENV_LAUNCHER__="",
841    argv0="",
842    py_setpath="",
843    real_executable="",
844    executable_dir="",
845    library="",
846    winreg=None,
847    build_prefix=None,
848    venv_prefix=None,
849)
850
851DEFAULT_CONFIG = dict(
852    home=None,
853    platlibdir=None,
854    pythonpath=None,
855    program_name=None,
856    prefix=None,
857    exec_prefix=None,
858    base_prefix=None,
859    base_exec_prefix=None,
860    executable=None,
861    base_executable="",
862    stdlib_dir=None,
863    platstdlib_dir=None,
864    module_search_paths=None,
865    module_search_paths_set=0,
866    pythonpath_env=None,
867    argv=None,
868    orig_argv=None,
869
870    isolated=0,
871    use_environment=1,
872    use_site=1,
873)
874
875class MockNTNamespace(dict):
876    def __init__(self, *a, argv0=None, config=None, **kw):
877        self.update(DEFAULT_NAMESPACE)
878        self["config"] = DEFAULT_CONFIG.copy()
879        self["os_name"] = "nt"
880        self["PLATLIBDIR"] = "DLLs"
881        self["PYWINVER"] = "9.8-XY"
882        self["VPATH"] = r"..\.."
883        super().__init__(*a, **kw)
884        if argv0:
885            self["config"]["orig_argv"] = [argv0]
886        if config:
887            self["config"].update(config)
888        self._files = {}
889        self._links = {}
890        self._dirs = set()
891        self._warnings = []
892
893    def add_known_file(self, path, lines=None):
894        self._files[path.casefold()] = list(lines or ())
895        self.add_known_dir(path.rpartition("\\")[0])
896
897    def add_known_xfile(self, path):
898        self.add_known_file(path)
899
900    def add_known_link(self, path, target):
901        self._links[path.casefold()] = target
902
903    def add_known_dir(self, path):
904        p = path.rstrip("\\").casefold()
905        while p:
906            self._dirs.add(p)
907            p = p.rpartition("\\")[0]
908
909    def __missing__(self, key):
910        try:
911            return getattr(self, key)
912        except AttributeError:
913            raise KeyError(key) from None
914
915    def abspath(self, path):
916        if self.isabs(path):
917            return path
918        return self.joinpath("C:\\Absolute", path)
919
920    def basename(self, path):
921        return path.rpartition("\\")[2]
922
923    def dirname(self, path):
924        name = path.rstrip("\\").rpartition("\\")[0]
925        if name[1:] == ":":
926            return name + "\\"
927        return name
928
929    def hassuffix(self, path, suffix):
930        return path.casefold().endswith(suffix.casefold())
931
932    def isabs(self, path):
933        return path[1:3] == ":\\"
934
935    def isdir(self, path):
936        if verbose:
937            print("Check if", path, "is a dir")
938        return path.casefold() in self._dirs
939
940    def isfile(self, path):
941        if verbose:
942            print("Check if", path, "is a file")
943        return path.casefold() in self._files
944
945    def ismodule(self, path):
946        if verbose:
947            print("Check if", path, "is a module")
948        path = path.casefold()
949        return path in self._files and path.rpartition(".")[2] == "py".casefold()
950
951    def isxfile(self, path):
952        if verbose:
953            print("Check if", path, "is a executable")
954        path = path.casefold()
955        return path in self._files and path.rpartition(".")[2] == "exe".casefold()
956
957    def joinpath(self, *path):
958        return ntpath.normpath(ntpath.join(*path))
959
960    def readlines(self, path):
961        try:
962            return self._files[path.casefold()]
963        except KeyError:
964            raise FileNotFoundError(path) from None
965
966    def realpath(self, path, _trail=None):
967        if verbose:
968            print("Read link from", path)
969        try:
970            link = self._links[path.casefold()]
971        except KeyError:
972            return path
973        if _trail is None:
974            _trail = set()
975        elif link.casefold() in _trail:
976            raise OSError("circular link")
977        _trail.add(link.casefold())
978        return self.realpath(link, _trail)
979
980    def warn(self, message):
981        self._warnings.append(message)
982        if verbose:
983            print(message)
984
985
986class MockWinreg:
987    HKEY_LOCAL_MACHINE = "HKLM"
988    HKEY_CURRENT_USER = "HKCU"
989
990    def __init__(self, keys):
991        self.keys = {k.casefold(): v for k, v in keys.items()}
992        self.open = {}
993
994    def __repr__(self):
995        return "<MockWinreg>"
996
997    def __eq__(self, other):
998        return isinstance(other, type(self))
999
1000    def open_keys(self):
1001        return list(self.open)
1002
1003    def OpenKeyEx(self, hkey, subkey):
1004        if verbose:
1005            print(f"OpenKeyEx({hkey}, {subkey})")
1006        key = f"{hkey}\\{subkey}".casefold()
1007        if key in self.keys:
1008            self.open[key] = self.open.get(key, 0) + 1
1009            return key
1010        raise FileNotFoundError()
1011
1012    def CloseKey(self, hkey):
1013        if verbose:
1014            print(f"CloseKey({hkey})")
1015        hkey = hkey.casefold()
1016        if hkey not in self.open:
1017            raise RuntimeError("key is not open")
1018        self.open[hkey] -= 1
1019        if not self.open[hkey]:
1020            del self.open[hkey]
1021
1022    def EnumKey(self, hkey, i):
1023        if verbose:
1024            print(f"EnumKey({hkey}, {i})")
1025        hkey = hkey.casefold()
1026        if hkey not in self.open:
1027            raise RuntimeError("key is not open")
1028        prefix = f'{hkey}\\'
1029        subkeys = [k[len(prefix):] for k in sorted(self.keys) if k.startswith(prefix)]
1030        subkeys[:] = [k for k in subkeys if '\\' not in k]
1031        for j, n in enumerate(subkeys):
1032            if j == i:
1033                return n.removeprefix(prefix)
1034        raise OSError("end of enumeration")
1035
1036    def QueryValue(self, hkey, subkey):
1037        if verbose:
1038            print(f"QueryValue({hkey}, {subkey})")
1039        hkey = hkey.casefold()
1040        if hkey not in self.open:
1041            raise RuntimeError("key is not open")
1042        if subkey:
1043            subkey = subkey.casefold()
1044            hkey = f'{hkey}\\{subkey}'
1045        try:
1046            return self.keys[hkey]
1047        except KeyError:
1048            raise OSError()
1049
1050
1051class MockPosixNamespace(dict):
1052    def __init__(self, *a, argv0=None, config=None, **kw):
1053        self.update(DEFAULT_NAMESPACE)
1054        self["config"] = DEFAULT_CONFIG.copy()
1055        self["os_name"] = "posix"
1056        self["PLATLIBDIR"] = "lib"
1057        self["WITH_NEXT_FRAMEWORK"] = 0
1058        super().__init__(*a, **kw)
1059        if argv0:
1060            self["config"]["orig_argv"] = [argv0]
1061        if config:
1062            self["config"].update(config)
1063        self._files = {}
1064        self._xfiles = set()
1065        self._links = {}
1066        self._dirs = set()
1067        self._warnings = []
1068
1069    def add_known_file(self, path, lines=None):
1070        self._files[path] = list(lines or ())
1071        self.add_known_dir(path.rpartition("/")[0])
1072
1073    def add_known_xfile(self, path):
1074        self.add_known_file(path)
1075        self._xfiles.add(path)
1076
1077    def add_known_link(self, path, target):
1078        self._links[path] = target
1079
1080    def add_known_dir(self, path):
1081        p = path.rstrip("/")
1082        while p:
1083            self._dirs.add(p)
1084            p = p.rpartition("/")[0]
1085
1086    def __missing__(self, key):
1087        try:
1088            return getattr(self, key)
1089        except AttributeError:
1090            raise KeyError(key) from None
1091
1092    def abspath(self, path):
1093        if self.isabs(path):
1094            return path
1095        return self.joinpath("/Absolute", path)
1096
1097    def basename(self, path):
1098        return path.rpartition("/")[2]
1099
1100    def dirname(self, path):
1101        return path.rstrip("/").rpartition("/")[0]
1102
1103    def hassuffix(self, path, suffix):
1104        return path.endswith(suffix)
1105
1106    def isabs(self, path):
1107        return path[0:1] == "/"
1108
1109    def isdir(self, path):
1110        if verbose:
1111            print("Check if", path, "is a dir")
1112        return path in self._dirs
1113
1114    def isfile(self, path):
1115        if verbose:
1116            print("Check if", path, "is a file")
1117        return path in self._files
1118
1119    def ismodule(self, path):
1120        if verbose:
1121            print("Check if", path, "is a module")
1122        return path in self._files and path.rpartition(".")[2] == "py"
1123
1124    def isxfile(self, path):
1125        if verbose:
1126            print("Check if", path, "is an xfile")
1127        return path in self._xfiles
1128
1129    def joinpath(self, *path):
1130        return posixpath.normpath(posixpath.join(*path))
1131
1132    def readlines(self, path):
1133        try:
1134            return self._files[path]
1135        except KeyError:
1136            raise FileNotFoundError(path) from None
1137
1138    def realpath(self, path, _trail=None):
1139        if verbose:
1140            print("Read link from", path)
1141        try:
1142            link = self._links[path]
1143        except KeyError:
1144            return path
1145        if _trail is None:
1146            _trail = set()
1147        elif link in _trail:
1148            raise OSError("circular link")
1149        _trail.add(link)
1150        return self.realpath(link, _trail)
1151
1152    def warn(self, message):
1153        self._warnings.append(message)
1154        if verbose:
1155            print(message)
1156
1157
1158def diff_dict(before, after, prefix="global"):
1159    diff = []
1160    for k in sorted(before):
1161        if k[:2] == "__":
1162            continue
1163        if k == "config":
1164            diff_dict(before[k], after[k], prefix="config")
1165            continue
1166        if k in after and after[k] != before[k]:
1167            diff.append((k, before[k], after[k]))
1168    if not diff:
1169        return
1170    max_k = max(len(k) for k, _, _ in diff)
1171    indent = " " * (len(prefix) + 1 + max_k)
1172    if verbose:
1173        for k, b, a in diff:
1174            if b:
1175                print("{}.{} -{!r}\n{} +{!r}".format(prefix, k.ljust(max_k), b, indent, a))
1176            else:
1177                print("{}.{} +{!r}".format(prefix, k.ljust(max_k), a))
1178
1179
1180def dump_dict(before, after, prefix="global"):
1181    if not verbose or not after:
1182        return
1183    max_k = max(len(k) for k in after)
1184    for k, v in sorted(after.items(), key=lambda i: i[0]):
1185        if k[:2] == "__":
1186            continue
1187        if k == "config":
1188            dump_dict(before[k], after[k], prefix="config")
1189            continue
1190        try:
1191            if v != before[k]:
1192                print("{}.{} {!r} (was {!r})".format(prefix, k.ljust(max_k), v, before[k]))
1193                continue
1194        except KeyError:
1195            pass
1196        print("{}.{} {!r}".format(prefix, k.ljust(max_k), v))
1197
1198
1199def getpath(ns, keys):
1200    before = copy.deepcopy(ns)
1201    failed = True
1202    try:
1203        exec(SOURCE, ns)
1204        failed = False
1205    finally:
1206        if failed:
1207            dump_dict(before, ns)
1208        else:
1209            diff_dict(before, ns)
1210    return {
1211        k: ns['config'].get(k, ns.get(k, ...))
1212        for k in keys
1213    }
1214