1import re
2import sys
3import string
4import subprocess
5import venv
6from tempfile import TemporaryDirectory
7
8from path import Path
9
10
11def remove_all(paths):
12    for path in paths:
13        path.rmtree() if path.isdir() else path.remove()
14
15
16def update_vendored():
17    update_pkg_resources()
18    update_setuptools()
19
20
21def rewrite_packaging(pkg_files, new_root):
22    """
23    Rewrite imports in packaging to redirect to vendored copies.
24    """
25    for file in pkg_files.glob('*.py'):
26        text = file.text()
27        text = re.sub(r' (pyparsing)', rf' {new_root}.\1', text)
28        text = text.replace(
29            'from six.moves.urllib import parse',
30            'from urllib import parse',
31        )
32        file.write_text(text)
33
34
35def rewrite_jaraco_text(pkg_files, new_root):
36    """
37    Rewrite imports in jaraco.text to redirect to vendored copies.
38    """
39    for file in pkg_files.glob('*.py'):
40        text = file.read_text()
41        text = re.sub(r' (jaraco\.)', rf' {new_root}.\1', text)
42        text = re.sub(r' (importlib_resources)', rf' {new_root}.\1', text)
43        # suppress loading of lorem_ipsum; ref #3072
44        text = re.sub(r'^lorem_ipsum.*\n$', '', text, flags=re.M)
45        file.write_text(text)
46
47
48def rewrite_jaraco(pkg_files, new_root):
49    """
50    Rewrite imports in jaraco.functools to redirect to vendored copies.
51    """
52    for file in pkg_files.glob('*.py'):
53        text = file.read_text()
54        text = re.sub(r' (more_itertools)', rf' {new_root}.\1', text)
55        file.write_text(text)
56    # required for zip-packaged setuptools #3084
57    pkg_files.joinpath('__init__.py').write_text('')
58
59
60def rewrite_importlib_resources(pkg_files, new_root):
61    """
62    Rewrite imports in importlib_resources to redirect to vendored copies.
63    """
64    for file in pkg_files.glob('*.py'):
65        text = file.read_text().replace('importlib_resources.abc', '.abc')
66        text = text.replace('zipp', '..zipp')
67        file.write_text(text)
68
69
70def rewrite_importlib_metadata(pkg_files, new_root):
71    """
72    Rewrite imports in importlib_metadata to redirect to vendored copies.
73    """
74    for file in pkg_files.glob('*.py'):
75        text = file.read_text().replace('typing_extensions', '..typing_extensions')
76        text = text.replace('import zipp', 'from .. import zipp')
77        file.write_text(text)
78
79
80def rewrite_more_itertools(pkg_files: Path):
81    """
82    Defer import of concurrent.futures. Workaround for #3090.
83    """
84    more_file = pkg_files.joinpath('more.py')
85    text = more_file.read_text()
86    text = re.sub(r'^.*concurrent.futures.*?\n', '', text, flags=re.MULTILINE)
87    text = re.sub(
88        'ThreadPoolExecutor',
89        '__import__("concurrent.futures").futures.ThreadPoolExecutor',
90        text,
91    )
92    more_file.write_text(text)
93
94
95def rewrite_nspektr(pkg_files: Path, new_root):
96    for file in pkg_files.glob('*.py'):
97        text = file.read_text()
98        text = re.sub(r' (more_itertools)', rf' {new_root}.\1', text)
99        text = re.sub(r' (jaraco\.\w+)', rf' {new_root}.\1', text)
100        text = re.sub(r' (packaging)', rf' {new_root}.\1', text)
101        text = re.sub(r' (importlib_metadata)', rf' {new_root}.\1', text)
102        file.write_text(text)
103
104
105def clean(vendor):
106    """
107    Remove all files out of the vendor directory except the meta
108    data (as pip uninstall doesn't support -t).
109    """
110    remove_all(
111        path
112        for path in vendor.glob('*')
113        if path.basename() != 'vendored.txt'
114    )
115
116
117def install(vendor):
118    clean(vendor)
119    install_args = [
120        sys.executable,
121        '-m', 'pip',
122        'install',
123        '-r', str(vendor / 'vendored.txt'),
124        '-t', str(vendor),
125    ]
126    subprocess.check_call(install_args)
127    (vendor / '__init__.py').write_text('')
128
129
130def update_pkg_resources():
131    vendor = Path('pkg_resources/_vendor')
132    install(vendor)
133    rewrite_packaging(vendor / 'packaging', 'pkg_resources.extern')
134    rewrite_jaraco_text(vendor / 'jaraco/text', 'pkg_resources.extern')
135    rewrite_jaraco(vendor / 'jaraco', 'pkg_resources.extern')
136    rewrite_importlib_resources(vendor / 'importlib_resources', 'pkg_resources.extern')
137    rewrite_more_itertools(vendor / "more_itertools")
138
139
140def update_setuptools():
141    vendor = Path('setuptools/_vendor')
142    install(vendor)
143    install_validate_pyproject(vendor)
144    rewrite_packaging(vendor / 'packaging', 'setuptools.extern')
145    rewrite_jaraco_text(vendor / 'jaraco/text', 'setuptools.extern')
146    rewrite_jaraco(vendor / 'jaraco', 'setuptools.extern')
147    rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern')
148    rewrite_importlib_metadata(vendor / 'importlib_metadata', 'setuptools.extern')
149    rewrite_more_itertools(vendor / "more_itertools")
150    rewrite_nspektr(vendor / "nspektr", 'setuptools.extern')
151
152
153def install_validate_pyproject(vendor):
154    """``validate-pyproject`` can be vendorized to remove all dependencies"""
155    req = next(
156        (x for x in (vendor / "vendored.txt").lines() if 'validate-pyproject' in x),
157        "validate-pyproject[all]"
158    )
159
160    pkg, _, _ = req.strip(string.whitespace + "#").partition("#")
161    pkg = pkg.strip()
162
163    opts = {}
164    if sys.version_info[:2] >= (3, 10):
165        opts["ignore_cleanup_errors"] = True
166
167    with TemporaryDirectory(**opts) as tmp:
168        env_builder = venv.EnvBuilder(with_pip=True)
169        env_builder.create(tmp)
170        context = env_builder.ensure_directories(tmp)
171        venv_python = getattr(context, 'env_exec_cmd', context.env_exe)
172
173        subprocess.check_call([venv_python, "-m", "pip", "install", pkg])
174        cmd = [
175            venv_python,
176            "-m",
177            "validate_pyproject.vendoring",
178            f"--output-dir={vendor / '_validate_pyproject' !s}",
179            "--enable-plugins",
180            "setuptools",
181            "distutils",
182            "--very-verbose"
183        ]
184        subprocess.check_call(cmd)
185
186
187__name__ == '__main__' and update_vendored()
188