Skip to content

Commit 9dce088

Browse files
rotudirk-thomas
authored andcommitted
use distutils.core.run_setup to extract more complete package information
1 parent 4d9c1c4 commit 9dce088

File tree

2 files changed

+115
-19
lines changed

2 files changed

+115
-19
lines changed

colcon_python_setup_py/package_identification/python_setup_py.py

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
# Copyright 2016-2018 Dirk Thomas
1+
# Copyright 2016-2019 Dirk Thomas
2+
# Copyright 2019 Rover Robotics via Dan Rose
23
# Licensed under the Apache License, Version 2.0
34

45
import ast
56
import distutils.core
7+
import multiprocessing
68
import os
79
from pathlib import Path
810
import runpy
@@ -13,8 +15,9 @@
1315
import subprocess
1416
import sys
1517
from threading import Lock
18+
import traceback
19+
import warnings
1620

17-
from colcon_core.package_identification import logger
1821
from colcon_core.package_identification \
1922
import PackageIdentificationExtensionPoint
2023
from colcon_core.package_identification.python import \
@@ -43,31 +46,35 @@ def identify(self, desc): # noqa: D102
4346
if not setup_py.is_file():
4447
return
4548

46-
kwargs = get_setup_arguments(setup_py)
47-
data = extract_data(**kwargs)
49+
config = get_setup_information(setup_py, env=os.environ)
50+
51+
name = config['metadata'].name
52+
if not name:
53+
raise RuntimeError(
54+
'Failed to determine Python package name in '
55+
"'{setup_py.parent}'".format_map(locals()))
4856

49-
if desc.type is not None and desc.type != 'python':
50-
logger.error('Package type already set to different value')
51-
raise RuntimeError('Package type already set to different value')
5257
desc.type = 'python'
53-
if desc.name is not None and desc.name != data['name']:
54-
logger.error('Package name already set to different value')
55-
raise RuntimeError('Package name already set to different value')
56-
desc.name = data['name']
57-
for key in ('build', 'run', 'test'):
58-
desc.dependencies[key] |= data['%s_depends' % key]
58+
if desc.name is None:
59+
desc.name = name
5960

60-
path = str(desc.path)
61+
desc.metadata['version'] = config['metadata'].version
62+
63+
for dependency_type, option_name in [
64+
('build', 'setup_requires'),
65+
('run', 'install_requires'),
66+
('test', 'tests_require')
67+
]:
68+
desc.dependencies[dependency_type] = {
69+
create_dependency_descriptor(d)
70+
for d in config[option_name] or ()}
6171

6272
def getter(env):
63-
nonlocal path
64-
return get_setup_arguments_with_context(
65-
os.path.join(path, 'setup.py'), env)
73+
nonlocal setup_py
74+
return get_setup_information(setup_py, env=env)
6675

6776
desc.metadata['get_python_setup_options'] = getter
6877

69-
desc.metadata['version'] = getter(os.environ)['version']
70-
7178

7279
cwd_lock = None
7380

