Skip to content

Commit 5b2e1b8

Browse files
authored
Move CLI code out of __main__.py to allow multiprocessing to work for python -m sphinxlint (#99)
1 parent 87dd645 commit 5b2e1b8

File tree

6 files changed

+248
-246
lines changed

6 files changed

+248
-246
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Repository = "https://github.com/sphinx-contrib/sphinx-lint"
3535
Changelog = "https://github.com/sphinx-contrib/sphinx-lint/releases"
3636

3737
[project.scripts]
38-
sphinx-lint = "sphinxlint.__main__:main"
38+
sphinx-lint = "sphinxlint.cli:main"
3939

4040
[tool.hatch]
4141
version.source = "vcs"

sphinxlint/__main__.py

Lines changed: 2 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -1,246 +1,6 @@
1-
import argparse
2-
import enum
3-
import multiprocessing
4-
import os
51
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))
2432

3+
from sphinxlint import cli
2444

2455
if __name__ == "__main__":
246-
sys.exit(main())
6+
sys.exit(cli.main())

0 commit comments

Comments
 (0)