Coverage for obj2cli/_cli_click.py: 52%
140 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 14:23 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 14:23 +0000
1import difflib
2import functools
3import importlib
4import importlib.resources
5import io
6import os
7import pickle
8import pkgutil
9import pprint
10import subprocess
11import sys
12from logging import DEBUG, INFO, basicConfig, getLogger
14import click
15import yaml
16from jinja2 import Environment
18from .parser import Parser
19from .version import VERSION
21log = getLogger(__name__)
23out_formats = {
24 "pickle": lambda d, f: pickle.dump(d, f),
25 "yaml": lambda d, f: yaml.dump(d, stream=f),
26 "pprint": lambda d, f: f.write(pprint.pformat(d).encode("utf-8")),
27}
29in_formats = {
30 "pickle": lambda f: pickle.load(f),
31 "yaml": lambda f: yaml.load(f, Loader=yaml.FullLoader),
32}
35@click.version_option(version=VERSION, prog_name="obj2cli")
36@click.group(invoke_without_command=True)
37@click.pass_context
38def cli(ctx):
39 if ctx.invoked_subcommand is None:
40 print(ctx.get_help())
43def set_verbose(flag):
44 fmt = "%(asctime)s %(levelname)s %(message)s"
45 if flag:
46 basicConfig(level=DEBUG, format=fmt)
47 else:
48 basicConfig(level=INFO, format=fmt)
51_cli_option = [
52 click.option("--verbose/--no-verbose"),
53]
55_cls_option = _cli_option + [
56 click.option("--module", type=str),
57 click.option("--package", type=str),
58 click.option("--class", "klass", type=str),
59]
61out_option = [
62 click.option(
63 "--output",
64 type=click.File("wb"),
65 default=sys.stdout.buffer,
66 show_default="STDOUT",
67 ),
68 click.option(
69 "--format",
70 type=click.Choice(out_formats.keys()),
71 default="yaml",
72 show_default=True,
73 ),
74]
76in_option = [
77 click.option(
78 "--input", type=click.File("rb"), default=sys.stdin.buffer, show_default="STDIN"
79 ),
80 click.option(
81 "--format",
82 type=click.Choice(in_formats.keys()),
83 default="yaml",
84 show_default=True,
85 ),
86]
89def multi_options(decs):
90 def deco(f):
91 for dec in reversed(decs):
92 f = dec(f)
93 return f
95 return deco
98def cli_option(func):
99 @functools.wraps(func)
100 def wrap(verbose, *args, **kwargs):
101 set_verbose(verbose)
102 return func(*args, **kwargs)
104 return multi_options(_cli_option)(wrap)
107def cls_option(func):
108 @functools.wraps(func)
109 def wrap(verbose, klass, module, package, *args, **kwargs):
110 set_verbose(verbose)
111 log.debug("loading module %s, package=%s", module, package)
112 mod = importlib.import_module(name=module, package=package)
113 if klass is not None:
114 cls = getattr(mod, klass)
115 log.debug("class: %s", cls)
116 else:
117 cls = None
118 return func(cls, *args, **kwargs)
120 return multi_options(_cls_option)(wrap)
123def resource_option(dest, dirname=None, ext=""):
124 if dirname is None: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true
125 dirname = dest
127 def _resource_option(func):
128 @functools.wraps(func)
129 def wrap(*args, **kwargs):
130 val = kwargs.pop(dest)
131 exval = kwargs.pop(f"{dest}_example")
132 if val is not None:
133 # file handle is handed to `func` and must stay open past this
134 # scope; the caller is responsible for closing it.
135 kwargs[dest] = open(val, "rb") # noqa: SIM115
136 else:
137 kwargs[dest] = io.BytesIO(
138 pkgutil.get_data(__package__, os.path.join(dirname, exval + ext))
139 )
140 return func(*args, **kwargs)
142 try:
143 names = [
144 p.name
145 for p in importlib.resources.files(__package__)
146 .joinpath(dirname)
147 .iterdir()
148 ]
149 except FileNotFoundError:
150 names = []
151 log.debug("resources in pkg=%s, dir=%s: %s", __package__, dirname, names)
152 names = [x[: -len(ext)] for x in filter(lambda f: f.endswith(ext), names)]
153 opts = [
154 click.option(f"--{dest}", type=click.Path()),
155 click.option(f"--{dest}-example", type=click.Choice(names)),
156 ]
157 return multi_options(opts)(wrap)
159 return _resource_option
162@cli.command()
163@cls_option
164@multi_options(out_option)
165def parse(cls, format, output):
166 ps = Parser()
167 log.debug("start parse: %s", cls.__name__)
168 data = ps.parse_cls(cls)
169 out_formats.get(format)(data, output)
170 # pickle.dump(data, sys.stdout.buffer)
173def template_args(data, cls=None):
174 ps = Parser()
175 funcs = {}
176 for k, v in data.items():
177 if k in ("__classmeta__", "__init__", "doc"):
178 continue
179 funcs[k] = ps.fn_args(v)
180 if cls is None:
181 cls = data.get("__classmeta__", {}).get("class", None)
182 return {
183 "klass": cls,
184 "parsed": data,
185 "constructor": ps.fn_args(data.get("__init__", {})),
186 "funcs": funcs,
187 "classmeta": data.get("__classmeta__", {}),
188 }
191@cli.command("generate")
192@cls_option
193@multi_options(in_option)
194@resource_option(dest="template", dirname="template", ext="_cli.j2")
195@click.option("--autopep8/--no-autopep8", default=False)
196def gen(cls, input, format, template, autopep8):
197 data = in_formats.get(format)(input)
198 env = Environment()
199 tmpl = env.from_string(template.read().decode("utf-8"))
200 res = tmpl.render(**template_args(data, cls))
201 if autopep8:
202 p = subprocess.Popen(["autopep8", "-"], stdin=subprocess.PIPE)
203 p.communicate(res.encode("utf8"))
204 else:
205 print(res)
208@cli.command()
209@cls_option
210@multi_options(in_option)
211@resource_option(dest="template", dirname="template", ext="_cli.j2")
212@click.argument("other", type=click.File("r"), required=True)
213def diff(cls, input, format, template, other):
214 data = in_formats.get(format)(input)
215 env = Environment()
216 tmpl = env.from_string(template.read())
217 res = tmpl.render(**template_args(data, cls))
218 before = other.read().split("\n")
219 current = res.split("\n")
220 differ = difflib.unified_diff(
221 before, current, fromfile="before.py", tofile="current.py", lineterm=""
222 )
223 print("\n".join(differ))
226@cli.command("print")
227@cli_option
228@multi_options(in_option)
229def show(input, format):
230 data = in_formats.get(format)(input)
231 pprint.pprint(data)
234@cli.command("print-tmpl-args")
235@cls_option
236@multi_options(in_option)
237def show_tmplarg(cls, input, format):
238 data = in_formats.get(format)(input)
239 pprint.pprint(template_args(data, cls))
242if __name__ == "__main__": 242 ↛ 243line 242 didn't jump to line 243 because the condition on line 242 was never true
243 cli()