~ubuntu-branches/debian/sid/python-doc8/sid

« back to all changes in this revision

Viewing changes to doc8/main.py

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2015-10-14 08:18:40 UTC
  • Revision ID: package-import@ubuntu.com-20151014081840-7hajogar155lmmeb
Tags: upstream-0.6.0
ImportĀ upstreamĀ versionĀ 0.6.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2014 Ivan Melnikov <iv at altlinux dot org>
 
2
#
 
3
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
 
4
#
 
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
 
8
#
 
9
#      http://www.apache.org/licenses/LICENSE-2.0
 
10
#
 
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
 
15
# under the License.
 
16
 
 
17
 
 
18
"""Check documentation for simple style requirements.
 
19
 
 
20
What is checked:
 
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
 
31
"""
 
32
 
 
33
import argparse
 
34
import collections
 
35
import logging
 
36
import os
 
37
import sys
 
38
 
 
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())))
 
43
 
 
44
import six
 
45
from six.moves import configparser
 
46
from stevedore import extension
 
47
 
 
48
from doc8 import checks
 
49
from doc8 import parser as file_parser
 
50
from doc8 import utils
 
51
from doc8 import version
 
52
 
 
53
FILE_PATTERNS = ['.rst', '.txt']
 
54
MAX_LINE_LENGTH = 79
 
55
CONFIG_FILENAMES = [
 
56
    "doc8.ini",
 
57
    "tox.ini",
 
58
    "pep8.ini",
 
59
    "setup.cfg",
 
60
]
 
61
 
 
62
 
 
63
def split_set_type(text, delimiter=","):
 
64
    return set([i.strip() for i in text.split(delimiter) if i.strip()])
 
65
 
 
66
 
 
67
def merge_sets(sets):
 
68
    m = set()
 
69
    for s in sets:
 
70
        m.update(s)
 
71
    return m
 
72
 
 
73
 
 
74
def parse_ignore_path_errors(entries):
 
75
    ignore_path_errors = collections.defaultdict(set)
 
76
    for path in entries:
 
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)
 
82
 
 
83
 
 
84
def extract_config(args):
 
85
    parser = configparser.RawConfigParser()
 
86
    read_files = []
 
87
    if args['config']:
 
88
        for fn in args['config']:
 
89
            with open(fn, 'r') as fh:
 
90
                parser.readfp(fh, filename=fn)
 
91
                read_files.append(fn)
 
92
    else:
 
93
        read_files.extend(parser.read(CONFIG_FILENAMES))
 
94
    if not read_files:
 
95
        return {}
 
96
    cfg = {}
 
97
    try:
 
98
        cfg['max_line_length'] = parser.getint("doc8", "max-line-length")
 
99
    except (configparser.NoSectionError, configparser.NoOptionError):
 
100
        pass
 
101
    try:
 
102
        cfg['ignore'] = split_set_type(parser.get("doc8", "ignore"))
 
103
    except (configparser.NoSectionError, configparser.NoOptionError):
 
104
        pass
 
105
    try:
 
106
        cfg['ignore_path'] = split_set_type(parser.get("doc8",
 
107
                                                       "ignore-path"))
 
108
    except (configparser.NoSectionError, configparser.NoOptionError):
 
109
        pass
 
110
    try:
 
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):
 
116
        pass
 
117
    try:
 
118
        cfg['allow_long_titles'] = parser.getboolean("doc8",
 
119
                                                     "allow-long-titles")
 
120
    except (configparser.NoSectionError, configparser.NoOptionError):
 
121
        pass
 
122
    try:
 
123
        cfg['sphinx'] = parser.getboolean("doc8", "sphinx")
 
124
    except (configparser.NoSectionError, configparser.NoOptionError):
 
125
        pass
 
126
    try:
 
127
        cfg['verbose'] = parser.getboolean("doc8", "verbose")
 
128
    except (configparser.NoSectionError, configparser.NoOptionError):
 
129
        pass
 
130
    try:
 
131
        cfg['file_encoding'] = parser.get("doc8", "file-encoding")
 
132
    except (configparser.NoSectionError, configparser.NoOptionError):
 
133
        pass
 
134
    try:
 
135
        cfg['default_extension'] = parser.get("doc8", "default-extension")
 
136
    except (configparser.NoSectionError, configparser.NoOptionError):
 
137
        pass
 
138
    try:
 
139
        extensions = parser.get("doc8", "extensions")
 
140
        extensions = extensions.split(",")
 
141
        extensions = [s.strip() for s in extensions if s.strip()]
 
142
        if extensions:
 
143
            cfg['extension'] = extensions
 
144
    except (configparser.NoSectionError, configparser.NoOptionError):
 
145
        pass
 
146
    return cfg
 
147
 
 
148
 
 
149
def fetch_checks(cfg):
 
150
    base = [
 
151
        checks.CheckValidity(cfg),
 
152
        checks.CheckTrailingWhitespace(cfg),
 
153
        checks.CheckIndentationNoTab(cfg),
 
154
        checks.CheckCarriageReturn(cfg),
 
155
        checks.CheckMaxLineLength(cfg),
 
156
        checks.CheckNewlineEndOfFile(cfg),
 
157
    ]
 
