Coverage for dirindex/_cli.py: 91%

140 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-05 14:22 +0000

1import fnmatch 

2import functools 

3import io 

4import mimetypes 

5import os 

6import pkgutil 

7import stat 

8import sys 

9import time 

10from logging import DEBUG, INFO, basicConfig, getLogger 

11 

12import click 

13from jinja2 import Environment 

14 

15from .version import VERSION 

16 

17log = getLogger(__name__) 

18 

19 

20@click.version_option(version=VERSION, prog_name="dirindex") 

21@click.group(invoke_without_command=True) 

22@click.pass_context 

23def cli(ctx): 

24 if ctx.invoked_subcommand is None: 

25 print(ctx.get_help()) 

26 

27 

28def set_verbose(flag): 

29 fmt = "%(asctime)s %(levelname)s %(message)s" 

30 if flag: 30 ↛ 31line 30 didn't jump to line 31 because the condition on line 30 was never true

31 basicConfig(level=DEBUG, format=fmt) 

32 else: 

33 basicConfig(level=INFO, format=fmt) 

34 

35 

36_cli_option = [ 

37 click.option("--verbose/--no-verbose"), 

38] 

39 

40 

41def multi_options(decs): 

42 def deco(f): 

43 for dec in reversed(decs): 

44 f = dec(f) 

45 return f 

46 

47 return deco 

48 

49 

50def cli_option(func): 

51 @functools.wraps(func) 

52 def wrap(verbose, *args, **kwargs): 

53 set_verbose(verbose) 

54 return func(*args, **kwargs) 

55 

56 return multi_options(_cli_option)(wrap) 

57 

58 

59def pattern_filter(target, pattern, hide): 

60 log.debug("pfilter: target=%s, pattern=%s, hide=%s", target, pattern, hide) 

61 rms = [] 

62 if hide: 

63 for h in hide: 

64 for rm in filter(lambda f: fnmatch.fnmatch(f, h), target): 

65 log.debug("remove(hide %s): %s", h, rm) 

66 rms.append(rm) 

67 if pattern: 67 ↛ 72line 67 didn't jump to line 72 because the condition on line 67 was always true

68 for p in pattern: 

69 for rm in filter(lambda f: not fnmatch.fnmatch(f, p), target): 

70 log.debug("remove(pattern %s): %s", p, rm) 

71 rms.append(rm) 

72 for rm in rms: 

73 target.remove(rm) 

74 return target 

75 

76 

77def fileopen(dir, fn, mode="w"): 

78 if fn == "-": 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true

79 if mode == "w": 

80 return sys.stdout 

81 else: 

82 return sys.stdin 

83 if dir: 

84 return open(os.path.join(dir, fn), mode) 

85 return open(fn, mode) 

86 

87 

88def mkent(fullpath, basedir): 

89 rel = os.path.relpath(fullpath, basedir) 

90 guess_type = mimetypes.guess_type(os.path.basename(fullpath)) 

91 res = { 

92 "name": rel, 

93 "fullpath": fullpath, 

94 "abspath": os.path.abspath(fullpath), 

95 "stat": os.stat(fullpath), 

96 } 

97 if guess_type[0] is not None: 

98 res["content_type"] = guess_type[0] 

99 if guess_type[1] is not None: 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true

100 res["content_encoding"] = guess_type[1] 

101 return res 

102 

103 

104def openresource(filename, resource_base="templates"): 

105 log.debug("filename: %s", filename) 

106 if filename == "-": 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true

107 return sys.stdin 

108 elif os.path.exists(filename): 

109 return fileopen(None, filename, "r") 

110 return io.StringIO( 

111 pkgutil.get_data(__package__, os.path.join(resource_base, filename)).decode( 

112 "utf-8" 

113 ) 

114 ) 

115 

116 

117@cli.command() 

118@cli_option 

119@click.option("--template", type=str) 

120@click.option("--dry/--no-dry", default=False, show_default=True) 

121@click.option("--recursive/--no-recursive", default=False, show_default=True) 

122@click.option("--single/--no-single", default=False, show_default=True) 

123@click.option("--pattern", type=str, default=["*"], show_default=True, multiple=True) 

124@click.option("--hide", type=str, default=None, show_default=True, multiple=True) 

125@click.option("--filename", default="index.html", show_default=True) 

126@click.argument("directory", type=click.Path(dir_okay=True, exists=True)) 

127def make(template, directory, dry, recursive, single, filename, pattern, hide): 

128 env = Environment(keep_trailing_newline=True) 

129 env.filters["strftime"] = lambda a, b: time.strftime(b, time.localtime(a)) 

130 env.filters["filemode"] = stat.filemode 

131 with openresource(template, "templates") as tfp: 

132 tmpl = env.from_string(tfp.read()) 

133 args = { 

134 "root": directory, 

135 "dir": [], 

136 "file": [], 

137 } 

138 if recursive and single: 

139 # output single file 

140 for root, dirs, files in os.walk(directory): 

141 dirs = pattern_filter(dirs, pattern, hide) 

142 files = pattern_filter(files, pattern, hide) 

143 for d in sorted(dirs): 

144 args["dir"].append(mkent(os.path.join(root, d), directory)) 

145 for f in sorted(files): 

146 args["file"].append(mkent(os.path.join(root, f), directory)) 

147 log.debug("output %s / %s arg=%s", directory, filename, args) 

148 if not dry: 148 ↛ exitline 148 didn't return from function 'make' because the condition on line 148 was always true

149 with fileopen(directory, filename, "w") as ofp: 

150 ofp.write(tmpl.render(**args)) 

151 elif recursive: 

152 # output to each directory 

153 for root, dirs, files in os.walk(directory): 

154 args = { 

155 "root": root, 

156 "dir": [], 

157 "file": [], 

158 } 

159 dirs = pattern_filter(dirs, pattern, hide) 

160 files = pattern_filter(files, pattern, hide) 

161 for d in sorted(dirs): 

162 args["dir"].append(mkent(os.path.join(root, d), root)) 

163 for f in sorted(files): 

164 args["file"].append(mkent(os.path.join(root, f), root)) 

165 log.debug("output %s / %s args=%s", root, filename, args) 

166 if not dry: 166 ↛ 153line 166 didn't jump to line 153 because the condition on line 166 was always true

167 with fileopen(root, filename, "w") as ofp: 

168 ofp.write(tmpl.render(**args)) 

169 else: 

170 # single directory 

171 files = os.listdir(directory) 

172 files = pattern_filter(files, pattern, hide) 

173 for f in sorted(files): 

174 ent = mkent(os.path.join(directory, f), directory) 

175 if stat.S_ISDIR(ent["stat"].st_mode): 

176 args["dir"].append(ent) 

177 else: 

178 args["file"].append(ent) 

179 log.debug("output %s / %s args=%s", directory, filename, args) 

180 if not dry: 180 ↛ exitline 180 didn't return from function 'make' because the condition on line 180 was always true

181 with fileopen(directory, filename, "w") as ofp: 

182 ofp.write(tmpl.render(**args)) 

183 

184 

185@cli.command() 

186@cli_option 

187@click.argument("name", type=str) 

188def read_resource(name): 

189 log.info("read resource %s", name) 

190 with openresource(name, "templates") as ifp: 

191 print(ifp.read()) 

192 

193 

194if __name__ == "__main__": 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true

195 cli()