forked from colcon/colcon-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpython.py
More file actions
185 lines (150 loc) · 6.01 KB
/
python.py
File metadata and controls
185 lines (150 loc) · 6.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# Copyright 2016-2019 Dirk Thomas
# Copyright 2019 Rover Robotics via Dan Rose
# Licensed under the Apache License, Version 2.0
import multiprocessing
import os
from traceback import format_exc
from typing import Optional
import warnings
from colcon_core.dependency_descriptor import DependencyDescriptor
from colcon_core.package_identification import logger
from colcon_core.package_identification \
import PackageIdentificationExtensionPoint
from colcon_core.plugin_system import satisfies_version
from colcon_core.run_setup_py import run_setup_py
from distlib.util import parse_requirement
from distlib.version import NormalizedVersion
_process_pool = multiprocessing.Pool()
class PythonPackageIdentification(PackageIdentificationExtensionPoint):
"""Identify Python packages with `setup.py` and opt. `setup.cfg` files."""
def __init__(self): # noqa: D107
super().__init__()
satisfies_version(
PackageIdentificationExtensionPoint.EXTENSION_POINT_VERSION,
'^1.0')
def identify(self, desc): # noqa: D102
if desc.type is not None and desc.type != 'python':
return
setup_py = desc.path / 'setup.py'
if not setup_py.is_file():
return
# after this point, we are convinced this is a Python package,
# so we should fail with an Exception instead of silently
config = get_setup_result(setup_py, env=None)
name = config['metadata'].name
if not name:
raise RuntimeError(
"The Python package in '{setup_py.parent}' has an invalid "
'package name'.format_map(locals()))
desc.type = 'python'
if desc.name is not None and desc.name != name:
raise RuntimeError(
"The Python package in '{setup_py.parent}' has the name "
"'{name}' which is different from the already set package "
"name '{desc.name}'".format_map(locals()))
desc.name = name
desc.metadata['version'] = config['metadata'].version
for dependency_type, option_name in [
('build', 'setup_requires'),
('run', 'install_requires'),
('test', 'tests_require')
]:
desc.dependencies[dependency_type] = {
create_dependency_descriptor(d)
for d in config[option_name] or ()}
def getter(env):
nonlocal setup_py
return get_setup_result(setup_py, env=env)
desc.metadata['get_python_setup_options'] = getter
def get_configuration(setup_cfg):
"""
Return the configuration values defined in the setup.cfg file.
The function exists for backward compatibility with older versions of
colcon-ros.
:param setup_cfg: The path of the setup.cfg file
:returns: The configuration data
:rtype: dict
"""
warnings.warn(
'colcon_core.package_identification.python.get_configuration() will '
'be removed in the future', DeprecationWarning, stacklevel=2)
config = get_setup_result(setup_cfg.parent / 'setup.py', env=None)
return {
'metadata': {'name': config['metadata'].name},
'options': config
}
def get_setup_result(setup_py, *, env: Optional[dict]):
"""
Spin up a subprocess to run setup.py, with the given environment.
:param setup_py: Path to a setup.py script
:param env: Environment variables to set before running setup.py
:return: Dictionary of data describing the package.
:raise: RuntimeError if the setup script encountered an error
"""
env_copy = os.environ.copy()
if env is not None:
env_copy.update(env)
try:
return _process_pool.apply(
run_setup_py,
kwds={
'cwd': os.path.abspath(str(setup_py.parent)),
'env': env_copy,
'script_args': ('--dry-run',),
'stop_after': 'config'
}
)
except Exception as e:
raise RuntimeError(
'Failure when trying to run setup script {}: {}'
.format(setup_py, format_exc())) from e
def create_dependency_descriptor(requirement_string):
"""
Create a DependencyDescriptor from a PEP440 compliant string.
See https://www.python.org/dev/peps/pep-0440/#version-specifiers
:param str requirement_string: a PEP440 compliant requirement string
:return: A descriptor with version constraints from the requirement string
:rtype: DependencyDescriptor
"""
symbol_mapping = {
'==': 'version_eq',
'!=': 'version_neq',
'<=': 'version_lte',
'>=': 'version_gte',
'>': 'version_gt',
'<': 'version_lt',
}
requirement = parse_requirement(requirement_string)
metadata = {}
for symbol, version in (requirement.constraints or []):
if symbol in symbol_mapping:
metadata[symbol_mapping[symbol]] = version
elif symbol == '~=':
metadata['version_gte'] = version
metadata['version_lt'] = _next_incompatible_version(version)
else:
logger.warn(
"Ignoring unknown symbol '{symbol}' in '{requirement}'"
.format_map(locals()))
return DependencyDescriptor(requirement.name, metadata=metadata)
def _next_incompatible_version(version):
"""
Find the next non-compatible version.
This is for use with the ~= compatible syntax. It will provide
the first version that this version must be less than in order
to be compatible.
:param str version: PEP 440 compliant version number
:return: The first version after this version that is not compatible
:rtype: str
"""
normalized = NormalizedVersion(version)
parse_tuple = normalized.parse(version)
version_tuple = parse_tuple[1]
*unchanged, increment, dropped = version_tuple
incremented = increment + 1
version = unchanged
version.append(incremented)
# versions have a minimum length of 2
if len(version) == 1:
version.append(0)
return '.'.join(map(str, version))