1313import subprocess
1414import sys
1515from threading import Lock
16+ import warnings
1617
1718from colcon_core .package_identification import logger
1819from 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
7285cwd_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 )
0 commit comments