blob: 3427d9ac3548a4bc90bdc09d321bf165343357b3 [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:
64 """Reads configuration files and work with the Distribution instance
65 """
66 def __init__(self, dist):
67 self.dist = dist
68 self.setup_hook = None
69
70 def run_hook(self, config):
71 if self.setup_hook is None:
72 return
73 # the hook gets only the config
74 self.setup_hook(config)
75
76 def find_config_files(self):
77 """Find as many configuration files as should be processed for this
78 platform, and return a list of filenames in the order in which they
79 should be parsed. The filenames returned are guaranteed to exist
80 (modulo nasty race conditions).
81
82 There are three possible config files: packaging.cfg in the
83 Packaging installation directory (ie. where the top-level
84 Packaging __inst__.py file lives), a file in the user's home
85 directory named .pydistutils.cfg on Unix and pydistutils.cfg
86 on Windows/Mac; and setup.cfg in the current directory.
87
88 The file in the user's home directory can be disabled with the
89 --no-user-cfg option.
90 """
91 files = []
92 check_environ()
93
94 # Where to look for the system-wide Packaging config file
95 sys_dir = os.path.dirname(sys.modules['packaging'].__file__)
96
97 # Look for the system config file
98 sys_file = os.path.join(sys_dir, "packaging.cfg")
99 if os.path.isfile(sys_file):
100 files.append(sys_file)
101
102 # What to call the per-user config file
103 if os.name == 'posix':
104 user_filename = ".pydistutils.cfg"
105 else:
106 user_filename = "pydistutils.cfg"
107
108 # And look for the user config file
109 if self.dist.want_user_cfg:
110 user_file = os.path.join(os.path.expanduser('~'), user_filename)
111 if os.path.isfile(user_file):
112 files.append(user_file)
113
114 # All platforms support local setup.cfg
115 local_file = "setup.cfg"
116 if os.path.isfile(local_file):
117 files.append(local_file)
118
119 if logger.isEnabledFor(logging.DEBUG):
120 logger.debug("using config files: %s", ', '.join(files))
121 return files
122
123 def _convert_metadata(self, name, value):
124 # converts a value found in setup.cfg into a valid metadata
125 # XXX
126 return value
127
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200128 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):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200153 value = split_multiline(value)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200154
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
Éric Araujo8474f292011-06-11 00:21:18 +0200166 filenames = value.split()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200167
Éric Araujo8474f292011-06-11 00:21:18 +0200168 # concatenate all files
169 value = []
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200170 for filename in filenames:
171 # will raise if file not found
172 with open(filename) as description_file:
Éric Araujo8474f292011-06-11 00:21:18 +0200173 value.append(description_file.read().strip())
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200174 # add filename as a required file
175 if filename not in metadata.requires_files:
176 metadata.requires_files.append(filename)
Éric Araujo8474f292011-06-11 00:21:18 +0200177 value = '\n'.join(value).strip()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200178 key = 'description'
179
180 if metadata.is_metadata_field(key):
181 metadata[key] = self._convert_metadata(key, value)
182
183 if 'files' in content:
184 files = content['files']
185 self.dist.package_dir = files.pop('packages_root', None)
186
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200187 files = dict((key, split_multiline(value)) for key, value in
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200188 files.items())
189
190 self.dist.packages = []
191
192 packages = files.get('packages', [])
193 if isinstance(packages, str):
194 packages = [packages]
195
196 for package in packages:
197 if ':' in package:
198 dir_, package = package.split(':')
199 self.dist.package_dir[package] = dir_
200 self.dist.packages.append(package)
201
202 self.dist.py_modules = files.get('modules', [])
203 if isinstance(self.dist.py_modules, str):
204 self.dist.py_modules = [self.dist.py_modules]
205 self.dist.scripts = files.get('scripts', [])
206 if isinstance(self.dist.scripts, str):
207 self.dist.scripts = [self.dist.scripts]
208
209 self.dist.package_data = {}
210 for data in files.get('package_data', []):
211 data = data.split('=')
212 if len(data) != 2:
213 continue # XXX error should never pass silently
214 key, value = data
215 self.dist.package_data[key.strip()] = value.strip()
216
217 self.dist.data_files = []
218 for data in files.get('data_files', []):
219 data = data.split('=')
220 if len(data) != 2:
221 continue
222 key, value = data
223 values = [v.strip() for v in value.split(',')]
224 self.dist.data_files.append((key, values))
225
226 # manifest template
227 self.dist.extra_files = files.get('extra_files', [])
228
229 resources = []
230 for rule in files.get('resources', []):
231 glob, destination = rule.split('=', 1)
232 rich_glob = glob.strip().split(' ', 1)
233 if len(rich_glob) == 2:
234 prefix, suffix = rich_glob
235 else:
236 assert len(rich_glob) == 1
237 prefix = ''
238 suffix = glob
239 if destination == '<exclude>':
240 destination = None
241 resources.append(
242 (prefix.strip(), suffix.strip(), destination.strip()))
243 self.dist.data_files = get_resources_dests(
244 cfg_directory, resources)
245
246 ext_modules = self.dist.ext_modules
247 for section_key in content:
248 labels = section_key.split('=')
249 if len(labels) == 2 and labels[0] == 'extension':
250 # labels[1] not used from now but should be implemented
251 # for extension build dependency
252 values_dct = content[section_key]
253 ext_modules.append(Extension(
254 values_dct.pop('name'),
255 _pop_values(values_dct, 'sources'),
256 _pop_values(values_dct, 'include_dirs'),
257 _pop_values(values_dct, 'define_macros'),
258 _pop_values(values_dct, 'undef_macros'),
259 _pop_values(values_dct, 'library_dirs'),
260 _pop_values(values_dct, 'libraries'),
261 _pop_values(values_dct, 'runtime_library_dirs'),
262 _pop_values(values_dct, 'extra_objects'),
263 _pop_values(values_dct, 'extra_compile_args'),
264 _pop_values(values_dct, 'extra_link_args'),
265 _pop_values(values_dct, 'export_symbols'),
266 _pop_values(values_dct, 'swig_opts'),
267 _pop_values(values_dct, 'depends'),
268 values_dct.pop('language', None),
269 values_dct.pop('optional', None),
270 **values_dct))
271
272 def parse_config_files(self, filenames=None):
273 if filenames is None:
274 filenames = self.find_config_files()
275
276 logger.debug("Distribution.parse_config_files():")
277
278 parser = RawConfigParser()
279
280 for filename in filenames:
281 logger.debug(" reading %s", filename)
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200282 parser.read(filename, encoding='utf-8')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200283
284 if os.path.split(filename)[-1] == 'setup.cfg':
285 self._read_setup_cfg(parser, filename)
286
287 for section in parser.sections():
288 if section == 'global':
289 if parser.has_option('global', 'compilers'):
290 self._load_compilers(parser.get('global', 'compilers'))
291
292 if parser.has_option('global', 'commands'):
293 self._load_commands(parser.get('global', 'commands'))
294
295 options = parser.options(section)
296 opt_dict = self.dist.get_option_dict(section)
297
298 for opt in options:
299 if opt == '__name__':
300 continue
301 val = parser.get(section, opt)
302 opt = opt.replace('-', '_')
303
304 if opt == 'sub_commands':
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200305 val = split_multiline(val)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200306 if isinstance(val, str):
307 val = [val]
308
309 # Hooks use a suffix system to prevent being overriden
310 # by a config file processed later (i.e. a hook set in
311 # the user config file cannot be replaced by a hook
312 # set in a project config file, unless they have the
313 # same suffix).
314 if (opt.startswith("pre_hook.") or
315 opt.startswith("post_hook.")):
316 hook_type, alias = opt.split(".")
317 hook_dict = opt_dict.setdefault(
318 hook_type, (filename, {}))[1]
319 hook_dict[alias] = val
320 else:
321 opt_dict[opt] = filename, val
322
323 # Make the RawConfigParser forget everything (so we retain
324 # the original filenames that options come from)
325 parser.__init__()
326
327 # If there was a "global" section in the config file, use it
328 # to set Distribution options.
329 if 'global' in self.dist.command_options:
330 for opt, (src, val) in self.dist.command_options['global'].items():
331 alias = self.dist.negative_opt.get(opt)
332 try:
333 if alias:
334 setattr(self.dist, alias, not strtobool(val))
335 elif opt == 'dry_run': # FIXME ugh!
336 setattr(self.dist, opt, strtobool(val))
337 else:
338 setattr(self.dist, opt, val)
339 except ValueError as msg:
340 raise PackagingOptionError(msg)
341
342 def _load_compilers(self, compilers):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200343 compilers = split_multiline(compilers)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200344 if isinstance(compilers, str):
345 compilers = [compilers]
346 for compiler in compilers:
347 set_compiler(compiler.strip())
348
349 def _load_commands(self, commands):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200350 commands = split_multiline(commands)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200351 if isinstance(commands, str):
352 commands = [commands]
353 for command in commands:
354 set_command(command.strip())