Skip to content

Commit 3377416

Browse files
authored
use distutils.core.run_setup to get package information (#30)
* use distutils.core.run_setup to get package information * cache get_setup_information() results
1 parent 7ddc7ac commit 3377416

File tree

2 files changed

+109
-11
lines changed

2 files changed

+109
-11
lines changed

colcon_python_setup_py/package_identification/python_setup_py.py

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import subprocess
1414
import sys
1515
from threading import Lock
16+
import warnings
1617

1718
from colcon_core.package_identification import logger
1819
from colcon_core.package_identification \
@@ -43,30 +44,42 @@ def identify(self, desc): # noqa: D102
4344
if not setup_py.is_file():
4445
return
4546

46-
kwargs = get_setup_arguments(setup_py)
47-
data = extract_data(**kwargs)
47+
config = get_setup_information(setup_py)
4848

4949
if desc.type is not None and desc.type != 'python':
5050
logger.error('Package type already set to different value')
5151
raise RuntimeError('Package type already set to different value')
5252
desc.type = 'python'
53-
if desc.name is not None and desc.name != data['name']:
53+
54+
name = config['metadata'].name
55+
if not name:
56+
logger.error(
57+
'Failed to determine Python package name in '
58+
"'{setup_py.parent}'".format_map(locals()))
59+
raise RuntimeError(
60+
'Failed to determine Python package name in '
61+
"'{setup_py.parent}'".format_map(locals()))
62+
if desc.name is not None and desc.name != name:
5463
logger.error('Package name already set to different value')
5564
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]
65+
desc.name = name
5966

60-
path = str(desc.path)
67+
for dependency_type, option_name in [
68+
('build', 'setup_requires'),
69+
('run', 'install_requires'),
70+
('test', 'tests_require')
71+
]:
72+
desc.dependencies[dependency_type] = {
73+
create_dependency_descriptor(d)
74+
for d in config[option_name] or ()}
6175

6276
def getter(env):
63-
nonlocal path
64-
return get_setup_arguments_with_context(
65-
os.path.join(path, 'setup.py'), env)
77+
nonlocal setup_py
78+
return get_setup_information(setup_py, env=env)
6679

6780
desc.metadata['get_python_setup_options'] = getter
6881

69-
desc.metadata['version'] = getter(os.environ)['version']
82+
desc.metadata['version'] = config['metadata'].version
7083

7184

7285
cwd_lock = None
@@ -84,6 +97,12 @@ def get_setup_arguments(setup_py):
8497
:returns: a dictionary containing the arguments of the setup() function
8598
:rtype: dict
8699
"""
100+
warnings.warn(
101+
'colcon_python_setup_py.package_identification.python_setup_py.'
102+
'get_setup_arguments() has been deprecated, use '
103+
'colcon_python_setup_py.package_identification.python_setup_py.'
104+
'get_setup_information() instead',
105+
stacklevel=2)
87106
global cwd_lock
88107
if not cwd_lock:
89108
cwd_lock = Lock()
@@ -137,6 +156,11 @@ def create_mock_setup_function(data):
137156
:returns: a function to replace distutils.core.setup and setuptools.setup
138157
:rtype: callable
139158
"""
159+
warnings.warn(
160+
'colcon_python_setup_py.package_identification.python_setup_py.'
161+
'create_mock_setup_function() will be removed in the future',
162+
DeprecationWarning, stacklevel=2)
163+
140164
def setup(*args, **kwargs):
141165
if args:
142166
raise RuntimeError(
@@ -160,6 +184,10 @@ def extract_data(**kwargs):
160184
:rtype: dict
161185
:raises RuntimeError: if the keywords don't contain `name`
162186
"""
187+
warnings.warn(
188+
'colcon_python_setup_py.package_identification.python_setup_py.'
189+
'extract_data() will be removed in the future',
190+
DeprecationWarning, stacklevel=2)
163191
if 'name' not in kwargs:
164192
raise RuntimeError(
165193
"setup() function invoked without the keyword argument 'name'")
@@ -186,6 +214,8 @@ def get_setup_arguments_with_context(setup_py, env):
186214
a separate Python interpreter is being used which can have an extended
187215
PYTHONPATH etc.
188216
217+
This function has been deprecated, use get_setup_information() instead.
218+
189219
:param setup_py: The path of the setup.py file
190220
:param dict env: The environment variables to use when invoking the file
191221
:returns: a dictionary containing the arguments of the setup() function
@@ -210,3 +240,67 @@ def get_setup_arguments_with_context(setup_py, env):
210240
output = result.stdout.decode('utf-8')
211241

212242
return ast.literal_eval(output)
243+
244+
245+
_setup_information_cache = {}
246+
247+
248+
def get_setup_information(setup_py, *, env=None):
249+
"""
250+
Dry run the setup.py file and get the configuration information.
251+
252+
A repeated invocation with the same arguments returns a cached result.
253+
254+
:param Path setup_py: path to a setup.py script
255+
:param dict env: environment variables to set before running setup.py
256+
:return: dictionary of data describing the package.
257+
:raise: RuntimeError if the setup script encountered an error
258+
"""
259+
global _setup_information_cache
260+
if env is None:
261+
env = os.environ
262+
hashable_env = (setup_py, ) + tuple(sorted(env.items()))
263+
if hashable_env not in _setup_information_cache:
264+
_setup_information_cache[hashable_env] = _get_setup_information(
265+
setup_py, env=env)
266+
return _setup_information_cache[hashable_env]
267+
268+
269+
def _get_setup_information(setup_py, *, env=None):
270+
code_lines = [
271+
'import sys',
272+
'from distutils.core import run_setup',
273+
274+
'dist = run_setup('
275+
" 'setup.py', script_args=('--dry-run',), stop_after='config')",
276+
277+
"skip_keys = ('cmdclass', 'distclass', 'ext_modules', 'metadata')",
278+
'data = {'
279+
' key: value for key, value in dist.__dict__.items() '
280+
' if ('
281+
# skip private properties
282+
" not key.startswith('_') and "
283+
# skip methods
284+
' not callable(value) and '
285+
# skip objects whose representation can't be evaluated
286+
' key not in skip_keys and '
287+
# skip display options since they have no value, using metadata instead
288+
' key not in dist.display_option_names'
289+
' )'
290+
'}',
291+
"data['metadata'] = {"
292+
' k: v for k, v in dist.metadata.__dict__.items() '
293+
# skip values with custom type OrderedSet
294+
" if k not in ('license_files', 'provides_extras')}",
295+
296+
"sys.stdout.buffer.write(repr(data).encode('utf-8'))"]
297+
298+
# invoke distutils.core.run_setup() in a separate interpreter
299+
cmd = [
300+
sys.executable, '-c', ';'.join(line.lstrip() for line in code_lines)]
301+
result = subprocess.run(
302+
cmd, stdout=subprocess.PIPE,
303+
cwd=os.path.abspath(str(setup_py.parent)), check=True, env=env)
304+
output = result.stdout.decode('utf-8')
305+
306+
return ast.literal_eval(output)

test/spell_check.words

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
apache
22
chdir
33
colcon
4+
distclass
5+
hashable
46
iterdir
7+
lstrip
58
noqa
69
pathlib
710
plugin
@@ -11,4 +14,5 @@ rtype
1114
runpy
1215
scspell
1316
setuptools
17+
stacklevel
1418
thomas

0 commit comments

Comments
 (0)