Skip to content

Commit 192fcda

Browse files
committed
switch to ProcessPool
It turns out most of what I was doing with multiprocessing.Process was reinventing Pool.apply. This also naturally throttles the number of simultaneous processes, which was a possible concern. Move setup.py-specific stuff to run_setup_py. I expect to reuse this for python build, plus isolating it makes the subprocess workload minimal.
1 parent 1864386 commit 192fcda

File tree

2 files changed

+64
-59
lines changed

2 files changed

+64
-59
lines changed

colcon_core/package_identification/python.py

Lines changed: 18 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Copyright 2016-2019 Dirk Thomas
2-
# Copyright 2019 Rover Robotics
2+
# Copyright 2019 Rover Robotics via Dan Rose
33
# Licensed under the Apache License, Version 2.0
44

55
import multiprocessing
66
import os
7+
from traceback import format_exc
78
from typing import Optional
89
import warnings
910

@@ -12,9 +13,12 @@
1213
from colcon_core.package_identification \
1314
import PackageIdentificationExtensionPoint
1415
from colcon_core.plugin_system import satisfies_version
16+
from colcon_core.run_setup_py import run_setup_py
1517
from distlib.util import parse_requirement
1618
from distlib.version import NormalizedVersion
1719

20+
_process_pool = multiprocessing.Pool()
21+
1822

1923
class PythonPackageIdentification(PackageIdentificationExtensionPoint):
2024
"""Identify Python packages with `setup.py` and opt. `setup.cfg` files."""
@@ -104,65 +108,20 @@ def get_setup_result(setup_py, *, env: Optional[dict]):
104108
if env is not None:
105109
env_copy.update(env)
106110

107-
conn_recv, conn_send = multiprocessing.Pipe(duplex=False)
108-
with conn_send:
109-
p = multiprocessing.Process(
110-
target=_get_setup_result_target,
111-
args=(os.path.abspath(str(setup_py)), env_copy, conn_send),
112-
)
113-
p.start()
114-
p.join()
115-
with conn_recv:
116-
result_or_exception_string = conn_recv.recv()
117-
118-
if isinstance(result_or_exception_string, dict):
119-
return result_or_exception_string
120-
raise RuntimeError(
121-
'Failure when trying to run setup script {}:\n{}'
122-
.format(setup_py, result_or_exception_string))
123-
124-
125-
def _get_setup_result_target(setup_py: str, env: dict, conn_send):
126-
"""
127-
Run setup.py in a modified environment.
128-
129-
Helper function for get_setup_metadata. The resulting dict or error
130-
will be sent via conn_send instead of returned or thrown.
131-
132-
:param setup_py: Absolute path to a setup.py script
133-
:param env: Environment variables to set before running setup.py
134-
:param conn_send: Connection to send the result as either a dict or an
135-
error string
136-
"""
137-
import distutils.core
138-
import traceback
139111
try:
140-
# need to be in setup.py's parent dir to detect any setup.cfg
141-
os.chdir(os.path.dirname(setup_py))
142-
143-
os.environ.clear()
144-
os.environ.update(env)
145-
146-
result = distutils.core.run_setup(
147-
str(setup_py), ('--dry-run',), stop_after='config')
148-
149-
# could just return all attrs in result.__dict__, but we take this
150-
# opportunity to filter a few things that don't need to be there
151-
conn_send.send({
152-
attr: value for attr, value in result.__dict__.items()
153-
if (
154-
# These *seem* useful but always have the value 0.
155-
# Look for their values in the 'metadata' object instead.
156-
attr not in result.display_option_names
157-
# Getter methods
158-
and not callable(value)
159-
# Private properties
160-
and not attr.startswith('_')
161-
# Objects that are generally not picklable
162-
and attr not in ('cmdclass', 'distclass', 'ext_modules')
163-
)})
164-
except BaseException:
165-
conn_send.send(traceback.format_exc())
112+
return _process_pool.apply(
113+
run_setup_py,
114+
kwds=dict(
115+
cwd=os.path.abspath(str(setup_py.parent)),
116+
env=env_copy,
117+
setup_args=('--dry-run',),
118+
stop_after='config'
119+
)
120+
)
121+
except Exception as e:
122+
raise RuntimeError(
123+
'Failure when trying to run setup script {}: {}'
124+
.format(setup_py, format_exc())) from e
166125

167126

168127
def create_dependency_descriptor(requirement_string):

colcon_core/run_setup_py.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2019 Rover Robotics via Dan Rose
2+
# Licensed under the Apache License, Version 2.0
3+
4+
import os
5+
import distutils.core
6+
7+
8+
def run_setup_py(cwd: str, env: dict, script_args=(), stop_after='run'):
9+
"""
10+
Modify the current process and run setup.py
11+
12+
This should be run in a subprocess so as not to dirty the state of the
13+
current process.
14+
15+
:param cwd: Absolute path to a directory containing a setup.py script
16+
:param env: Environment variables to set before running setup.py
17+
:param script_args: command-line arguments to pass to setup.py
18+
:param stop_after: which
19+
:returns: The properties of a Distribution object, minus some useless
20+
and/or unpicklable properties
21+
"""
22+
23+
# need to be in setup.py's parent dir to detect any setup.cfg
24+
os.chdir(cwd)
25+
26+
os.environ.clear()
27+
os.environ.update(env)
28+
29+
result = distutils.core.run_setup(
30+
'setup.py', script_args=script_args, stop_after=stop_after)
31+
32+
# could just return all attrs in result.__dict__, but we take this
33+
# opportunity to filter a few things that don't need to be there
34+
return {
35+
attr: value for attr, value in result.__dict__.items()
36+
if (
37+
# These *seem* useful but always have the value 0.
38+
# Look for their values in the 'metadata' object instead.
39+
attr not in result.display_option_names
40+
# Getter methods
41+
and not callable(value)
42+
# Private properties
43+
and not attr.startswith('_')
44+
# Objects that are generally not picklable
45+
and attr not in ('cmdclass', 'distclass', 'ext_modules')
46+
)}

0 commit comments

Comments
 (0)