# -*- coding: utf-8 -*-
"""A program for testing and validating PVL text.
The ``pvl_validate`` program will read a file with PVL text (any of
the kinds of files that :func:`pvl.load` reads) and will report
on which of the various PVL dialects were able to load that PVL
text, and then also reports on whether the ``pvl`` library can encode
the Python Objects back out to PVL text.
You can imagine some PVL text that could be loaded, but is not able
to be written out in a particular strict PVL dialect (like PDS3
labels).
"""
# Copyright 2020-2021, ``pvl`` library authors.
#
# Reuse is permitted under the terms of the license.
# The AUTHORS file and the LICENSE file are at the
# top level of this library.
import argparse
import logging
from collections import OrderedDict
import pvl
from .lexer import LexerError
from .grammar import (
PVLGrammar,
ODLGrammar,
PDSGrammar,
ISISGrammar,
OmniGrammar,
)
from .parser import ParseError, PVLParser, ODLParser, OmniParser
from .decoder import PVLDecoder, ODLDecoder, PDSLabelDecoder, OmniDecoder
from .encoder import PVLEncoder, ODLEncoder, ISISEncoder, PDSLabelEncoder
# Some assembly required for the dialects.
# We are going to be explicit here, because these arguments are
# are different than the defaults for these classes, especially for the
# parsers and decoders, as we want to be strict and not permissive here.
_pvl_g = PVLGrammar()
_pvl_d = PVLDecoder(grammar=_pvl_g)
_odl_g = ODLGrammar()
_odl_d = ODLDecoder(grammar=_odl_g)
_pds_g = PDSGrammar()
_pds_d = PDSLabelDecoder(grammar=_pds_g)
_isis_g = ISISGrammar()
_isis_d = OmniDecoder(grammar=_isis_g)
_omni_g = OmniGrammar()
_omni_d = OmniDecoder(grammar=_omni_g)
dialects = OrderedDict(
PDS3=dict(
parser=ODLParser(grammar=_pds_g, decoder=_pds_d),
grammar=_pds_g,
decoder=_pds_d,
encoder=PDSLabelEncoder(grammar=_pds_g, decoder=_pds_d),
),
ODL=dict(
parser=ODLParser(grammar=_odl_g, decoder=_odl_d),
grammar=_odl_g,
decoder=_odl_d,
encoder=ODLEncoder(grammar=_odl_g, decoder=_odl_d),
),
PVL=dict(
parser=PVLParser(grammar=_pvl_g, decoder=_pvl_d),
grammar=_pvl_g,
decoder=_pvl_d,
encoder=PVLEncoder(grammar=_pvl_g, decoder=_pvl_d),
),
ISIS=dict(
parser=OmniParser(grammar=_isis_g, decoder=_isis_d),
grammar=_isis_g,
decoder=_isis_d,
encoder=ISISEncoder(grammar=_isis_g, decoder=_isis_d),
),
Omni=dict(
parser=OmniParser(grammar=_omni_g, decoder=_omni_d),
grammar=_omni_g,
decoder=_omni_d,
encoder=PVLEncoder(grammar=_omni_g, decoder=_omni_d),
),
)
[docs]def arg_parser():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="Will report the errors that are encountered. A second v will "
"include tracebacks for non-pvl exceptions. ",
)
p.add_argument("--version", action="version", version=pvl.__version__)
p.add_argument(
"file", nargs="+", help="file containing PVL text to validate."
)
return p
[docs]def main(argv=None):
args = arg_parser().parse_args(argv)
logging.basicConfig(
format="%(levelname)s: %(message)s", level=(60 - 20 * args.verbose)
)
results_list = list()
for f in args.file:
pvl_text = pvl.get_text_from(f)
results = dict()
for k, v in dialects.items():
results[k] = pvl_flavor(pvl_text, k, v, f, args.verbose)
results_list.append((f, results))
# Writing the flavors out again to preserve order.
if args.verbose > 0:
print(f"pvl library version: {pvl.__version__}")
print(report(results_list, list(dialects.keys())))
return
[docs]def pvl_flavor(
text, dialect, decenc: dict, filename, verbose=False
) -> tuple((bool, bool)):
"""Returns a two-tuple of booleans which indicate
whether the *text* could be loaded and then encoded.
The first boolean in the two-tuple indicates whether the *text*
could be loaded with the given parser, grammar, and decoder.
The second indicates whether the loaded PVL object could be
encoded with the given encoder, grammar, and decoder. If the
first element is False, the second will be None.
"""
loads = None
encodes = None
try:
some_pvl = pvl.loads(text, **decenc)
loads = True
try:
pvl.dumps(some_pvl, **decenc)
encodes = True
except (LexerError, ParseError, ValueError) as err:
logging.error(f"{dialect} encode error {filename} {err}")
encodes = False
except (LexerError, ParseError) as err:
logging.error(f"{dialect} load error {filename} {err}")
loads = False
except: # noqa E722
if verbose <= 1:
logging.error(
f"{dialect} load error {filename}, try -vv for more info."
)
else:
logging.exception(f"{dialect} load error {filename}")
logging.error(f"End {dialect} load error {filename}")
loads = False
return loads, encodes
[docs]def report(reports: list, flavors: list) -> str:
"""Returns a multi-line string which is the
pretty-printed report given the list of
*reports*.
"""
if len(reports[0][1]) != len(flavors):
raise IndexError(
"The length of the report list keys "
f"({len(reports[0][1])}) "
"and the length of the flavors list "
f"({len(flavors)}) aren't the same."
)
if len(reports) > 1:
return report_many(reports, flavors)
r = reports[0][1]
lines = list()
loads = {True: "Loads", False: "does NOT load"}
encodes = {True: "Encodes", False: "does NOT encode", None: ""}
col1w = len(max(flavors, key=len))
col2w = len(max(loads.values(), key=len))
col3w = len(max(encodes.values(), key=len))
for k in flavors:
lines.append(
build_line(
[k, loads[r[k][0]], encodes[r[k][1]]], [col1w, col2w, col3w]
)
)
return "\n".join(lines)
[docs]def report_many(r_list: list, flavors: list) -> str:
"""Returns a multi-line, table-like string which
is the pretty-printed report of the items in *r_list*.
"""
lines = list()
loads = {True: "L", False: "No L"}
encodes = {True: "E", False: "No E", None: ""}
col1w = len(max([x[0] for x in r_list], key=len))
col2w = len(max(loads.values(), key=len))
col3w = len(max(encodes.values(), key=len))
flavorw = col2w + col3w + 1
header = ["File"] + flavors
headerw = [col1w] + [flavorw] * len(flavors)
rule = [" " * col1w] + [" " * flavorw] * len(flavors)
rule_line = build_line(rule, headerw).replace("|", "+").replace(" ", "-")
lines.append(rule_line)
lines.append(build_line(header, headerw))
lines.append(rule_line)
for r in r_list:
cells = [r[0]]
widths = [col1w]
for f in flavors:
# cells.append(loads[r[1][f][0]] + ' ' + encodes[r[1][f][1]])
cells.append(
"{0:^{w2}} {1:^{w3}}".format(
loads[r[1][f][0]], encodes[r[1][f][1]], w2=col2w, w3=col3w
)
)
widths.append(flavorw)
lines.append(build_line(cells, widths))
return "\n".join(lines)
[docs]def build_line(elements: list, widths: list, sep=" | ") -> str:
"""Returns a string formatted from the *elements* and *widths*
provided.
"""
cells = list()
cells.append("{0:<{width}}".format(elements[0], width=widths[0]))
for e, w in zip(elements[1:], widths[1:]):
cells.append("{0:^{width}}".format(e, width=w))
return sep.join(cells)