setup_py.py
Go to the documentation of this file.
1 import ast
2 import collections
3 import os
4 import sys
5 
6 # Version-Dependent AST operations
7 if sys.version_info.major == 3 and sys.version_info.minor >= 8:
8  def is_constant(el):
9  return isinstance(el.value, ast.Constant)
10 
11  get_source_segment = ast.get_source_segment
12 else:
13  def is_constant(el):
14  return isinstance(el.value, ast.Str)
15 
16  def get_source_segment(source, node):
17  """Get source code segment of the *source* that generated *node*.
18  If some location information (`lineno`, `end_lineno`, `col_offset`,
19  or `end_col_offset`) is missing, return None.
20  """
21  try:
22  lineno = node.lineno - 1
23  end_lineno = node.end_lineno - 1
24  col_offset = node.col_offset
25  end_col_offset = node.end_col_offset
26  except AttributeError:
27  return None
28 
29  lines = ast._splitlines_no_ff(source)
30  if end_lineno == lineno:
31  return lines[lineno].encode()[col_offset:end_col_offset].decode()
32 
33  first = lines[lineno].encode()[col_offset:].decode()
34  last = lines[end_lineno].encode()[:end_col_offset].decode()
35  lines = lines[lineno + 1:end_lineno]
36 
37  lines.insert(0, first)
38  lines.append(last)
39  return ''.join(lines)
40 
41 HELPER_FUNCTIONS = {
42  'catkin_pkg.python_setup': 'generate_distutils_setup'
43 }
44 
45 IMPORT_TEMPLATE = 'from {} import {}\n'
46 QUOTE_CHARS = ['"', "'"]
47 LINE_LENGTH = 100
48 
49 
50 def python_collection_to_lines(obj, indent=4, multi_line=False):
51  """Convert a python collection (list/tuple/dict) to a sequence of lines, NOT including brackets
52 
53  indent is the size of the indent
54  multi_line determines whether lists and tuples should be spread on multiple lines by default a la
55  multi_line=False : [1, 2, 3]
56  multi_line=True : [
57  1,
58  2,
59  3,
60  ]
61  """
62  if isinstance(obj, dict):
63  items = []
64  for k, v in obj.items():
65  s = k + ': '
66  sub_lines = python_to_lines(v, len(s) + indent, indent + 4)
67  joiner = '\n' + (' ' * 4)
68  s += joiner.join(sub_lines)
69  items.append(s)
70  elif multi_line:
71  lines = []
72  for item in obj:
73  sub_lines = python_to_lines(item, indent, indent + 4)
74  s = sub_lines[0]
75  for line in sub_lines[1:]:
76  s += '\n' + ' ' * (indent + 4) + line
77  s += ','
78  lines.append(s)
79  return lines
80  else:
81  items = []
82  for item in obj:
83  items += python_to_lines(item, indent, indent + 4)
84 
85  line = ', '.join(items)
86  if indent + len(line) >= LINE_LENGTH:
87  multi_line = True
88  if multi_line:
89  return [item for item in items[:-1]] + [items[-1]]
90 
91  return [line]
92 
93 
94 def python_to_lines(obj, initial_length=0, indent=4):
95  """Convert a python object to the properly formatted python code.
96 
97  initial_length is the length of the string before the object
98  indent is the size of the indent that each line should have
99  """
100  if isinstance(obj, list):
101  brackets = '[]'
102  elif isinstance(obj, tuple):
103  brackets = '()'
104  elif isinstance(obj, dict) or isinstance(obj, collections.OrderedDict):
105  brackets = '{}'
106  else:
107  # For other objects, we just return the string representation of the object as a single line
108  return [str(obj)]
109 
110  # Generate the inner lines, assuming not multi_line
111  inner_lines = python_collection_to_lines(obj, indent, multi_line=False)
112 
113  # If the inner contents can fit on a single line, return the single line
114  if len(inner_lines) == 1 and '\n' not in inner_lines[0]:
115  inner_line = inner_lines[0]
116  if initial_length + len(inner_line) < LINE_LENGTH:
117  return [brackets[0] + inner_line + brackets[1]]
118 
119  # Regenerate the inner lines, assuming it IS multi_line
120  inner_lines = python_collection_to_lines(obj, indent, multi_line=True)
121  # Place the open and closing brackets on their own lines surrounding the inner_lines
122  lines = [brackets[0]]
123  for line in inner_lines:
124  # Indent each line a little more
125  line = line.replace('\n', '\n ')
126  lines.append(' ' + line)
127  lines.append(brackets[1])
128  return lines
129 
130 
131 def quote_string(s, quote_char="'"):
132  """Utility function to wrap an arbitrary string in a quote character"""
133  return quote_char + s + quote_char
134 
135 
136 def contains_quoted_string(container, s):
137  """Utility function to determine if the string is present in the container wrapped in some quote char"""
138  for quote_char in QUOTE_CHARS:
139  quoted_value = quote_string(s, quote_char)
140  if quoted_value in container:
141  return quoted_value
142 
143 
144 class SetupPy:
145  """
146  Representation of a setup.py file, covering a large range of different styles
147 
148  The core operation is generating the dictionary of arguments sent to the setup function.
149  If a helper function (like generate_distutils_setup) is used, there won't be many arguments.
150 
151  Key fields:
152  hash_bang (bool) - Whether the file starts with a #!/usr/bin/env python
153  imports (list of tuples) - Used for generating the import statements like `from X import Y`
154  First element of tuple is a string of the name of the module (e.g. X)
155  Second element is the string(s) representing the function(s) to import (e.g. Y)
156  declare_package_name (bool) - Whether to declare the package name as a string for later use
157  helper_function (optional string) - Name of module to find the helper function to call (see HELPER_FUNCTIONS)
158  helper_variable (optional string) - Name of variable to store the results of the helper function in.
159  args (ordered dictionary) - Arguments to pass to setup/helper_function
160  The keys are regular strings that represent the name of the variable.
161  The values are more complex.
162  """
163 
164  def __init__(self, pkg_name, file_path):
165  self.pkg_name = pkg_name
166  self.file_path = file_path
167 
168  self.args = collections.OrderedDict()
169 
170  if not os.path.exists(self.file_path):
171  self.changed = True
172  self.hash_bang = True
173  self.imports = [('distutils.core', 'setup'), ('catkin_pkg.python_setup', 'generate_distutils_setup')]
174  self.declare_package_name = False
175  self.helper_function = 'catkin_pkg.python_setup'
176  self.helper_variable = 'package_info'
177  self.args['packages'] = [quote_string(pkg_name)]
178  self.args['package_dir'] = {quote_string(''): quote_string('src')}
179  return
180 
181  original_contents = open(file_path, 'r').read()
182  self.changed = False
183  self.hash_bang = (original_contents[0] == '#')
184  self.imports = []
185  self.declare_package_name = False
186  self.helper_function = None
187  self.helper_variable = None
188 
189  # Split into imports / body
190  import_elements = []
191  body_elements = []
192  for el in ast.parse(original_contents).body:
193  if isinstance(el, ast.ImportFrom):
194  import_elements.append(el)
195  else:
196  body_elements.append(el)
197 
198  # Examine Imports
199  for el in import_elements:
200  if el.module in HELPER_FUNCTIONS:
201  self.helper_function = el.module
202  self.imports.append((el.module, [x.name for x in el.names]))
203 
204  def ast_to_python(el):
205  """Helper function to convert an ast element to its Python data structure"""
206  if isinstance(el, ast.Dict):
207  d = {}
208  for k, v in zip(el.keys, el.values):
209  d[ast_to_python(k)] = ast_to_python(v)
210  return d
211  elif isinstance(el, ast.List):
212  return [ast_to_python(elt) for elt in el.elts]
213  elif isinstance(el, ast.Tuple):
214  return tuple(ast_to_python(elt) for elt in el.elts)
215  else:
216  return get_source_segment(original_contents, el)
217 
218  # Determine variable name and dictionary args
219  for el in body_elements:
220  if isinstance(el, ast.Assign):
221  if is_constant(el):
222  self.declare_package_name = True
223  else:
224  self.helper_variable = el.targets[0].id
225 
226  for keyword in el.value.keywords:
227  self.args[keyword.arg] = ast_to_python(keyword.value)
228 
229  elif isinstance(el, ast.Expr) and self.helper_variable is None:
230  for keyword in el.value.keywords:
231  self.args[keyword.arg] = ast_to_python(keyword.value)
232 
233  def write(self):
234  if not self.changed:
235  return
236  with open(self.file_path, 'w') as f:
237  f.write(str(self))
238 
239  def __repr__(self):
240  s = ''
241 
242  if self.hash_bang:
243  s += '#!/usr/bin/env python\n\n'
244 
245  for module, names in self.imports:
246  if isinstance(names, list):
247  names = ', '.join(sorted(names))
248  s += IMPORT_TEMPLATE.format(module, names)
249 
250  s += '\n'
251 
252  if self.declare_package_name:
253  s += "package_name = '{}'\n\n".format(self.pkg_name)
254 
255  if self.helper_function:
256  function_name = HELPER_FUNCTIONS[self.helper_function]
257  s += '{} = {}(\n'.format(self.helper_variable, function_name)
258  final_piece = ')\n\nsetup(**{})\n'.format(self.helper_variable)
259  else:
260  s += 'setup(\n'
261  final_piece = ')\n'
262 
263  for k, v in self.args.items():
264  line = ' {}='.format(k)
265  lines = python_to_lines(v, len(line))
266  s += line + lines[0]
267  for line in lines[1:]:
268  s += '\n ' + line
269  s += ',\n'
270 
271  s += final_piece
272  return s
def quote_string(s, quote_char="'")
Definition: setup_py.py:131
def contains_quoted_string(container, s)
Definition: setup_py.py:136
def __init__(self, pkg_name, file_path)
Definition: setup_py.py:164
def python_collection_to_lines(obj, indent=4, multi_line=False)
Definition: setup_py.py:50
def python_to_lines(obj, initial_length=0, indent=4)
Definition: setup_py.py:94


ros_introspection
Author(s):
autogenerated on Wed Jun 22 2022 02:45:33