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