Skip to content

Commit 054c21f

Browse files
committed
Switch cli to click
Signed-off-by: Cristian Le <cristian.le@mpsd.mpg.de>
1 parent 7a6f1c9 commit 054c21f

File tree

7 files changed

+266
-261
lines changed

7 files changed

+266
-261
lines changed

fmf/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
__version__ = importlib.metadata.version("fmf")
1212

1313
__all__ = [
14+
"__version__",
1415
"Context",
1516
"Tree",
1617
"filter",

fmf/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .cli import main
2+
3+
main()

fmf/cli.py

Lines changed: 170 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -16,205 +16,189 @@
1616
of available options.
1717
"""
1818

19-
import argparse
20-
import os
21-
import os.path
22-
import shlex
23-
import sys
19+
import functools
20+
from pathlib import Path
21+
22+
import click
23+
from click_option_group import optgroup
2424

2525
import fmf
2626
import fmf.utils as utils
2727

2828
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29-
# Parser
29+
# Common option groups
3030
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3131

3232

33-
class Parser:
34-
""" Command line options parser """
35-
36-
def __init__(self, arguments=None, path=None):
37-
""" Prepare the parser. """
38-
# Change current working directory (used for testing)
39-
if path is not None:
40-
os.chdir(path)
41-
# Split command line if given as a string (used for testing)
42-
if isinstance(arguments, str):
43-
self.arguments = shlex.split(arguments)
44-
# Otherwise use sys.argv
45-
if arguments is None:
46-
self.arguments = sys.argv
47-
# Enable debugging output if requested
48-
if "--debug" in self.arguments:
49-
utils.log.setLevel(utils.LOG_DEBUG)
50-
# Show current version and exit
51-
if "--version" in self.arguments:
52-
self.output = f"{fmf.__version__}"
53-
print(self.output)
54-
return
55-
56-
# Handle subcommands (mapped to format_* methods)
57-
self.parser = argparse.ArgumentParser(
58-
usage="fmf command [options]\n" + __doc__)
59-
self.parser.add_argument(
60-
"--version", action="store_true",
61-
help="print fmf version with commit hash and exit")
62-
self.parser.add_argument('command', help='Command to run')
63-
self.command = self.parser.parse_args(self.arguments[1:2]).command
64-
if not hasattr(self, "command_" + self.command):
65-
self.parser.print_help()
66-
raise utils.GeneralError(
67-
"Unrecognized command: '{0}'".format(self.command))
68-
# Initialize the rest and run the subcommand
69-
self.output = ""
70-
getattr(self, "command_" + self.command)()
71-
72-
def options_select(self):
73-
""" Select by name, filter """
74-
group = self.parser.add_argument_group("Select")
75-
group.add_argument(
76-
"--key", dest="keys", action="append", default=[],
77-
help="Key content definition (required attributes)")
78-
group.add_argument(
79-
"--name", dest="names", action="append", default=[],
80-
help="List objects with name matching regular expression")
81-
group.add_argument(
82-
"--source", dest="sources", action="append", default=[],
83-
help="List objects defined in specified source files")
84-
group.add_argument(
85-
"--filter", dest="filters", action="append", default=[],
86-
help="Apply advanced filter (see 'pydoc fmf.filter')")
87-
group.add_argument(
88-
"--condition", dest="conditions", action="append", default=[],
89-
metavar="EXPR",
90-
help="Use arbitrary Python expression for filtering")
91-
group.add_argument(
92-
"--whole", dest="whole", action="store_true",
93-
help="Consider the whole tree (leaves only by default)")
94-
95-
def options_formatting(self):
96-
""" Formating options """
97-
group = self.parser.add_argument_group("Format")
98-
group.add_argument(
99-
"--format", dest="formatting", default=None,
100-
help="Custom output format using the {} expansion")
101-
group.add_argument(
102-
"--value", dest="values", action="append", default=[],
103-
help="Values for the custom formatting string")
104-
105-
def options_utils(self):
106-
""" Utilities """
107-
group = self.parser.add_argument_group("Utils")
108-
group.add_argument(
109-
"--path", action="append", dest="paths",
110-
help="Path to the metadata tree (default: current directory)")
111-
group.add_argument(
112-
"--verbose", action="store_true",
113-
help="Print information about parsed files to stderr")
114-
group.add_argument(
115-
"--debug", action="store_true",
116-
help="Turn on debugging output, do not catch exceptions")
117-
118-
def command_ls(self):
119-
""" List names """
120-
self.parser = argparse.ArgumentParser(
121-
description="List names of available objects")
122-
self.options_select()
123-
self.options_utils()
124-
self.options = self.parser.parse_args(self.arguments[2:])
125-
self.show(brief=True)
126-
127-
def command_clean(self):
128-
""" Clean cache """
129-
self.parser = argparse.ArgumentParser(
130-
description="Remove cache directory and its content")
131-
self.clean()
132-
133-
def command_show(self):
134-
""" Show metadata """
135-
self.parser = argparse.ArgumentParser(
136-
description="Show metadata of available objects")
137-
self.options_select()
138-
self.options_formatting()
139-
self.options_utils()
140-
self.options = self.parser.parse_args(self.arguments[2:])
141-
self.show(brief=False)
142-
143-
def command_init(self):
144-
""" Initialize tree """
145-
self.parser = argparse.ArgumentParser(
146-
description="Initialize a new metadata tree")
147-
self.options_utils()
148-
self.options = self.parser.parse_args(self.arguments[2:])
149-
# For each path create an .fmf directory and version file
150-
for path in self.options.paths or ["."]:
151-
root = fmf.Tree.init(path)
152-
print("Metadata tree '{0}' successfully initialized.".format(root))
153-
154-
def show(self, brief=False):
155-
""" Show metadata for each path given """
156-
output = []
157-
for path in self.options.paths or ["."]:
158-
if self.options.verbose:
159-
utils.info("Checking {0} for metadata.".format(path))
160-
tree = fmf.Tree(path)
161-
for node in tree.prune(
162-
self.options.whole,
163-
self.options.keys,
164-
self.options.names,
165-
self.options.filters,
166-
self.options.conditions,
167-
self.options.sources):
168-
if brief:
169-
show = node.show(brief=True)
170-
else:
171-
show = node.show(
172-
brief=False,
173-
formatting=self.options.formatting,
174-
values=self.options.values)
175-
# List source files when in debug mode
176-
if self.options.debug:
177-
for source in node.sources:
178-
show += utils.color("{0}\n".format(source), "blue")
179-
if show is not None:
180-
output.append(show)
181-
182-
# Print output and summary
183-
if brief or self.options.formatting:
184-
joined = "".join(output)
185-
else:
186-
joined = "\n".join(output)
187-
print(joined, end="")
188-
if self.options.verbose:
189-
utils.info("Found {0}.".format(
190-
utils.listed(len(output), "object")))
191-
self.output = joined
192-
193-
def clean(self):
194-
""" Remove cache directory """
195-
try:
196-
cache = utils.get_cache_directory(create=False)
197-
utils.clean_cache_directory()
198-
print("Cache directory '{0}' has been removed.".format(cache))
199-
except Exception as error: # pragma: no cover
200-
utils.log.error(
201-
"Unable to remove cache, exception was: {0}".format(error))
33+
def _select_options(func):
34+
"""Select group options"""
35+
36+
@optgroup.group("Select")
37+
@optgroup.option("--key", "keys", metavar="KEY", default=[], multiple=True,
38+
help="Key content definition (required attributes)")
39+
@optgroup.option("--name", "names", metavar="NAME", default=[], multiple=True,
40+
help="List objects with name matching regular expression")
41+
@optgroup.option("--source", "sources", metavar="SOURCE", default=[], multiple=True,
42+
help="List objects defined in specified source files")
43+
@optgroup.option("--filter", "filters", metavar="FILTER", default=[], multiple=True,
44+
help="Apply advanced filter (see 'pydoc fmf.filter')")
45+
@optgroup.option("--condition", "conditions", metavar="EXPR", default=[], multiple=True,
46+
help="Use arbitrary Python expression for filtering")
47+
@optgroup.option("--whole", is_flag=True, default=False,
48+
help="Consider the whole tree (leaves only by default)")
49+
@functools.wraps(func)
50+
def wrapper(*args, **kwargs):
51+
# Hack to group the options into one variable
52+
select = {
53+
opt: kwargs.pop(opt)
54+
for opt in ("keys", "names", "sources", "filters", "conditions", "whole")
55+
}
56+
return func(*args, select=select, **kwargs)
57+
58+
return wrapper
59+
60+
61+
def _format_options(func):
62+
"""Formating group options"""
63+
64+
@optgroup.group("Format")
65+
@optgroup.option("--format", "formatting", metavar="FORMAT", default=None,
66+
help="Custom output format using the {} expansion")
67+
@optgroup.option("--value", "values", metavar="VALUE", default=[], multiple=True,
68+
help="Values for the custom formatting string")
69+
@functools.wraps(func)
70+
def wrapper(*args, **kwargs):
71+
# Hack to group the options into one variable
72+
format = {
73+
opt: kwargs.pop(opt)
74+
for opt in ("formatting", "values")
75+
}
76+
return func(*args, format=format, **kwargs)
77+
78+
return wrapper
79+
80+
81+
def _utils_options(func):
82+
"""Utilities group options"""
83+
84+
@optgroup.group("Utils")
85+
@optgroup.option("--path", "paths", metavar="PATH", multiple=True,
86+
type=Path, default=["."],
87+
show_default="current directory",
88+
help="Path to the metadata tree")
89+
@functools.wraps(func)
90+
def wrapper(*args, **kwargs):
91+
return func(*args, **kwargs)
92+
93+
return wrapper
20294

20395

20496
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
20597
# Main
20698
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
207-
208-
def main(arguments=None, path=None):
209-
""" Parse options, do what is requested """
210-
parser = Parser(arguments, path)
211-
return parser.output
99+
class CatchAllExceptions(click.Group):
100+
def __call__(self, *args, **kwargs):
101+
# TODO: This actually has no effect
102+
try:
103+
return self.main(*args, **kwargs)
104+
except fmf.utils.GeneralError as error:
105+
# TODO: Better handling of --debug
106+
if "--debug" not in kwargs:
107+
fmf.utils.log.error(error)
108+
raise
109+
110+
111+
@click.group("fmf", cls=CatchAllExceptions)
112+
@click.version_option(fmf.__version__, message="%(version)s")
113+
@click.option("--verbose", is_flag=True, default=False, type=bool,
114+
help="Print information about parsed files to stderr")
115+
@click.option("--debug", "-d", count=True, default=0, type=int,
116+
help="Provide debugging information. Repeat to see more details.")
117+
@click.pass_context
118+
def main(ctx, debug, verbose) -> None:
119+
"""This is command line interface for the Flexible Metadata Format."""
120+
ctx.ensure_object(dict)
121+
if debug:
122+
utils.log.setLevel(debug)
123+
ctx.obj["verbose"] = verbose
124+
ctx.obj["debug"] = debug
212125

213126

214-
def cli_entry():
127+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
128+
# Sub-commands
129+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
130+
@main.command("ls")
131+
@_select_options
132+
@_utils_options
133+
@click.pass_context
134+
def ls(ctx, paths, select) -> None:
135+
"""List names of available objects"""
136+
_show(ctx, paths, select, brief=True)
137+
138+
139+
@main.command("clean")
140+
def clean() -> None:
141+
"""Remove cache directory and its content"""
142+
_clean()
143+
144+
145+
@main.command("show")
146+
@_select_options
147+
@_format_options
148+
@_utils_options
149+
@click.pass_context
150+
def show(ctx, paths, select, format) -> None:
151+
"""Show metadata of available objects"""
152+
_show(ctx, paths, select, format_opts=format, brief=False)
153+
154+
155+
@main.command("init")
156+
@_utils_options
157+
def init(paths) -> None:
158+
"""Initialize a new metadata tree"""
159+
# For each path create an .fmf directory and version file
160+
for path in paths:
161+
root = fmf.Tree.init(path)
162+
click.echo("Metadata tree '{0}' successfully initialized.".format(root))
163+
164+
165+
def _show(ctx, paths, select_opts, format_opts=None, brief=False):
166+
""" Show metadata for each path given """
167+
output = []
168+
for path in paths:
169+
if ctx.obj["verbose"]:
170+
utils.info("Checking {0} for metadata.".format(path))
171+
tree = fmf.Tree(path)
172+
for node in tree.prune(**select_opts):
173+
if brief:
174+
show = node.show(brief=True)
175+
else:
176+
assert format_opts is not None
177+
show = node.show(brief=False, **format_opts)
178+
# List source files when in debug mode
179+
if ctx.obj["debug"]:
180+
for source in node.sources:
181+
show += utils.color("{0}\n".format(source), "blue")
182+
if show is not None:
183+
output.append(show)
184+
185+
# Print output and summary
186+
if brief or format_opts and format_opts["formatting"]:
187+
joined = "".join(output)
188+
else:
189+
joined = "\n".join(output)
190+
click.echo(joined, nl=False)
191+
if ctx.obj["verbose"]:
192+
utils.info("Found {0}.".format(
193+
utils.listed(len(output), "object")))
194+
195+
196+
def _clean():
197+
"""Remove cache directory"""
215198
try:
216-
main()
217-
except fmf.utils.GeneralError as error:
218-
if "--debug" not in sys.argv:
219-
fmf.utils.log.error(error)
220-
raise
199+
cache = utils.get_cache_directory(create=False)
200+
utils.clean_cache_directory()
201+
click.echo("Cache directory '{0}' has been removed.".format(cache))
202+
except Exception as error: # pragma: no cover
203+
utils.log.error(
204+
"Unable to remove cache, exception was: {0}".format(error))

0 commit comments

Comments
 (0)