blob: be75da98a3e278396efa44d85aa2522c44ed6fb2 [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
166 if isinstance(value, list):
167 filenames = value
168 else:
169 filenames = value.split()
170
171 # concatenate each files
172 value = ''
173 for filename in filenames:
174 # will raise if file not found
175 with open(filename) as description_file:
176 value += description_file.read().strip() + '\n'
177 # add filename as a required file
178 if filename not in metadata.requires_files:
179 metadata.requires_files.append(filename)
180 value = value.strip()
181 key = 'description'
182
183 if metadata.is_metadata_field(key):
184 metadata[key] = self._convert_metadata(key, value)
185
186 if 'files' in content:
187 files = content['files']
188 self.dist.package_dir = files.pop('packages_root', None)
189
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200190 files = dict((key, split_multiline(value)) for key, value in
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200191 files.items())
192
193 self.dist.packages = []
194
195 packages = files.get('packages', [])
196 if isinstance(packages, str):
197 packages = [packages]
198
199 for package in packages:
200 if ':' in package:
201 dir_, package = package.split(':')
202 self.dist.package_dir[package] = dir_
203 self.dist.packages.append(package)
204
205 self.dist.py_modules = files.get('modules', [])
206 if isinstance(self.dist.py_modules, str):
207 self.dist.py_modules = [self.dist.py_modules]
208 self.dist.scripts = files.get('scripts', [])
209 if isinstance(self.dist.scripts, str):
210 self.dist.scripts = [self.dist.scripts]
211
212 self.dist.package_data = {}
213 for data in files.get('package_data', []):
214 data = data.split('=')
215 if len(data) != 2:
216 continue # XXX error should never pass silently
217 key, value = data
218 self.dist.package_data[key.strip()] = value.strip()
219
220 self.dist.data_files = []
221 for data in files.get('data_files', []):
222 data = data.split('=')
223 if len(data) != 2:
224 continue
225 key, value = data
226 values = [v.strip() for v in value.split(',')]
227 self.dist.data_files.append((key, values))
228
229 # manifest template
230 self.dist.extra_files = files.get('extra_files', [])
231
232 resources = []
233 for rule in files.get('resources', []):
234 glob, destination = rule.split('=', 1)
235 rich_glob = glob.strip().split(' ', 1)
236 if len(rich_glob) == 2:
237 prefix, suffix = rich_glob
238 else:
239 assert len(rich_glob) == 1
240 prefix = ''
241 suffix = glob
242 if destination == '<exclude>':
243 destination = None
244 resources.append(
245 (prefix.strip(), suffix.strip(), destination.strip()))
246 self.dist.data_files = get_resources_dests(
247 cfg_directory, resources)
248
249 ext_modules = self.dist.ext_modules
250 for section_key in content:
251 labels = section_key.split('=')
252 if len(labels) == 2 and labels[0] == 'extension':
253 # labels[1] not used from now but should be implemented
254 # for extension build dependency
255 values_dct = content[section_key]
256 ext_modules.append(Extension(
257 values_dct.pop('name'),
258 _pop_values(values_dct, 'sources'),
259 _pop_values(values_dct, 'include_dirs'),
260 _pop_values(values_dct, 'define_macros'),
261 _pop_values(values_dct, 'undef_macros'),
262 _pop_values(values_dct, 'library_dirs'),
263 _pop_values(values_dct, 'libraries'),
264 _pop_values(values_dct, 'runtime_library_dirs'),
265 _pop_values(values_dct, 'extra_objects'),
266 _pop_values(values_dct, 'extra_compile_args'),
267 _pop_values(values_dct, 'extra_link_args'),
268 _pop_values(values_dct, 'export_symbols'),
269 _pop_values(values_dct, 'swig_opts'),
270 _pop_values(values_dct, 'depends'),
271 values_dct.pop('language', None),
272 values_dct.pop('optional', None),
273 **values_dct))
274
275 def parse_config_files(self, filenames=None):
276 if filenames is None:
277 filenames = self.find_config_files()
278
279 logger.debug("Distribution.parse_config_files():")
280
281 parser = RawConfigParser()
282
283 for filename in filenames:
284 logger.debug(" reading %s", filename)
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200285 parser.read(filename, encoding='utf-8')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200286
287 if os.path.split(filename)[-1] == 'setup.cfg':
288 self._read_setup_cfg(parser, filename)
289
290 for section in parser.sections():
291 if section == 'global':
292 if parser.has_option('global', 'compilers'):
293 self._load_compilers(parser.get('global', 'compilers'))
294
295 if parser.has_option('global', 'commands'):
296 self._load_commands(parser.get('global', 'commands'))
297
298 options = parser.options(section)
299 opt_dict = self.dist.get_option_dict(section)
300
301 for opt in options:
302 if opt == '__name__':
303 continue
304 val = parser.get(section, opt)
305 opt = opt.replace('-', '_')
306
307 if opt == 'sub_commands':
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200308 val = split_multiline(val)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200309 if isinstance(val, str):
310 val = [val]
311
312 # Hooks use a suffix system to prevent being overriden
313 # by a config file processed later (i.e. a hook set in
314 # the user config file cannot be replaced by a hook
315 # set in a project config file, unless they have the
316 # same suffix).
317 if (opt.startswith("pre_hook.") or
318 opt.startswith("post_hook.")):
319 hook_type, alias = opt.split(".")
320 hook_dict = opt_dict.setdefault(
321 hook_type, (filename, {}))[1]
322 hook_dict[alias] = val
323 else:
324 opt_dict[opt] = filename, val
325
326 # Make the RawConfigParser forget everything (so we retain
327 # the original filenames that options come from)
328 parser.__init__()
329
330 # If there was a "global" section in the config file, use it
331 # to set Distribution options.
332 if 'global' in self.dist.command_options:
333 for opt, (src, val) in self.dist.command_options['global'].items():
334 alias = self.dist.negative_opt.get(opt)
335 try:
336 if alias:
337 setattr(self.dist, alias, not strtobool(val))
338 elif opt == 'dry_run': # FIXME ugh!
339 setattr(self.dist, opt, strtobool(val))
340 else:
341 setattr(self.dist, opt, val)
342 except ValueError as msg:
343 raise PackagingOptionError(msg)
344
345 def _load_compilers(self, compilers):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200346 compilers = split_multiline(compilers)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200347 if isinstance(compilers, str):
348 compilers = [compilers]
349 for compiler in compilers:
350 set_compiler(compiler.strip())
351
352 def _load_commands(self, commands):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200353 commands = split_multiline(commands)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200354 if isinstance(commands, str):
355 commands = [commands]
356 for command in commands:
357 set_command(command.strip())