158
    mgr = extension.ExtensionManager(
 
159
        namespace='doc8.extension.check',
 
160
        invoke_on_load=True,
 
161
        invoke_args=(cfg.copy(),),
 
162
    )
 
163
    addons = []
 
164
    for e in mgr:
 
165
        addons.append(e.obj)
 
166
    return base + addons
 
167
 
 
168
 
 
169
def setup_logging(verbose):
 
170
    if verbose:
 
171
        level = logging.DEBUG
 
172
    else:
 
173
        level = logging.ERROR
 
174
    logging.basicConfig(level=level,
 
175
                        format='%(levelname)s: %(message)s', stream=sys.stdout)
 
176
 
 
177
 
 
178
def scan(cfg):
 
179
    print("Scanning...")
 
180
    files = collections.deque()
 
181
    ignored_paths = cfg.get('ignore_path', [])
 
182
    files_ignored = 0
 
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:
 
188
        if ignoreable:
 
189
            files_ignored += 1
 
190
            if cfg.get('verbose'):
 
191
                print("  Ignoring '%s'" % (filename))
 
192
        else:
 
193
            f = file_parser.parse(filename,
 
194
                                  default_extension=default_extension,
 
195
                                  encoding=file_encoding)
 
196
            files.append(f)
 
197
            if cfg.get('verbose'):
 
198
                print("  Selecting '%s'" % (filename))
 
199
    return (files, files_ignored)
 
200
 
 
201
 
 
202
def validate(cfg, files):
 
203
    print("Validating...")
 
204
    error_counts = {}
 
205
    ignoreables = frozenset(cfg.get('ignore', []))
 
206
    ignore_targeted = cfg.get('ignore_path_errors', {})
 
207
    while files:
 
208
        f = files.popleft()
 
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):
 
214
            try:
 
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)
 
221
            try:
 
222
                extension_matcher = c.EXT_MATCHER
 
223
            except AttributeError:
 
224
                pass
 
225
            else:
 
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))
 
231
                    continue
 
232
            try:
 
233
                reports = set(c.REPORTS)
 
234
            except AttributeError:
 
235
                pass
 
236
            else:
 
237
                reports = reports - targeted_ignoreables
 
238
                if not reports:
 
239
                    if cfg.get('verbose'):
 
240
                        print("  Skipping check '%s', determined to only"
 
241
                              " check ignoreable codes" % check_name)
 
242
                    continue
 
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:
 
248
                        continue
 
249
                    if not isinstance(line_num, (float, int)):
 
250
                        line_num = "?"
 
251
                    if cfg.get('verbose'):
 
252
                        print('    - %s:%s: %s %s'
 
253
                              % (f.filename, line_num, code, message))
 
254
                    else:
 
255
                        print('%s:%s: %s %s'
 
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:
 
262
                            continue
 
263
                        if cfg.get('verbose'):
 
264
                            print('    - %s:%s: %s %s'
 
265
                                  % (f.filename, line_num, code, message))
 
266
                        else:
 
267
                            print('%s:%s: %s %s'
 
268
                                  % (f.filename, line_num, code, message))
 
269
                        error_counts[check_name] += 1
 
270
            else:
 
271
                raise TypeError("Unknown check type: %s, %s"
 
272
                                % (type(c), c))
 
273
    return error_counts
 
274
 
 
275
 
 
276
def main():
 
277
    parser = argparse.ArgumentParser(
 
278
        prog='doc8',
 
279
        description=__doc__,
 
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,
 
289
                        default=[])
 
290
    parser.add_argument("--allow-long-titles", action="store_true",
 
291
                        help="allow long section titles (default: false).",
 
292
                        default=False)
 
293
    parser.add_argument("--ignore", action="append", metavar="code",
 
294
                        help="ignore the given error code(s).",
 
295
                        type=split_set_type,
 
296
                        default=[])
 
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',
 
310
                        metavar='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',
 
317
                        metavar='encoding')
 
318
    parser.add_argument("--max-line-length", action="store", metavar="int",
 
319
                        type=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",
 
324
                        metavar="extension",
 
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())
 
335
        return 0
 
336
    args['ignore'] = merge_sets(args['ignore'])
 
337
    cfg = extract_config(args)
 
338
    args['ignore'].update(cfg.pop("ignore", set()))
 
339
    if 'sphinx' in cfg:
 
340
        args['sphinx'] = cfg.pop("sphinx")
 
341
    args['extension'].extend(cfg.pop('extension', []))
 
342
    args['ignore_path'].extend(cfg.pop('ignore_path', []))
 
343
 
 
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)
 
350
            else:
 
351
                cfg['ignore_path_errors'][path] = set(ignores)
 
352
 
 
353
    args.update(cfg)
 
354
    setup_logging(args.get('verbose'))
 
355
 
 
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))
 
360
 
 
361
    print("=" * 8)
 
362
    print("Total files scanned = %s" % (files_selected))
 
363
    print("Total files ignored = %s" % (files_ignored))
 
364
    print("Total accumulated errors = %s" % (total_errors))
 
365
    if error_counts:
 
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))
 
370
    if total_errors:
 
371
        return 1
 
372
    else:
 
373
        return 0
 
374
 
 
375
 
 
376
if __name__ == "__main__":
 
377
    sys.exit(main())