|
1 |
| -import argparse |
2 |
| -import enum |
3 |
| -import multiprocessing |
4 |
| -import os |
5 | 1 | import sys
|
6 |
| -from itertools import chain, starmap |
7 |
| - |
8 |
| -from sphinxlint import check_file, __version__ |
9 |
| -from sphinxlint.checkers import all_checkers |
10 |
| -from sphinxlint.sphinxlint import CheckersOptions |
11 |
| - |
12 |
| - |
13 |
| -class SortField(enum.Enum): |
14 |
| - """Fields available for sorting error reports""" |
15 |
| - |
16 |
| - FILENAME = 0 |
17 |
| - LINE = 1 |
18 |
| - ERROR_TYPE = 2 |
19 |
| - |
20 |
| - @staticmethod |
21 |
| - def as_supported_options(): |
22 |
| - return ",".join(field.name.lower() for field in SortField) |
23 |
| - |
24 |
| - |
25 |
| -def parse_args(argv=None): |
26 |
| - """Parse command line argument.""" |
27 |
| - if argv is None: |
28 |
| - argv = sys.argv |
29 |
| - parser = argparse.ArgumentParser(description=__doc__) |
30 |
| - |
31 |
| - enabled_checkers_names = { |
32 |
| - checker.name for checker in all_checkers.values() if checker.enabled |
33 |
| - } |
34 |
| - |
35 |
| - class EnableAction(argparse.Action): |
36 |
| - def __call__(self, parser, namespace, values, option_string=None): |
37 |
| - if values == "all": |
38 |
| - enabled_checkers_names.update(set(all_checkers.keys())) |
39 |
| - else: |
40 |
| - enabled_checkers_names.update(values.split(",")) |
41 |
| - |
42 |
| - class DisableAction(argparse.Action): |
43 |
| - def __call__(self, parser, namespace, values, option_string=None): |
44 |
| - if values == "all": |
45 |
| - enabled_checkers_names.clear() |
46 |
| - else: |
47 |
| - enabled_checkers_names.difference_update(values.split(",")) |
48 |
| - |
49 |
| - class StoreSortFieldAction(argparse.Action): |
50 |
| - def __call__(self, parser, namespace, values, option_string=None): |
51 |
| - sort_fields = [] |
52 |
| - for field_name in values.split(","): |
53 |
| - try: |
54 |
| - sort_fields.append(SortField[field_name.upper()]) |
55 |
| - except KeyError: |
56 |
| - raise ValueError( |
57 |
| - f"Unsupported sort field: {field_name}, supported values are {SortField.as_supported_options()}" |
58 |
| - ) from None |
59 |
| - setattr(namespace, self.dest, sort_fields) |
60 |
| - |
61 |
| - class StoreNumJobsAction(argparse.Action): |
62 |
| - def __call__(self, parser, namespace, values, option_string=None): |
63 |
| - setattr(namespace, self.dest, self.job_count(values)) |
64 |
| - |
65 |
| - @staticmethod |
66 |
| - def job_count(values): |
67 |
| - if values == "auto": |
68 |
| - return os.cpu_count() |
69 |
| - return max(int(values), 1) |
70 |
| - |
71 |
| - parser.add_argument( |
72 |
| - "-v", |
73 |
| - "--verbose", |
74 |
| - action="store_true", |
75 |
| - help="verbose (print all checked file names)", |
76 |
| - ) |
77 |
| - parser.add_argument( |
78 |
| - "-i", |
79 |
| - "--ignore", |
80 |
| - action="append", |
81 |
| - help="ignore subdir or file path", |
82 |
| - default=[], |
83 |
| - ) |
84 |
| - parser.add_argument( |
85 |
| - "-d", |
86 |
| - "--disable", |
87 |
| - action=DisableAction, |
88 |
| - help='comma-separated list of checks to disable. Give "all" to disable them all. ' |
89 |
| - "Can be used in conjunction with --enable (it's evaluated left-to-right). " |
90 |
| - '"--disable all --enable trailing-whitespace" can be used to enable a ' |
91 |
| - "single check.", |
92 |
| - ) |
93 |
| - parser.add_argument( |
94 |
| - "-e", |
95 |
| - "--enable", |
96 |
| - action=EnableAction, |
97 |
| - help='comma-separated list of checks to enable. Give "all" to enable them all. ' |
98 |
| - "Can be used in conjunction with --disable (it's evaluated left-to-right). " |
99 |
| - '"--enable all --disable trailing-whitespace" can be used to enable ' |
100 |
| - "all but one check.", |
101 |
| - ) |
102 |
| - parser.add_argument( |
103 |
| - "--list", |
104 |
| - action="store_true", |
105 |
| - help="List enabled checkers and exit. " |
106 |
| - "Can be used to see which checkers would be used with a given set of " |
107 |
| - "--enable and --disable options.", |
108 |
| - ) |
109 |
| - parser.add_argument( |
110 |
| - "--max-line-length", |
111 |
| - help="Maximum number of characters on a single line.", |
112 |
| - default=80, |
113 |
| - type=int, |
114 |
| - ) |
115 |
| - parser.add_argument( |
116 |
| - "-s", |
117 |
| - "--sort-by", |
118 |
| - action=StoreSortFieldAction, |
119 |
| - help="comma-separated list of fields used to sort errors by. Available " |
120 |
| - f"fields are: {SortField.as_supported_options()}", |
121 |
| - ) |
122 |
| - parser.add_argument( |
123 |
| - "-j", |
124 |
| - "--jobs", |
125 |
| - metavar="N", |
126 |
| - action=StoreNumJobsAction, |
127 |
| - help="Run in parallel with N processes. Defaults to 'auto', " |
128 |
| - "which sets N to the number of logical CPUs. " |
129 |
| - "Values <= 1 are all considered 1.", |
130 |
| - default=StoreNumJobsAction.job_count("auto") |
131 |
| - ) |
132 |
| - parser.add_argument( |
133 |
| - "-V", "--version", action="version", version=f"%(prog)s {__version__}" |
134 |
| - ) |
135 |
| - |
136 |
| - parser.add_argument("paths", default=".", nargs="*") |
137 |
| - args = parser.parse_args(argv[1:]) |
138 |
| - try: |
139 |
| - enabled_checkers = {all_checkers[name] for name in enabled_checkers_names} |
140 |
| - except KeyError as err: |
141 |
| - print(f"Unknown checker: {err.args[0]}.") |
142 |
| - sys.exit(2) |
143 |
| - return enabled_checkers, args |
144 |
| - |
145 |
| - |
146 |
| -def walk(path, ignore_list): |
147 |
| - """Wrapper around os.walk with an ignore list. |
148 |
| -
|
149 |
| - It also allows giving a file, thus yielding just that file. |
150 |
| - """ |
151 |
| - if os.path.isfile(path): |
152 |
| - if path in ignore_list: |
153 |
| - return |
154 |
| - yield path if path[:2] != "./" else path[2:] |
155 |
| - return |
156 |
| - for root, dirs, files in os.walk(path): |
157 |
| - # ignore subdirs in ignore list |
158 |
| - if any(ignore in root for ignore in ignore_list): |
159 |
| - del dirs[:] |
160 |
| - continue |
161 |
| - for file in files: |
162 |
| - file = os.path.join(root, file) |
163 |
| - # ignore files in ignore list |
164 |
| - if any(ignore in file for ignore in ignore_list): |
165 |
| - continue |
166 |
| - yield file if file[:2] != "./" else file[2:] |
167 |
| - |
168 |
| - |
169 |
| -def _check_file(todo): |
170 |
| - """Wrapper to call check_file with arguments given by |
171 |
| - multiprocessing.imap_unordered.""" |
172 |
| - return check_file(*todo) |
173 |
| - |
174 |
| - |
175 |
| -def sort_errors(results, sorted_by): |
176 |
| - """Flattens and potentially sorts errors based on user prefernces""" |
177 |
| - if not sorted_by: |
178 |
| - for results in results: |
179 |
| - yield from results |
180 |
| - return |
181 |
| - errors = list(error for errors in results for error in errors) |
182 |
| - # sorting is stable in python, so we can sort in reverse order to get the |
183 |
| - # ordering specified by the user |
184 |
| - for sort_field in reversed(sorted_by): |
185 |
| - if sort_field == SortField.ERROR_TYPE: |
186 |
| - errors.sort(key=lambda error: error.checker_name) |
187 |
| - elif sort_field == SortField.FILENAME: |
188 |
| - errors.sort(key=lambda error: error.filename) |
189 |
| - elif sort_field == SortField.LINE: |
190 |
| - errors.sort(key=lambda error: error.line_no) |
191 |
| - yield from errors |
192 |
| - |
193 |
| - |
194 |
| -def print_errors(errors): |
195 |
| - """Print errors (or a message if nothing is to be printed).""" |
196 |
| - qty = 0 |
197 |
| - for error in errors: |
198 |
| - print(error) |
199 |
| - qty += 1 |
200 |
| - if qty == 0: |
201 |
| - print("No problems found.") |
202 |
| - return qty |
203 |
| - |
204 |
| - |
205 |
| -def main(argv=None): |
206 |
| - enabled_checkers, args = parse_args(argv) |
207 |
| - options = CheckersOptions.from_argparse(args) |
208 |
| - if args.list: |
209 |
| - if not enabled_checkers: |
210 |
| - print("No checkers selected.") |
211 |
| - return 0 |
212 |
| - print(f"{len(enabled_checkers)} checkers selected:") |
213 |
| - for check in sorted(enabled_checkers, key=lambda fct: fct.name): |
214 |
| - if args.verbose: |
215 |
| - print(f"- {check.name}: {check.__doc__}") |
216 |
| - else: |
217 |
| - print(f"- {check.name}: {check.__doc__.splitlines()[0]}") |
218 |
| - if not args.verbose: |
219 |
| - print("\n(Use `--list --verbose` to know more about each check)") |
220 |
| - return 0 |
221 |
| - |
222 |
| - for path in args.paths: |
223 |
| - if not os.path.exists(path): |
224 |
| - print(f"Error: path {path} does not exist") |
225 |
| - return 2 |
226 |
| - |
227 |
| - todo = [ |
228 |
| - (path, enabled_checkers, options) |
229 |
| - for path in chain.from_iterable(walk(path, args.ignore) for path in args.paths) |
230 |
| - ] |
231 |
| - |
232 |
| - if args.jobs == 1 or len(todo) < 8: |
233 |
| - count = print_errors(sort_errors(starmap(check_file, todo), args.sort_by)) |
234 |
| - else: |
235 |
| - with multiprocessing.Pool(processes=args.jobs) as pool: |
236 |
| - count = print_errors( |
237 |
| - sort_errors(pool.imap_unordered(_check_file, todo), args.sort_by) |
238 |
| - ) |
239 |
| - pool.close() |
240 |
| - pool.join() |
241 |
| - |
242 |
| - return int(bool(count)) |
243 | 2 |
|
| 3 | +from sphinxlint import cli |
244 | 4 |
|
245 | 5 | if __name__ == "__main__":
|
246 |
| - sys.exit(main()) |
| 6 | + sys.exit(cli.main()) |
0 commit comments