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