1
# Copyright (C) 2014 Ivan Melnikov <iv at altlinux dot org>
3
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6
# not use this file except in compliance with the License. You may obtain
7
# a copy of the License at
9
# http://www.apache.org/licenses/LICENSE-2.0
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
# License for the specific language governing permissions and limitations
18
"""Check documentation for simple style requirements.
21
- invalid rst format - D000
22
- lines should not be longer than 79 characters - D001
23
- RST exception: line with no whitespace except in the beginning
24
- RST exception: lines with http or https urls
25
- RST exception: literal blocks
26
- RST exception: rst target directives
27
- no trailing whitespace - D002
28
- no tabulation for indentation - D003
29
- no carriage returns (use unix newlines) - D004
30
- no newline at end of file - D005
39
if __name__ == '__main__':
40
# Only useful for when running directly (for dev/debugging).
41
sys.path.insert(0, os.path.abspath(os.getcwd()))
42
sys.path.insert(0, os.path.abspath(os.path.join(os.pardir, os.getcwd())))
45
from six.moves import configparser
46
from stevedore import extension
48
from doc8 import checks
49
from doc8 import parser as file_parser
50
from doc8 import utils
51
from doc8 import version
53
FILE_PATTERNS = ['.rst', '.txt']
63
def split_set_type(text, delimiter=","):
64
return set([i.strip() for i in text.split(delimiter) if i.strip()])
74
def parse_ignore_path_errors(entries):
75
ignore_path_errors = collections.defaultdict(set)
77
path, ignored_errors = path.split(";", 1)
78
path = os.path.abspath(path.strip())
79
ignored_errors = split_set_type(ignored_errors, delimiter=";")
80
ignore_path_errors[path].update(ignored_errors)
81
return dict(ignore_path_errors)
84
def extract_config(args):
85
parser = configparser.RawConfigParser()
88
for fn in args['config']:
89
with open(fn, 'r') as fh:
90
parser.readfp(fh, filename=fn)
93
read_files.extend(parser.read(CONFIG_FILENAMES))
98
cfg['max_line_length'] = parser.getint("doc8", "max-line-length")
99
except (configparser.NoSectionError, configparser.NoOptionError):
102
cfg['ignore'] = split_set_type(parser.get("doc8", "ignore"))
103
except (configparser.NoSectionError, configparser.NoOptionError):
106
cfg['ignore_path'] = split_set_type(parser.get("doc8",
108
except (configparser.NoSectionError, configparser.NoOptionError):
111
ignore_path_errors = parser.get("doc8", "ignore-path-errors")
112
ignore_path_errors = split_set_type(ignore_path_errors)
113
ignore_path_errors = parse_ignore_path_errors(ignore_path_errors)
114
cfg['ignore_path_errors'] = ignore_path_errors
115
except (configparser.NoSectionError, configparser.NoOptionError):
118
cfg['allow_long_titles'] = parser.getboolean("doc8",
120
except (configparser.NoSectionError, configparser.NoOptionError):
123
cfg['sphinx'] = parser.getboolean("doc8", "sphinx")
124
except (configparser.NoSectionError, configparser.NoOptionError):
127
cfg['verbose'] = parser.getboolean("doc8", "verbose")
128
except (configparser.NoSectionError, configparser.NoOptionError):
131
cfg['file_encoding'] = parser.get("doc8", "file-encoding")
132
except (configparser.NoSectionError, configparser.NoOptionError):
135
cfg['default_extension'] = parser.get("doc8", "default-extension")
136
except (configparser.NoSectionError, configparser.NoOptionError):
139
extensions = parser.get("doc8", "extensions")
140
extensions = extensions.split(",")
141
extensions = [s.strip() for s in extensions if s.strip()]
143
cfg['extension'] = extensions
144
except (configparser.NoSectionError, configparser.NoOptionError):
149
def fetch_checks(cfg):
151
checks.CheckValidity(cfg),
152
checks.CheckTrailingWhitespace(cfg),
153
checks.CheckIndentationNoTab(cfg),
154
checks.CheckCarriageReturn(cfg),
155
checks.CheckMaxLineLength(cfg),
156
checks.CheckNewlineEndOfFile(cfg),
158
mgr = extension.ExtensionManager(
159
namespace='doc8.extension.check',
161
invoke_args=(cfg.copy(),),
169
def setup_logging(verbose):
171
level = logging.DEBUG
173
level = logging.ERROR
174
logging.basicConfig(level=level,
175
format='%(levelname)s: %(message)s', stream=sys.stdout)
180
files = collections.deque()
181
ignored_paths = cfg.get('ignore_path', [])
183
file_iter = utils.find_files(cfg.get('paths', []),
184
cfg.get('extension', []), ignored_paths)
185
default_extension = cfg.get('default_extension')
186
file_encoding = cfg.get('file_encoding')
187
for filename, ignoreable in file_iter:
190
if cfg.get('verbose'):
191
print(" Ignoring '%s'" % (filename))
193
f = file_parser.parse(filename,
194
default_extension=default_extension,
195
encoding=file_encoding)
197
if cfg.get('verbose'):
198
print(" Selecting '%s'" % (filename))
199
return (files, files_ignored)
202
def validate(cfg, files):
203
print("Validating...")
205
ignoreables = frozenset(cfg.get('ignore', []))
206
ignore_targeted = cfg.get('ignore_path_errors', {})
209
if cfg.get('verbose'):
210
print("Validating %s" % f)
211
targeted_ignoreables = set(ignore_targeted.get(f.filename, set()))
212
targeted_ignoreables.update(ignoreables)
213
for c in fetch_checks(cfg):
215
# http://legacy.python.org/dev/peps/pep-3155/
216
check_name = c.__class__.__qualname__
217
except AttributeError:
218
check_name = ".".join([c.__class__.__module__,
219
c.__class__.__name__])
220
error_counts.setdefault(check_name, 0)
222
extension_matcher = c.EXT_MATCHER
223
except AttributeError:
226
if not extension_matcher.match(f.extension):
227
if cfg.get('verbose'):
228
print(" Skipping check '%s' since it does not"
229
" understand parsing a file with extension '%s'"
230
% (check_name, f.extension))
233
reports = set(c.REPORTS)
234
except AttributeError:
237
reports = reports - targeted_ignoreables
239
if cfg.get('verbose'):
240
print(" Skipping check '%s', determined to only"
241
" check ignoreable codes" % check_name)
243
if cfg.get('verbose'):
244
print(" Running check '%s'" % check_name)
245
if isinstance(c, checks.ContentCheck):
246
for line_num, code, message in c.report_iter(f):
247
if code in targeted_ignoreables:
249
if not isinstance(line_num, (float, int)):
251
if cfg.get('verbose'):
252
print(' - %s:%s: %s %s'
253
% (f.filename, line_num, code, message))
256
% (f.filename, line_num, code, message))
257
error_counts[check_name] += 1
258
elif isinstance(c, checks.LineCheck):
259
for line_num, line in enumerate(f.lines_iter(), 1):
260
for code, message in c.report_iter(line):
261
if code in targeted_ignoreables:
263
if cfg.get('verbose'):
264
print(' - %s:%s: %s %s'
265
% (f.filename, line_num, code, message))
268
% (f.filename, line_num, code, message))
269
error_counts[check_name] += 1
271
raise TypeError("Unknown check type: %s, %s"
277
parser = argparse.ArgumentParser(
280
formatter_class=argparse.RawDescriptionHelpFormatter)
281
default_configs = ", ".join(CONFIG_FILENAMES)
282
parser.add_argument("paths", metavar='path', type=str, nargs='*',
283
help=("path to scan for doc files"
284
" (default: current directory)."),
285
default=[os.getcwd()])
286
parser.add_argument("--config", metavar='path', action="append",
287
help="user config file location"
288
" (default: %s)." % default_configs,
290
parser.add_argument("--allow-long-titles", action="store_true",
291
help="allow long section titles (default: false).",
293
parser.add_argument("--ignore", action="append", metavar="code",
294
help="ignore the given error code(s).",
297
parser.add_argument("--no-sphinx", action="store_false",
298
help="do not ignore sphinx specific false positives.",
299
default=True, dest='sphinx')
300
parser.add_argument("--ignore-path", action="append", default=[],
301
help="ignore the given directory or file (globs"
302
" are supported).", metavar='path')
303
parser.add_argument("--ignore-path-errors", action="append", default=[],
304
help="ignore the given specific errors in the"
305
" provided file.", metavar='path')
306
parser.add_argument("--default-extension", action="store",
307
help="default file extension to use when a file is"
308
" found without a file extension.",
309
default='', dest='default_extension',
311
parser.add_argument("--file-encoding", action="store",
312
help="override encoding to use when attempting"
313
" to determine an input files text encoding "
314
"(providing this avoids using `chardet` to"
315
" automatically detect encoding/s)",
316
default='', dest='file_encoding',
318
parser.add_argument("--max-line-length", action="store", metavar="int",
320
help="maximum allowed line"
321
" length (default: %s)." % MAX_LINE_LENGTH,
322
default=MAX_LINE_LENGTH)
323
parser.add_argument("-e", "--extension", action="append",
325
help="check file extensions of the given type"
326
" (default: %s)." % ", ".join(FILE_PATTERNS),
327
default=list(FILE_PATTERNS))
328
parser.add_argument("-v", "--verbose", dest="verbose", action='store_true',
329
help="run in verbose mode.", default=False)
330
parser.add_argument("--version", dest="version", action='store_true',
331
help="show the version and exit.", default=False)
332
args = vars(parser.parse_args())
333
if args.get('version'):
334
print(version.version_string())
336
args['ignore'] = merge_sets(args['ignore'])
337
cfg = extract_config(args)
338
args['ignore'].update(cfg.pop("ignore", set()))
340
args['sphinx'] = cfg.pop("sphinx")
341
args['extension'].extend(cfg.pop('extension', []))
342
args['ignore_path'].extend(cfg.pop('ignore_path', []))
344
cfg.setdefault('ignore_path_errors', {})
345
for tmp_ignore_path_error in args.pop('ignore_path_errors', []):
346
tmp_ignores = parse_ignore_path_errors(tmp_ignore_path_error)
347
for path, ignores in six.iteritems(tmp_ignores):
348
if path in cfg['ignore_path_errors']:
349
cfg['ignore_path_errors'][path].update(ignores)
351
cfg['ignore_path_errors'][path] = set(ignores)
354
setup_logging(args.get('verbose'))
356
files, files_ignored = scan(args)
357
files_selected = len(files)
358
error_counts = validate(args, files)
359
total_errors = sum(six.itervalues(error_counts))
362
print("Total files scanned = %s" % (files_selected))
363
print("Total files ignored = %s" % (files_ignored))
364
print("Total accumulated errors = %s" % (total_errors))
366
print("Detailed error counts:")
367
for check_name in sorted(six.iterkeys(error_counts)):
368
check_errors = error_counts[check_name]
369
print(" - %s = %s" % (check_name, check_errors))
376
if __name__ == "__main__":