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 @ 1227
History | View | Annotate | Download (42.8 KB)
1 |
# 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) |