blob: ab026a83b6876198b9377f54700a21a5cab37edc [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('.')
Éric Araujod9299e92011-09-01 07:01:13 +020023 parent = '.'.join(parts[:-1])
24 if parent not in packages:
25 # we could log a warning instead of raising, but what's the use
26 # of letting people build modules they can't import?
27 raise PackagingOptionError(
28 'parent package for extension %r not found' % name)
29
30
Tarek Ziade1231a4e2011-05-19 13:07:25 +020031def _pop_values(values_dct, key):
32 """Remove values from the dictionary and convert them as a list"""
33 vals_str = values_dct.pop(key, '')
34 if not vals_str:
35 return
36 fields = []
Tarek Ziade91f0e342011-05-21 12:00:10 +020037 # the line separator is \n for setup.cfg files
38 for field in vals_str.split('\n'):
Tarek Ziade1231a4e2011-05-19 13:07:25 +020039 tmp_vals = field.split('--')
40 if len(tmp_vals) == 2 and not interpret(tmp_vals[1]):
41 continue
42 fields.append(tmp_vals[0])
43 # Get bash options like `gcc -print-file-name=libgcc.a` XXX bash options?
44 vals = split(' '.join(fields))
45 if vals:
46 return vals
47
48
49def _rel_path(base, path):
Tarek Ziadeec9b76d2011-05-21 11:48:16 +020050 # normalizes and returns a lstripped-/-separated path
51 base = base.replace(os.path.sep, '/')
52 path = path.replace(os.path.sep, '/')
Tarek Ziade1231a4e2011-05-19 13:07:25 +020053 assert path.startswith(base)
54 return path[len(base):].lstrip('/')
55
56
57def get_resources_dests(resources_root, rules):
58 """Find destinations for resources files"""
59 destinations = {}
60 for base, suffix, dest in rules:
61 prefix = os.path.join(resources_root, base)
62 for abs_base in iglob(prefix):
63 abs_glob = os.path.join(abs_base, suffix)
64 for abs_path in iglob(abs_glob):
65 resource_file = _rel_path(resources_root, abs_path)
66 if dest is None: # remove the entry if it was here
67 destinations.pop(resource_file, None)
68 else:
69 rel_path = _rel_path(abs_base, abs_path)
Tarek Ziadeec9b76d2011-05-21 11:48:16 +020070 rel_dest = dest.replace(os.path.sep, '/').rstrip('/')
71 destinations[resource_file] = rel_dest + '/' + rel_path
Tarek Ziade1231a4e2011-05-19 13:07:25 +020072 return destinations
73
74
75class Config:
Éric Araujo643cb732011-06-11 00:33:38 +020076 """Class used to work with configuration files"""
Tarek Ziade1231a4e2011-05-19 13:07:25 +020077 def __init__(self, dist):
78 self.dist = dist
Éric Araujo643cb732011-06-11 00:33:38 +020079 self.setup_hooks = []
Tarek Ziade1231a4e2011-05-19 13:07:25 +020080
Éric Araujo643cb732011-06-11 00:33:38 +020081 def run_hooks(self, config):
82 """Run setup hooks in the order defined in the spec."""
83 for hook in self.setup_hooks:
84 hook(config)
Tarek Ziade1231a4e2011-05-19 13:07:25 +020085
86 def find_config_files(self):
87 """Find as many configuration files as should be processed for this
88 platform, and return a list of filenames in the order in which they
89 should be parsed. The filenames returned are guaranteed to exist
90 (modulo nasty race conditions).
91
92 There are three possible config files: packaging.cfg in the
93 Packaging installation directory (ie. where the top-level
94 Packaging __inst__.py file lives), a file in the user's home
95 directory named .pydistutils.cfg on Unix and pydistutils.cfg
96 on Windows/Mac; and setup.cfg in the current directory.
97
98 The file in the user's home directory can be disabled with the
99 --no-user-cfg option.
100 """
101 files = []
102 check_environ()
103
104 # Where to look for the system-wide Packaging config file
105 sys_dir = os.path.dirname(sys.modules['packaging'].__file__)
106
107 # Look for the system config file
108 sys_file = os.path.join(sys_dir, "packaging.cfg")
109 if os.path.isfile(sys_file):
110 files.append(sys_file)
111
112 # What to call the per-user config file
113 if os.name == 'posix':
114 user_filename = ".pydistutils.cfg"
115 else:
116 user_filename = "pydistutils.cfg"
117
118 # And look for the user config file
119 if self.dist.want_user_cfg:
120 user_file = os.path.join(os.path.expanduser('~'), user_filename)
121 if os.path.isfile(user_file):
122 files.append(user_file)
123
124 # All platforms support local setup.cfg
125 local_file = "setup.cfg"
126 if os.path.isfile(local_file):
127 files.append(local_file)
128
129 if logger.isEnabledFor(logging.DEBUG):
130 logger.debug("using config files: %s", ', '.join(files))
131 return files
132
133 def _convert_metadata(self, name, value):
134 # converts a value found in setup.cfg into a valid metadata
135 # XXX
136 return value
137
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200138 def _read_setup_cfg(self, parser, cfg_filename):
139 cfg_directory = os.path.dirname(os.path.abspath(cfg_filename))
140 content = {}
141 for section in parser.sections():
142 content[section] = dict(parser.items(section))
143
Éric Araujo643cb732011-06-11 00:33:38 +0200144 # global setup hooks are called first
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200145 if 'global' in content:
Éric Araujo643cb732011-06-11 00:33:38 +0200146 if 'setup_hooks' in content['global']:
147 setup_hooks = split_multiline(content['global']['setup_hooks'])
148
Éric Araujo3e425ac2011-06-19 21:23:43 +0200149 # add project directory to sys.path, to allow hooks to be
150 # distributed with the project
151 sys.path.insert(0, cfg_directory)
152 try:
153 for line in setup_hooks:
154 try:
155 hook = resolve_name(line)
156 except ImportError as e:
Éric Araujod9299e92011-09-01 07:01:13 +0200157 logger.warning('cannot find setup hook: %s',
158 e.args[0])
Éric Araujo3e425ac2011-06-19 21:23:43 +0200159 else:
160 self.setup_hooks.append(hook)
161 self.run_hooks(content)
162 finally:
163 sys.path.pop(0)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200164
165 metadata = self.dist.metadata
166
167 # setting the metadata values
168 if 'metadata' in content:
169 for key, value in content['metadata'].items():
170 key = key.replace('_', '-')
171 if metadata.is_multi_field(key):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200172 value = split_multiline(value)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200173
174 if key == 'project-url':
175 value = [(label.strip(), url.strip())
176 for label, url in
177 [v.split(',') for v in value]]
178
179 if key == 'description-file':
180 if 'description' in content['metadata']:
181 msg = ("description and description-file' are "
182 "mutually exclusive")
183 raise PackagingOptionError(msg)
184
Éric Araujo8474f292011-06-11 00:21:18 +0200185 filenames = value.split()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200186
Éric Araujo8474f292011-06-11 00:21:18 +0200187 # concatenate all files
188 value = []
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200189 for filename in filenames:
190 # will raise if file not found
191 with open(filename) as description_file:
Éric Araujo8474f292011-06-11 00:21:18 +0200192 value.append(description_file.read().strip())
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200193 # add filename as a required file
194 if filename not in metadata.requires_files:
195 metadata.requires_files.append(filename)
Éric Araujo8474f292011-06-11 00:21:18 +0200196 value = '\n'.join(value).strip()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200197 key = 'description'
198
199 if metadata.is_metadata_field(key):
200 metadata[key] = self._convert_metadata(key, value)
201
202 if 'files' in content:
203 files = content['files']
204 self.dist.package_dir = files.pop('packages_root', None)
205
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200206 files = dict((key, split_multiline(value)) for key, value in
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200207 files.items())
208
209 self.dist.packages = []
210
211 packages = files.get('packages', [])
212 if isinstance(packages, str):
213 packages = [packages]
214
215 for package in packages:
216 if ':' in package:
217 dir_, package = package.split(':')
218 self.dist.package_dir[package] = dir_
219 self.dist.packages.append(package)
220
221 self.dist.py_modules = files.get('modules', [])
222 if isinstance(self.dist.py_modules, str):
223 self.dist.py_modules = [self.dist.py_modules]
224 self.dist.scripts = files.get('scripts', [])
225 if isinstance(self.dist.scripts, str):
226 self.dist.scripts = [self.dist.scripts]
227
228 self.dist.package_data = {}
Éric Araujo31aefde2012-02-04 21:53:07 +0100229 # bookkeeping for the loop below
230 firstline = True
231 prev = None
232
Éric Araujo1f2bcd32011-09-10 18:22:04 +0200233 for line in files.get('package_data', []):
Éric Araujo31aefde2012-02-04 21:53:07 +0100234 if '=' in line:
235 # package name -- file globs or specs
236 key, value = line.split('=')
237 prev = self.dist.package_data[key.strip()] = value.split()
238 elif firstline:
239 # invalid continuation on the first line
240 raise PackagingOptionError(
241 'malformed package_data first line: %r (misses "=")' %
242 line)
243 else:
244 # continuation, add to last seen package name
245 prev.extend(line.split())
246
247 firstline = False
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200248
249 self.dist.data_files = []
250 for data in files.get('data_files', []):
251 data = data.split('=')
252 if len(data) != 2:
253 continue
254 key, value = data
255 values = [v.strip() for v in value.split(',')]
256 self.dist.data_files.append((key, values))
257
258 # manifest template
259 self.dist.extra_files = files.get('extra_files', [])
260
261 resources = []
262 for rule in files.get('resources', []):
263 glob, destination = rule.split('=', 1)
264 rich_glob = glob.strip().split(' ', 1)
265 if len(rich_glob) == 2:
266 prefix, suffix = rich_glob
267 else:
268 assert len(rich_glob) == 1
269 prefix = ''
270 suffix = glob
271 if destination == '<exclude>':
272 destination = None
273 resources.append(
274 (prefix.strip(), suffix.strip(), destination.strip()))
275 self.dist.data_files = get_resources_dests(
276 cfg_directory, resources)
277
278 ext_modules = self.dist.ext_modules
279 for section_key in content:
Éric Araujo336b4e42011-09-01 06:29:11 +0200280 # no str.partition in 2.4 :(
281 labels = section_key.split(':')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200282 if len(labels) == 2 and labels[0] == 'extension':
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200283 values_dct = content[section_key]
Éric Araujo336b4e42011-09-01 06:29:11 +0200284 if 'name' in values_dct:
285 raise PackagingOptionError(
286 'extension name should be given as [extension: name], '
287 'not as key')
Éric Araujod9299e92011-09-01 07:01:13 +0200288 name = labels[1].strip()
289 _check_name(name, self.dist.packages)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200290 ext_modules.append(Extension(
Éric Araujod9299e92011-09-01 07:01:13 +0200291 name,
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200292 _pop_values(values_dct, 'sources'),
293 _pop_values(values_dct, 'include_dirs'),
294 _pop_values(values_dct, 'define_macros'),
295 _pop_values(values_dct, 'undef_macros'),
296 _pop_values(values_dct, 'library_dirs'),
297 _pop_values(values_dct, 'libraries'),
298 _pop_values(values_dct, 'runtime_library_dirs'),
299 _pop_values(values_dct, 'extra_objects'),
300 _pop_values(values_dct, 'extra_compile_args'),
301 _pop_values(values_dct, 'extra_link_args'),
302 _pop_values(values_dct, 'export_symbols'),
303 _pop_values(values_dct, 'swig_opts'),
304 _pop_values(values_dct, 'depends'),
305 values_dct.pop('language', None),
306 values_dct.pop('optional', None),
307 **values_dct))
308
309 def parse_config_files(self, filenames=None):
310 if filenames is None:
311 filenames = self.find_config_files()
312
313 logger.debug("Distribution.parse_config_files():")
314
315 parser = RawConfigParser()
316
317 for filename in filenames:
318 logger.debug(" reading %s", filename)
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200319 parser.read(filename, encoding='utf-8')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200320
321 if os.path.split(filename)[-1] == 'setup.cfg':
322 self._read_setup_cfg(parser, filename)
323
324 for section in parser.sections():
325 if section == 'global':
326 if parser.has_option('global', 'compilers'):
327 self._load_compilers(parser.get('global', 'compilers'))
328
329 if parser.has_option('global', 'commands'):
330 self._load_commands(parser.get('global', 'commands'))
331
332 options = parser.options(section)
333 opt_dict = self.dist.get_option_dict(section)
334
335 for opt in options:
336 if opt == '__name__':
337 continue
338 val = parser.get(section, opt)
339 opt = opt.replace('-', '_')
340
341 if opt == 'sub_commands':
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200342 val = split_multiline(val)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200343 if isinstance(val, str):
344 val = [val]
345
346 # Hooks use a suffix system to prevent being overriden
347 # by a config file processed later (i.e. a hook set in
348 # the user config file cannot be replaced by a hook
349 # set in a project config file, unless they have the
350 # same suffix).
351 if (opt.startswith("pre_hook.") or
352 opt.startswith("post_hook.")):
353 hook_type, alias = opt.split(".")
354 hook_dict = opt_dict.setdefault(
355 hook_type, (filename, {}))[1]
356 hook_dict[alias] = val
357 else:
358 opt_dict[opt] = filename, val
359
360 # Make the RawConfigParser forget everything (so we retain
361 # the original filenames that options come from)
362 parser.__init__()
363
364 # If there was a "global" section in the config file, use it
365 # to set Distribution options.
366 if 'global' in self.dist.command_options:
367 for opt, (src, val) in self.dist.command_options['global'].items():
368 alias = self.dist.negative_opt.get(opt)
369 try:
370 if alias:
371 setattr(self.dist, alias, not strtobool(val))
372 elif opt == 'dry_run': # FIXME ugh!
373 setattr(self.dist, opt, strtobool(val))
374 else:
375 setattr(self.dist, opt, val)
376 except ValueError as msg:
377 raise PackagingOptionError(msg)
378
379 def _load_compilers(self, compilers):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200380 compilers = split_multiline(compilers)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200381 if isinstance(compilers, str):
382 compilers = [compilers]
383 for compiler in compilers:
384 set_compiler(compiler.strip())
385
386 def _load_commands(self, commands):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200387 commands = split_multiline(commands)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200388 if isinstance(commands, str):
389 commands = [commands]
390 for command in commands:
391 set_command(command.strip())