blob: 6bbfdc30e9c91636c047ab00d57657931f20cd6c [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
12from packaging.util import check_environ, iglob, resolve_name, strtobool
13from packaging.compiler import set_compiler
14from packaging.command import set_command
15from packaging.markers import interpret
16
17
18def _pop_values(values_dct, key):
19 """Remove values from the dictionary and convert them as a list"""
20 vals_str = values_dct.pop(key, '')
21 if not vals_str:
22 return
23 fields = []
24 for field in vals_str.split(os.linesep):
25 tmp_vals = field.split('--')
26 if len(tmp_vals) == 2 and not interpret(tmp_vals[1]):
27 continue
28 fields.append(tmp_vals[0])
29 # Get bash options like `gcc -print-file-name=libgcc.a` XXX bash options?
30 vals = split(' '.join(fields))
31 if vals:
32 return vals
33
34
35def _rel_path(base, path):
Tarek Ziadeec9b76d2011-05-21 11:48:16 +020036 # normalizes and returns a lstripped-/-separated path
37 base = base.replace(os.path.sep, '/')
38 path = path.replace(os.path.sep, '/')
Tarek Ziade1231a4e2011-05-19 13:07:25 +020039 assert path.startswith(base)
40 return path[len(base):].lstrip('/')
41
42
43def get_resources_dests(resources_root, rules):
44 """Find destinations for resources files"""
45 destinations = {}
46 for base, suffix, dest in rules:
47 prefix = os.path.join(resources_root, base)
48 for abs_base in iglob(prefix):
49 abs_glob = os.path.join(abs_base, suffix)
50 for abs_path in iglob(abs_glob):
51 resource_file = _rel_path(resources_root, abs_path)
52 if dest is None: # remove the entry if it was here
53 destinations.pop(resource_file, None)
54 else:
55 rel_path = _rel_path(abs_base, abs_path)
Tarek Ziadeec9b76d2011-05-21 11:48:16 +020056 rel_dest = dest.replace(os.path.sep, '/').rstrip('/')
57 destinations[resource_file] = rel_dest + '/' + rel_path
Tarek Ziade1231a4e2011-05-19 13:07:25 +020058 return destinations
59
60
61class Config:
62 """Reads configuration files and work with the Distribution instance
63 """
64 def __init__(self, dist):
65 self.dist = dist
66 self.setup_hook = None
67
68 def run_hook(self, config):
69 if self.setup_hook is None:
70 return
71 # the hook gets only the config
72 self.setup_hook(config)
73
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
126 def _multiline(self, value):
127 value = [v for v in
128 [v.strip() for v in value.split('\n')]
129 if v != '']
130 return value
131
132 def _read_setup_cfg(self, parser, cfg_filename):
133 cfg_directory = os.path.dirname(os.path.abspath(cfg_filename))
134 content = {}
135 for section in parser.sections():
136 content[section] = dict(parser.items(section))
137
138 # global:setup_hook is called *first*
139 if 'global' in content:
140 if 'setup_hook' in content['global']:
141 setup_hook = content['global']['setup_hook']
142 try:
143 self.setup_hook = resolve_name(setup_hook)
144 except ImportError as e:
145 logger.warning('could not import setup_hook: %s',
146 e.args[0])
147 else:
148 self.run_hook(content)
149
150 metadata = self.dist.metadata
151
152 # setting the metadata values
153 if 'metadata' in content:
154 for key, value in content['metadata'].items():
155 key = key.replace('_', '-')
156 if metadata.is_multi_field(key):
157 value = self._multiline(value)
158
159 if key == 'project-url':
160 value = [(label.strip(), url.strip())
161 for label, url in
162 [v.split(',') for v in value]]
163
164 if key == 'description-file':
165 if 'description' in content['metadata']:
166 msg = ("description and description-file' are "
167 "mutually exclusive")
168 raise PackagingOptionError(msg)
169
170 if isinstance(value, list):
171 filenames = value
172 else:
173 filenames = value.split()
174
175 # concatenate each files
176 value = ''
177 for filename in filenames:
178 # will raise if file not found
179 with open(filename) as description_file:
180 value += description_file.read().strip() + '\n'
181 # add filename as a required file
182 if filename not in metadata.requires_files:
183 metadata.requires_files.append(filename)
184 value = value.strip()
185 key = 'description'
186
187 if metadata.is_metadata_field(key):
188 metadata[key] = self._convert_metadata(key, value)
189
190 if 'files' in content:
191 files = content['files']
192 self.dist.package_dir = files.pop('packages_root', None)
193
194 files = dict((key, self._multiline(value)) for key, value in
195 files.items())
196
197 self.dist.packages = []
198
199 packages = files.get('packages', [])
200 if isinstance(packages, str):
201 packages = [packages]
202
203 for package in packages:
204 if ':' in package:
205 dir_, package = package.split(':')
206 self.dist.package_dir[package] = dir_
207 self.dist.packages.append(package)
208
209 self.dist.py_modules = files.get('modules', [])
210 if isinstance(self.dist.py_modules, str):
211 self.dist.py_modules = [self.dist.py_modules]
212 self.dist.scripts = files.get('scripts', [])
213 if isinstance(self.dist.scripts, str):
214 self.dist.scripts = [self.dist.scripts]
215
216 self.dist.package_data = {}
217 for data in files.get('package_data', []):
218 data = data.split('=')
219 if len(data) != 2:
220 continue # XXX error should never pass silently
221 key, value = data
222 self.dist.package_data[key.strip()] = value.strip()
223
224 self.dist.data_files = []
225 for data in files.get('data_files', []):
226 data = data.split('=')
227 if len(data) != 2:
228 continue
229 key, value = data
230 values = [v.strip() for v in value.split(',')]
231 self.dist.data_files.append((key, values))
232
233 # manifest template
234 self.dist.extra_files = files.get('extra_files', [])
235
236 resources = []
237 for rule in files.get('resources', []):
238 glob, destination = rule.split('=', 1)
239 rich_glob = glob.strip().split(' ', 1)
240 if len(rich_glob) == 2:
241 prefix, suffix = rich_glob
242 else:
243 assert len(rich_glob) == 1
244 prefix = ''
245 suffix = glob
246 if destination == '<exclude>':
247 destination = None
248 resources.append(
249 (prefix.strip(), suffix.strip(), destination.strip()))
250 self.dist.data_files = get_resources_dests(
251 cfg_directory, resources)
252
253 ext_modules = self.dist.ext_modules
254 for section_key in content:
255 labels = section_key.split('=')
256 if len(labels) == 2 and labels[0] == 'extension':
257 # labels[1] not used from now but should be implemented
258 # for extension build dependency
259 values_dct = content[section_key]
260 ext_modules.append(Extension(
261 values_dct.pop('name'),
262 _pop_values(values_dct, 'sources'),
263 _pop_values(values_dct, 'include_dirs'),
264 _pop_values(values_dct, 'define_macros'),
265 _pop_values(values_dct, 'undef_macros'),
266 _pop_values(values_dct, 'library_dirs'),
267 _pop_values(values_dct, 'libraries'),
268 _pop_values(values_dct, 'runtime_library_dirs'),
269 _pop_values(values_dct, 'extra_objects'),
270 _pop_values(values_dct, 'extra_compile_args'),
271 _pop_values(values_dct, 'extra_link_args'),
272 _pop_values(values_dct, 'export_symbols'),
273 _pop_values(values_dct, 'swig_opts'),
274 _pop_values(values_dct, 'depends'),
275 values_dct.pop('language', None),
276 values_dct.pop('optional', None),
277 **values_dct))
278
279 def parse_config_files(self, filenames=None):
280 if filenames is None:
281 filenames = self.find_config_files()
282
283 logger.debug("Distribution.parse_config_files():")
284
285 parser = RawConfigParser()
286
287 for filename in filenames:
288 logger.debug(" reading %s", filename)
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200289 parser.read(filename, encoding='utf-8')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200290
291 if os.path.split(filename)[-1] == 'setup.cfg':
292 self._read_setup_cfg(parser, filename)
293
294 for section in parser.sections():
295 if section == 'global':
296 if parser.has_option('global', 'compilers'):
297 self._load_compilers(parser.get('global', 'compilers'))
298
299 if parser.has_option('global', 'commands'):
300 self._load_commands(parser.get('global', 'commands'))
301
302 options = parser.options(section)
303 opt_dict = self.dist.get_option_dict(section)
304
305 for opt in options:
306 if opt == '__name__':
307 continue
308 val = parser.get(section, opt)
309 opt = opt.replace('-', '_')
310
311 if opt == 'sub_commands':
312 val = self._multiline(val)
313 if isinstance(val, str):
314 val = [val]
315
316 # Hooks use a suffix system to prevent being overriden
317 # by a config file processed later (i.e. a hook set in
318 # the user config file cannot be replaced by a hook
319 # set in a project config file, unless they have the
320 # same suffix).
321 if (opt.startswith("pre_hook.") or
322 opt.startswith("post_hook.")):
323 hook_type, alias = opt.split(".")
324 hook_dict = opt_dict.setdefault(
325 hook_type, (filename, {}))[1]
326 hook_dict[alias] = val
327 else:
328 opt_dict[opt] = filename, val
329
330 # Make the RawConfigParser forget everything (so we retain
331 # the original filenames that options come from)
332 parser.__init__()
333
334 # If there was a "global" section in the config file, use it
335 # to set Distribution options.
336 if 'global' in self.dist.command_options:
337 for opt, (src, val) in self.dist.command_options['global'].items():
338 alias = self.dist.negative_opt.get(opt)
339 try:
340 if alias:
341 setattr(self.dist, alias, not strtobool(val))
342 elif opt == 'dry_run': # FIXME ugh!
343 setattr(self.dist, opt, strtobool(val))
344 else:
345 setattr(self.dist, opt, val)
346 except ValueError as msg:
347 raise PackagingOptionError(msg)
348
349 def _load_compilers(self, compilers):
350 compilers = self._multiline(compilers)
351 if isinstance(compilers, str):
352 compilers = [compilers]
353 for compiler in compilers:
354 set_compiler(compiler.strip())
355
356 def _load_commands(self, commands):
357 commands = self._multiline(commands)
358 if isinstance(commands, str):
359 commands = [commands]
360 for command in commands:
361 set_command(command.strip())