Source code for pvl.pvl_validate

# -*- 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)