diff --git a/.github/workflows/code-cover.yml b/.github/workflows/code-cover.yml index de1bb4d..cf615b5 100644 --- a/.github/workflows/code-cover.yml +++ b/.github/workflows/code-cover.yml @@ -26,6 +26,7 @@ jobs: python -m pip install --upgrade pip python -m pip install pytest python -m pip install pytest-cov + python -m pip install multidict - name: Install pvl run: python -m pip install -e . - name: Test with pytest and generate coverage report diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 9f41856..7b73005 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -30,6 +30,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install pytest flake8 + python -m pip install multidict - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/HISTORY.rst b/HISTORY.rst index fd64b0c..0303c97 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -30,6 +30,30 @@ and the release date, in year-month-day format (see examples below). Unreleased ---------- +1.2.1 (2021-05-31) +------------------ + +Added ++++++ +* So many tests, increased coverage by about 10%. + +Fixed ++++++ +* Attempting to import `pvl.new` without *multidict* being available, + will now properly yield an ImportError. +* The `dump()` and `dumps()` functions now properly overwritten in `pvl.new`. +* All encoders that descended from PVLEncoder didn't properly have group_class and + object_class arguments to their constructors, now they do. +* The `char_allowed()` function in grammar objects now raises a more useful ValueError + than just a generic Exception. +* The new `collections.PVLMultiDict` wasn't correctly inserting Mapping objects with + the `insert_before()` and `insert_after()` methods. +* The `token.Token` class's `__index__()` function didn't always properly return an + index. +* The `token.Token` class's `__float__()` function would return int objects if the + token could be converted to int. Now always returns floats. + + 1.2.0 (2021-03-27) ------------------ diff --git a/Makefile b/Makefile index 14251fe..abef2e8 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,9 @@ lint: test: python -m pytest --doctest-modules --doctest-glob='*.rst' +test-min: + python -m pytest --doctest-modules --ignore=pvl/new.py + test-all: tox diff --git a/README.rst b/README.rst index 31a6954..c3ef8a8 100644 --- a/README.rst +++ b/README.rst @@ -12,6 +12,7 @@ pvl .. image:: https://codecov.io/gh/planetarypy/pvl/branch/master/graph/badge.svg?token=uWqotcPTGR :target: https://codecov.io/gh/planetarypy/pvl + :alt: Codecov coverage .. image:: https://img.shields.io/pypi/v/pvl.svg?style=flat-square @@ -38,7 +39,7 @@ Python implementation of a PVL (Parameter Value Language) library. * Support for Python 3.6 and higher (avaiable via pypi and conda). * `PlanetaryPy`_ Affiliate Package. -PVL is a markup language, similar to XML, commonly employed for +PVL is a markup language, like JSON or YAML, commonly employed for entries in the Planetary Data System used by NASA to archive mission data, among other uses. This package supports both encoding and decoding a variety of PVL 'flavors' including PVL itself, ODL, @@ -214,7 +215,7 @@ Feedback, issues, and contributions are always gratefully welcomed. See the environment. -.. _PlanetaryPy: https://github.com/planetarypy +.. _PlanetaryPy: https://planetarypy.org .. _USGS ISIS Cube Labels: http://isis.astrogeology.usgs.gov/ .. _NASA PDS 3 Labels: https://pds.nasa.gov .. _image: https://github.com/planetarypy/pvl/raw/master/tests/data/pattern.cub diff --git a/docs/conf.py b/docs/conf.py index 0d3aa35..a8f51f5 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # General information about the project. project = u'pvl' -copyright = u'2015, 2017, 2019-2020, pvl Developers' +copyright = u'2015, 2017, 2019-2021, pvl Developers' # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout diff --git a/pvl/__init__.py b/pvl/__init__.py index 8189795..f497d21 100755 --- a/pvl/__init__.py +++ b/pvl/__init__.py @@ -24,7 +24,7 @@ __author__ = "The pvl Developers" __email__ = "rbeyer@rossbeyer.net" -__version__ = "1.2.0" +__version__ = "1.2.1" __all__ = [ "load", "loads", diff --git a/pvl/collections.py b/pvl/collections.py index c5f8c95..5679915 100644 --- a/pvl/collections.py +++ b/pvl/collections.py @@ -18,7 +18,7 @@ is no fundamental Python type for a quantity, so we define the Quantity class (formerly the Units class). """ -# Copyright 2015, 2017, 2019-2020, ``pvl`` library authors. +# Copyright 2015, 2017, 2019-2021, ``pvl`` library authors. # # Reuse is permitted under the terms of the license. # The AUTHORS file and the LICENSE file are at the @@ -268,9 +268,6 @@ def extend(self, *args, **kwargs): if isinstance(iterable, abc.Mapping) or hasattr(iterable, "items"): for key, value in iterable.items(): self.append(key, value) - elif hasattr(iterable, "keys"): - for key in iterable.keys(): - self.append(key, iterable[key]) else: for key, value in iterable: self.append(key, value) @@ -393,20 +390,6 @@ def insert(self, index: int, *args) -> None: return - def __insert_wrapper(func): - """Make sure the arguments given to the insert methods are correct.""" - - def check_func(self, key, new_item, instance=0): - if key not in self.keys(): - raise KeyError(f"{key} not a key in label") - if not isinstance(new_item, (list, OrderedMultiDict)): - raise TypeError("The new item must be a list or PVLModule") - if isinstance(new_item, OrderedMultiDict): - new_item = list(new_item) - return func(self, key, new_item, instance) - - return check_func - def key_index(self, key, instance: int = 0) -> int: """Get the index of the key to insert before or after.""" if key not in self: @@ -586,7 +569,7 @@ def _insert_item( index = index + 1 if is_after else index if isinstance(new_item, abc.Mapping): - tuple_iter = new_item.items() + tuple_iter = tuple(new_item.items()) else: tuple_iter = new_item self.insert(index, tuple_iter) diff --git a/pvl/encoder.py b/pvl/encoder.py index 4c8a916..2a77ac3 100644 --- a/pvl/encoder.py +++ b/pvl/encoder.py @@ -524,6 +524,8 @@ def __init__( aggregation_end=True, end_delimiter=False, newline="\r\n", + group_class=PVLGroup, + object_class=PVLObject ): if grammar is None: @@ -546,6 +548,8 @@ def __init__( aggregation_end, end_delimiter, newline, + group_class=group_class, + object_class=object_class ) def encode(self, module: abc.Mapping) -> str: @@ -862,6 +866,8 @@ def __init__( indent=2, width=80, aggregation_end=True, + group_class=PVLGroup, + object_class=PVLObject, convert_group_to_object=True, tab_replace=4, symbol_single_quote=True, @@ -882,6 +888,8 @@ def __init__( aggregation_end, end_delimiter=False, newline="\r\n", + group_class=group_class, + object_class=object_class ) self.convert_group_to_object = convert_group_to_object @@ -1147,6 +1155,8 @@ def __init__( aggregation_end=True, end_delimiter=False, newline="\n", + group_class=PVLGroup, + object_class=PVLObject ): if grammar is None: @@ -1163,4 +1173,6 @@ def __init__( aggregation_end, end_delimiter, newline, + group_class=group_class, + object_class=object_class ) diff --git a/pvl/grammar.py b/pvl/grammar.py index 7c68521..40f653f 100755 --- a/pvl/grammar.py +++ b/pvl/grammar.py @@ -154,7 +154,10 @@ def char_allowed(self, char): set with some exclusions. """ if len(char) != 1: - raise Exception + raise ValueError( + f"This function only takes single characters and it was given " + f"{len(char)} ('{char}')." + ) o = ord(char) @@ -207,8 +210,7 @@ def char_allowed(self, char): characters than PVL, but appears to allow more control characters to be in quoted strings than PVL does. """ - if len(char) != 1: - raise Exception + super().char_allowed(char) try: char.encode(encoding="ascii") diff --git a/pvl/new.py b/pvl/new.py index 6244eed..4d6ac9b 100755 --- a/pvl/new.py +++ b/pvl/new.py @@ -17,18 +17,33 @@ be the new PVLMultiDict objects. """ -# Copyright 2015, 2017, 2019-2020, ``pvl`` library authors. +# Copyright 2015, 2017, 2019-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 inspect +import io import urllib.request +from pathlib import Path + +try: # noqa: C901 + # In order to access super class attributes for our derived class, we must + # import the native Python version, instead of the default Cython version. + from multidict._multidict_py import MultiDict # noqa: F401 +except ImportError as err: + raise ImportError( + "The multidict library is not present, so the new PVLMultiDict is not " + "available, and pvl.new can't be imported. In order to do so, install " + "the multidict package", + ImportWarning, + ) from err from pvl import * # noqa: F401,F403 from pvl import get_text_from, decode_by_char +from .encoder import PDSLabelEncoder, PVLEncoder from .parser import PVLParser, OmniParser from .collections import PVLModuleNew, PVLGroupNew, PVLObjectNew @@ -132,3 +147,63 @@ def loads(s: str, parser=None, grammar=None, decoder=None, **kwargs): raise TypeError("The parser must be an instance of pvl.PVLParser.") return parser.parse(s) + + +def dump(module, path, **kwargs): + """Serialize *module* as PVL text to the provided *path*. + + :param module: a ``PVLModule`` or ``dict``-like object to serialize. + :param path: an :class:`os.PathLike` + :param ``**kwargs``: the keyword arguments to pass to :func:`dumps()`. + + If *path* is an :class:`os.PathLike`, it will attempt to be opened + and the serialized module will be written into that file via + the :func:`pathlib.Path.write_text()` function, and will return + what that function returns. + + If *path* is not an :class:`os.PathLike`, it will be assumed to be an + already-opened file object, and ``.write()`` will be applied + on that object to write the serialized module, and will return + what that function returns. + """ + try: + p = Path(path) + return p.write_text(dumps(module, **kwargs)) + + except TypeError: + # Not an os.PathLike, maybe it is an already-opened file object + try: + if isinstance(path, io.TextIOBase): + return path.write(dumps(module, **kwargs)) + else: + return path.write(dumps(module, **kwargs).encode()) + except AttributeError: + # Not a path, not an already-opened file. + raise TypeError( + "Expected an os.PathLike or an already-opened " + "file object for writing, but got neither." + ) + + +def dumps(module, encoder=None, grammar=None, decoder=None, **kwargs) -> str: + """Returns a string where the *module* object has been serialized + to PVL syntax. + + :param module: a ``PVLModule`` or ``dict`` like object to serialize. + :param encoder: defaults to :class:`pvl.parser.PDSLabelEncoder()`. + :param grammar: defaults to :class:`pvl.grammar.ODLGrammar()`. + :param decoder: defaults to :class:`pvl.decoder.ODLDecoder()`. + :param ``**kwargs``: the keyword arguments to pass to the encoder + class if *encoder* is none. + """ + if encoder is None: + encoder = PDSLabelEncoder( + grammar=grammar, + decoder=decoder, + group_class=PVLGroupNew, + object_class=PVLObjectNew, + **kwargs) + elif not isinstance(encoder, PVLEncoder): + raise TypeError("The encoder must be an instance of pvl.PVLEncoder.") + + return encoder.encode(module) diff --git a/pvl/pvl_translate.py b/pvl/pvl_translate.py index 9934625..9a786ed 100644 --- a/pvl/pvl_translate.py +++ b/pvl/pvl_translate.py @@ -8,7 +8,7 @@ will raise errors. """ -# Copyright 2020, ``pvl`` library authors. +# 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 @@ -80,14 +80,10 @@ def arg_parser(formats): return parser -def main(): - args = arg_parser(formats).parse_args() +def main(argv=None): + args = arg_parser(formats).parse_args(argv) some_pvl = pvl.load(args.infile) formats[args.output_format].dump(some_pvl, args.outfile) return - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/pvl/pvl_validate.py b/pvl/pvl_validate.py index 0c74c8d..96b9cbb 100644 --- a/pvl/pvl_validate.py +++ b/pvl/pvl_validate.py @@ -20,7 +20,6 @@ import argparse import logging -import sys from collections import OrderedDict import pvl @@ -102,8 +101,8 @@ def arg_parser(): return p -def main(): - args = arg_parser().parse_args() +def main(argv=None): + args = arg_parser().parse_args(argv) logging.basicConfig( format="%(levelname)s: %(message)s", level=(60 - 20 * args.verbose) @@ -252,7 +251,3 @@ def build_line(elements: list, widths: list, sep=" | ") -> str: cells.append("{0:^{width}}".format(e, width=w)) return sep.join(cells) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/pvl/token.py b/pvl/token.py index 21d0271..7478554 100644 --- a/pvl/token.py +++ b/pvl/token.py @@ -54,10 +54,17 @@ def __repr__(self): return f"{self.__class__.__name__}('{self}', " f"'{self.grammar}')" def __index__(self): - return self.decoder.decode_non_decimal(str(self)) + if self.is_decimal(): + try: + return self.decoder.decode_non_decimal(str(self)) + except ValueError: + if int(str(self)) == float(str(self)): + return int(str(self)) + + raise ValueError(f"The {self:r} cannot be used as an index.") def __float__(self): - return self.decoder.decode_decimal(str(self)) + return float(self.decoder.decode_decimal(str(self))) def split(self, sep=None, maxsplit=-1) -> list: """Extends ``str.split()`` that calling split() on a Token @@ -151,9 +158,8 @@ def is_comment(self) -> bool: return False def is_quote(self) -> bool: - """Return true if the Token is a comment character (or - multicharacter comment delimiter) according to the - Token's grammar, false otherwise. + """Return true if the Token is a quote character + according to the Token's grammar, false otherwise. """ if self in self.grammar.quotes: return True diff --git a/setup.cfg b/setup.cfg index 5bae844..7f4531d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.2.0 +current_version = 1.2.1 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? diff --git a/setup.py b/setup.py index db7249d..1263ec5 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name='pvl', - version='1.2.0', + version='1.2.1', description=( 'Python implementation for PVL (Parameter Value Language) ' 'parsing and encoding.' diff --git a/tests/test_collections.py b/tests/test_collections.py index dc916fd..61b5126 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -6,12 +6,18 @@ # Reuse is permitted under the terms of the license. # The AUTHORS file and the LICENSE file are at the # top level of this library. - +from abc import ABC from collections import abc import unittest import pvl -from pvl.collections import OrderedMultiDict +from pvl.collections import ( + KeysView, + MappingView, + MutableMappingSequence, + OrderedMultiDict, + ValuesView +) class DictLike(abc.Mapping): @@ -27,6 +33,54 @@ def __iter__(self): def __len__(self): return len(self.list) + def __delitem__(self, key): + pass + + def __setitem__(self, key, value): + pass + + def insert(self): + pass + + +class TestClasses(unittest.TestCase): + def test_MutableMappingSequence(self): + class Concrete(DictLike, MutableMappingSequence, ABC): + def append(self, key, value): + super().append(key, value) + + def getall(self, key): + super().getall(key) + + def popall(self, key): + super().popall(key) + + mms = Concrete() + mms.append("key", "value") + mms.getall("key") + mms.popall("key") + + def test_MappingView(self): + m = MappingView([("a", 1), ("b", 2)]) + self.assertEqual( + "MappingView([('a', 1), ('b', 2)])", + repr(m) + ) + + def test_KeysView(self): + k = KeysView([("a", 1), ("b", 2)]) + self.assertEqual( + "KeysView(['a', 'b'])", + repr(k) + ) + + def test_ValuesView(self): + v = ValuesView([("a", 1), ("b", 2)]) + self.assertEqual( + "ValuesView([1, 2])", + repr(v) + ) + class TestMultiDicts(unittest.TestCase): def setUp(self): @@ -493,6 +547,17 @@ def test_insert_before_after_raises(self): TypeError, module.insert_after, "a", [("fo", "ba"), 2] ) + def test_repr(self): + module = OrderedMultiDict([("a", 1), ("b", 2), ("a", 3)]) + self.assertEqual( + """OrderedMultiDict([ + ('a', 1) + ('b', 2) + ('a', 3) +])""", + repr(module) + ) + class TestDifferences(unittest.TestCase): def test_as_list(self): @@ -764,3 +829,55 @@ def test_equality(self): self.assertEqual(newmod, newobj) except ImportError: pass + + +class TestMultiDict(unittest.TestCase): + + def test_repr(self): + try: + from pvl.collections import PVLMultiDict + the_list = [("a", 1), ("b", 2)] + m = PVLMultiDict(the_list) + self.assertEqual( + "PVLMultiDict([('a', 1), ('b', 2)])", + repr(m) + ) + + except ImportError: + pass + + def test_str(self): + try: + from pvl.collections import PVLMultiDict + the_list = [("a", 1), ("b", 2)] + m = PVLMultiDict(the_list) + self.assertEqual( + """PVLMultiDict([ + ('a', 1) + ('b', 2) +])""", + str(m) + ) + + z = PVLMultiDict() + self.assertEqual( + "PVLMultiDict()", + str(z) + ) + + except ImportError: + pass + + def test_insert(self): + try: + from pvl.collections import PVLMultiDict + the_list = [("a", 1), ("b", 2)] + m = PVLMultiDict(the_list) + m.insert_after("a", {"z": 10, "y": 9}) + self.assertEqual( + PVLMultiDict([("a", 1), ("z", 10), ("y", 9), ("b", 2)]), + m + ) + + except ImportError: + pass \ No newline at end of file diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..f6ec349 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +"""This module has tests for the pvl.exceptions functions.""" + +# Copyright 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 unittest + +import pvl.exceptions + + +class TestMock(unittest.TestCase): + + def test_LexerError(self): + e = pvl.exceptions.LexerError("lex error", "This is the document.", 2, "Th") + self.assertEqual( + ( + pvl.exceptions.LexerError, + ("lex error", "This is the document.", 1, "Th") + ), + e.__reduce__() + ) diff --git a/tests/test_grammar.py b/tests/test_grammar.py index e9cc98c..c0c40d8 100644 --- a/tests/test_grammar.py +++ b/tests/test_grammar.py @@ -18,7 +18,7 @@ import re import unittest -from pvl.grammar import PVLGrammar +from pvl.grammar import PVLGrammar, ODLGrammar class TestLeapSeconds(unittest.TestCase): @@ -124,11 +124,20 @@ def test_leap_second_Yj_re(self): with self.subTest(string=s): self.assertIsNotNone(self.g.leap_second_Yj_re.fullmatch(s)) + +class TestAllowed(unittest.TestCase): + def test_allowed(self): + g = PVLGrammar() for c in ("a", "b", " ", "\n"): with self.subTest(char=c): - self.assertTrue(self.g.char_allowed(c)) + self.assertTrue(g.char_allowed(c)) for c in ("\b", chr(127)): with self.subTest(char=c): - self.assertFalse(self.g.char_allowed(c)) + self.assertFalse(g.char_allowed(c)) + + self.assertRaises(ValueError, g.char_allowed, "too long") + + odlg = ODLGrammar() + self.assertFalse(odlg.char_allowed("😆")) diff --git a/tests/test_lexer.py b/tests/test_lexer.py index f79559d..8312f41 100644 --- a/tests/test_lexer.py +++ b/tests/test_lexer.py @@ -332,6 +332,22 @@ def test_lex_char(self): ), ) + self.assertRaises( + ValueError, + Lexer.lex_char, + "a", + "b", + "c", + "", + dict(state="bogus preserve state", end="end"), + g, + dict( + chars={"k", "v", "/", "*"}, + single_comments={"k": "v"}, + multi_chars={"/", "*"}, + ), + ) + def test_lexer_recurse(self): def foo(tokens): two = list() diff --git a/tests/test_new.py b/tests/test_new.py new file mode 100644 index 0000000..99d20d6 --- /dev/null +++ b/tests/test_new.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python +"""This module has unit tests for the pvl __init__ functions.""" + +# Copyright 2019, Ross A. Beyer (rbeyer@seti.org) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import call, create_autospec, mock_open, patch + +from pathlib import Path + +import pvl +import pvl.new as pvln +from pvl.collections import PVLModuleNew as PVLModule +from pvl.collections import PVLGroupNew as PVLGroup +from pvl.collections import PVLObjectNew as PVLObject + +data_dir = Path("tests/data") + + +class TestLoadS(unittest.TestCase): + def test_loads(self): + some_pvl = """ +a = b +GROUP = c + c = d +END_GROUP +e =false +END""" + decoded = PVLModule(a="b", c=PVLGroup(c="d"), e=False) + self.assertEqual(decoded, pvln.loads(some_pvl)) + + self.assertEqual(PVLModule(a="b"), pvln.loads("a=b")) + + +class TestLoad(unittest.TestCase): + def setUp(self): + self.simple = data_dir / "pds3" / "simple_image_1.lbl" + rawurl = "https://raw.githubusercontent.com/planetarypy/pvl/main/" + self.url = rawurl + str(self.simple) + self.simplePVL = PVLModule( + { + "PDS_VERSION_ID": "PDS3", + "RECORD_TYPE": "FIXED_LENGTH", + "RECORD_BYTES": 824, + "LABEL_RECORDS": 1, + "FILE_RECORDS": 601, + "^IMAGE": 2, + "IMAGE": PVLObject( + { + "LINES": 600, + "LINE_SAMPLES": 824, + "SAMPLE_TYPE": "MSB_INTEGER", + "SAMPLE_BITS": 8, + "MEAN": 51.67785396440129, + "MEDIAN": 50.0, + "MINIMUM": 0, + "MAXIMUM": 255, + "STANDARD_DEVIATION": 16.97019, + "CHECKSUM": 25549531, + } + ), + } + ) + + def test_load_w_open(self): + with open(self.simple) as f: + self.assertEqual(self.simplePVL, pvln.load(f)) + + def test_load_w_Path(self): + self.assertEqual(self.simplePVL, pvln.load(self.simple)) + + def test_load_w_string_path(self): + string_path = str(self.simple) + self.assertEqual(self.simplePVL, pvln.load(string_path)) + + def test_loadu(self): + self.assertEqual(self.simplePVL, pvln.loadu(self.url)) + self.assertEqual( + self.simplePVL, pvln.loadu(self.simple.resolve().as_uri()) + ) + + @patch("pvl.new.loads") + @patch("pvl.new.decode_by_char") + def test_loadu_args(self, m_decode, m_loads): + pvln.loadu(self.url, data=None) + pvln.loadu(self.url, noturlopen="should be passed to loads") + m_decode.assert_called() + self.assertNotIn("data", m_loads.call_args_list[0][1]) + self.assertIn("noturlopen", m_loads.call_args_list[1][1]) + + def test_load_w_quantity(self): + try: + from astropy import units as u + from pvl.decoder import OmniDecoder + + pvl_file = "tests/data/pds3/units1.lbl" + km_upper = u.def_unit("KM", u.km) + m_upper = u.def_unit("M", u.m) + u.add_enabled_units([km_upper, m_upper]) + label = pvln.load( + pvl_file, decoder=OmniDecoder(quantity_cls=u.Quantity) + ) + self.assertEqual(label["FLOAT_UNIT"], u.Quantity(0.414, "KM")) + except ImportError: + pass + + +class TestISIScub(unittest.TestCase): + def setUp(self): + self.cub = data_dir / "pattern.cub" + self.cubpvl = PVLModule( + IsisCube=PVLObject( + Core=PVLObject( + StartByte=65537, + Format="Tile", + TileSamples=128, + TileLines=128, + Dimensions=PVLGroup(Samples=90, Lines=90, Bands=1), + Pixels=PVLGroup( + Type="Real", ByteOrder="Lsb", Base=0.0, Multiplier=1.0 + ), + ) + ), + Label=PVLObject(Bytes=65536), + ) + + def test_load_cub(self): + self.assertEqual(self.cubpvl, pvln.load(self.cub)) + + def test_load_cub_opened(self): + with open(self.cub, "rb") as f: + self.assertEqual(self.cubpvl, pvln.load(f)) + + +class TestDumpS(unittest.TestCase): + def setUp(self): + self.module = PVLModule( + a="b", + staygroup=PVLGroup(c="d"), + obj=PVLGroup(d="e", f=PVLGroup(g="h")), + ) + + def test_dumps_PDS(self): + s = """A = b\r +GROUP = staygroup\r + C = d\r +END_GROUP = staygroup\r +OBJECT = obj\r + D = e\r + GROUP = f\r + G = h\r + END_GROUP = f\r +END_OBJECT = obj\r +END\r\n""" + self.assertEqual(s, pvln.dumps(self.module)) + + def test_dumps_PVL(self): + s = """a = b; +BEGIN_GROUP = staygroup; + c = d; +END_GROUP = staygroup; +BEGIN_GROUP = obj; + d = e; + BEGIN_GROUP = f; + g = h; + END_GROUP = f; +END_GROUP = obj; +END;""" + + self.assertEqual( + s, pvln.dumps( + self.module, + encoder=pvl.encoder.PVLEncoder( + group_class=PVLGroup, object_class=PVLObject + ) + ) + ) + + def test_dumps_ODL(self): + + s = """A = b\r +GROUP = staygroup\r + C = d\r +END_GROUP = staygroup\r +GROUP = obj\r + D = e\r + GROUP = f\r + G = h\r + END_GROUP = f\r +END_GROUP = obj\r +END\r\n""" + + self.assertEqual( + s, pvln.dumps(self.module, encoder=pvl.encoder.ODLEncoder( + group_class=PVLGroup, object_class=PVLObject + )) + ) + + +class TestDump(unittest.TestCase): + def setUp(self): + self.module = PVLModule( + a="b", + staygroup=PVLGroup(c="d"), + obj=PVLGroup(d="e", f=PVLGroup(g="h")), + ) + self.string = """A = b\r +GROUP = staygroup\r + C = d\r +END_GROUP = staygroup\r +OBJECT = obj\r + D = e\r + GROUP = f\r + G = h\r + END_GROUP = f\r +END_OBJECT = obj\r +END\r\n""" + + def test_dump_Path(self): + mock_path = create_autospec(Path) + with patch("pvl.new.Path", autospec=True, return_value=mock_path): + pvln.dump(self.module, Path("dummy")) + self.assertEqual( + [call.write_text(self.string)], mock_path.method_calls + ) + + @patch("builtins.open", mock_open()) + def test_dump_file_object(self): + with open("dummy", "w") as f: + pvln.dump(self.module, f) + self.assertEqual( + [call.write(self.string.encode())], f.method_calls + ) + + def test_not_dumpable(self): + f = 5 + self.assertRaises(TypeError, pvln.dump, self.module, f) diff --git a/tests/test_pvl_translate.py b/tests/test_pvl_translate.py new file mode 100644 index 0000000..a450341 --- /dev/null +++ b/tests/test_pvl_translate.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +"""This module has tests for the pvl_translate functions.""" + +# Copyright 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 unittest +from unittest.mock import patch, PropertyMock, MagicMock + +import pvl.pvl_translate as pvl_trans +from pvl.encoder import PDSLabelEncoder + + +class TestMock(unittest.TestCase): + def test_Writer(self): + w = pvl_trans.Writer() + self.assertRaises(Exception, w.dump, dict(a="b"), "dummy.txt") + + @patch("pvl.dump") + def test_PVLWriter(self, m_dump): + e = PDSLabelEncoder() + w = pvl_trans.PVLWriter(e) + d = dict(a="b") + f = "dummy.pathlike" + w.dump(d, f) + m_dump.assert_called_once_with(d, f, encoder=e) + + @patch("json.dump") + def test_JSONWriter(self, m_dump): + w = pvl_trans.JSONWriter() + d = dict(a="b") + f = "dummy.pathlike" + w.dump(d, f) + m_dump.assert_called_once_with(d, f) + + def test_arg_parser(self): + p = pvl_trans.arg_parser(pvl_trans.formats) + self.assertIsInstance(p, argparse.ArgumentParser) + + @patch("pvl.pvl_translate.JSONWriter") + @patch("pvl.pvl_translate.PVLWriter") + @patch("pvl.pvl_translate.arg_parser") + @patch("pvl.load") + @patch("pvl.dump") + def test_main(self, m_dump, m_load, m_parser, m_PVLWriter, m_JSONWriter): + m_parser().parse_args().output_format = "PDS3" + self.assertIsNone(pvl_trans.main()) + diff --git a/tests/test_pvl_validate.py b/tests/test_pvl_validate.py new file mode 100644 index 0000000..598ec70 --- /dev/null +++ b/tests/test_pvl_validate.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +"""This module has tests for the pvl_validate functions.""" + +# Copyright 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 unittest +from unittest.mock import patch + +import pvl.pvl_validate as pvl_val + + +class TestMock(unittest.TestCase): + def setUp(self): + self.flavors = ["chocolate", "vanilla"] + self.report1 = ( + "choc.txt", {"chocolate": (True, True), "vanilla": (True, False)} + ) + self.report2 = ( + "van.txt", {"chocolate": (False, False), "vanilla": (True, True)} + ) + + def test_arg_parser(self): + p = pvl_val.arg_parser() + self.assertIsInstance(p, argparse.ArgumentParser) + + @patch("pvl.get_text_from", return_value="a=b") + def test_main(self, m_get): + self.assertIsNone(pvl_val.main("dummy.txt")) + + self.assertIsNone(pvl_val.main(["-v", "dummy.txt"])) + + def test_pvl_flavor(self): + dialect = "PDS3" + loads, encodes = pvl_val.pvl_flavor( + "a = b", dialect, pvl_val.dialects[dialect], "dummy.txt" + ) + self.assertEqual(True, loads) + self.assertEqual(True, encodes) + + loads, encodes = pvl_val.pvl_flavor( + "foo", dialect, pvl_val.dialects[dialect], "dummy.txt" + ) + self.assertEqual(False, loads) + self.assertEqual(None, encodes) + + loads, encodes = pvl_val.pvl_flavor( + "set_with_float = {1.5}", + dialect, + pvl_val.dialects[dialect], + "dummy.txt" + ) + self.assertEqual(True, loads) + self.assertEqual(False, encodes) + + with patch("pvl.pvl_validate.pvl.loads", side_effect=Exception("bogus")): + loads, encodes = pvl_val.pvl_flavor( + "a=b", dialect, pvl_val.dialects[dialect], "dummy.txt" + ) + loads, encodes = pvl_val.pvl_flavor( + "a=b", + dialect, + pvl_val.dialects[dialect], + "dummy.txt", + verbose=2 + ) + self.assertEqual(False, loads) + self.assertEqual(None, encodes) + + def test_report(self): + self.assertRaises( + IndexError, + pvl_val.report, + [["report", ], ], + self.flavors + ) + + with patch("pvl.pvl_validate.report_many", return_value="many"): + self.assertEqual( + "many", + pvl_val.report([self.report1, self.report2], self.flavors) + ) + + self.assertEqual( + """\ +chocolate | Loads | Encodes +vanilla | Loads | does NOT encode""", + pvl_val.report([self.report1, ], self.flavors) + ) + + def test_report_many(self): + self.assertEqual( + """\ +---------+-----------+---------- +File | chocolate | vanilla +---------+-----------+---------- +choc.txt | L E | L No E +van.txt | No L No E | L E """, + pvl_val.report_many( + [self.report1, self.report2], self.flavors + ) + ) + + def test_build_line(self): + self.assertEqual( + "a | b ", + pvl_val.build_line(['a', 'b'], [3, 4]) + ) diff --git a/tests/test_token.py b/tests/test_token.py index ea5bb3e..02a771d 100644 --- a/tests/test_token.py +++ b/tests/test_token.py @@ -18,6 +18,7 @@ import unittest from pvl.grammar import PVLGrammar +from pvl.decoder import PVLDecoder from pvl.token import Token @@ -26,6 +27,11 @@ def test_init(self): s = "token" self.assertEqual(s, Token(s)) self.assertEqual(s, Token(s, grammar=PVLGrammar())) + self.assertEqual(s, Token(s, decoder=PVLDecoder())) + self.assertRaises(TypeError, Token, s, grammar="not a grammar") + self.assertRaises( + TypeError, Token, s, grammar=PVLGrammar(), decoder="not a decoder" + ) def test_is_comment(self): c = Token("/* comment */") @@ -178,6 +184,7 @@ def test_is_space(self): with self.subTest(string=s): t = Token(s) self.assertTrue(t.is_space()) + self.assertTrue(t.isspace()) for s in ("not space", ""): with self.subTest(string=s): @@ -185,7 +192,7 @@ def test_is_space(self): self.assertFalse(t.is_space()) def test_is_WSC(self): - for s in (" /*com*/ ", "/*c1*/\n/*c2*/"): + for s in (" /*com*/ ", "/*c1*/\n/*c2*/", " "): with self.subTest(string=s): t = Token(s) self.assertTrue(t.is_WSC()) @@ -202,13 +209,28 @@ def test_is_delimiter(self): t = Token("not") self.assertFalse(t.is_delimiter()) + def test_is_quote(self): + for s in ('"', "'"): + with self.subTest(string=s): + t = Token(s) + self.assertTrue(t.is_quote()) + + t = Token("not a quote mark") + self.assertFalse(t.is_quote()) + def test_is_unquoted_string(self): for s in ("Hello", "Product", "Group"): with self.subTest(string=s): t = Token(s) self.assertTrue(t.is_unquoted_string()) - for s in ("/*comment*/", "2001-027", '"quoted"'): + for s in ( + "/*comment*/", + "second line of comment*/", + "2001-027", + '"quoted"', + "\t" + ): with self.subTest(string=s): t = Token(s) self.assertFalse(t.is_unquoted_string()) @@ -291,3 +313,26 @@ def test_split(self): for x in t_list: with self.subTest(token=x): self.assertIsInstance(x, Token) + + def test_index(self): + s = "3" + t = Token(s) + self.assertEqual(3, int(t)) + self.assertEqual(3, t.__index__()) + self.assertRaises(ValueError, Token("3.4").__index__) + self.assertRaises(ValueError, Token("a").__index__) + + def test_float(self): + s = "3.14" + t = Token(s) + self.assertEqual(3.14, float(t)) + + def test_lstrip(self): + s = " leftward space " + t = Token(s) + self.assertEqual("leftward space ", t.lstrip()) + + def test_rstrip(self): + s = " rightward space " + t = Token(s) + self.assertEqual(" rightward space", t.rstrip()) \ No newline at end of file diff --git a/tox.ini b/tox.ini index 4f1b5a5..bade049 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ commands = pytest deps = pytest + multidict [testenv:py{36, 37, 38}-allopts] deps =