Coverage for sphinxlint/__main___REMOTE_7374.py: 0%
121 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-24 18:46 +0100
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-24 18:46 +0100
1import argparse
2import enum
3import multiprocessing
4import os
5import sys
6from itertools import chain, starmap
8from sphinxlint import check_file
9from sphinxlint.checkers import all_checkers
10from sphinxlint.sphinxlint import CheckersOptions
13class SortField(enum.Enum):
14 """Fields available for sorting error reports"""
16 FILENAME = 0
17 LINE = 1
18 ERROR_TYPE = 2
20 @staticmethod
21 def as_supported_options():
22 return ",".join(field.name.lower() for field in SortField)
25def parse_args(argv=None):
26 """Parse command line argument."""
27 if argv is None:
28 argv = sys.argv
29 parser = argparse.ArgumentParser(description=__doc__)
31 enabled_checkers_names = {
32 checker.name for checker in all_checkers.values() if checker.enabled
33 }
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(","))
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(","))
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}, "
58 "supported values are {SortField.as_supported_options()}"
59 ) from None
60 setattr(namespace, self.dest, sort_fields)
62 parser.add_argument(
63 "-v",
64 "--verbose",
65 action="store_true",
66 help="verbose (print all checked file names)",
67 )
68 parser.add_argument(
69 "-i",
70 "--ignore",
71 action="append",
72 help="ignore subdir or file path",
73 default=[],
74 )
75 parser.add_argument(
76 "-d",
77 "--disable",
78 action=DisableAction,
79 help='comma-separated list of checks to disable. Give "all" to disable them all. '
80 "Can be used in conjunction with --enable (it's evaluated left-to-right). "
81 '"--disable all --enable trailing-whitespace" can be used to enable a '
82 "single check.",
83 )
84 parser.add_argument(
85 "-e",
86 "--enable",
87 action=EnableAction,
88 help='comma-separated list of checks to enable. Give "all" to enable them all. '
89 "Can be used in conjunction with --disable (it's evaluated left-to-right). "
90 '"--enable all --disable trailing-whitespace" can be used to enable '
91 "all but one check.",
92 )
93 parser.add_argument(
94 "--list",
95 action="store_true",
96 help="List enabled checkers and exit. "
97 "Can be used to see which checkers would be used with a given set of "
98 "--enable and --disable options.",
99 )
100 parser.add_argument(
101 "--max-line-length",
102 help="Maximum number of characters on a single line.",
103 default=80,
104 type=int,
105 )
106 parser.add_argument(
107 "-s",
108 "--sort-by",
109 action=StoreSortFieldAction,
110 help="comma-separated list of fields used to sort errors by. Available "
111 f"fields are: {SortField.as_supported_options()}",
112 )
114 parser.add_argument("paths", default=".", nargs="*")
115 args = parser.parse_args(argv[1:])
116 try:
117 enabled_checkers = {all_checkers[name] for name in enabled_checkers_names}
118 except KeyError as err:
119 print(f"Unknown checker: {err.args[0]}.")
120 sys.exit(2)
121 return enabled_checkers, args
124def walk(path, ignore_list):
125 """Wrapper around os.walk with an ignore list.
127 It also allows giving a file, thus yielding just that file.
128 """
129 if os.path.isfile(path):
130 if path in ignore_list:
131 return
132 yield path if path[:2] != "./" else path[2:]
133 return
134 for root, dirs, files in os.walk(path):
135 # ignore subdirs in ignore list
136 if any(ignore in root for ignore in ignore_list):
137 del dirs[:]
138 continue
139 for file in files:
140 file = os.path.join(root, file)
141 # ignore files in ignore list
142 if any(ignore in file for ignore in ignore_list):
143 continue
144 yield file if file[:2] != "./" else file[2:]
147def _check_file(todo):
148 """Wrapper to call check_file with arguments given by
149 multiprocessing.imap_unordered."""
150 return check_file(*todo)
153def sort_errors(results, sorted_by):
154 """Flattens and potentially sorts errors based on user prefernces"""
155 if not sorted_by:
156 for result in results:
157 yield from result
158 return
159 errors = list(error for errors in results for error in errors)
160 # sorting is stable in python, so we can sort in reverse order to get the
161 # ordering specified by the user
162 for sort_field in reversed(sorted_by):
163 if sort_field == SortField.ERROR_TYPE:
164 errors.sort(key=lambda error: error.checker_name)
165 elif sort_field == SortField.FILENAME:
166 errors.sort(key=lambda error: error.filename)
167 elif sort_field == SortField.LINE:
168 errors.sort(key=lambda error: error.line_no)
169 yield from errors
172def print_errors(errors):
173 """Print errors (or a message if nothing is to be printed)."""
174 qty = 0
175 for error in errors:
176 print(error)
177 qty += 1
178 if qty == 0:
179 print("No problems found.")
180 return qty
183def main(argv=None):
184 enabled_checkers, args = parse_args(argv)
185 options = CheckersOptions.from_argparse(args)
186 if args.list:
187 if not enabled_checkers:
188 print("No checkers selected.")
189 return 0
190 print(f"{len(enabled_checkers)} checkers selected:")
191 for check in sorted(enabled_checkers, key=lambda fct: fct.name):
192 if args.verbose:
193 print(f"- {check.name}: {check.__doc__}")
194 else:
195 print(f"- {check.name}: {check.__doc__.splitlines()[0]}")
196 if not args.verbose:
197 print("\n(Use `--list --verbose` to know more about each check)")
198 return 0
200 for path in args.paths:
201 if not os.path.exists(path):
202 print(f"Error: path {path} does not exist")
203 return 2
205 todo = [
206 (path, enabled_checkers, options)
207 for path in chain.from_iterable(walk(path, args.ignore) for path in args.paths)
208 ]
210 if len(todo) < 8:
211 count = print_errors(sort_errors(starmap(check_file, todo), args.sort_by))
212 else:
213 with multiprocessing.Pool() as pool:
214 count = print_errors(
215 sort_errors(pool.imap_unordered(_check_file, todo), args.sort_by)
216 )
217 pool.close()
218 pool.join()
220 return int(bool(count))
223if __name__ == "__main__":
224 sys.exit(main())