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\Lib",
42                r"C:\Python\DLLs",
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\Lib",
68                r"C:\CPython\PCbuild\amd64",
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\Lib",
138                r"C:\Python\DLLs",
139            ],
140        )
141        actual = getpath(ns, expected)
142        self.assertEqual(expected, actual)
143
144        ns["config"]["use_environment"] = 0
145        ns["config"]["module_search_paths_set"] = 0
146        ns["config"]["module_search_paths"] = None
147        expected = dict(
148            module_search_paths_set=1,
149            module_search_paths=[
150                r"C:\Python\python98.zip",
151                r"C:\Python\Lib",
152                r"C:\Python\DLLs",
153            ],
154        )
155        actual = getpath(ns, expected)
156        self.assertEqual(expected, actual)
157
158    def test_symlink_normal_win32(self):
159        "Test a 'standard' install layout via symlink on Windows."
160        ns = MockNTNamespace(
161            argv0=r"C:\LinkedFrom\python.exe",
162            real_executable=r"C:\Python\python.exe",
163        )
164        ns.add_known_xfile(r"C:\LinkedFrom\python.exe")
165        ns.add_known_xfile(r"C:\Python\python.exe")
166        ns.add_known_link(r"C:\LinkedFrom\python.exe", r"C:\Python\python.exe")
167        ns.add_known_file(r"C:\Python\Lib\os.py")
168        ns.add_known_dir(r"C:\Python\DLLs")
169        expected = dict(
170            executable=r"C:\LinkedFrom\python.exe",
171            base_executable=r"C:\LinkedFrom\python.exe",
172            prefix=r"C:\Python",
173            exec_prefix=r"C:\Python",
174            module_search_paths_set=1,
175            module_search_paths=[
176                r"C:\Python\python98.zip",
177                r"C:\Python\Lib",
178                r"C:\Python\DLLs",
179            ],
180        )
181        actual = getpath(ns, expected)
182        self.assertEqual(expected, actual)
183
184    def test_symlink_buildtree_win32(self):
185        "Test an in-build-tree layout via symlink on Windows."
186        ns = MockNTNamespace(
187            argv0=r"C:\LinkedFrom\python.exe",
188            real_executable=r"C:\CPython\PCbuild\amd64\python.exe",
189        )
190        ns.add_known_xfile(r"C:\LinkedFrom\python.exe")
191        ns.add_known_xfile(r"C:\CPython\PCbuild\amd64\python.exe")
192        ns.add_known_link(r"C:\LinkedFrom\python.exe", r"C:\CPython\PCbuild\amd64\python.exe")
193        ns.add_known_file(r"C:\CPython\Lib\os.py")
194        ns.add_known_file(r"C:\CPython\PCbuild\amd64\pybuilddir.txt", [""])
195        expected = dict(
196            executable=r"C:\LinkedFrom\python.exe",
197            base_executable=r"C:\LinkedFrom\python.exe",
198            prefix=r"C:\CPython",
199            exec_prefix=r"C:\CPython",
200            build_prefix=r"C:\CPython",
201            _is_python_build=1,
202            module_search_paths_set=1,
203            module_search_paths=[
204                r"C:\CPython\PCbuild\amd64\python98.zip",
205                r"C:\CPython\Lib",
206                r"C:\CPython\PCbuild\amd64",
207            ],
208        )
209        actual = getpath(ns, expected)
210        self.assertEqual(expected, actual)
211
212    def test_buildtree_pythonhome_win32(self):
213        "Test an out-of-build-tree layout on Windows with PYTHONHOME override."
214        ns = MockNTNamespace(
215            argv0=r"C:\Out\python.exe",
216            real_executable=r"C:\Out\python.exe",
217            ENV_PYTHONHOME=r"C:\CPython",
218        )
219        ns.add_known_xfile(r"C:\Out\python.exe")
220        ns.add_known_file(r"C:\CPython\Lib\os.py")
221        ns.add_known_file(r"C:\Out\pybuilddir.txt", [""])
222        expected = dict(
223            executable=r"C:\Out\python.exe",
224            base_executable=r"C:\Out\python.exe",
225            prefix=r"C:\CPython",
226            exec_prefix=r"C:\CPython",
227            # This build_prefix is a miscalculation, because we have
228            # moved the output direction out of the prefix.
229            # Specify PYTHONHOME to get the correct prefix/exec_prefix
230            build_prefix="C:\\",
231            _is_python_build=1,
232            module_search_paths_set=1,
233            module_search_paths=[
234                r"C:\Out\python98.zip",
235                r"C:\CPython\Lib",
236                r"C:\Out",
237            ],
238        )
239        actual = getpath(ns, expected)
240        self.assertEqual(expected, actual)
241
242    def test_normal_posix(self):
243        "Test a 'standard' install layout on *nix"
244        ns = MockPosixNamespace(
245            PREFIX="/usr",
246            argv0="python",
247            ENV_PATH="/usr/bin",
248        )
249        ns.add_known_xfile("/usr/bin/python")
250        ns.add_known_file("/usr/lib/python9.8/os.py")
251        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
252        expected = dict(
253            executable="/usr/bin/python",
254            base_executable="/usr/bin/python",
255            prefix="/usr",
256            exec_prefix="/usr",
257            module_search_paths_set=1,
258            module_search_paths=[
259                "/usr/lib/python98.zip",
260                "/usr/lib/python9.8",
261                "/usr/lib/python9.8/lib-dynload",
262            ],
263        )
264        actual = getpath(ns, expected)
265        self.assertEqual(expected, actual)
266
267    def test_buildpath_posix(self):
268        """Test an in-build-tree layout on POSIX.
269
270        This layout is discovered from the presence of pybuilddir.txt, which
271        contains the relative path from the executable's directory to the
272        platstdlib path.
273        """
274        ns = MockPosixNamespace(
275            argv0=r"/home/cpython/python",
276            PREFIX="/usr/local",
277        )
278        ns.add_known_xfile("/home/cpython/python")
279        ns.add_known_xfile("/usr/local/bin/python")
280        ns.add_known_file("/home/cpython/pybuilddir.txt", ["build/lib.linux-x86_64-9.8"])
281        ns.add_known_file("/home/cpython/Lib/os.py")
282        ns.add_known_dir("/home/cpython/lib-dynload")
283        expected = dict(
284            executable="/home/cpython/python",
285            prefix="/usr/local",
286            exec_prefix="/usr/local",
287            base_executable="/home/cpython/python",
288            build_prefix="/home/cpython",
289            _is_python_build=1,
290            module_search_paths_set=1,
291            module_search_paths=[
292                "/usr/local/lib/python98.zip",
293                "/home/cpython/Lib",
294                "/home/cpython/build/lib.linux-x86_64-9.8",
295            ],
296        )
297        actual = getpath(ns, expected)
298        self.assertEqual(expected, actual)
299
300    def test_venv_posix(self):
301        "Test a venv layout on *nix."
302        ns = MockPosixNamespace(
303            argv0="python",
304            PREFIX="/usr",
305            ENV_PATH="/venv/bin:/usr/bin",
306        )
307        ns.add_known_xfile("/usr/bin/python")
308        ns.add_known_xfile("/venv/bin/python")
309        ns.add_known_file("/usr/lib/python9.8/os.py")
310        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
311        ns.add_known_file("/venv/pyvenv.cfg", [
312            r"home = /usr/bin"
313        ])
314        expected = dict(
315            executable="/venv/bin/python",
316            prefix="/usr",
317            exec_prefix="/usr",
318            base_executable="/usr/bin/python",
319            base_prefix="/usr",
320            base_exec_prefix="/usr",
321            module_search_paths_set=1,
322            module_search_paths=[
323                "/usr/lib/python98.zip",
324                "/usr/lib/python9.8",
325                "/usr/lib/python9.8/lib-dynload",
326            ],
327        )
328        actual = getpath(ns, expected)
329        self.assertEqual(expected, actual)
330
331    def test_symlink_normal_posix(self):
332        "Test a 'standard' install layout via symlink on *nix"
333        ns = MockPosixNamespace(
334            PREFIX="/usr",
335            argv0="/linkfrom/python",
336        )
337        ns.add_known_xfile("/linkfrom/python")
338        ns.add_known_xfile("/usr/bin/python")
339        ns.add_known_link("/linkfrom/python", "/usr/bin/python")
340        ns.add_known_file("/usr/lib/python9.8/os.py")
341        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
342        expected = dict(
343            executable="/linkfrom/python",
344            base_executable="/linkfrom/python",
345            prefix="/usr",
346            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_symlink_buildpath_posix(self):
358        """Test an in-build-tree layout on POSIX.
359
360        This layout is discovered from the presence of pybuilddir.txt, which
361        contains the relative path from the executable's directory to the
362        platstdlib path.
363        """
364        ns = MockPosixNamespace(
365            argv0=r"/linkfrom/python",
366            PREFIX="/usr/local",
367        )
368        ns.add_known_xfile("/linkfrom/python")
369        ns.add_known_xfile("/home/cpython/python")
370        ns.add_known_link("/linkfrom/python", "/home/cpython/python")
371        ns.add_known_xfile("/usr/local/bin/python")
372        ns.add_known_file("/home/cpython/pybuilddir.txt", ["build/lib.linux-x86_64-9.8"])
373        ns.add_known_file("/home/cpython/Lib/os.py")
374        ns.add_known_dir("/home/cpython/lib-dynload")
375        expected = dict(
376            executable="/linkfrom/python",
377            prefix="/usr/local",
378            exec_prefix="/usr/local",
379            base_executable="/linkfrom/python",
380            build_prefix="/home/cpython",
381            _is_python_build=1,
382            module_search_paths_set=1,
383            module_search_paths=[
384                "/usr/local/lib/python98.zip",
385                "/home/cpython/Lib",
386                "/home/cpython/build/lib.linux-x86_64-9.8",
387            ],
388        )
389        actual = getpath(ns, expected)
390        self.assertEqual(expected, actual)
391
392    def test_custom_platlibdir_posix(self):
393        "Test an install with custom platlibdir on *nix"
394        ns = MockPosixNamespace(
395            PREFIX="/usr",
396            argv0="/linkfrom/python",
397            PLATLIBDIR="lib64",
398        )
399        ns.add_known_xfile("/usr/bin/python")
400        ns.add_known_file("/usr/lib64/python9.8/os.py")
401        ns.add_known_dir("/usr/lib64/python9.8/lib-dynload")
402        expected = dict(
403            executable="/linkfrom/python",
404            base_executable="/linkfrom/python",
405            prefix="/usr",
406            exec_prefix="/usr",
407            module_search_paths_set=1,
408            module_search_paths=[
409                "/usr/lib64/python98.zip",
410                "/usr/lib64/python9.8",
411                "/usr/lib64/python9.8/lib-dynload",
412            ],
413        )
414        actual = getpath(ns, expected)
415        self.assertEqual(expected, actual)
416
417    def test_venv_macos(self):
418        """Test a venv layout on macOS.
419
420        This layout is discovered when 'executable' and 'real_executable' match,
421        but $__PYVENV_LAUNCHER__ has been set to the original process.
422        """
423        ns = MockPosixNamespace(
424            os_name="darwin",
425            argv0="/usr/bin/python",
426            PREFIX="/usr",
427            ENV___PYVENV_LAUNCHER__="/framework/Python9.8/python",
428            real_executable="/usr/bin/python",
429        )
430        ns.add_known_xfile("/usr/bin/python")
431        ns.add_known_xfile("/framework/Python9.8/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("/framework/Python9.8/pyvenv.cfg", [
435            "home = /usr/bin"
436        ])
437        expected = dict(
438            executable="/framework/Python9.8/python",
439            prefix="/usr",
440            exec_prefix="/usr",
441            base_executable="/usr/bin/python",
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_macos(self):
455        "Test a 'standard' install layout via symlink on macOS"
456        ns = MockPosixNamespace(
457            os_name="darwin",
458            PREFIX="/usr",
459            argv0="python",
460            ENV_PATH="/linkfrom:/usr/bin",
461            # real_executable on macOS matches the invocation path
462            real_executable="/linkfrom/python",
463        )
464        ns.add_known_xfile("/linkfrom/python")
465        ns.add_known_xfile("/usr/bin/python")
466        ns.add_known_link("/linkfrom/python", "/usr/bin/python")
467        ns.add_known_file("/usr/lib/python9.8/os.py")
468        ns.add_known_dir("/usr/lib/python9.8/lib-dynload")
469        expected = dict(
470            executable="/linkfrom/python",
471            base_executable="/linkfrom/python",
472            prefix="/usr",
473            exec_prefix="/usr",
474            module_search_paths_set=1,
475            module_search_paths=[
476                "/usr/lib/python98.zip",
477                "/usr/lib/python9.8",
478                "/usr/lib/python9.8/lib-dynload",
479            ],
480        )
481        actual = getpath(ns, expected)
482        self.assertEqual(expected, actual)
483
484    def test_symlink_buildpath_macos(self):
485        """Test an in-build-tree layout via symlink on macOS.
486
487        This layout is discovered from the presence of pybuilddir.txt, which
488        contains the relative path from the executable's directory to the
489        platstdlib path.
490        """
491        ns = MockPosixNamespace(
492            os_name="darwin",
493            argv0=r"python",
494            ENV_PATH="/linkfrom:/usr/bin",
495            PREFIX="/usr/local",
496            # real_executable on macOS matches the invocation path
497            real_executable="/linkfrom/python",
498        )
499        ns.add_known_xfile("/linkfrom/python")
500        ns.add_known_xfile("/home/cpython/python")
501        ns.add_known_link("/linkfrom/python", "/home/cpython/python")
502        ns.add_known_xfile("/usr/local/bin/python")
503        ns.add_known_file("/home/cpython/pybuilddir.txt", ["build/lib.macos-9.8"])
504        ns.add_known_file("/home/cpython/Lib/os.py")
505        ns.add_known_dir("/home/cpython/lib-dynload")
506        expected = dict(
507            executable="/linkfrom/python",
508            prefix="/usr/local",
509            exec_prefix="/usr/local",
510            base_executable="/linkfrom/python",
511            build_prefix="/home/cpython",
512            _is_python_build=1,
513            module_search_paths_set=1,
514            module_search_paths=[
515                "/usr/local/lib/python98.zip",
516                "/home/cpython/Lib",
517                "/home/cpython/build/lib.macos-9.8",
518            ],
519        )
520        actual = getpath(ns, expected)
521        self.assertEqual(expected, actual)
522
523
524# ******************************************************************************
525
526DEFAULT_NAMESPACE = dict(
527    PREFIX="",
528    EXEC_PREFIX="",
529    PYTHONPATH="",
530    VPATH="",
531    PLATLIBDIR="",
532    PYDEBUGEXT="",
533    VERSION_MAJOR=9,    # fixed version number for ease
534    VERSION_MINOR=8,    # of testing
535    PYWINVER=None,
536    EXE_SUFFIX=None,
537
538    ENV_PATH="",
539    ENV_PYTHONHOME="",
540    ENV_PYTHONEXECUTABLE="",
541    ENV___PYVENV_LAUNCHER__="",
542    argv0="",
543    py_setpath="",
544    real_executable="",
545    executable_dir="",
546    library="",
547    winreg=None,
548    build_prefix=None,
549    venv_prefix=None,
550)
551
552DEFAULT_CONFIG = dict(
553    home=None,
554    platlibdir=None,
555    pythonpath=None,
556    program_name=None,
557    prefix=None,
558    exec_prefix=None,
559    base_prefix=None,
560    base_exec_prefix=None,
561    executable=None,
562    base_executable="",
563    stdlib_dir=None,
564    platstdlib_dir=None,
565    module_search_paths=None,
566    module_search_paths_set=0,
567    pythonpath_env=None,
568    argv=None,
569    orig_argv=None,
570
571    isolated=0,
572    use_environment=1,
573    use_site=1,
574)
575
576class MockNTNamespace(dict):
577    def __init__(self, *a, argv0=None, config=None, **kw):
578        self.update(DEFAULT_NAMESPACE)
579        self["config"] = DEFAULT_CONFIG.copy()
580        self["os_name"] = "nt"
581        self["PLATLIBDIR"] = "DLLs"
582        self["PYWINVER"] = "9.8-XY"
583        self["VPATH"] = r"..\.."
584        super().__init__(*a, **kw)
585        if argv0:
586            self["config"]["orig_argv"] = [argv0]
587        if config:
588            self["config"].update(config)
589        self._files = {}
590        self._links = {}
591        self._dirs = set()
592        self._warnings = []
593
594    def add_known_file(self, path, lines=None):
595        self._files[path.casefold()] = list(lines or ())
596        self.add_known_dir(path.rpartition("\\")[0])
597
598    def add_known_xfile(self, path):
599        self.add_known_file(path)
600
601    def add_known_link(self, path, target):
602        self._links[path.casefold()] = target
603
604    def add_known_dir(self, path):
605        p = path.rstrip("\\").casefold()
606        while p:
607            self._dirs.add(p)
608            p = p.rpartition("\\")[0]
609
610    def __missing__(self, key):
611        try:
612            return getattr(self, key)
613        except AttributeError:
614            raise KeyError(key) from None
615
616    def abspath(self, path):
617        if self.isabs(path):
618            return path
619        return self.joinpath("C:\\Absolute", path)
620
621    def basename(self, path):
622        return path.rpartition("\\")[2]
623
624    def dirname(self, path):
625        name = path.rstrip("\\").rpartition("\\")[0]
626        if name[1:] == ":":
627            return name + "\\"
628        return name
629
630    def hassuffix(self, path, suffix):
631        return path.casefold().endswith(suffix.casefold())
632
633    def isabs(self, path):
634        return path[1:3] == ":\\"
635
636    def isdir(self, path):
637        if verbose:
638            print("Check if", path, "is a dir")
639        return path.casefold() in self._dirs
640
641    def isfile(self, path):
642        if verbose:
643            print("Check if", path, "is a file")
644        return path.casefold() in self._files
645
646    def ismodule(self, path):
647        if verbose:
648            print("Check if", path, "is a module")
649        path = path.casefold()
650        return path in self._files and path.rpartition(".")[2] == "py".casefold()
651
652    def isxfile(self, path):
653        if verbose:
654            print("Check if", path, "is a executable")
655        path = path.casefold()
656        return path in self._files and path.rpartition(".")[2] == "exe".casefold()
657
658    def joinpath(self, *path):
659        return ntpath.normpath(ntpath.join(*path))
660
661    def readlines(self, path):
662        try:
663            return self._files[path.casefold()]
664        except KeyError:
665            raise FileNotFoundError(path) from None
666
667    def realpath(self, path, _trail=None):
668        if verbose:
669            print("Read link from", path)
670        try:
671            link = self._links[path.casefold()]
672        except KeyError:
673            return path
674        if _trail is None:
675            _trail = set()
676        elif link.casefold() in _trail:
677            raise OSError("circular link")
678        _trail.add(link.casefold())
679        return self.realpath(link, _trail)
680
681    def warn(self, message):
682        self._warnings.append(message)
683        if verbose:
684            print(message)
685
686
687class MockWinreg:
688    HKEY_LOCAL_MACHINE = "HKLM"
689    HKEY_CURRENT_USER = "HKCU"
690
691    def __init__(self, keys):
692        self.keys = {k.casefold(): v for k, v in keys.items()}
693        self.open = {}
694
695    def __repr__(self):
696        return "<MockWinreg>"
697
698    def __eq__(self, other):
699        return isinstance(other, type(self))
700
701    def open_keys(self):
702        return list(self.open)
703
704    def OpenKeyEx(self, hkey, subkey):
705        if verbose:
706            print(f"OpenKeyEx({hkey}, {subkey})")
707        key = f"{hkey}\\{subkey}".casefold()
708        if key in self.keys:
709            self.open[key] = self.open.get(key, 0) + 1
710            return key
711        raise FileNotFoundError()
712
713    def CloseKey(self, hkey):
714        if verbose:
715            print(f"CloseKey({hkey})")
716        hkey = hkey.casefold()
717        if hkey not in self.open:
718            raise RuntimeError("key is not open")
719        self.open[hkey] -= 1
720        if not self.open[hkey]:
721            del self.open[hkey]
722
723    def EnumKey(self, hkey, i):
724        if verbose:
725            print(f"EnumKey({hkey}, {i})")
726        hkey = hkey.casefold()
727        if hkey not in self.open:
728            raise RuntimeError("key is not open")
729        prefix = f'{hkey}\\'
730        subkeys = [k[len(prefix):] for k in sorted(self.keys) if k.startswith(prefix)]
731        subkeys[:] = [k for k in subkeys if '\\' not in k]
732        for j, n in enumerate(subkeys):
733            if j == i:
734                return n.removeprefix(prefix)
735        raise OSError("end of enumeration")
736
737    def QueryValue(self, hkey):
738        if verbose:
739            print(f"QueryValue({hkey})")
740        hkey = hkey.casefold()
741        if hkey not in self.open:
742            raise RuntimeError("key is not open")
743        try:
744            return self.keys[hkey]
745        except KeyError:
746            raise OSError()
747
748
749class MockPosixNamespace(dict):
750    def __init__(self, *a, argv0=None, config=None, **kw):
751        self.update(DEFAULT_NAMESPACE)
752        self["config"] = DEFAULT_CONFIG.copy()
753        self["os_name"] = "posix"
754        self["PLATLIBDIR"] = "lib"
755        super().__init__(*a, **kw)
756        if argv0:
757            self["config"]["orig_argv"] = [argv0]
758        if config:
759            self["config"].update(config)
760        self._files = {}
761        self._xfiles = set()
762        self._links = {}
763        self._dirs = set()
764        self._warnings = []
765
766    def add_known_file(self, path, lines=None):
767        self._files[path] = list(lines or ())
768        self.add_known_dir(path.rpartition("/")[0])
769
770    def add_known_xfile(self, path):
771        self.add_known_file(path)
772        self._xfiles.add(path)
773
774    def add_known_link(self, path, target):
775        self._links[path] = target
776
777    def add_known_dir(self, path):
778        p = path.rstrip("/")
779        while p:
780            self._dirs.add(p)
781            p = p.rpartition("/")[0]
782
783    def __missing__(self, key):
784        try:
785            return getattr(self, key)
786        except AttributeError:
787            raise KeyError(key) from None
788
789    def abspath(self, path):
790        if self.isabs(path):
791            return path
792        return self.joinpath("/Absolute", path)
793
794    def basename(self, path):
795        return path.rpartition("/")[2]
796
797    def dirname(self, path):
798        return path.rstrip("/").rpartition("/")[0]
799
800    def hassuffix(self, path, suffix):
801        return path.endswith(suffix)
802
803    def isabs(self, path):
804        return path[0:1] == "/"
805
806    def isdir(self, path):
807        if verbose:
808            print("Check if", path, "is a dir")
809        return path in self._dirs
810
811    def isfile(self, path):
812        if verbose:
813            print("Check if", path, "is a file")
814        return path in self._files
815
816    def ismodule(self, path):
817        if verbose:
818            print("Check if", path, "is a module")
819        return path in self._files and path.rpartition(".")[2] == "py"
820
821    def isxfile(self, path):
822        if verbose:
823            print("Check if", path, "is an xfile")
824        return path in self._xfiles
825
826    def joinpath(self, *path):
827        return posixpath.normpath(posixpath.join(*path))
828
829    def readlines(self, path):
830        try:
831            return self._files[path]
832        except KeyError:
833            raise FileNotFoundError(path) from None
834
835    def realpath(self, path, _trail=None):
836        if verbose:
837            print("Read link from", path)
838        try:
839            link = self._links[path]
840        except KeyError:
841            return path
842        if _trail is None:
843            _trail = set()
844        elif link in _trail:
845            raise OSError("circular link")
846        _trail.add(link)
847        return self.realpath(link, _trail)
848
849    def warn(self, message):
850        self._warnings.append(message)
851        if verbose:
852            print(message)
853
854
855def diff_dict(before, after, prefix="global"):
856    diff = []
857    for k in sorted(before):
858        if k[:2] == "__":
859            continue
860        if k == "config":
861            diff_dict(before[k], after[k], prefix="config")
862            continue
863        if k in after and after[k] != before[k]:
864            diff.append((k, before[k], after[k]))
865    if not diff:
866        return
867    max_k = max(len(k) for k, _, _ in diff)
868    indent = " " * (len(prefix) + 1 + max_k)
869    if verbose:
870        for k, b, a in diff:
871            if b:
872                print("{}.{} -{!r}\n{} +{!r}".format(prefix, k.ljust(max_k), b, indent, a))
873            else:
874                print("{}.{} +{!r}".format(prefix, k.ljust(max_k), a))
875
876
877def dump_dict(before, after, prefix="global"):
878    if not verbose or not after:
879        return
880    max_k = max(len(k) for k in after)
881    for k, v in sorted(after.items(), key=lambda i: i[0]):
882        if k[:2] == "__":
883            continue
884        if k == "config":
885            dump_dict(before[k], after[k], prefix="config")
886            continue
887        try:
888            if v != before[k]:
889                print("{}.{} {!r} (was {!r})".format(prefix, k.ljust(max_k), v, before[k]))
890                continue
891        except KeyError:
892            pass
893        print("{}.{} {!r}".format(prefix, k.ljust(max_k), v))
894
895
896def getpath(ns, keys):
897    before = copy.deepcopy(ns)
898    failed = True
899    try:
900        exec(SOURCE, ns)
901        failed = False
902    finally:
903        if failed:
904            dump_dict(before, ns)
905        else:
906            diff_dict(before, ns)
907    return {
908        k: ns['config'].get(k, ns.get(k, ...))
909        for k in keys
910    }
911