import sys
import logging
import datetime
import textwrap
from itertools import chain
import click
from .. import __version__, PROGRAM, session
from ..env import show_tracebacks
from ..utilities import networkx_layouts, graphviz_layouts, add_linenos, ngettext
from ..utilities.logging import FinesseStreamHandler
from ..script.exceptions import KatScriptError
LOGGER = logging.getLogger(PROGRAM)
[docs]def set_verbosity(ctx, option, value):
"""Callback to set verbosity for root CLI command."""
# If neither -v nor -q is set, value is 0.
if not value:
return
if option.name == "verbose":
setter = session.louder
elif option.name == "quiet":
setter = session.quieter
else:
raise ValueError("Unsupported option.")
# If -v or -q was specified N times, we want to get increasingly louder/quieter by
# N times.
for _ in range(value):
setter()
[docs]def set_log_display_level(ctx, option, value):
"""Callback to set log verbosity for root CLI command."""
if not value:
return
state = ctx.ensure_object(KatState)
state.log_display_level = value
[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 ..env 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 = f"{exception.rubric()}\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
syntax = ""
if exception.syntax is not None:
syntax = f"\n\nSyntax: {click.style(exception.syntax, fg='green')}"
message = error + "\n".join(finallines) + syntax
return message
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 (if possible).",
)
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="neato",
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.",
)
legacy_option = click.option(
"--legacy",
is_flag=True,
default=False,
show_default=True,
help="Specify that the input file uses Finesse 2 syntax.",
)
# The current default verbosity is already maximum, so this currently has no effect.
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."
),
)
log_display_level_option = click.option(
"--log-level",
type=click.Choice(("warning", "info", "debug")),
default="warning",
show_default=True,
callback=set_log_display_level,
expose_value=False,
help="Set minimum log severity level to display.",
)
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).",
)
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).",
)
[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.
"""
LOG_DISPLAY_DEFAULT_LEVEL = 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.
self._log_handler = FinesseStreamHandler()
self._log_handler.setFormatter(ClickLogColorFormatter())
self._log_handler.setStream(click.get_text_stream("stderr"))
LOGGER.addHandler(self._log_handler)
# Set *default* log display level.
# NOTE: the user's requested log display level is set by
# :func:`set_log_display_level`.
self.log_display_level = self.LOG_DISPLAY_DEFAULT_LEVEL
@property
def log_display_level(self):
"""Log verbosity on stdout."""
return LOGGER.getEffectiveLevel()
@log_display_level.setter
def log_display_level(self, log_display_level):
try:
log_display_level = log_display_level.upper()
except AttributeError:
# Probably an int.
pass
LOGGER.setLevel(log_display_level)
@property
def isverbose(self):
"""Verbose output enabled.
Returns True if the verbosity is enough for info messages to be displayed.
"""
return session.verbose_for("info")
@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."""
self._log_handler.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.isverbose:
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)