Coverage for dirindex/_cli.py: 91%
140 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 14:22 +0000
« 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
12import click
13from jinja2 import Environment
15from .version import VERSION
17log = getLogger(__name__)
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())
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)
36_cli_option = [
37 click.option("--verbose/--no-verbose"),
38]
41def multi_options(decs):
42 def deco(f):
43 for dec in reversed(decs):
44 f = dec(f)
45 return f
47 return deco
50def cli_option(func):
51 @functools.wraps(func)
52 def wrap(verbose, *args, **kwargs):
53 set_verbose(verbose)
54 return func(*args, **kwargs)
56 return multi_options(_cli_option)(wrap)
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
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)
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
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 )
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))
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())
194if __name__ == "__main__": 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true
195 cli()