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