blob: 9875f6881c95f740081109cd37dfbf13c26ce6ea [file] [log] [blame]
Tarek Ziade1231a4e2011-05-19 13:07:25 +02001"""Utilities to find and read config files used by packaging."""
2
3import os
4import sys
5import logging
6
7from shlex import split
8from configparser import RawConfigParser
9from packaging import logger
10from packaging.errors import PackagingOptionError
11from packaging.compiler.extension import Extension
12from packaging.util import check_environ, iglob, resolve_name, strtobool
13from packaging.compiler import set_compiler
14from packaging.command import set_command
15from packaging.markers import interpret
16
17
18def _pop_values(values_dct, key):
19 """Remove values from the dictionary and convert them as a list"""
20 vals_str = values_dct.pop(key, '')
21 if not vals_str:
22 return
23 fields = []
24 for field in vals_str.split(os.linesep):
25 tmp_vals = field.split('--')
26 if len(tmp_vals) == 2 and not interpret(tmp_vals[1]):
27 continue
28 fields.append(tmp_vals[0])
29 # Get bash options like `gcc -print-file-name=libgcc.a` XXX bash options?
30 vals = split(' '.join(fields))
31 if vals:
32 return vals
33
34
35def _rel_path(base, path):
36 assert path.startswith(base)
37 return path[len(base):].lstrip('/')
38
39
40def get_resources_dests(resources_root, rules):
41 """Find destinations for resources files"""
42 destinations = {}
43 for base, suffix, dest in rules:
44 prefix = os.path.join(resources_root, base)
45 for abs_base in iglob(prefix):
46 abs_glob = os.path.join(abs_base, suffix)
47 for abs_path in iglob(abs_glob):
48 resource_file = _rel_path(resources_root, abs_path)
49 if dest is None: # remove the entry if it was here
50 destinations.pop(resource_file, None)
51 else:
52 rel_path = _rel_path(abs_base, abs_path)
53 destinations[resource_file] = os.path.join(dest, rel_path)
54 return destinations
55
56
57class Config:
58 """Reads configuration files and work with the Distribution instance
59 """
60 def __init__(self, dist):
61 self.dist = dist
62 self.setup_hook = None
63
64 def run_hook(self, config):
65 if self.setup_hook is None:
66 return
67 # the hook gets only the config
68 self.setup_hook(config)
69
70 def find_config_files(self):
71 """Find as many configuration files as should be processed for this
72 platform, and return a list of filenames in the order in which they
73 should be parsed. The filenames returned are guaranteed to exist
74 (modulo nasty race conditions).
75
76 There are three possible config files: packaging.cfg in the
77 Packaging installation directory (ie. where the top-level
78 Packaging __inst__.py file lives), a file in the user's home
79 directory named .pydistutils.cfg on Unix and pydistutils.cfg
80 on Windows/Mac; and setup.cfg in the current directory.
81
82 The file in the user's home directory can be disabled with the
83 --no-user-cfg option.
84 """
85 files = []
86 check_environ()
87
88 # Where to look for the system-wide Packaging config file
89 sys_dir = os.path.dirname(sys.modules['packaging'].__file__)
90
91 # Look for the system config file
92 sys_file = os.path.join(sys_dir, "packaging.cfg")
93 if os.path.isfile(sys_file):
94 files.append(sys_file)
95
96 # What to call the per-user config file
97 if os.name == 'posix':
98 user_filename = ".pydistutils.cfg"
99 else:
100 user_filename = "pydistutils.cfg"
101
102 # And look for the user config file
103 if self.dist.want_user_cfg:
104 user_file = os.path.join(os.path.expanduser('~'), user_filename)
105 if os.path.isfile(user_file):
106 files.append(user_file)
107
108 # All platforms support local setup.cfg
109 local_file = "setup.cfg"
110 if os.path.isfile(local_file):
111 files.append(local_file)
112
113 if logger.isEnabledFor(logging.DEBUG):
114 logger.debug("using config files: %s", ', '.join(files))
115 return files
116
117 def _convert_metadata(self, name, value):
118 # converts a value found in setup.cfg into a valid metadata
119 # XXX
120 return value
121
122 def _multiline(self, value):
123 value = [v for v in
124 [v.strip() for v in value.split('\n')]
125 if v != '']
126 return value
127
128 def _read_setup_cfg(self, parser, cfg_filename):
129 cfg_directory = os.path.dirname(os.path.abspath(cfg_filename))
130 content = {}
131 for section in parser.sections():
132 content[section] = dict(parser.items(section))
133
134 # global:setup_hook is called *first*
135 if 'global' in content:
136 if 'setup_hook' in content['global']:
137 setup_hook = content['global']['setup_hook']
138 try:
139 self.setup_hook = resolve_name(setup_hook)
140 except ImportError as e:
141 logger.warning('could not import setup_hook: %s',
142 e.args[0])
143 else:
144 self.run_hook(content)
145
146 metadata = self.dist.metadata
147
148 # setting the metadata values
149 if 'metadata' in content:
150 for key, value in content['metadata'].items():
151 key = key.replace('_', '-')
152 if metadata.is_multi_field(key):
153 value = self._multiline(value)
154
155 if key == 'project-url':
156 value = [(label.strip(), url.strip())
157 for label, url in
158 [v.split(',') for v in value]]
159
160 if key == 'description-file':
161 if 'description' in content['metadata']:
162 msg = ("description and description-file' are "
163 "mutually exclusive")
164 raise PackagingOptionError(msg)
165
166 if isinstance(value, list):
167 filenames = value
168 else:
169 filenames = value.split()
170
171 # concatenate each files
172 value = ''
173 for filename in filenames:
174 # will raise if file not found
175 with open(filename) as description_file:
176 value += description_file.read().strip() + '\n'
177 # add filename as a required file
178 if filename not in metadata.requires_files:
179 metadata.requires_files.append(filename)
180 value = value.strip()
181 key = 'description'
182
183 if metadata.is_metadata_field(key):
184 metadata[key] = self._convert_metadata(key, value)
185
186 if 'files' in content:
187 files = content['files']
188 self.dist.package_dir = files.pop('packages_root', None)
189
190 files = dict((key, self._multiline(value)) for key, value in
191 files.items())
192
193 self.dist.packages = []
194
195 packages = files.get('packages', [])
196 if isinstance(packages, str):
197 packages = [packages]
198
199 for package in packages:
200 if ':' in package:
201 dir_, package = package.split(':')
202 self.dist.package_dir[package] = dir_
203 self.dist.packages.append(package)
204
205 self.dist.py_modules = files.get('modules', [])
206 if isinstance(self.dist.py_modules, str):
207 self.dist.py_modules = [self.dist.py_modules]
208 self.dist.scripts = files.get('scripts', [])
209 if isinstance(self.dist.scripts, str):
210 self.dist.scripts = [self.dist.scripts]
211
212 self.dist.package_data = {}
213 for data in files.get('package_data', []):
214 data = data.split('=')
215 if len(data) != 2:
216 continue # XXX error should never pass silently
217 key, value = data
218 self.dist.package_data[key.strip()] = value.strip()
219
220 self.dist.data_files = []
221 for data in files.get('data_files', []):
222 data = data.split('=')
223 if len(data) != 2:
224 continue
225 key, value = data
226 values = [v.strip() for v in value.split(',')]
227 self.dist.data_files.append((key, values))
228
229 # manifest template
230 self.dist.extra_files = files.get('extra_files', [])
231
232 resources = []
233 for rule in files.get('resources', []):
234 glob, destination = rule.split('=', 1)
235 rich_glob = glob.strip().split(' ', 1)
236 if len(rich_glob) == 2:
237 prefix, suffix = rich_glob
238 else:
239 assert len(rich_glob) == 1
240 prefix = ''
241 suffix = glob
242 if destination == '<exclude>':
243 destination = None
244 resources.append(
245 (prefix.strip(), suffix.strip(), destination.strip()))
246 self.dist.data_files = get_resources_dests(
247 cfg_directory, resources)
248
249 ext_modules = self.dist.ext_modules
250 for section_key in content:
251 labels = section_key.split('=')
252 if len(labels) == 2 and labels[0] == 'extension':
253 # labels[1] not used from now but should be implemented
254 # for extension build dependency
255 values_dct = content[section_key]
256 ext_modules.append(Extension(
257 values_dct.pop('name'),
258 _pop_values(values_dct, 'sources'),
259 _pop_values(values_dct, 'include_dirs'),
260 _pop_values(values_dct, 'define_macros'),
261 _pop_values(values_dct, 'undef_macros'),
262 _pop_values(values_dct, 'library_dirs'),
263 _pop_values(values_dct, 'libraries'),
264 _pop_values(values_dct, 'runtime_library_dirs'),
265 _pop_values(values_dct, 'extra_objects'),
266 _pop_values(values_dct, 'extra_compile_args'),
267 _pop_values(values_dct, 'extra_link_args'),
268 _pop_values(values_dct, 'export_symbols'),
269 _pop_values(values_dct, 'swig_opts'),
270 _pop_values(values_dct, 'depends'),
271 values_dct.pop('language', None),
272 values_dct.pop('optional', None),
273 **values_dct))
274
275 def parse_config_files(self, filenames=None):
276 if filenames is None:
277 filenames = self.find_config_files()
278
279 logger.debug("Distribution.parse_config_files():")
280
281 parser = RawConfigParser()
282
283 for filename in filenames:
284 logger.debug(" reading %s", filename)
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200285 parser.read(filename, encoding='utf-8')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200286
287 if os.path.split(filename)[-1] == 'setup.cfg':
288 self._read_setup_cfg(parser, filename)
289
290 for section in parser.sections():
291 if section == 'global':
292 if parser.has_option('global', 'compilers'):
293 self._load_compilers(parser.get('global', 'compilers'))
294
295 if parser.has_option('global', 'commands'):
296 self._load_commands(parser.get('global', 'commands'))
297
298 options = parser.options(section)
299 opt_dict = self.dist.get_option_dict(section)
300
301 for opt in options:
302 if opt == '__name__':
303 continue
304 val = parser.get(section, opt)
305 opt = opt.replace('-', '_')
306
307 if opt == 'sub_commands':
308 val = self._multiline(val)
309 if isinstance(val, str):
310 val = [val]
311
312 # Hooks use a suffix system to prevent being overriden
313 # by a config file processed later (i.e. a hook set in
314 # the user config file cannot be replaced by a hook
315 # set in a project config file, unless they have the
316 # same suffix).
317 if (opt.startswith("pre_hook.") or
318 opt.startswith("post_hook.")):
319 hook_type, alias = opt.split(".")
320 hook_dict = opt_dict.setdefault(
321 hook_type, (filename, {}))[1]
322 hook_dict[alias] = val
323 else:
324 opt_dict[opt] = filename, val
325
326 # Make the RawConfigParser forget everything (so we retain
327 # the original filenames that options come from)
328 parser.__init__()
329
330 # If there was a "global" section in the config file, use it
331 # to set Distribution options.
332 if 'global' in self.dist.command_options:
333 for opt, (src, val) in self.dist.command_options['global'].items():
334 alias = self.dist.negative_opt.get(opt)
335 try:
336 if alias:
337 setattr(self.dist, alias, not strtobool(val))
338 elif opt == 'dry_run': # FIXME ugh!
339 setattr(self.dist, opt, strtobool(val))
340 else:
341 setattr(self.dist, opt, val)
342 except ValueError as msg:
343 raise PackagingOptionError(msg)
344
345 def _load_compilers(self, compilers):
346 compilers = self._multiline(compilers)
347 if isinstance(compilers, str):
348 compilers = [compilers]
349 for compiler in compilers:
350 set_compiler(compiler.strip())
351
352 def _load_commands(self, commands):
353 commands = self._multiline(commands)
354 if isinstance(commands, str):
355 commands = [commands]
356 for command in commands:
357 set_command(command.strip())