blob: e02800e96052b9bced731aa66705e0e9822376da [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
Éric Araujo1c1d9a52011-06-10 23:26:31 +020012from packaging.util import (check_environ, iglob, resolve_name, strtobool,
13 split_multiline)
Tarek Ziade1231a4e2011-05-19 13:07:25 +020014from packaging.compiler import set_compiler
15from packaging.command import set_command
16from packaging.markers import interpret
17
18
Éric Araujod9299e92011-09-01 07:01:13 +020019def _check_name(name, packages):
20 if '.' not in name:
21 return
22 parts = name.split('.')
23 modname = parts[-1]
24 parent = '.'.join(parts[:-1])
25 if parent not in packages:
26 # we could log a warning instead of raising, but what's the use
27 # of letting people build modules they can't import?
28 raise PackagingOptionError(
29 'parent package for extension %r not found' % name)
30
31
Tarek Ziade1231a4e2011-05-19 13:07:25 +020032def _pop_values(values_dct, key):
33 """Remove values from the dictionary and convert them as a list"""
34 vals_str = values_dct.pop(key, '')
35 if not vals_str:
36 return
37 fields = []
Tarek Ziade91f0e342011-05-21 12:00:10 +020038 # the line separator is \n for setup.cfg files
39 for field in vals_str.split('\n'):
Tarek Ziade1231a4e2011-05-19 13:07:25 +020040 tmp_vals = field.split('--')
41 if len(tmp_vals) == 2 and not interpret(tmp_vals[1]):
42 continue
43 fields.append(tmp_vals[0])
44 # Get bash options like `gcc -print-file-name=libgcc.a` XXX bash options?
45 vals = split(' '.join(fields))
46 if vals:
47 return vals
48
49
50def _rel_path(base, path):
Tarek Ziadeec9b76d2011-05-21 11:48:16 +020051 # normalizes and returns a lstripped-/-separated path
52 base = base.replace(os.path.sep, '/')
53 path = path.replace(os.path.sep, '/')
Tarek Ziade1231a4e2011-05-19 13:07:25 +020054 assert path.startswith(base)
55 return path[len(base):].lstrip('/')
56
57
58def get_resources_dests(resources_root, rules):
59 """Find destinations for resources files"""
60 destinations = {}
61 for base, suffix, dest in rules:
62 prefix = os.path.join(resources_root, base)
63 for abs_base in iglob(prefix):
64 abs_glob = os.path.join(abs_base, suffix)
65 for abs_path in iglob(abs_glob):
66 resource_file = _rel_path(resources_root, abs_path)
67 if dest is None: # remove the entry if it was here
68 destinations.pop(resource_file, None)
69 else:
70 rel_path = _rel_path(abs_base, abs_path)
Tarek Ziadeec9b76d2011-05-21 11:48:16 +020071 rel_dest = dest.replace(os.path.sep, '/').rstrip('/')
72 destinations[resource_file] = rel_dest + '/' + rel_path
Tarek Ziade1231a4e2011-05-19 13:07:25 +020073 return destinations
74
75
76class Config:
Éric Araujo643cb732011-06-11 00:33:38 +020077 """Class used to work with configuration files"""
Tarek Ziade1231a4e2011-05-19 13:07:25 +020078 def __init__(self, dist):
79 self.dist = dist
Éric Araujo643cb732011-06-11 00:33:38 +020080 self.setup_hooks = []
Tarek Ziade1231a4e2011-05-19 13:07:25 +020081
Éric Araujo643cb732011-06-11 00:33:38 +020082 def run_hooks(self, config):
83 """Run setup hooks in the order defined in the spec."""
84 for hook in self.setup_hooks:
85 hook(config)
Tarek Ziade1231a4e2011-05-19 13:07:25 +020086
87 def find_config_files(self):
88 """Find as many configuration files as should be processed for this
89 platform, and return a list of filenames in the order in which they
90 should be parsed. The filenames returned are guaranteed to exist
91 (modulo nasty race conditions).
92
93 There are three possible config files: packaging.cfg in the
94 Packaging installation directory (ie. where the top-level
95 Packaging __inst__.py file lives), a file in the user's home
96 directory named .pydistutils.cfg on Unix and pydistutils.cfg
97 on Windows/Mac; and setup.cfg in the current directory.
98
99 The file in the user's home directory can be disabled with the
100 --no-user-cfg option.
101 """
102 files = []
103 check_environ()
104
105 # Where to look for the system-wide Packaging config file
106 sys_dir = os.path.dirname(sys.modules['packaging'].__file__)
107
108 # Look for the system config file
109 sys_file = os.path.join(sys_dir, "packaging.cfg")
110 if os.path.isfile(sys_file):
111 files.append(sys_file)
112
113 # What to call the per-user config file
114 if os.name == 'posix':
115 user_filename = ".pydistutils.cfg"
116 else:
117 user_filename = "pydistutils.cfg"
118
119 # And look for the user config file
120 if self.dist.want_user_cfg:
121 user_file = os.path.join(os.path.expanduser('~'), user_filename)
122 if os.path.isfile(user_file):
123 files.append(user_file)
124
125 # All platforms support local setup.cfg
126 local_file = "setup.cfg"
127 if os.path.isfile(local_file):
128 files.append(local_file)
129
130 if logger.isEnabledFor(logging.DEBUG):
131 logger.debug("using config files: %s", ', '.join(files))
132 return files
133
134 def _convert_metadata(self, name, value):
135 # converts a value found in setup.cfg into a valid metadata
136 # XXX
137 return value
138
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200139 def _read_setup_cfg(self, parser, cfg_filename):
140 cfg_directory = os.path.dirname(os.path.abspath(cfg_filename))
141 content = {}
142 for section in parser.sections():
143 content[section] = dict(parser.items(section))
144
Éric Araujo643cb732011-06-11 00:33:38 +0200145 # global setup hooks are called first
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200146 if 'global' in content:
Éric Araujo643cb732011-06-11 00:33:38 +0200147 if 'setup_hooks' in content['global']:
148 setup_hooks = split_multiline(content['global']['setup_hooks'])
149
Éric Araujo3e425ac2011-06-19 21:23:43 +0200150 # add project directory to sys.path, to allow hooks to be
151 # distributed with the project
152 sys.path.insert(0, cfg_directory)
153 try:
154 for line in setup_hooks:
155 try:
156 hook = resolve_name(line)
157 except ImportError as e:
Éric Araujod9299e92011-09-01 07:01:13 +0200158 logger.warning('cannot find setup hook: %s',
159 e.args[0])
Éric Araujo3e425ac2011-06-19 21:23:43 +0200160 else:
161 self.setup_hooks.append(hook)
162 self.run_hooks(content)
163 finally:
164 sys.path.pop(0)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200165
166 metadata = self.dist.metadata
167
168 # setting the metadata values
169 if 'metadata' in content:
170 for key, value in content['metadata'].items():
171 key = key.replace('_', '-')
172 if metadata.is_multi_field(key):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200173 value = split_multiline(value)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200174
175 if key == 'project-url':
176 value = [(label.strip(), url.strip())
177 for label, url in
178 [v.split(',') for v in value]]
179
180 if key == 'description-file':
181 if 'description' in content['metadata']:
182 msg = ("description and description-file' are "
183 "mutually exclusive")
184 raise PackagingOptionError(msg)
185
Éric Araujo8474f292011-06-11 00:21:18 +0200186 filenames = value.split()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200187
Éric Araujo8474f292011-06-11 00:21:18 +0200188 # concatenate all files
189 value = []
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200190 for filename in filenames:
191 # will raise if file not found
192 with open(filename) as description_file:
Éric Araujo8474f292011-06-11 00:21:18 +0200193 value.append(description_file.read().strip())
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200194 # add filename as a required file
195 if filename not in metadata.requires_files:
196 metadata.requires_files.append(filename)
Éric Araujo8474f292011-06-11 00:21:18 +0200197 value = '\n'.join(value).strip()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200198 key = 'description'
199
200 if metadata.is_metadata_field(key):
201 metadata[key] = self._convert_metadata(key, value)
202
203 if 'files' in content:
204 files = content['files']
205 self.dist.package_dir = files.pop('packages_root', None)
206
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200207 files = dict((key, split_multiline(value)) for key, value in
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200208 files.items())
209
210 self.dist.packages = []
211
212 packages = files.get('packages', [])
213 if isinstance(packages, str):
214 packages = [packages]
215
216 for package in packages:
217 if ':' in package:
218 dir_, package = package.split(':')
219 self.dist.package_dir[package] = dir_
220 self.dist.packages.append(package)
221
222 self.dist.py_modules = files.get('modules', [])
223 if isinstance(self.dist.py_modules, str):
224 self.dist.py_modules = [self.dist.py_modules]
225 self.dist.scripts = files.get('scripts', [])
226 if isinstance(self.dist.scripts, str):
227 self.dist.scripts = [self.dist.scripts]
228
229 self.dist.package_data = {}
230 for data in files.get('package_data', []):
231 data = data.split('=')
232 if len(data) != 2:
Éric Araujo95fc53f2011-09-01 05:11:29 +0200233 continue # FIXME errors should never pass silently
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200234 key, value = data
235 self.dist.package_data[key.strip()] = value.strip()
236
237 self.dist.data_files = []
238 for data in files.get('data_files', []):
239 data = data.split('=')
240 if len(data) != 2:
241 continue
242 key, value = data
243 values = [v.strip() for v in value.split(',')]
244 self.dist.data_files.append((key, values))
245
246 # manifest template
247 self.dist.extra_files = files.get('extra_files', [])
248
249 resources = []
250 for rule in files.get('resources', []):
251 glob, destination = rule.split('=', 1)
252 rich_glob = glob.strip().split(' ', 1)
253 if len(rich_glob) == 2:
254 prefix, suffix = rich_glob
255 else:
256 assert len(rich_glob) == 1
257 prefix = ''
258 suffix = glob
259 if destination == '<exclude>':
260 destination = None
261 resources.append(
262 (prefix.strip(), suffix.strip(), destination.strip()))
263 self.dist.data_files = get_resources_dests(
264 cfg_directory, resources)
265
266 ext_modules = self.dist.ext_modules
267 for section_key in content:
Éric Araujo336b4e42011-09-01 06:29:11 +0200268 # no str.partition in 2.4 :(
269 labels = section_key.split(':')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200270 if len(labels) == 2 and labels[0] == 'extension':
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200271 values_dct = content[section_key]
Éric Araujo336b4e42011-09-01 06:29:11 +0200272 if 'name' in values_dct:
273 raise PackagingOptionError(
274 'extension name should be given as [extension: name], '
275 'not as key')
Éric Araujod9299e92011-09-01 07:01:13 +0200276 name = labels[1].strip()
277 _check_name(name, self.dist.packages)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200278 ext_modules.append(Extension(
Éric Araujod9299e92011-09-01 07:01:13 +0200279 name,
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200280 _pop_values(values_dct, 'sources'),
281 _pop_values(values_dct, 'include_dirs'),
282 _pop_values(values_dct, 'define_macros'),
283 _pop_values(values_dct, 'undef_macros'),
284 _pop_values(values_dct, 'library_dirs'),
285 _pop_values(values_dct, 'libraries'),
286 _pop_values(values_dct, 'runtime_library_dirs'),
287 _pop_values(values_dct, 'extra_objects'),
288 _pop_values(values_dct, 'extra_compile_args'),
289 _pop_values(values_dct, 'extra_link_args'),
290 _pop_values(values_dct, 'export_symbols'),
291 _pop_values(values_dct, 'swig_opts'),
292 _pop_values(values_dct, 'depends'),
293 values_dct.pop('language', None),
294 values_dct.pop('optional', None),
295 **values_dct))
296
297 def parse_config_files(self, filenames=None):
298 if filenames is None:
299 filenames = self.find_config_files()
300
301 logger.debug("Distribution.parse_config_files():")
302
303 parser = RawConfigParser()
304
305 for filename in filenames:
306 logger.debug(" reading %s", filename)
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200307 parser.read(filename, encoding='utf-8')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200308
309 if os.path.split(filename)[-1] == 'setup.cfg':
310 self._read_setup_cfg(parser, filename)
311
312 for section in parser.sections():
313 if section == 'global':
314 if parser.has_option('global', 'compilers'):
315 self._load_compilers(parser.get('global', 'compilers'))
316
317 if parser.has_option('global', 'commands'):
318 self._load_commands(parser.get('global', 'commands'))
319
320 options = parser.options(section)
321 opt_dict = self.dist.get_option_dict(section)
322
323 for opt in options:
324 if opt == '__name__':
325 continue
326 val = parser.get(section, opt)
327 opt = opt.replace('-', '_')
328
329 if opt == 'sub_commands':
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200330 val = split_multiline(val)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200331 if isinstance(val, str):
332 val = [val]
333
334 # Hooks use a suffix system to prevent being overriden
335 # by a config file processed later (i.e. a hook set in
336 # the user config file cannot be replaced by a hook
337 # set in a project config file, unless they have the
338 # same suffix).
339 if (opt.startswith("pre_hook.") or
340 opt.startswith("post_hook.")):
341 hook_type, alias = opt.split(".")
342 hook_dict = opt_dict.setdefault(
343 hook_type, (filename, {}))[1]
344 hook_dict[alias] = val
345 else:
346 opt_dict[opt] = filename, val
347
348 # Make the RawConfigParser forget everything (so we retain
349 # the original filenames that options come from)
350 parser.__init__()
351
352 # If there was a "global" section in the config file, use it
353 # to set Distribution options.
354 if 'global' in self.dist.command_options:
355 for opt, (src, val) in self.dist.command_options['global'].items():
356 alias = self.dist.negative_opt.get(opt)
357 try:
358 if alias:
359 setattr(self.dist, alias, not strtobool(val))
360 elif opt == 'dry_run': # FIXME ugh!
361 setattr(self.dist, opt, strtobool(val))
362 else:
363 setattr(self.dist, opt, val)
364 except ValueError as msg:
365 raise PackagingOptionError(msg)
366
367 def _load_compilers(self, compilers):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200368 compilers = split_multiline(compilers)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200369 if isinstance(compilers, str):
370 compilers = [compilers]
371 for compiler in compilers:
372 set_compiler(compiler.strip())
373
374 def _load_commands(self, commands):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200375 commands = split_multiline(commands)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200376 if isinstance(commands, str):
377 commands = [commands]
378 for command in commands:
379 set_command(command.strip())