@@ -84,6 +91,12 @@ def get_setup_arguments(setup_py):
8491
:returns: a dictionary containing the arguments of the setup() function
8592
:rtype: dict
8693
"""
94+
warnings.warn(
95+
'colcon_python_setup_py.package_identification.python_setup_py.'
96+
'get_setup_arguments() has been deprecated, use '
97+
'colcon_python_setup_py.package_identification.python_setup_py.'
98+
'get_setup_information() instead',
99+
stacklevel=2)
87100
global cwd_lock
88101
if not cwd_lock:
89102
cwd_lock = Lock()
@@ -137,6 +150,11 @@ def create_mock_setup_function(data):
137150
:returns: a function to replace distutils.core.setup and setuptools.setup
138151
:rtype: callable
139152
"""
153+
warnings.warn(
154+
'colcon_python_setup_py.package_identification.python_setup_py.'
155+
'create_mock_setup_function() will be removed in the future',
156+
DeprecationWarning, stacklevel=2)
157+
140158
def setup(*args, **kwargs):
141159
if args:
142160
raise RuntimeError(
@@ -160,6 +178,10 @@ def extract_data(**kwargs):
160178
:rtype: dict
161179
:raises RuntimeError: if the keywords don't contain `name`
162180
"""
181+
warnings.warn(
182+
'colcon_python_setup_py.package_identification.python_setup_py.'
183+
'extract_data() will be removed in the future',
184+
DeprecationWarning, stacklevel=2)
163185
if 'name' not in kwargs:
164186
raise RuntimeError(
165187
"setup() function invoked without the keyword argument 'name'")
@@ -186,6 +208,8 @@ def get_setup_arguments_with_context(setup_py, env):
186208
a separate Python interpreter is being used which can have an extended
187209
PYTHONPATH etc.
188210
211+
This function has been deprecated, use get_setup_information() instead.
212+
189213
:param setup_py: The path of the setup.py file
190214
:param dict env: The environment variables to use when invoking the file
191215
:returns: a dictionary containing the arguments of the setup() function
@@ -210,3 +234,70 @@ def get_setup_arguments_with_context(setup_py, env):
210234
output = result.stdout.decode('utf-8')
211235

212236
return ast.literal_eval(output)
237+
238+
239+
_process_pool = multiprocessing.Pool()
240+
241+
242+
def get_setup_information(setup_py, *, env):
243+
"""
244+
Dry run the setup.py file and get the configuration information.
245+
246+
:param Path setup_py: path to a setup.py script
247+
:param dict env: environment variables to set before running setup.py
248+
:return: dictionary of data describing the package.
249+
:raise: RuntimeError if the setup script encountered an error
250+
"""
251+
try:
252+
return _process_pool.apply(
253+
run_setup_py,
254+
kwds={
255+
'cwd': os.path.abspath(str(setup_py.parent)),
256+
'env': env,
257+
'script_args': ('--dry-run',),
258+
'stop_after': 'config'
259+
}
260+
)
261+
except Exception as e:
262+
raise RuntimeError(
263+
"Failed to dry run setup script '{setup_py}': "
264+
.format_map(locals()) + traceback.format_exc()) from e
265+
266+
267+
def run_setup_py(cwd, env, script_args=(), stop_after='run'):
268+
"""
269+
Modify the current process and run setup.py.
270+
271+
This should be run in a subprocess to not affect the state of the current
272+
process.
273+
274+
:param str cwd: absolute path to a directory containing a setup.py script
275+
:param dict env: environment variables to set before running setup.py
276+
:param script_args: command-line arguments to pass to setup.py
277+
:param stop_after: tells setup() when to stop processing
278+
:returns: the public properties of a Distribution object, minus objects
279+
with are generally not picklable
280+
"""
281+
# need to be in setup.py's parent dir to detect any setup.cfg
282+
os.chdir(cwd)
283+
284+
os.environ.clear()
285+
os.environ.update(env)
286+
287+
result = distutils.core.run_setup(
288+
'setup.py', script_args=script_args, stop_after=stop_after)
289+
290+
return {
291+
key: value for key, value in result.__dict__.items()
292+
if (
293+
# Private properties
294+
not key.startswith('_') and
295+
# Getter methods
296+
not callable(value) and
297+
# Objects that are generally not picklable
298+
key not in ('cmdclass', 'distclass', 'ext_modules') and
299+
# These *seem* useful but always have the value 0.
300+
# Look for their values in the 'metadata' object instead.
301+
key not in result.display_option_names
302+
)
303+
}

test/spell_check.words

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
apache
22
chdir
33
colcon
4+
distclass
45
iterdir
6+
kwds
57
noqa
68
pathlib
9+
picklable
710
plugin
811
pytest
912
pythonpath
1013
rtype
1114
runpy
1215
scspell
1316
setuptools
17+
stacklevel
1418
thomas
19+
traceback

0 commit comments

Comments
 (0)