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

1import argparse 

2import enum 

3import multiprocessing 

4import os 

5import sys 

6from itertools import chain, starmap 

7 

8from sphinxlint import check_file 

9from sphinxlint.checkers import all_checkers 

10from sphinxlint.sphinxlint import CheckersOptions 

11 

12 

13class 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 

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__) 

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}, " 

58 "supported values are {SortField.as_supported_options()}" 

59 ) from None 

60 setattr(namespace, self.dest, sort_fields) 

61 

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 ) 

113 

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 

122 

123 

124def walk(path, ignore_list): 

125 """Wrapper around os.walk with an ignore list. 

126 

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:] 

145 

146 

147def _check_file(todo): 

148 """Wrapper to call check_file with arguments given by 

149 multiprocessing.imap_unordered.""" 

150 return check_file(*todo) 

151 

152 

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 

170 

171 

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 

181 

182 

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 

199 

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 

204 

205 todo = [ 

206 (path, enabled_checkers, options) 

207 for path in chain.from_iterable(walk(path, args.ignore) for path in args.paths) 

208 ] 

209 

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() 

219 

220 return int(bool(count)) 

221 

222 

223if __name__ == "__main__": 

224 sys.exit(main())