Coverage for sphinxlint/__main___BASE_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}, supported values are {SortField.as_supported_options()}" 

58 ) from None 

59 setattr(namespace, self.dest, sort_fields) 

60 

61 parser.add_argument( 

62 "-v", 

63 "--verbose", 

64 action="store_true", 

65 help="verbose (print all checked file names)", 

66 ) 

67 parser.add_argument( 

68 "-i", 

69 "--ignore", 

70 action="append", 

71 help="ignore subdir or file path", 

72 default=[], 

73 ) 

74 parser.add_argument( 

75 "-d", 

76 "--disable", 

77 action=DisableAction, 

78 help='comma-separated list of checks to disable. Give "all" to disable them all. ' 

79 "Can be used in conjunction with --enable (it's evaluated left-to-right). " 

80 '"--disable all --enable trailing-whitespace" can be used to enable a ' 

81 "single check.", 

82 ) 

83 parser.add_argument( 

84 "-e", 

85 "--enable", 

86 action=EnableAction, 

87 help='comma-separated list of checks to enable. Give "all" to enable them all. ' 

88 "Can be used in conjunction with --disable (it's evaluated left-to-right). " 

89 '"--enable all --disable trailing-whitespace" can be used to enable ' 

90 "all but one check.", 

91 ) 

92 parser.add_argument( 

93 "--list", 

94 action="store_true", 

95 help="List enabled checkers and exit. " 

96 "Can be used to see which checkers would be used with a given set of " 

97 "--enable and --disable options.", 

98 ) 

99 parser.add_argument( 

100 "--max-line-length", 

101 help="Maximum number of characters on a single line.", 

102 default=80, 

103 type=int, 

104 ) 

105 parser.add_argument( 

106 "-s", 

107 "--sort-by", 

108 action=StoreSortFieldAction, 

109 help="comma-separated list of fields used to sort errors by. Available " 

110 f"fields are: {SortField.as_supported_options()}", 

111 ) 

112 

113 parser.add_argument("paths", default=".", nargs="*") 

114 args = parser.parse_args(argv[1:]) 

115 try: 

116 enabled_checkers = {all_checkers[name] for name in enabled_checkers_names} 

117 except KeyError as err: 

118 print(f"Unknown checker: {err.args[0]}.") 

119 sys.exit(2) 

120 return enabled_checkers, args 

121 

122 

123def walk(path, ignore_list): 

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

125 

126 It also allows giving a file, thus yielding just that file. 

127 """ 

128 if os.path.isfile(path): 

129 if path in ignore_list: 

130 return 

131 yield path if path[:2] != "./" else path[2:] 

132 return 

133 for root, dirs, files in os.walk(path): 

134 # ignore subdirs in ignore list 

135 if any(ignore in root for ignore in ignore_list): 

136 del dirs[:] 

137 continue 

138 for file in files: 

139 file = os.path.join(root, file) 

140 # ignore files in ignore list 

141 if any(ignore in file for ignore in ignore_list): 

142 continue 

143 yield file if file[:2] != "./" else file[2:] 

144 

145 

146def _check_file(todo): 

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

148 multiprocessing.imap_unordered.""" 

149 return check_file(*todo) 

150 

151 

152def sort_errors(results, sorted_by): 

153 """Flattens and potentially sorts errors based on user prefernces""" 

154 if not sorted_by: 

155 for results in results: 

156 yield from results 

157 return 

158 errors = list(error for errors in results for error in errors) 

159 # sorting is stable in python, so we can sort in reverse order to get the 

160 # ordering specified by the user 

161 for sort_field in reversed(sorted_by): 

162 if sort_field == SortField.ERROR_TYPE: 

163 errors.sort(key=lambda error: error.checker_name) 

164 elif sort_field == SortField.FILENAME: 

165 errors.sort(key=lambda error: error.filename) 

166 elif sort_field == SortField.LINE: 

167 errors.sort(key=lambda error: error.line_no) 

168 yield from errors 

169 

170 

171def print_errors(errors): 

172 """Print errors (or a message if nothing is to be printed).""" 

173 qty = 0 

174 for error in errors: 

175 print(error) 

176 qty += 1 

177 if qty == 0: 

178 print("No problems found.") 

179 return qty 

180 

181 

182def main(argv=None): 

183 enabled_checkers, args = parse_args(argv) 

184 options = CheckersOptions.from_argparse(args) 

185 if args.list: 

186 if not enabled_checkers: 

187 print("No checkers selected.") 

188 return 0 

189 print(f"{len(enabled_checkers)} checkers selected:") 

190 for check in sorted(enabled_checkers, key=lambda fct: fct.name): 

191 if args.verbose: 

192 print(f"- {check.name}: {check.__doc__}") 

193 else: 

194 print(f"- {check.name}: {check.__doc__.splitlines()[0]}") 

195 if not args.verbose: 

196 print("\n(Use `--list --verbose` to know more about each check)") 

197 return 0 

198 

199 for path in args.paths: 

200 if not os.path.exists(path): 

201 print(f"Error: path {path} does not exist") 

202 return 2 

203 

204 todo = [ 

205 (path, enabled_checkers, options) 

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

207 ] 

208 

209 if len(todo) < 8: 

210 count = print_errors(sort_errors(starmap(check_file, todo), args.sort_by)) 

211 else: 

212 with multiprocessing.Pool() as pool: 

213 count = print_errors( 

214 sort_errors(pool.imap_unordered(_check_file, todo), args.sort_by) 

215 ) 

216 pool.close() 

217 pool.join() 

218 

219 return int(bool(count)) 

220 

221 

222if __name__ == "__main__": 

223 sys.exit(main())