-
Notifications
You must be signed in to change notification settings - Fork 21
/
projectdata.py
259 lines (210 loc) · 8.63 KB
/
projectdata.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# Extracts information from a project that has a distutils setup.py file.
import build
import email
import email.policy
import logging
import os
import pathlib
import pep517
import sys
import tempfile
import tokenize
from copy import copy
from distutils import core
from setuptools import config
METADATA_MAP = {
"summary": "description",
"classifier": "classifiers",
"project_url": "project_urls",
"home_page": "url",
"description": "long_description",
"description_content_type": "long_description_content_type",
"requires_python": "python_requires",
}
def get_build_data(path):
with tempfile.TemporaryDirectory() as tempdir:
metadata_dir = build.ProjectBuilder(str(path), runner=pep517.quiet_subprocess_runner).prepare("wheel", tempdir)
with open(pathlib.Path(metadata_dir) / "METADATA", "rb") as metadata_file:
metadata = email.message_from_binary_file(metadata_file, policy=email.policy.compat32)
if "Description" not in metadata.keys():
# Having the description as a payload tends to add two newlines, we clean that up here:
long_description = metadata.get_payload().strip() + "\n"
data = {"long_description": long_description}
for key in set(metadata.keys()):
value = metadata.get_all(key)
if len(value) == 1:
value = value[0]
if value.strip() == "UNKNOWN":
continue
key = key.lower().replace("-", "_")
if key in METADATA_MAP:
key = METADATA_MAP[key]
data[key] = value
return data
def get_setupcfg_data(path):
# Note: By default, setup.cfg will read the pyroma.git/setup.cfg - forcing explicit setup.cfg under test's file path
data = config.setupcfg.read_configuration(str(pathlib.Path(path) / "setup.cfg"))
metadata = data["metadata"]
return metadata
def get_data(path):
try:
return get_build_data(path)
except build.BuildException as e:
if "no pyproject.toml or setup.py" in e.args[0]:
# It couldn't build the package, because there is no setup.py or pyproject.toml.
# Let's see if there is a setup.cfg:
try:
metadata = get_setupcfg_data(path)
# Yes, there's a setup.cfg. Pyroma accepted this earlier, but that was probably
# a mistake. For the time being, warn for it, but in a future version just fail.
metadata["_missing_build_system"] = True
return metadata
except Exception:
# No, that didn't work. Hide this second exception and raise the first:
pass
raise e
except Exception:
logging.exception("Exception raised during metadata preparation")
metadata = get_setuppy_data(path)
metadata["_stoneage_setuppy"] = True
return metadata
class FakeContext:
def __init__(self, path):
self._path = path
def __enter__(self):
self._old_path = os.path.abspath(os.curdir)
if self._old_path in sys.path:
sys.path.remove(self._old_path)
os.chdir(self._path)
if self._path not in sys.path:
sys.path.insert(0, self._path)
self._path_appended = True
else:
self._path_appended = False
def __exit__(self, exc_type, exc_val, exc_tb):
if self._path_appended:
sys.path.remove(self._path)
sys.path.append(self._old_path)
os.chdir(self._old_path)
class SetupMonkey:
used_setuptools = False
def distutils_setup_replacement(self, **kw):
self._distutils_setup(**kw)
def setuptools_setup_replacement(self, **kw):
self.used_setuptools = True
self._setuptools_setup(**kw)
def get_data(self):
return self._kw
def __enter__(self):
import distutils.core
self._distutils_setup = distutils.core.setup
distutils.core.setup = self.distutils_setup_replacement
try:
import setuptools
self._setuptools_setup = setuptools.setup
setuptools.setup = self.setuptools_setup_replacement
except ImportError:
self._setuptools_setup = None
self._kw = {}
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import distutils.core
distutils.core.setup = self._distutils_setup
if self._setuptools_setup is not None:
import setuptools
setuptools.setup = self._setuptools_setup
# This is a version of distutils run_setup() that doesn't give
# up just because Setuptools throws errors if you try to exec it.
def run_setup(script_name, script_args=None, stop_after="run"):
"""Run a setup script in a somewhat controlled environment, and
return the Distribution instance that drives things. This is useful
if you need to find out the distribution meta-data (passed as
keyword args from 'script' to 'setup()', or the contents of the
config files or command-line.
'script_name' is a file that will be run with 'execfile()';
'sys.argv[0]' will be replaced with 'script' for the duration of the
call. 'script_args' is a list of strings; if supplied,
'sys.argv[1:]' will be replaced by 'script_args' for the duration of
the call.
'stop_after' tells 'setup()' when to stop processing; possible
values:
init
stop after the Distribution instance has been created and
populated with the keyword arguments to 'setup()'
config
stop after config files have been parsed (and their data
stored in the Distribution instance)
commandline
stop after the command-line ('sys.argv[1:]' or 'script_args')
have been parsed (and the data stored in the Distribution)
run [default]
stop after all commands have been run (the same as if 'setup()'
had been called in the usual way
Returns the Distribution instance, which provides all information
used to drive the Distutils.
"""
if stop_after not in ("init", "config", "commandline", "run"):
raise ValueError(f"invalid value for 'stop_after': {stop_after!r}")
core._setup_stop_after = stop_after
save_argv = copy(sys.argv)
glocals = copy(globals())
glocals["__file__"] = script_name
glocals["__name__"] = "__main__"
try:
try:
sys.argv[0] = script_name
if script_args is not None:
sys.argv[1:] = script_args
with tokenize.open(script_name) as f:
exec(f.read(), glocals, glocals)
finally:
sys.argv = save_argv
core._setup_stop_after = None
except Exception:
logging.warning("Exception when running setup.", exc_info=True)
if core._setup_distribution is None:
raise RuntimeError(
f"'distutils.core.setup()' was never called -- perhaps '{script_name}' is not a Distutils setup script?"
)
# I wonder if the setup script's namespace -- g and l -- would be of
# any interest to callers?
return core._setup_distribution
def get_setuppy_data(path):
"""
Returns data from a package directory.
'path' should be an absolute path.
"""
metadata = {}
# Run the imported setup to get the metadata.
with FakeContext(path):
with SetupMonkey() as sm:
if os.path.isfile("setup.py"):
try:
distro = run_setup("setup.py", stop_after="config")
metadata = {}
for k, v in distro.metadata.__dict__.items():
if k[0] == "_" or not v:
continue
if all(not x for x in v):
continue
metadata[k] = v
if sm.used_setuptools:
for extras in ["cmdclass", "zip_safe", "test_suite"]:
v = getattr(distro, extras, None)
if v is not None and v not in ([], {}):
metadata[extras] = v
except Exception as e:
# Looks like setup.py is broken.
logging.exception(e)
metadata = {}
elif os.path.isfile("setup.cfg"):
try:
from setuptools import config
data = config.read_configuration("setup.cfg")
metadata = data["metadata"]
metadata["_setuptools"] = True
except Exception as e:
logging.exception(e)
else:
logging.exception("Neither setup.py nor setup.cfg was found")
return metadata