blob: 366faea4ede9a99e8e7d9d7700313c873cca3dba [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 = {}
Éric Araujo1f2bcd32011-09-10 18:22:04 +0200230 for line in files.get('package_data', []):
231 data = line.split('=')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200232 if len(data) != 2:
Éric Araujo1f2bcd32011-09-10 18:22:04 +0200233 raise ValueError('invalid line for package_data: %s '
234 '(misses "=")' % line)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200235 key, value = data
236 self.dist.package_data[key.strip()] = value.strip()
237
238 self.dist.data_files = []
239 for data in files.get('data_files', []):
240 data = data.split('=')
241 if len(data) != 2:
242 continue
243 key, value = data
244 values = [v.strip() for v in value.split(',')]
245 self.dist.data_files.append((key, values))
246
247 # manifest template
248 self.dist.extra_files = files.get('extra_files', [])
249
250 resources = []
251 for rule in files.get('resources', []):
252 glob, destination = rule.split('=', 1)
253 rich_glob = glob.strip().split(' ', 1)
254 if len(rich_glob) == 2:
255 prefix, suffix = rich_glob
256 else:
257 assert len(rich_glob) == 1
258 prefix = ''
259 suffix = glob
260 if destination == '<exclude>':
261 destination = None
262 resources.append(
263 (prefix.strip(), suffix.strip(), destination.strip()))
264 self.dist.data_files = get_resources_dests(
265 cfg_directory, resources)
266
267 ext_modules = self.dist.ext_modules
268 for section_key in content:
Éric Araujo336b4e42011-09-01 06:29:11 +0200269 # no str.partition in 2.4 :(
270 labels = section_key.split(':')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200271 if len(labels) == 2 and labels[0] == 'extension':
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200272 values_dct = content[section_key]
Éric Araujo336b4e42011-09-01 06:29:11 +0200273 if 'name' in values_dct:
274 raise PackagingOptionError(
275 'extension name should be given as [extension: name], '
276 'not as key')
Éric Araujod9299e92011-09-01 07:01:13 +0200277 name = labels[1].strip()
278 _check_name(name, self.dist.packages)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200279 ext_modules.append(Extension(
Éric Araujod9299e92011-09-01 07:01:13 +0200280 name,
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200281 _pop_values(values_dct, 'sources'),
282 _pop_values(values_dct, 'include_dirs'),
283 _pop_values(values_dct, 'define_macros'),
284 _pop_values(values_dct, 'undef_macros'),
285 _pop_values(values_dct, 'library_dirs'),
286 _pop_values(values_dct, 'libraries'),
287 _pop_values(values_dct, 'runtime_library_dirs'),
288 _pop_values(values_dct, 'extra_objects'),
289 _pop_values(values_dct, 'extra_compile_args'),
290 _pop_values(values_dct, 'extra_link_args'),
291 _pop_values(values_dct, 'export_symbols'),
292 _pop_values(values_dct, 'swig_opts'),
293 _pop_values(values_dct, 'depends'),
294 values_dct.pop('language', None),
295 values_dct.pop('optional', None),
296 **values_dct))
297
298 def parse_config_files(self, filenames=None):
299 if filenames is None:
300 filenames = self.find_config_files()
301
302 logger.debug("Distribution.parse_config_files():")
303
304 parser = RawConfigParser()
305
306 for filename in filenames:
307 logger.debug(" reading %s", filename)
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200308 parser.read(filename, encoding='utf-8')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200309
310 if os.path.split(filename)[-1] == 'setup.cfg':
311 self._read_setup_cfg(parser, filename)
312
313 for section in parser.sections():
314 if section == 'global':
315 if parser.has_option('global', 'compilers'):
316 self._load_compilers(parser.get('global', 'compilers'))
317
318 if parser.has_option('global', 'commands'):
319 self._load_commands(parser.get('global', 'commands'))
320
321 options = parser.options(section)
322 opt_dict = self.dist.get_option_dict(section)
323
324 for opt in options:
325 if opt == '__name__':
326 continue
327 val = parser.get(section, opt)
328 opt = opt.replace('-', '_')
329
330 if opt == 'sub_commands':
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200331 val = split_multiline(val)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200332 if isinstance(val, str):
333 val = [val]
334
335 # Hooks use a suffix system to prevent being overriden
336 # by a config file processed later (i.e. a hook set in
337 # the user config file cannot be replaced by a hook
338 # set in a project config file, unless they have the
339 # same suffix).
340 if (opt.startswith("pre_hook.") or
341 opt.startswith("post_hook.")):
342 hook_type, alias = opt.split(".")
343 hook_dict = opt_dict.setdefault(
344 hook_type, (filename, {}))[1]
345 hook_dict[alias] = val
346 else:
347 opt_dict[opt] = filename, val
348
349 # Make the RawConfigParser forget everything (so we retain
350 # the original filenames that options come from)
351 parser.__init__()
352
353 # If there was a "global" section in the config file, use it
354 # to set Distribution options.
355 if 'global' in self.dist.command_options:
356 for opt, (src, val) in self.dist.command_options['global'].items():
357 alias = self.dist.negative_opt.get(opt)
358 try:
359 if alias:
360 setattr(self.dist, alias, not strtobool(val))
361 elif opt == 'dry_run': # FIXME ugh!
362 setattr(self.dist, opt, strtobool(val))
363 else:
364 setattr(self.dist, opt, val)
365 except ValueError as msg:
366 raise PackagingOptionError(msg)
367
368 def _load_compilers(self, compilers):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200369 compilers = split_multiline(compilers)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200370 if isinstance(compilers, str):
371 compilers = [compilers]
372 for compiler in compilers:
373 set_compiler(compiler.strip())
374
375 def _load_commands(self, commands):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200376 commands = split_multiline(commands)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200377 if isinstance(commands, str):
378 commands = [commands]
379 for command in commands:
380 set_command(command.strip())