gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / pylint / utils.py @ 1026
History | View | Annotate | Download (42.8 KB)
1 | 745 | jjdelcerro | # Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
|
---|---|---|---|
2 | # http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||
3 | #
|
||
4 | # This program is free software; you can redistribute it and/or modify it under
|
||
5 | # the terms of the GNU General Public License as published by the Free Software
|
||
6 | # Foundation; either version 2 of the License, or (at your option) any later
|
||
7 | # version.
|
||
8 | #
|
||
9 | # This program is distributed in the hope that it will be useful, but WITHOUT
|
||
10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||
11 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||
12 | #
|
||
13 | # You should have received a copy of the GNU General Public License along with
|
||
14 | # this program; if not, write to the Free Software Foundation, Inc.,
|
||
15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
16 | """some various utilities and helper classes, most of them used in the
|
||
17 | main pylint class
|
||
18 | """
|
||
19 | from __future__ import print_function |
||
20 | |||
21 | import collections |
||
22 | import itertools |
||
23 | import os |
||
24 | from os.path import dirname, basename, splitext, exists, isdir, join, normpath |
||
25 | import re |
||
26 | import sys |
||
27 | import tokenize |
||
28 | import warnings |
||
29 | import textwrap |
||
30 | |||
31 | import six |
||
32 | from six.moves import zip # pylint: disable=redefined-builtin |
||
33 | |||
34 | from astroid import nodes, Module |
||
35 | from astroid.modutils import modpath_from_file, get_module_files, \ |
||
36 | file_from_modpath, load_module_from_file |
||
37 | |||
38 | from pylint.interfaces import IRawChecker, ITokenChecker, UNDEFINED, implements |
||
39 | from pylint.reporters.ureports.nodes import Section |
||
40 | |||
41 | |||
42 | class UnknownMessage(Exception): |
||
43 | """raised when a unregistered message id is encountered"""
|
||
44 | |||
45 | class EmptyReport(Exception): |
||
46 | """raised when a report is empty and so should not be displayed"""
|
||
47 | |||
48 | |||
49 | MSG_TYPES = { |
||
50 | 'I' : 'info', |
||
51 | 'C' : 'convention', |
||
52 | 'R' : 'refactor', |
||
53 | 'W' : 'warning', |
||
54 | 'E' : 'error', |
||
55 | 'F' : 'fatal' |
||
56 | } |
||
57 | MSG_TYPES_LONG = {v: k for k, v in six.iteritems(MSG_TYPES)} |
||
58 | |||
59 | MSG_TYPES_STATUS = { |
||
60 | 'I' : 0, |
||
61 | 'C' : 16, |
||
62 | 'R' : 8, |
||
63 | 'W' : 4, |
||
64 | 'E' : 2, |
||
65 | 'F' : 1 |
||
66 | } |
||
67 | |||
68 | DEPRECATED_ALIASES = { |
||
69 | # New name, deprecated name.
|
||
70 | 'repr': 'backquote', |
||
71 | 'expr': 'discard', |
||
72 | 'assignname': 'assname', |
||
73 | 'assignattr': 'assattr', |
||
74 | 'attribute': 'getattr', |
||
75 | 'call': 'callfunc', |
||
76 | 'importfrom': 'from', |
||
77 | 'classdef': 'class', |
||
78 | 'functiondef': 'function', |
||
79 | 'generatorexp': 'genexpr', |
||
80 | } |
||
81 | |||
82 | _MSG_ORDER = 'EWRCIF'
|
||
83 | MSG_STATE_SCOPE_CONFIG = 0
|
||
84 | MSG_STATE_SCOPE_MODULE = 1
|
||
85 | MSG_STATE_CONFIDENCE = 2
|
||
86 | |||
87 | # Allow stopping after the first semicolon encountered,
|
||
88 | # so that an option can be continued with the reasons
|
||
89 | # why it is active or disabled.
|
||
90 | OPTION_RGX = re.compile(r'\s*#.*\bpylint:\s*([^;]+);{0,1}')
|
||
91 | |||
92 | # The line/node distinction does not apply to fatal errors and reports.
|
||
93 | _SCOPE_EXEMPT = 'FR'
|
||
94 | |||
95 | class WarningScope(object): |
||
96 | LINE = 'line-based-msg'
|
||
97 | NODE = 'node-based-msg'
|
||
98 | |||
99 | _MsgBase = collections.namedtuple( |
||
100 | '_MsgBase',
|
||
101 | ['msg_id', 'symbol', 'msg', 'C', 'category', 'confidence', |
||
102 | 'abspath', 'path', 'module', 'obj', 'line', 'column']) |
||
103 | |||
104 | |||
105 | class Message(_MsgBase): |
||
106 | """This class represent a message to be issued by the reporters"""
|
||
107 | def __new__(cls, msg_id, symbol, location, msg, confidence): |
||
108 | return _MsgBase.__new__(
|
||
109 | cls, msg_id, symbol, msg, msg_id[0], MSG_TYPES[msg_id[0]], |
||
110 | confidence, *location) |
||
111 | |||
112 | def format(self, template): |
||
113 | """Format the message according to the given template.
|
||
114 |
|
||
115 | The template format is the one of the format method :
|
||
116 | cf. http://docs.python.org/2/library/string.html#formatstrings
|
||
117 | """
|
||
118 | # For some reason, _asdict on derived namedtuples does not work with
|
||
119 | # Python 3.4. Needs some investigation.
|
||
120 | return template.format(**dict(zip(self._fields, self))) |
||
121 | |||
122 | |||
123 | def get_module_and_frameid(node): |
||
124 | """return the module name and the frame id in the module"""
|
||
125 | frame = node.frame() |
||
126 | module, obj = '', []
|
||
127 | while frame:
|
||
128 | if isinstance(frame, Module): |
||
129 | module = frame.name |
||
130 | else:
|
||
131 | obj.append(getattr(frame, 'name', '<lambda>')) |
||
132 | try:
|
||
133 | frame = frame.parent.frame() |
||
134 | except AttributeError: |
||
135 | frame = None
|
||
136 | obj.reverse() |
||
137 | return module, '.'.join(obj) |
||
138 | |||
139 | def category_id(cid): |
||
140 | cid = cid.upper() |
||
141 | if cid in MSG_TYPES: |
||
142 | return cid
|
||
143 | return MSG_TYPES_LONG.get(cid)
|
||
144 | |||
145 | |||
146 | def _decoding_readline(stream, encoding): |
||
147 | return lambda: stream.readline().decode(encoding, 'replace') |
||
148 | |||
149 | |||
150 | def tokenize_module(module): |
||
151 | with module.stream() as stream: |
||
152 | readline = stream.readline |
||
153 | if sys.version_info < (3, 0): |
||
154 | if module.file_encoding is not None: |
||
155 | readline = _decoding_readline(stream, module.file_encoding) |
||
156 | |||
157 | return list(tokenize.generate_tokens(readline)) |
||
158 | return list(tokenize.tokenize(readline)) |
||
159 | |||
160 | def build_message_def(checker, msgid, msg_tuple): |
||
161 | if implements(checker, (IRawChecker, ITokenChecker)):
|
||
162 | default_scope = WarningScope.LINE |
||
163 | else:
|
||
164 | default_scope = WarningScope.NODE |
||
165 | options = {} |
||
166 | if len(msg_tuple) > 3: |
||
167 | (msg, symbol, descr, options) = msg_tuple |
||
168 | elif len(msg_tuple) > 2: |
||
169 | (msg, symbol, descr) = msg_tuple[:3]
|
||
170 | else:
|
||
171 | # messages should have a symbol, but for backward compatibility
|
||
172 | # they may not.
|
||
173 | (msg, descr) = msg_tuple |
||
174 | warnings.warn("[pylint 0.26] description of message %s doesn't include "
|
||
175 | "a symbolic name" % msgid, DeprecationWarning) |
||
176 | symbol = None
|
||
177 | options.setdefault('scope', default_scope)
|
||
178 | return MessageDefinition(checker, msgid, msg, descr, symbol, **options)
|
||
179 | |||
180 | |||
181 | class MessageDefinition(object): |
||
182 | def __init__(self, checker, msgid, msg, descr, symbol, scope, |
||
183 | minversion=None, maxversion=None, old_names=None): |
||
184 | self.checker = checker
|
||
185 | assert len(msgid) == 5, 'Invalid message id %s' % msgid |
||
186 | assert msgid[0] in MSG_TYPES, \ |
||
187 | 'Bad message type %s in %r' % (msgid[0], msgid) |
||
188 | self.msgid = msgid
|
||
189 | self.msg = msg
|
||
190 | self.descr = descr
|
||
191 | self.symbol = symbol
|
||
192 | self.scope = scope
|
||
193 | self.minversion = minversion
|
||
194 | self.maxversion = maxversion
|
||
195 | self.old_names = old_names or [] |
||
196 | |||
197 | def may_be_emitted(self): |
||
198 | """return True if message may be emitted using the current interpreter"""
|
||
199 | if self.minversion is not None and self.minversion > sys.version_info: |
||
200 | return False |
||
201 | if self.maxversion is not None and self.maxversion <= sys.version_info: |
||
202 | return False |
||
203 | return True |
||
204 | |||
205 | def format_help(self, checkerref=False): |
||
206 | """return the help string for the given message id"""
|
||
207 | desc = self.descr
|
||
208 | if checkerref:
|
||
209 | desc += ' This message belongs to the %s checker.' % \
|
||
210 | self.checker.name
|
||
211 | title = self.msg
|
||
212 | if self.symbol: |
||
213 | msgid = '%s (%s)' % (self.symbol, self.msgid) |
||
214 | else:
|
||
215 | msgid = self.msgid
|
||
216 | if self.minversion or self.maxversion: |
||
217 | restr = [] |
||
218 | if self.minversion: |
||
219 | restr.append('< %s' % '.'.join([str(n) for n in self.minversion])) |
||
220 | if self.maxversion: |
||
221 | restr.append('>= %s' % '.'.join([str(n) for n in self.maxversion])) |
||
222 | restr = ' or '.join(restr)
|
||
223 | if checkerref:
|
||
224 | desc += " It can't be emitted when using Python %s." % restr
|
||
225 | else:
|
||
226 | desc += " This message can't be emitted when using Python %s." % restr
|
||
227 | desc = _normalize_text(' '.join(desc.split()), indent=' ') |
||
228 | if title != '%s': |
||
229 | title = title.splitlines()[0]
|
||
230 | return ':%s: *%s*\n%s' % (msgid, title, desc) |
||
231 | return ':%s:\n%s' % (msgid, desc) |
||
232 | |||
233 | |||
234 | class MessagesHandlerMixIn(object): |
||
235 | """a mix-in class containing all the messages related methods for the main
|
||
236 | lint class
|
||
237 | """
|
||
238 | |||
239 | def __init__(self): |
||
240 | self._msgs_state = {}
|
||
241 | self.msg_status = 0 |
||
242 | |||
243 | def _checker_messages(self, checker): |
||
244 | for checker in self._checkers[checker.lower()]: |
||
245 | for msgid in checker.msgs: |
||
246 | yield msgid
|
||
247 | |||
248 | def disable(self, msgid, scope='package', line=None, ignore_unknown=False): |
||
249 | """don't output message of the given id"""
|
||
250 | assert scope in ('package', 'module') |
||
251 | # handle disable=all by disabling all categories
|
||
252 | if msgid == 'all': |
||
253 | for msgid in MSG_TYPES: |
||
254 | self.disable(msgid, scope, line)
|
||
255 | return
|
||
256 | # msgid is a category?
|
||
257 | catid = category_id(msgid) |
||
258 | if catid is not None: |
||
259 | for _msgid in self.msgs_store._msgs_by_category.get(catid): |
||
260 | self.disable(_msgid, scope, line)
|
||
261 | return
|
||
262 | # msgid is a checker name?
|
||
263 | if msgid.lower() in self._checkers: |
||
264 | msgs_store = self.msgs_store
|
||
265 | for checker in self._checkers[msgid.lower()]: |
||
266 | for _msgid in checker.msgs: |
||
267 | if _msgid in msgs_store._alternative_names: |
||
268 | self.disable(_msgid, scope, line)
|
||
269 | return
|
||
270 | # msgid is report id?
|
||
271 | if msgid.lower().startswith('rp'): |
||
272 | self.disable_report(msgid)
|
||
273 | return
|
||
274 | |||
275 | try:
|
||
276 | # msgid is a symbolic or numeric msgid.
|
||
277 | msg = self.msgs_store.check_message_id(msgid)
|
||
278 | except UnknownMessage:
|
||
279 | if ignore_unknown:
|
||
280 | return
|
||
281 | raise
|
||
282 | |||
283 | if scope == 'module': |
||
284 | self.file_state.set_msg_status(msg, line, False) |
||
285 | if msg.symbol != 'locally-disabled': |
||
286 | self.add_message('locally-disabled', line=line, |
||
287 | args=(msg.symbol, msg.msgid)) |
||
288 | |||
289 | else:
|
||
290 | msgs = self._msgs_state
|
||
291 | msgs[msg.msgid] = False
|
||
292 | # sync configuration object
|
||
293 | self.config.disable = [self._message_symbol(mid) |
||
294 | for mid, val in six.iteritems(msgs) |
||
295 | if not val] |
||
296 | |||
297 | def _message_symbol(self, msgid): |
||
298 | """Get the message symbol of the given message id
|
||
299 |
|
||
300 | Return the original message id if the message does not
|
||
301 | exist.
|
||
302 | """
|
||
303 | try:
|
||
304 | return self.msgs_store.check_message_id(msgid).symbol |
||
305 | except UnknownMessage:
|
||
306 | return msgid
|
||
307 | |||
308 | def enable(self, msgid, scope='package', line=None, ignore_unknown=False): |
||
309 | """reenable message of the given id"""
|
||
310 | assert scope in ('package', 'module') |
||
311 | if msgid == 'all': |
||
312 | for msgid_ in MSG_TYPES: |
||
313 | self.enable(msgid_, scope=scope, line=line)
|
||
314 | if not self._python3_porting_mode: |
||
315 | # Don't activate the python 3 porting checker if it
|
||
316 | # wasn't activated explicitly.
|
||
317 | self.disable('python3') |
||
318 | return
|
||
319 | catid = category_id(msgid) |
||
320 | # msgid is a category?
|
||
321 | if catid is not None: |
||
322 | for msgid in self.msgs_store._msgs_by_category.get(catid): |
||
323 | self.enable(msgid, scope, line)
|
||
324 | return
|
||
325 | # msgid is a checker name?
|
||
326 | if msgid.lower() in self._checkers: |
||
327 | for checker in self._checkers[msgid.lower()]: |
||
328 | for msgid_ in checker.msgs: |
||
329 | self.enable(msgid_, scope, line)
|
||
330 | return
|
||
331 | # msgid is report id?
|
||
332 | if msgid.lower().startswith('rp'): |
||
333 | self.enable_report(msgid)
|
||
334 | return
|
||
335 | |||
336 | try:
|
||
337 | # msgid is a symbolic or numeric msgid.
|
||
338 | msg = self.msgs_store.check_message_id(msgid)
|
||
339 | except UnknownMessage:
|
||
340 | if ignore_unknown:
|
||
341 | return
|
||
342 | raise
|
||
343 | |||
344 | if scope == 'module': |
||
345 | self.file_state.set_msg_status(msg, line, True) |
||
346 | self.add_message('locally-enabled', line=line, args=(msg.symbol, msg.msgid)) |
||
347 | else:
|
||
348 | msgs = self._msgs_state
|
||
349 | msgs[msg.msgid] = True
|
||
350 | # sync configuration object
|
||
351 | self.config.enable = [mid for mid, val in six.iteritems(msgs) if val] |
||
352 | |||
353 | def get_message_state_scope(self, msgid, line=None, confidence=UNDEFINED): |
||
354 | """Returns the scope at which a message was enabled/disabled."""
|
||
355 | if self.config.confidence and confidence.name not in self.config.confidence: |
||
356 | return MSG_STATE_CONFIDENCE
|
||
357 | try:
|
||
358 | if line in self.file_state._module_msgs_state[msgid]: |
||
359 | return MSG_STATE_SCOPE_MODULE
|
||
360 | except (KeyError, TypeError): |
||
361 | return MSG_STATE_SCOPE_CONFIG
|
||
362 | |||
363 | def is_message_enabled(self, msg_descr, line=None, confidence=None): |
||
364 | """return true if the message associated to the given message id is
|
||
365 | enabled
|
||
366 |
|
||
367 | msgid may be either a numeric or symbolic message id.
|
||
368 | """
|
||
369 | if self.config.confidence and confidence: |
||
370 | if confidence.name not in self.config.confidence: |
||
371 | return False |
||
372 | try:
|
||
373 | msgid = self.msgs_store.check_message_id(msg_descr).msgid
|
||
374 | except UnknownMessage:
|
||
375 | # The linter checks for messages that are not registered
|
||
376 | # due to version mismatch, just treat them as message IDs
|
||
377 | # for now.
|
||
378 | msgid = msg_descr |
||
379 | if line is None: |
||
380 | return self._msgs_state.get(msgid, True) |
||
381 | try:
|
||
382 | return self.file_state._module_msgs_state[msgid][line] |
||
383 | except KeyError: |
||
384 | return self._msgs_state.get(msgid, True) |
||
385 | |||
386 | def add_message(self, msg_descr, line=None, node=None, args=None, confidence=UNDEFINED): |
||
387 | """Adds a message given by ID or name.
|
||
388 |
|
||
389 | If provided, the message string is expanded using args
|
||
390 |
|
||
391 | AST checkers should must the node argument (but may optionally
|
||
392 | provide line if the line number is different), raw and token checkers
|
||
393 | must provide the line argument.
|
||
394 | """
|
||
395 | msg_info = self.msgs_store.check_message_id(msg_descr)
|
||
396 | msgid = msg_info.msgid |
||
397 | # backward compatibility, message may not have a symbol
|
||
398 | symbol = msg_info.symbol or msgid
|
||
399 | # Fatal messages and reports are special, the node/scope distinction
|
||
400 | # does not apply to them.
|
||
401 | if msgid[0] not in _SCOPE_EXEMPT: |
||
402 | if msg_info.scope == WarningScope.LINE:
|
||
403 | assert node is None and line is not None, ( |
||
404 | 'Message %s must only provide line, got line=%s, node=%s' % (msgid, line, node))
|
||
405 | elif msg_info.scope == WarningScope.NODE:
|
||
406 | # Node-based warnings may provide an override line.
|
||
407 | assert node is not None, 'Message %s must provide Node, got None' |
||
408 | |||
409 | if line is None and node is not None: |
||
410 | line = node.fromlineno |
||
411 | if hasattr(node, 'col_offset'): |
||
412 | col_offset = node.col_offset # XXX measured in bytes for utf-8, divide by two for chars?
|
||
413 | else:
|
||
414 | col_offset = None
|
||
415 | # should this message be displayed
|
||
416 | if not self.is_message_enabled(msgid, line, confidence): |
||
417 | self.file_state.handle_ignored_message(
|
||
418 | self.get_message_state_scope(msgid, line, confidence),
|
||
419 | msgid, line, node, args, confidence) |
||
420 | return
|
||
421 | # update stats
|
||
422 | msg_cat = MSG_TYPES[msgid[0]]
|
||
423 | self.msg_status |= MSG_TYPES_STATUS[msgid[0]] |
||
424 | self.stats[msg_cat] += 1 |
||
425 | self.stats['by_module'][self.current_name][msg_cat] += 1 |
||
426 | try:
|
||
427 | self.stats['by_msg'][symbol] += 1 |
||
428 | except KeyError: |
||
429 | self.stats['by_msg'][symbol] = 1 |
||
430 | # expand message ?
|
||
431 | msg = msg_info.msg |
||
432 | if args:
|
||
433 | msg %= args |
||
434 | # get module and object
|
||
435 | if node is None: |
||
436 | module, obj = self.current_name, '' |
||
437 | abspath = self.current_file
|
||
438 | else:
|
||
439 | module, obj = get_module_and_frameid(node) |
||
440 | abspath = node.root().file |
||
441 | path = abspath.replace(self.reporter.path_strip_prefix, '') |
||
442 | # add the message
|
||
443 | self.reporter.handle_message(
|
||
444 | Message(msgid, symbol, |
||
445 | (abspath, path, module, obj, line or 1, col_offset or 0), msg, confidence)) |
||
446 | |||
447 | def print_full_documentation(self): |
||
448 | """output a full documentation in ReST format"""
|
||
449 | print("Pylint global options and switches")
|
||
450 | print("----------------------------------")
|
||
451 | print("")
|
||
452 | print("Pylint provides global options and switches.")
|
||
453 | print("")
|
||
454 | |||
455 | by_checker = {} |
||
456 | for checker in self.get_checkers(): |
||
457 | if checker.name == 'master': |
||
458 | if checker.options:
|
||
459 | for section, options in checker.options_by_section(): |
||
460 | if section is None: |
||
461 | title = 'General options'
|
||
462 | else:
|
||
463 | title = '%s options' % section.capitalize()
|
||
464 | print(title) |
||
465 | print('~' * len(title)) |
||
466 | _rest_format_section(sys.stdout, None, options)
|
||
467 | print("")
|
||
468 | else:
|
||
469 | try:
|
||
470 | by_checker[checker.name][0] += checker.options_and_values()
|
||
471 | by_checker[checker.name][1].update(checker.msgs)
|
||
472 | by_checker[checker.name][2] += checker.reports
|
||
473 | except KeyError: |
||
474 | by_checker[checker.name] = [list(checker.options_and_values()),
|
||
475 | dict(checker.msgs),
|
||
476 | list(checker.reports)]
|
||
477 | |||
478 | print("Pylint checkers' options and switches")
|
||
479 | print("-------------------------------------")
|
||
480 | print("")
|
||
481 | print("Pylint checkers can provide three set of features:")
|
||
482 | print("")
|
||
483 | print("* options that control their execution,")
|
||
484 | print("* messages that they can raise,")
|
||
485 | print("* reports that they can generate.")
|
||
486 | print("")
|
||
487 | print("Below is a list of all checkers and their features.")
|
||
488 | print("")
|
||
489 | |||
490 | for checker, (options, msgs, reports) in six.iteritems(by_checker): |
||
491 | title = '%s checker' % (checker.replace("_", " ").title()) |
||
492 | print(title) |
||
493 | print('~' * len(title)) |
||
494 | print("")
|
||
495 | print("Verbatim name of the checker is ``%s``." % checker)
|
||
496 | print("")
|
||
497 | if options:
|
||
498 | title = 'Options'
|
||
499 | print(title) |
||
500 | print('^' * len(title)) |
||
501 | _rest_format_section(sys.stdout, None, options)
|
||
502 | print("")
|
||
503 | if msgs:
|
||
504 | title = 'Messages'
|
||
505 | print(title) |
||
506 | print('~' * len(title)) |
||
507 | for msgid, msg in sorted(six.iteritems(msgs), |
||
508 | key=lambda kv: (_MSG_ORDER.index(kv[0][0]), kv[1])): |
||
509 | msg = build_message_def(checker, msgid, msg) |
||
510 | print(msg.format_help(checkerref=False))
|
||
511 | print("")
|
||
512 | if reports:
|
||
513 | title = 'Reports'
|
||
514 | print(title) |
||
515 | print('~' * len(title)) |
||
516 | for report in reports: |
||
517 | print(':%s: %s' % report[:2]) |
||
518 | print("")
|
||
519 | print("")
|
||
520 | |||
521 | |||
522 | class FileState(object): |
||
523 | """Hold internal state specific to the currently analyzed file"""
|
||
524 | |||
525 | def __init__(self, modname=None): |
||
526 | self.base_name = modname
|
||
527 | self._module_msgs_state = {}
|
||
528 | self._raw_module_msgs_state = {}
|
||
529 | self._ignored_msgs = collections.defaultdict(set) |
||
530 | self._suppression_mapping = {}
|
||
531 | |||
532 | def collect_block_lines(self, msgs_store, module_node): |
||
533 | """Walk the AST to collect block level options line numbers."""
|
||
534 | for msg, lines in six.iteritems(self._module_msgs_state): |
||
535 | self._raw_module_msgs_state[msg] = lines.copy()
|
||
536 | orig_state = self._module_msgs_state.copy()
|
||
537 | self._module_msgs_state = {}
|
||
538 | self._suppression_mapping = {}
|
||
539 | self._collect_block_lines(msgs_store, module_node, orig_state)
|
||
540 | |||
541 | def _collect_block_lines(self, msgs_store, node, msg_state): |
||
542 | """Recursivly walk (depth first) AST to collect block level options line
|
||
543 | numbers.
|
||
544 | """
|
||
545 | for child in node.get_children(): |
||
546 | self._collect_block_lines(msgs_store, child, msg_state)
|
||
547 | first = node.fromlineno |
||
548 | last = node.tolineno |
||
549 | # first child line number used to distinguish between disable
|
||
550 | # which are the first child of scoped node with those defined later.
|
||
551 | # For instance in the code below:
|
||
552 | #
|
||
553 | # 1. def meth8(self):
|
||
554 | # 2. """test late disabling"""
|
||
555 | # 3. # pylint: disable=E1102
|
||
556 | # 4. print self.blip
|
||
557 | # 5. # pylint: disable=E1101
|
||
558 | # 6. print self.bla
|
||
559 | #
|
||
560 | # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6
|
||
561 | #
|
||
562 | # this is necessary to disable locally messages applying to class /
|
||
563 | # function using their fromlineno
|
||
564 | if (isinstance(node, (nodes.Module, nodes.ClassDef, nodes.FunctionDef)) |
||
565 | and node.body):
|
||
566 | firstchildlineno = node.body[0].fromlineno
|
||
567 | else:
|
||
568 | firstchildlineno = last |
||
569 | for msgid, lines in six.iteritems(msg_state): |
||
570 | for lineno, state in list(lines.items()): |
||
571 | original_lineno = lineno |
||
572 | if first > lineno or last < lineno: |
||
573 | continue
|
||
574 | # Set state for all lines for this block, if the
|
||
575 | # warning is applied to nodes.
|
||
576 | if msgs_store.check_message_id(msgid).scope == WarningScope.NODE:
|
||
577 | if lineno > firstchildlineno:
|
||
578 | state = True
|
||
579 | first_, last_ = node.block_range(lineno) |
||
580 | else:
|
||
581 | first_ = lineno |
||
582 | last_ = last |
||
583 | for line in range(first_, last_+1): |
||
584 | # do not override existing entries
|
||
585 | if line in self._module_msgs_state.get(msgid, ()): |
||
586 | continue
|
||
587 | if line in lines: # state change in the same block |
||
588 | state = lines[line] |
||
589 | original_lineno = line |
||
590 | if not state: |
||
591 | self._suppression_mapping[(msgid, line)] = original_lineno
|
||
592 | try:
|
||
593 | self._module_msgs_state[msgid][line] = state
|
||
594 | except KeyError: |
||
595 | self._module_msgs_state[msgid] = {line: state}
|
||
596 | del lines[lineno]
|
||
597 | |||
598 | def set_msg_status(self, msg, line, status): |
||
599 | """Set status (enabled/disable) for a given message at a given line"""
|
||
600 | assert line > 0 |
||
601 | try:
|
||
602 | self._module_msgs_state[msg.msgid][line] = status
|
||
603 | except KeyError: |
||
604 | self._module_msgs_state[msg.msgid] = {line: status}
|
||
605 | |||
606 | def handle_ignored_message(self, state_scope, msgid, line, |
||
607 | node, args, confidence): # pylint: disable=unused-argument
|
||
608 | """Report an ignored message.
|
||
609 |
|
||
610 | state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG,
|
||
611 | depending on whether the message was disabled locally in the module,
|
||
612 | or globally. The other arguments are the same as for add_message.
|
||
613 | """
|
||
614 | if state_scope == MSG_STATE_SCOPE_MODULE:
|
||
615 | try:
|
||
616 | orig_line = self._suppression_mapping[(msgid, line)]
|
||
617 | self._ignored_msgs[(msgid, orig_line)].add(line)
|
||
618 | except KeyError: |
||
619 | pass
|
||
620 | |||
621 | def iter_spurious_suppression_messages(self, msgs_store): |
||
622 | for warning, lines in six.iteritems(self._raw_module_msgs_state): |
||
623 | for line, enable in six.iteritems(lines): |
||
624 | if not enable and (warning, line) not in self._ignored_msgs: |
||
625 | yield 'useless-suppression', line, \ |
||
626 | (msgs_store.get_msg_display_string(warning),) |
||
627 | # don't use iteritems here, _ignored_msgs may be modified by add_message
|
||
628 | for (warning, from_), lines in list(self._ignored_msgs.items()): |
||
629 | for line in lines: |
||
630 | yield 'suppressed-message', line, \ |
||
631 | (msgs_store.get_msg_display_string(warning), from_) |
||
632 | |||
633 | |||
634 | class MessagesStore(object): |
||
635 | """The messages store knows information about every possible message but has
|
||
636 | no particular state during analysis.
|
||
637 | """
|
||
638 | |||
639 | def __init__(self): |
||
640 | # Primary registry for all active messages (i.e. all messages
|
||
641 | # that can be emitted by pylint for the underlying Python
|
||
642 | # version). It contains the 1:1 mapping from symbolic names
|
||
643 | # to message definition objects.
|
||
644 | self._messages = {}
|
||
645 | # Maps alternative names (numeric IDs, deprecated names) to
|
||
646 | # message definitions. May contain several names for each definition
|
||
647 | # object.
|
||
648 | self._alternative_names = {}
|
||
649 | self._msgs_by_category = collections.defaultdict(list) |
||
650 | |||
651 | @property
|
||
652 | def messages(self): |
||
653 | """The list of all active messages."""
|
||
654 | return six.itervalues(self._messages) |
||
655 | |||
656 | def add_renamed_message(self, old_id, old_symbol, new_symbol): |
||
657 | """Register the old ID and symbol for a warning that was renamed.
|
||
658 |
|
||
659 | This allows users to keep using the old ID/symbol in suppressions.
|
||
660 | """
|
||
661 | msg = self.check_message_id(new_symbol)
|
||
662 | msg.old_names.append((old_id, old_symbol)) |
||
663 | self._alternative_names[old_id] = msg
|
||
664 | self._alternative_names[old_symbol] = msg
|
||
665 | |||
666 | def register_messages(self, checker): |
||
667 | """register a dictionary of messages
|
||
668 |
|
||
669 | Keys are message ids, values are a 2-uple with the message type and the
|
||
670 | message itself
|
||
671 |
|
||
672 | message ids should be a string of len 4, where the two first characters
|
||
673 | are the checker id and the two last the message id in this checker
|
||
674 | """
|
||
675 | chkid = None
|
||
676 | for msgid, msg_tuple in six.iteritems(checker.msgs): |
||
677 | msg = build_message_def(checker, msgid, msg_tuple) |
||
678 | assert msg.symbol not in self._messages, \ |
||
679 | 'Message symbol %r is already defined' % msg.symbol
|
||
680 | # avoid duplicate / malformed ids
|
||
681 | assert msg.msgid not in self._alternative_names, \ |
||
682 | 'Message id %r is already defined' % msgid
|
||
683 | assert chkid is None or chkid == msg.msgid[1:3], \ |
||
684 | 'Inconsistent checker part in message id %r' % msgid
|
||
685 | chkid = msg.msgid[1:3] |
||
686 | self._messages[msg.symbol] = msg
|
||
687 | self._alternative_names[msg.msgid] = msg
|
||
688 | for old_id, old_symbol in msg.old_names: |
||
689 | self._alternative_names[old_id] = msg
|
||
690 | self._alternative_names[old_symbol] = msg
|
||
691 | self._msgs_by_category[msg.msgid[0]].append(msg.msgid) |
||
692 | |||
693 | def check_message_id(self, msgid): |
||
694 | """returns the Message object for this message.
|
||
695 |
|
||
696 | msgid may be either a numeric or symbolic id.
|
||
697 |
|
||
698 | Raises UnknownMessage if the message id is not defined.
|
||
699 | """
|
||
700 | if msgid[1:].isdigit(): |
||
701 | msgid = msgid.upper() |
||
702 | for source in (self._alternative_names, self._messages): |
||
703 | try:
|
||
704 | return source[msgid]
|
||
705 | except KeyError: |
||
706 | pass
|
||
707 | raise UnknownMessage('No such message id %s' % msgid) |
||
708 | |||
709 | def get_msg_display_string(self, msgid): |
||
710 | """Generates a user-consumable representation of a message.
|
||
711 |
|
||
712 | Can be just the message ID or the ID and the symbol.
|
||
713 | """
|
||
714 | return repr(self.check_message_id(msgid).symbol) |
||
715 | |||
716 | def help_message(self, msgids): |
||
717 | """display help messages for the given message identifiers"""
|
||
718 | for msgid in msgids: |
||
719 | try:
|
||
720 | print(self.check_message_id(msgid).format_help(checkerref=True)) |
||
721 | print("")
|
||
722 | except UnknownMessage as ex: |
||
723 | print(ex) |
||
724 | print("")
|
||
725 | continue
|
||
726 | |||
727 | def list_messages(self): |
||
728 | """output full messages list documentation in ReST format"""
|
||
729 | msgs = sorted(six.itervalues(self._messages), key=lambda msg: msg.msgid) |
||
730 | for msg in msgs: |
||
731 | if not msg.may_be_emitted(): |
||
732 | continue
|
||
733 | print(msg.format_help(checkerref=False))
|
||
734 | print("")
|
||
735 | |||
736 | |||
737 | class ReportsHandlerMixIn(object): |
||
738 | """a mix-in class containing all the reports and stats manipulation
|
||
739 | related methods for the main lint class
|
||
740 | """
|
||
741 | def __init__(self): |
||
742 | self._reports = collections.defaultdict(list) |
||
743 | self._reports_state = {}
|
||
744 | |||
745 | def report_order(self): |
||
746 | """ Return a list of reports, sorted in the order
|
||
747 | in which they must be called.
|
||
748 | """
|
||
749 | return list(self._reports) |
||
750 | |||
751 | def register_report(self, reportid, r_title, r_cb, checker): |
||
752 | """register a report
|
||
753 |
|
||
754 | reportid is the unique identifier for the report
|
||
755 | r_title the report's title
|
||
756 | r_cb the method to call to make the report
|
||
757 | checker is the checker defining the report
|
||
758 | """
|
||
759 | reportid = reportid.upper() |
||
760 | self._reports[checker].append((reportid, r_title, r_cb))
|
||
761 | |||
762 | def enable_report(self, reportid): |
||
763 | """disable the report of the given id"""
|
||
764 | reportid = reportid.upper() |
||
765 | self._reports_state[reportid] = True |
||
766 | |||
767 | def disable_report(self, reportid): |
||
768 | """disable the report of the given id"""
|
||
769 | reportid = reportid.upper() |
||
770 | self._reports_state[reportid] = False |
||
771 | |||
772 | def report_is_enabled(self, reportid): |
||
773 | """return true if the report associated to the given identifier is
|
||
774 | enabled
|
||
775 | """
|
||
776 | return self._reports_state.get(reportid, True) |
||
777 | |||
778 | def make_reports(self, stats, old_stats): |
||
779 | """render registered reports"""
|
||
780 | sect = Section('Report',
|
||
781 | '%s statements analysed.'% (self.stats['statement'])) |
||
782 | for checker in self.report_order(): |
||
783 | for reportid, r_title, r_cb in self._reports[checker]: |
||
784 | if not self.report_is_enabled(reportid): |
||
785 | continue
|
||
786 | report_sect = Section(r_title) |
||
787 | try:
|
||
788 | r_cb(report_sect, stats, old_stats) |
||
789 | except EmptyReport:
|
||
790 | continue
|
||
791 | report_sect.report_id = reportid |
||
792 | sect.append(report_sect) |
||
793 | return sect
|
||
794 | |||
795 | def add_stats(self, **kwargs): |
||
796 | """add some stats entries to the statistic dictionary
|
||
797 | raise an AssertionError if there is a key conflict
|
||
798 | """
|
||
799 | for key, value in six.iteritems(kwargs): |
||
800 | if key[-1] == '_': |
||
801 | key = key[:-1]
|
||
802 | assert key not in self.stats |
||
803 | self.stats[key] = value
|
||
804 | return self.stats |
||
805 | |||
806 | |||
807 | def expand_modules(files_or_modules, black_list): |
||
808 | """take a list of files/modules/packages and return the list of tuple
|
||
809 | (file, module name) which have to be actually checked
|
||
810 | """
|
||
811 | result = [] |
||
812 | errors = [] |
||
813 | for something in files_or_modules: |
||
814 | if exists(something):
|
||
815 | # this is a file or a directory
|
||
816 | try:
|
||
817 | modname = '.'.join(modpath_from_file(something))
|
||
818 | except ImportError: |
||
819 | modname = splitext(basename(something))[0]
|
||
820 | if isdir(something):
|
||
821 | filepath = join(something, '__init__.py')
|
||
822 | else:
|
||
823 | filepath = something |
||
824 | else:
|
||
825 | # suppose it's a module or package
|
||
826 | modname = something |
||
827 | try:
|
||
828 | filepath = file_from_modpath(modname.split('.'))
|
||
829 | if filepath is None: |
||
830 | continue
|
||
831 | except (ImportError, SyntaxError) as ex: |
||
832 | # FIXME p3k : the SyntaxError is a Python bug and should be
|
||
833 | # removed as soon as possible http://bugs.python.org/issue10588
|
||
834 | errors.append({'key': 'fatal', 'mod': modname, 'ex': ex}) |
||
835 | continue
|
||
836 | if getattr(filepath,"copy",None)==None: |
||
837 | filepath = normpath(filepath) |
||
838 | else:
|
||
839 | filepath = filepath.copy(normpath(filepath)) |
||
840 | result.append({'path': filepath, 'name': modname, 'isarg': True, |
||
841 | 'basepath': filepath, 'basename': modname}) |
||
842 | if not (modname.endswith('.__init__') or modname == '__init__') \ |
||
843 | and '__init__.py' in filepath: |
||
844 | for subfilepath in get_module_files(dirname(filepath), black_list): |
||
845 | if filepath == subfilepath:
|
||
846 | continue
|
||
847 | submodname = '.'.join(modpath_from_file(subfilepath))
|
||
848 | result.append({'path': subfilepath, 'name': submodname, |
||
849 | 'isarg': False, |
||
850 | 'basepath': filepath, 'basename': modname}) |
||
851 | return result, errors
|
||
852 | |||
853 | |||
854 | class PyLintASTWalker(object): |
||
855 | |||
856 | def __init__(self, linter): |
||
857 | # callbacks per node types
|
||
858 | self.nbstatements = 0 |
||
859 | self.visit_events = collections.defaultdict(list) |
||
860 | self.leave_events = collections.defaultdict(list) |
||
861 | self.linter = linter
|
||
862 | |||
863 | def _is_method_enabled(self, method): |
||
864 | if not hasattr(method, 'checks_msgs'): |
||
865 | return True |
||
866 | for msg_desc in method.checks_msgs: |
||
867 | if self.linter.is_message_enabled(msg_desc): |
||
868 | return True |
||
869 | return False |
||
870 | |||
871 | def add_checker(self, checker): |
||
872 | """walk to the checker's dir and collect visit and leave methods"""
|
||
873 | # XXX : should be possible to merge needed_checkers and add_checker
|
||
874 | vcids = set()
|
||
875 | lcids = set()
|
||
876 | visits = self.visit_events
|
||
877 | leaves = self.leave_events
|
||
878 | for member in dir(checker): |
||
879 | cid = member[6:]
|
||
880 | if cid == 'default': |
||
881 | continue
|
||
882 | if member.startswith('visit_'): |
||
883 | v_meth = getattr(checker, member)
|
||
884 | # don't use visit_methods with no activated message:
|
||
885 | if self._is_method_enabled(v_meth): |
||
886 | visits[cid].append(v_meth) |
||
887 | vcids.add(cid) |
||
888 | elif member.startswith('leave_'): |
||
889 | l_meth = getattr(checker, member)
|
||
890 | # don't use leave_methods with no activated message:
|
||
891 | if self._is_method_enabled(l_meth): |
||
892 | leaves[cid].append(l_meth) |
||
893 | lcids.add(cid) |
||
894 | visit_default = getattr(checker, 'visit_default', None) |
||
895 | if visit_default:
|
||
896 | for cls in nodes.ALL_NODE_CLASSES: |
||
897 | cid = cls.__name__.lower() |
||
898 | if cid not in vcids: |
||
899 | visits[cid].append(visit_default) |
||
900 | # for now we have no "leave_default" method in Pylint
|
||
901 | |||
902 | def walk(self, astroid): |
||
903 | """call visit events of astroid checkers for the given node, recurse on
|
||
904 | its children, then leave events.
|
||
905 | """
|
||
906 | cid = astroid.__class__.__name__.lower() |
||
907 | |||
908 | # Detect if the node is a new name for a deprecated alias.
|
||
909 | # In this case, favour the methods for the deprecated
|
||
910 | # alias if any, in order to maintain backwards
|
||
911 | # compatibility.
|
||
912 | old_cid = DEPRECATED_ALIASES.get(cid) |
||
913 | visit_events = () |
||
914 | leave_events = () |
||
915 | |||
916 | if old_cid:
|
||
917 | visit_events = self.visit_events.get(old_cid, ())
|
||
918 | leave_events = self.leave_events.get(old_cid, ())
|
||
919 | if visit_events or leave_events: |
||
920 | msg = ("Implemented method {meth}_{old} instead of {meth}_{new}. "
|
||
921 | "This will be supported until Pylint 2.0.")
|
||
922 | if visit_events:
|
||
923 | warnings.warn(msg.format(meth="visit", old=old_cid, new=cid),
|
||
924 | PendingDeprecationWarning)
|
||
925 | if leave_events:
|
||
926 | warnings.warn(msg.format(meth="leave", old=old_cid, new=cid),
|
||
927 | PendingDeprecationWarning)
|
||
928 | |||
929 | visit_events = itertools.chain(visit_events, |
||
930 | self.visit_events.get(cid, ()))
|
||
931 | leave_events = itertools.chain(leave_events, |
||
932 | self.leave_events.get(cid, ()))
|
||
933 | |||
934 | if astroid.is_statement:
|
||
935 | self.nbstatements += 1 |
||
936 | # generate events for this node on each checker
|
||
937 | for cb in visit_events or (): |
||
938 | cb(astroid) |
||
939 | # recurse on children
|
||
940 | for child in astroid.get_children(): |
||
941 | self.walk(child)
|
||
942 | for cb in leave_events or (): |
||
943 | cb(astroid) |
||
944 | |||
945 | |||
946 | PY_EXTS = ('.py', '.pyc', '.pyo', '.pyw', '.so', '.dll') |
||
947 | |||
948 | def register_plugins(linter, directory): |
||
949 | """load all module and package in the given directory, looking for a
|
||
950 | 'register' function in each one, used to register pylint checkers
|
||
951 | """
|
||
952 | imported = {} |
||
953 | for filename in os.listdir(directory): |
||
954 | base, extension = splitext(filename) |
||
955 | if base in imported or base == '__pycache__': |
||
956 | continue
|
||
957 | if extension in PY_EXTS and base != '__init__' or ( |
||
958 | not extension and isdir(join(directory, base))): |
||
959 | try:
|
||
960 | module = load_module_from_file(join(directory, filename)) |
||
961 | except ValueError: |
||
962 | # empty module name (usually emacs auto-save files)
|
||
963 | continue
|
||
964 | except ImportError as exc: |
||
965 | print("Problem importing module %s: %s" % (filename, exc),
|
||
966 | file=sys.stderr) |
||
967 | else:
|
||
968 | if hasattr(module, 'register'): |
||
969 | module.register(linter) |
||
970 | imported[base] = 1
|
||
971 | |||
972 | def get_global_option(checker, option, default=None): |
||
973 | """ Retrieve an option defined by the given *checker* or
|
||
974 | by all known option providers.
|
||
975 |
|
||
976 | It will look in the list of all options providers
|
||
977 | until the given *option* will be found.
|
||
978 | If the option wasn't found, the *default* value will be returned.
|
||
979 | """
|
||
980 | # First, try in the given checker's config.
|
||
981 | # After that, look in the options providers.
|
||
982 | |||
983 | try:
|
||
984 | return getattr(checker.config, option.replace("-", "_")) |
||
985 | except AttributeError: |
||
986 | pass
|
||
987 | for provider in checker.linter.options_providers: |
||
988 | for options in provider.options: |
||
989 | if options[0] == option: |
||
990 | return getattr(provider.config, option.replace("-", "_")) |
||
991 | return default
|
||
992 | |||
993 | |||
994 | def deprecated_option(shortname=None, opt_type=None, help_msg=None): |
||
995 | def _warn_deprecated(option, optname, *args): # pylint: disable=unused-argument |
||
996 | msg = ("Warning: option %s is obsolete and "
|
||
997 | "it is slated for removal in Pylint 1.6.\n")
|
||
998 | sys.stderr.write(msg % (optname,)) |
||
999 | |||
1000 | option = { |
||
1001 | 'help': help_msg,
|
||
1002 | 'hide': True, |
||
1003 | 'type': opt_type,
|
||
1004 | 'action': 'callback', |
||
1005 | 'callback': _warn_deprecated,
|
||
1006 | 'deprecated': True |
||
1007 | } |
||
1008 | if shortname:
|
||
1009 | option['shortname'] = shortname
|
||
1010 | return option
|
||
1011 | |||
1012 | |||
1013 | def _splitstrip(string, sep=','): |
||
1014 | """return a list of stripped string by splitting the string given as
|
||
1015 | argument on `sep` (',' by default). Empty string are discarded.
|
||
1016 |
|
||
1017 | >>> _splitstrip('a, b, c , 4,,')
|
||
1018 | ['a', 'b', 'c', '4']
|
||
1019 | >>> _splitstrip('a')
|
||
1020 | ['a']
|
||
1021 | >>>
|
||
1022 |
|
||
1023 | :type string: str or unicode
|
||
1024 | :param string: a csv line
|
||
1025 |
|
||
1026 | :type sep: str or unicode
|
||
1027 | :param sep: field separator, default to the comma (',')
|
||
1028 |
|
||
1029 | :rtype: str or unicode
|
||
1030 | :return: the unquoted string (or the input string if it wasn't quoted)
|
||
1031 | """
|
||
1032 | return [word.strip() for word in string.split(sep) if word.strip()] |
||
1033 | |||
1034 | |||
1035 | def _unquote(string): |
||
1036 | """remove optional quotes (simple or double) from the string
|
||
1037 |
|
||
1038 | :type string: str or unicode
|
||
1039 | :param string: an optionally quoted string
|
||
1040 |
|
||
1041 | :rtype: str or unicode
|
||
1042 | :return: the unquoted string (or the input string if it wasn't quoted)
|
||
1043 | """
|
||
1044 | if not string: |
||
1045 | return string
|
||
1046 | if string[0] in '"\'': |
||
1047 | string = string[1:]
|
||
1048 | if string[-1] in '"\'': |
||
1049 | string = string[:-1]
|
||
1050 | return string
|
||
1051 | |||
1052 | |||
1053 | def _normalize_text(text, line_len=80, indent=''): |
||
1054 | """Wrap the text on the given line length."""
|
||
1055 | return '\n'.join(textwrap.wrap(text, width=line_len, initial_indent=indent, |
||
1056 | subsequent_indent=indent)) |
||
1057 | |||
1058 | |||
1059 | def _check_csv(value): |
||
1060 | if isinstance(value, (list, tuple)): |
||
1061 | return value
|
||
1062 | return _splitstrip(value)
|
||
1063 | |||
1064 | |||
1065 | if six.PY2:
|
||
1066 | def _encode(string, encoding): |
||
1067 | # pylint: disable=undefined-variable
|
||
1068 | if isinstance(string, unicode): |
||
1069 | return string.encode(encoding)
|
||
1070 | return str(string) |
||
1071 | else:
|
||
1072 | def _encode(string, _): |
||
1073 | return str(string) |
||
1074 | |||
1075 | def _get_encoding(encoding, stream): |
||
1076 | encoding = encoding or getattr(stream, 'encoding', None) |
||
1077 | if not encoding: |
||
1078 | import locale |
||
1079 | encoding = locale.getpreferredencoding() |
||
1080 | return encoding
|
||
1081 | |||
1082 | |||
1083 | def _comment(string): |
||
1084 | """return string as a comment"""
|
||
1085 | lines = [line.strip() for line in string.splitlines()] |
||
1086 | return '# ' + ('%s# ' % os.linesep).join(lines) |
||
1087 | |||
1088 | |||
1089 | def _format_option_value(optdict, value): |
||
1090 | """return the user input's value from a 'compiled' value"""
|
||
1091 | if isinstance(value, (list, tuple)): |
||
1092 | value = ','.join(value)
|
||
1093 | elif isinstance(value, dict): |
||
1094 | value = ','.join('%s:%s' % (k, v) for k, v in value.items()) |
||
1095 | elif hasattr(value, 'match'): # optdict.get('type') == 'regexp' |
||
1096 | # compiled regexp
|
||
1097 | value = value.pattern |
||
1098 | elif optdict.get('type') == 'yn': |
||
1099 | value = value and 'yes' or 'no' |
||
1100 | elif isinstance(value, six.string_types) and value.isspace(): |
||
1101 | value = "'%s'" % value
|
||
1102 | return value
|
||
1103 | |||
1104 | |||
1105 | def _ini_format_section(stream, section, options, encoding=None, doc=None): |
||
1106 | """format an options section using the INI format"""
|
||
1107 | encoding = _get_encoding(encoding, stream) |
||
1108 | if doc:
|
||
1109 | print(_encode(_comment(doc), encoding), file=stream) |
||
1110 | print('[%s]' % section, file=stream)
|
||
1111 | _ini_format(stream, options, encoding) |
||
1112 | |||
1113 | |||
1114 | def _ini_format(stream, options, encoding): |
||
1115 | """format options using the INI format"""
|
||
1116 | for optname, optdict, value in options: |
||
1117 | value = _format_option_value(optdict, value) |
||
1118 | help = optdict.get('help')
|
||
1119 | if help:
|
||
1120 | help = _normalize_text(help, line_len=79, indent='# ') |
||
1121 | print(file=stream) |
||
1122 | print(_encode(help, encoding), file=stream) |
||
1123 | else:
|
||
1124 | print(file=stream) |
||
1125 | if value is None: |
||
1126 | print('#%s=' % optname, file=stream)
|
||
1127 | else:
|
||
1128 | value = _encode(value, encoding).strip() |
||
1129 | print('%s=%s' % (optname, value), file=stream)
|
||
1130 | |||
1131 | format_section = _ini_format_section |
||
1132 | |||
1133 | |||
1134 | def _rest_format_section(stream, section, options, encoding=None, doc=None): |
||
1135 | """format an options section using as ReST formatted output"""
|
||
1136 | encoding = _get_encoding(encoding, stream) |
||
1137 | if section:
|
||
1138 | print('%s\n%s' % (section, "'"*len(section)), file=stream) |
||
1139 | if doc:
|
||
1140 | print(_encode(_normalize_text(doc, line_len=79, indent=''), encoding), file=stream) |
||
1141 | print(file=stream) |
||
1142 | for optname, optdict, value in options: |
||
1143 | help = optdict.get('help')
|
||
1144 | print(':%s:' % optname, file=stream)
|
||
1145 | if help:
|
||
1146 | help = _normalize_text(help, line_len=79, indent=' ') |
||
1147 | print(_encode(help, encoding), file=stream) |
||
1148 | if value:
|
||
1149 | value = _encode(_format_option_value(optdict, value), encoding) |
||
1150 | print(file=stream) |
||
1151 | print(' Default: ``%s``' % value.replace("`` ", "```` ``"), file=stream) |