|
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