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
45import ast
56import distutils .core
7+ import multiprocessing
68import os
79from pathlib import Path
810import runpy
1315import subprocess
1416import sys
1517from threading import Lock
18+ import traceback
19+ import warnings
1620
17- from colcon_core .package_identification import logger
1821from colcon_core .package_identification \
1922 import PackageIdentificationExtensionPoint
2023from 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
7279cwd_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+ }
0 commit comments