import sys
import logging
import datetime
import textwrap
from itertools import chain
import click
import tabulate
from .. import __version__, PROGRAM
from ..environment import show_tracebacks
from ..utilities import networkx_layouts, graphviz_layouts, add_linenos, ngettext
from ..script.exceptions import KatScriptError
from ..config import log_handler_instance
LOGGER = logging.getLogger(PROGRAM)
[docs]def set_verbosity(ctx, option, value):
"""Callback to set stdout verbosity for root CLI command."""
# If neither -v nor -q is set, value is 0.
if not value:
return
state = ctx.ensure_object(KatState)
if option.name == "verbose":
offset = -10 * value
elif option.name == "quiet":
offset = 10 * value
else:
raise ValueError("Unsupported option.")
state.verbosity = state.VERBOSITY_DEFAULT + offset
[docs]def set_debug(ctx, option, value):
if value is None:
return
state = ctx.ensure_object(KatState)
state.debug = value
[docs]def set_log_excludes(ctx, option, value):
if value is None:
return
state = ctx.ensure_object(KatState)
for exclude in value:
state.exclude(exclude)
[docs]def print_banner(ctx, option, value):
"""Show the Finesse banner and exit."""
if not value or ctx.resilient_parsing: # Keep this check as-is!
return
state = ctx.ensure_object(KatState)
state.print_banner(exit_=True)
[docs]def parse_path(state, path, legacy=False, **error_kwargs):
from finesse.script import parse_file, parse_legacy_file
try:
if legacy:
return parse_legacy_file(path)
else:
return parse_file(path)
except Exception as error:
state.print_error(error, "PARSING ERROR:", **error_kwargs)
[docs]def plot_graph(state, *args, **kwargs):
from finesse.plotting.graph import plot_graph as finesse_plot_graph
finesse_plot_graph(*args, **kwargs)
[docs]def list_graph_layouts(ctx, option, value):
"""Callback to list available graph layouts."""
from ..environment import has_pygraphviz
if not value:
return
state = ctx.ensure_object(KatState)
state.print("Available layouts:", fg="green")
nx_layouts = networkx_layouts()
gv_layouts = graphviz_layouts()
layouts = sorted(chain(nx_layouts, gv_layouts))
gv_suffix = click.style("*", fg="yellow")
for layout in layouts:
suffix = gv_suffix if layout in gv_layouts else ""
state.print(f"- {layout}{suffix}")
state.print()
if has_pygraphviz():
state.print(
"*This layout can also be plotted directly in graphviz using the "
"--graphviz flag",
fg="yellow",
)
else:
state.print("Install pygraphviz/graphviz for more layouts", fg="yellow")
sys.exit()
def _fancy_format_kat_error(exception):
"""Format each marked error in red."""
assert isinstance(exception, KatScriptError)
error_msg = exception.rubric()
msg = f"{error_msg}\n"
errorlines = []
linenos = []
errorlinenos = set()
for lineno, linechunks in exception.chunkify().items():
line = ""
for bounds, is_error in linechunks:
chunktxt = exception.container.script(bounds)
if is_error:
errorlinenos.add(lineno)
if len(chunktxt) == chunktxt.count(" "):
# Error chunk is just spaces. Replace with interpunct.
chunktxt = click.style("·" * len(chunktxt), fg="red")
elif chunktxt == "\n":
# Error is a newline. Replace with carriage return symbol. Also add the newline
# anyway without colouring it red.
chunktxt = click.style("↵", fg="red") + chunktxt
line += click.style(chunktxt, fg="red")
else:
line += chunktxt
# Get rid of trailing newline.
try:
line = line.splitlines()[0]
except IndexError:
# Line is empty.
pass
linenos.append(lineno)
errorlines.append(line)
# Add line numbers.
numberedlines = add_linenos(linenos, errorlines)
# Add message for missing lines.
finallines = []
lastlineno = None
for lineno, line in zip(linenos, numberedlines):
if lastlineno:
diff = lineno - lastlineno
if diff > 1:
missingmsg = ngettext(diff - 1, "%d missing line", "%d missing lines")
finallines.append(f" *** {missingmsg} ***")
prefix = "-->" if lineno in errorlinenos else " "
finallines.append(prefix + line)
lastlineno = lineno
return msg + "\n".join(finallines)
input_file_argument = click.argument("input_file", type=click.File("r"))
output_file_argument = click.argument("output_file", type=click.File("w"), default="-")
plot_option = click.option(
"--plot/--no-plot",
default=True,
show_default=True,
help="Display results as figure.",
)
trace_option = click.option(
"--trace/--no-trace",
default=False,
show_default=True,
help="Displays the results of a beam trace of the model.",
)
graphviz_option = click.option(
"--graphviz",
is_flag=True,
default=False,
show_default=True,
help="Generate layout and display using Graphviz.",
)
list_graph_layouts_option = click.option(
"--list-graph-layouts",
callback=list_graph_layouts,
is_flag=True,
default=False,
expose_value=False,
is_eager=True,
help="Show available graph layouts and exit.",
)
graph_layout_argument = click.option(
"--layout", type=str, default="spring", help="Graph layout algorithm to use.",
)
network_type_argument = click.option(
"-t",
"--type",
"network_type",
type=click.Choice(("full", "components", "optical")),
default="full",
help="Network to plot.",
)
table_format_argument = click.option(
"--table-format",
type=click.Choice(tabulate.tabulate_formats),
default="fancy_grid",
help="Table format to use.",
)
legacy_option = click.option(
"--legacy",
is_flag=True,
default=False,
show_default=True,
help="Specify that the input file uses Finesse 2 syntax.",
)
verbose_option = click.option(
"-v",
"--verbose",
count=True,
callback=set_verbosity,
expose_value=False,
is_eager=True,
help="Increase verbosity of log output (can be specified multiple times).",
)
quiet_option = click.option(
"-q",
"--quiet",
count=True,
callback=set_verbosity,
expose_value=False,
help="Decrease verbosity of log output (can be specified multiple times).",
)
fancy_error_option = click.option(
"--fancy-errors/--no-fancy-errors",
is_flag=True,
default=True,
callback=set_fancy_error_formatting,
expose_value=False,
show_default=True,
help="Highlight script error locations in red rather than marking them on the following line.",
)
debug_option = click.option(
"--debug",
is_flag=True,
default=False,
callback=set_debug,
expose_value=False,
show_default=True,
help="Enable debug mode (print tracebacks).",
)
log_exclude_option = click.option(
"--log-exclude",
multiple=True,
callback=set_log_excludes,
expose_value=False,
help="Ignore log records from a particular logger (wildcards allowed).",
)
[docs]class KatState:
"""Shared state for all CLI subcommands.
This object gets built by Click when the CLI is called and encapsulates global
settings for use by individual commands/groups.
"""
VERBOSITY_DEFAULT = logging.WARNING
[docs] def __init__(self):
# Fancy error flag. This is NOT the default value for the CLI - it is set on by
# default via the @fancy_error_option decorators.
self.fancy_errors = False
# Set up the Finesse logger.
handler = log_handler_instance()
handler.setFormatter(ClickLogColorFormatter())
handler.setStream(click.get_text_stream("stderr"))
LOGGER.addHandler(handler)
# Set *default* verbosity.
# NOTE: the user's requested verbosity is set by :func:`set_verbosity`.
self.verbosity = self.VERBOSITY_DEFAULT
@property
def verbosity(self):
"""Log verbosity on stdout."""
return LOGGER.getEffectiveLevel()
@verbosity.setter
def verbosity(self, verbosity):
verbosity = min(logging.CRITICAL, max(logging.DEBUG, verbosity))
LOGGER.setLevel(verbosity)
@property
def verbose(self):
"""Verbose output enabled.
Returns True if the verbosity is enough for INFO or DEBUG messages to be
displayed.
"""
return self.verbosity <= self.VERBOSITY_DEFAULT
@property
def debug(self):
return show_tracebacks()
@debug.setter
def debug(self, value):
show_tracebacks(value)
[docs] def exclude(self, pattern):
"""Exclude records from loggers with names matching the specified pattern."""
log_handler_instance().exclude(pattern)
[docs] def print(self, text="", indent=0, error=False, exit_=False, exit_code=0, **kwargs):
text = str(text)
if indent > 0:
text = textwrap.indent(text, " " * 4)
click.secho(text, err=error, **kwargs)
if exit_:
self.exit(exit_code)
[docs] def print_error(
self, exception_or_msg, title=None, exit_=True, exit_code=1, **kwargs
):
if title:
self.print(title, error=True, fg="red", bold=True)
if self.debug and isinstance(exception_or_msg, BaseException):
# Print the full traceback.
from traceback import format_exception
# Only print the traceback part of the exception, because the message is printed below.
# FIXME: the `etype` is not used here, and the second parameter set to None results in
# "NoneType: None" being printed by `TracebackException` after the traceback...
etype, _, tb = sys.exc_info()
trace = "".join(format_exception(etype, None, tb))
self.print(trace, indent=1)
if self.fancy_errors and isinstance(exception_or_msg, KatScriptError):
msg = _fancy_format_kat_error(exception_or_msg)
fg = None
else:
# Format the whole error as red.
msg = str(exception_or_msg)
fg = "red"
self.print(msg, error=True, fg=fg, **kwargs)
if exit_:
self.exit(exit_code)
[docs] def print_banner(self, kat_file=None, **kwargs):
"""Print the Finesse banner."""
if not self.verbose:
return
input_file = f"Input file: {kat_file.name}" if kat_file else ""
timenow = datetime.datetime.now().strftime("%c").strip()
timestr = f"{timenow:>60}"
banner = rf"""
------------------------------------------------------------------------
FINESSE {__version__}
o_.-=. Frequency domain INterferomEter Simulation SoftwarE
(\'".\| http://www.gwoptics.org/finesse/
.>' (_--.
_=/d ,^\ {input_file}
~~ \)-' '
/ |
' ' {timestr}
------------------------------------------------------------------------
"""
self.print(textwrap.dedent(banner).strip(), **kwargs)
[docs] def exit(self, code=0):
"""Stop execution."""
sys.exit(code)