From ad92cfe0d1bd4f43c253175950ebe18c9acc7d8f Mon Sep 17 00:00:00 2001 From: Daniel Cellucci Date: Tue, 18 Oct 2022 00:40:56 +0000 Subject: [PATCH] first commit --- .gitignore | 3 + README.md | 0 hirise_blender/__init__.py | 26 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 225 bytes .../hirise_blender.cpython-310.pyc | Bin 0 -> 844 bytes hirise_blender/hirise_blender.py | 166 +++ hirise_blender/planetaryimage/__init__.py | 17 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 410 bytes .../__pycache__/cubefile.cpython-310.pyc | Bin 0 -> 7633 bytes .../__pycache__/decoders.cpython-310.pyc | Bin 0 -> 1906 bytes .../__pycache__/image.cpython-310.pyc | Bin 0 -> 6325 bytes .../__pycache__/pds3image.cpython-310.pyc | Bin 0 -> 9110 bytes .../__pycache__/specialpixels.cpython-310.pyc | Bin 0 -> 1505 bytes hirise_blender/planetaryimage/cubefile.py | 255 ++++ hirise_blender/planetaryimage/decoders.py | 49 + hirise_blender/planetaryimage/image.py | 217 +++ hirise_blender/planetaryimage/pds3image.py | 349 +++++ .../planetaryimage/specialpixels.py | 84 ++ hirise_blender/pvl/__init__.py | 268 ++++ .../pvl/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 7658 bytes .../__pycache__/collections.cpython-310.pyc | Bin 0 -> 24724 bytes .../pvl/__pycache__/decoder.cpython-310.pyc | Bin 0 -> 16552 bytes .../pvl/__pycache__/encoder.cpython-310.pyc | Bin 0 -> 34585 bytes .../__pycache__/exceptions.cpython-310.pyc | Bin 0 -> 2972 bytes .../pvl/__pycache__/grammar.cpython-310.pyc | Bin 0 -> 8156 bytes .../pvl/__pycache__/lexer.cpython-310.pyc | Bin 0 -> 9466 bytes .../pvl/__pycache__/parser.cpython-310.pyc | Bin 0 -> 26379 bytes .../pvl/__pycache__/token.cpython-310.pyc | Bin 0 -> 10638 bytes hirise_blender/pvl/collections.py | 704 ++++++++++ hirise_blender/pvl/decoder.py | 552 ++++++++ hirise_blender/pvl/encoder.py | 1178 +++++++++++++++++ hirise_blender/pvl/exceptions.py | 79 ++ hirise_blender/pvl/grammar.py | 345 +++++ hirise_blender/pvl/lexer.py | 435 ++++++ hirise_blender/pvl/new.py | 209 +++ hirise_blender/pvl/parser.py | 955 +++++++++++++ hirise_blender/pvl/pvl_translate.py | 89 ++ hirise_blender/pvl/pvl_validate.py | 253 ++++ hirise_blender/pvl/token.py | 335 +++++ hirise_blender/six/__init__.py | 3 + .../six/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 251 bytes .../six/__pycache__/six.cpython-310.pyc | Bin 0 -> 27584 bytes hirise_blender/six/six.py | 998 ++++++++++++++ poetry.lock | 180 +++ pyproject.toml | 17 + 45 files changed, 7766 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 hirise_blender/__init__.py create mode 100644 hirise_blender/__pycache__/__init__.cpython-310.pyc create mode 100644 hirise_blender/__pycache__/hirise_blender.cpython-310.pyc create mode 100644 hirise_blender/hirise_blender.py create mode 100755 hirise_blender/planetaryimage/__init__.py create mode 100644 hirise_blender/planetaryimage/__pycache__/__init__.cpython-310.pyc create mode 100644 hirise_blender/planetaryimage/__pycache__/cubefile.cpython-310.pyc create mode 100644 hirise_blender/planetaryimage/__pycache__/decoders.cpython-310.pyc create mode 100644 hirise_blender/planetaryimage/__pycache__/image.cpython-310.pyc create mode 100644 hirise_blender/planetaryimage/__pycache__/pds3image.cpython-310.pyc create mode 100644 hirise_blender/planetaryimage/__pycache__/specialpixels.cpython-310.pyc create mode 100644 hirise_blender/planetaryimage/cubefile.py create mode 100644 hirise_blender/planetaryimage/decoders.py create mode 100644 hirise_blender/planetaryimage/image.py create mode 100644 hirise_blender/planetaryimage/pds3image.py create mode 100644 hirise_blender/planetaryimage/specialpixels.py create mode 100644 hirise_blender/pvl/__init__.py create mode 100644 hirise_blender/pvl/__pycache__/__init__.cpython-310.pyc create mode 100644 hirise_blender/pvl/__pycache__/collections.cpython-310.pyc create mode 100644 hirise_blender/pvl/__pycache__/decoder.cpython-310.pyc create mode 100644 hirise_blender/pvl/__pycache__/encoder.cpython-310.pyc create mode 100644 hirise_blender/pvl/__pycache__/exceptions.cpython-310.pyc create mode 100644 hirise_blender/pvl/__pycache__/grammar.cpython-310.pyc create mode 100644 hirise_blender/pvl/__pycache__/lexer.cpython-310.pyc create mode 100644 hirise_blender/pvl/__pycache__/parser.cpython-310.pyc create mode 100644 hirise_blender/pvl/__pycache__/token.cpython-310.pyc create mode 100644 hirise_blender/pvl/collections.py create mode 100644 hirise_blender/pvl/decoder.py create mode 100644 hirise_blender/pvl/encoder.py create mode 100644 hirise_blender/pvl/exceptions.py create mode 100755 hirise_blender/pvl/grammar.py create mode 100644 hirise_blender/pvl/lexer.py create mode 100755 hirise_blender/pvl/new.py create mode 100644 hirise_blender/pvl/parser.py create mode 100644 hirise_blender/pvl/pvl_translate.py create mode 100644 hirise_blender/pvl/pvl_validate.py create mode 100644 hirise_blender/pvl/token.py create mode 100644 hirise_blender/six/__init__.py create mode 100644 hirise_blender/six/__pycache__/__init__.cpython-310.pyc create mode 100644 hirise_blender/six/__pycache__/six.cpython-310.pyc create mode 100644 hirise_blender/six/six.py create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..666bb8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.pytest_cache/ +tests/ +.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/hirise_blender/__init__.py b/hirise_blender/__init__.py new file mode 100644 index 0000000..6995358 --- /dev/null +++ b/hirise_blender/__init__.py @@ -0,0 +1,26 @@ +import bpy + +bl_info = { + "name": "Import HIRISE Image", + "blender": (3, 30, 0), + "category": "Object", +} + +if 'bpy' in locals(): + print('HIRISE Reloading') + from importlib import reload + import sys + for k, v in list(sys.modules.items()): + if k.startswith('hirise_blender.'): + reload(v) + +from .hirise_blender import ImportIMGData, menu_func_import + +# Register and add to the "file selector" menu (required to use F3 search "Text Import Operator" for quick access). +def register(): + bpy.utils.register_class(ImportIMGData) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + +def unregister(): + bpy.utils.unregister_class(ImportIMGData) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) \ No newline at end of file diff --git a/hirise_blender/__pycache__/__init__.cpython-310.pyc b/hirise_blender/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da5ea62a08fae9c48463b3fed49dc6c364de2824 GIT binary patch literal 225 zcmd1j<>g`kf}_uUlbwL{V-N=!FabFZKwK;aBvKes7;_kM8KW2(8B&*Y^$}CQePs&NnOGzyPi{E08k5A0WiH~2& zP{ayU2PS^m>SyHVrs}68r{?68CMRd=r)QSvqv}TI#>Z#oWtPOp>lIYq;;_lhPbtkw SwF5b&m<>qqFfcK3{09I*zc~^B literal 0 HcmV?d00001 diff --git a/hirise_blender/__pycache__/hirise_blender.cpython-310.pyc b/hirise_blender/__pycache__/hirise_blender.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbceabe07407ce5166e55d97ba66e39d61260a3b GIT binary patch literal 844 zcmaJ<&u`N(6t*2Fal5V{Z45ZUH5Yo|iV$K*XopouDyuch9LCSA2tPY=ox799E?B$ z7pTN3#z+tm6eJBgPg5$W2tS}S6p@I*M=};{M$7{lT_Ly~Ogc?^*BBZl*!5n$m^?lx z`H8%S1RCca>9_PC2toHjoA+QWI>k%U;uZEBK9c)rK`dY|X-ii43i#M?8TtAmXoJ&u z$yR7VyQl|IX#2}8e-gnAi7vki7Ev25|5hVGXF(ga;XTwwP>tJIP{3g=o1i2dU;kXM z*O!BXvaXC(`sCreh5G(wy*2^xO9qZqjiWWUQ^$^YBQ-Cj3u~jabP<+7@^qQfkF=YkN9bH3m!mVVXc0R z0>QA!*wNRru?aQ|Y8~GV<6#^vxzavre4ha}cYT&+@LHv_EMe)eF1VJKn~QE@H`5RG zysR(G5H1_fxuf>yTR-3|+ko!?b+DogRdBAh77sbZX|k@B{TqpH!6 zeKj^-?sK0K<*UGq^MD6fhiei?huO+x1=N{qSMB){_F!xYt~#7bThy7X7v)dCY!hko zgMX1+C6`^aTCV30dA5ZvylXaauMK?Qq9mrS-wjjmp5w5a2$41Q&N?BwNC?O?ZLmBN zVvm!wEzkt`#^5avCBj}1Hc)s}s%ozLF$3d_JZ-n?tH37}H3p)2*)4op!nc yu`y66aR2S^X9i>{gf4U~L{he0c#~Vz7`W}%u4X76^QcD|W7PX%<6e*b6Z#D?Ja5kc literal 0 HcmV?d00001 diff --git a/hirise_blender/planetaryimage/__pycache__/cubefile.cpython-310.pyc b/hirise_blender/planetaryimage/__pycache__/cubefile.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c327a57e033bcf2e164dfb755725c789762daf07 GIT binary patch literal 7633 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 W`C2TZ8>FsM#$~AZkUFML=>G*8|M!3Z literal 0 HcmV?d00001 diff --git a/hirise_blender/planetaryimage/__pycache__/decoders.cpython-310.pyc b/hirise_blender/planetaryimage/__pycache__/decoders.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b47af1fedaed064ee6f61c13d33f7c0c3b3e6fe GIT binary patch literal 1906 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 literal 0 HcmV?d00001 diff --git a/hirise_blender/planetaryimage/__pycache__/pds3image.cpython-310.pyc b/hirise_blender/planetaryimage/__pycache__/pds3image.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00b0d26069e0bb1d884e9fe787616e06f70d430d GIT binary patch literal 9110 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 literal 0 HcmV?d00001 diff --git a/hirise_blender/planetaryimage/__pycache__/specialpixels.cpython-310.pyc b/hirise_blender/planetaryimage/__pycache__/specialpixels.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0ddf2fd9b63ac11ee42a9bec843b7fe521053ff GIT binary patch literal 1505 zcma)6OK%iM5bl}Tm)F>U4GC|WHyeoek^@2!N^p>3$+ny<2(*_~v(>xX&P>m$d&bxi zk|V*D`~nWiB~tzizhF3W$(|zRh95gr&CbdzO9Z+#U)5Jz)zw{9+Hhv3%5a&tEA8Ja zjJ+c=xn_}khoAii0a%lP1Fr8h9q_tN(}e;QU$UkLCA8BnoQc4m0%i8tl)hLSjgO0W8c3!I)V3u z@c2Q{CR@M=zVtNe!`4y2}M`he1D%M5IEUXFnAGk?bEoU`1?>D=IeszoG*iOtzZL zHr5_2E`7h;T3dSZV7bwD$(%3J3-~SKXXg+iw(dM<&v9U1x!|5=*m7^R<|IXkw))aw z?^(@FiaHMZTS>KJ!T_s_Z$+brM&qHw-^`Rz?EF;kg@LR?Tl#+7ZmW7%MfILC%E}h@ zQiC*yY2O!GM#5~VKy+o@j%F&G5W9dGr&@vdMYb^0JZ7+T{W-@>p%~EB1}VW1p;k^j z9KNJLBh~bivjqz!_t3KTG>> from planetaryimage import CubeFile + >>> image = CubeFile.open('tests/data/pattern.cub') + >>> # Examples of CubeFile Attributes + >>> image.base + 0.0 + >>> image.multiplier + 1.0 + >>> image.specials['His'] + -3.4028233e+38 + >>> image.tile_lines + 128 + >>> image.tile_samples + 128 + >>> image.tile_shape + (128, 128) + + """ + + PIXEL_TYPES = { + 'UnsignedByte': numpy.dtype('uint8'), + 'SignedByte': numpy.dtype('int8'), + 'UnsignedWord': numpy.dtype('uint16'), + 'SignedWord': numpy.dtype('int16'), + 'UnsignedInteger': numpy.dtype('uint32'), + 'SignedInteger': numpy.dtype('int32'), + 'Real': numpy.dtype('float32'), + 'Double': numpy.dtype('float64') + } + + BYTE_ORDERS = { + 'NoByteOrder': '=', # system + 'Lsb': '<', # little-endian + 'Msb': '>' # big-endian + } + + SPECIAL_PIXELS = SPECIAL_PIXELS + + def _save(self, file_to_write, overwrite): + raise NotImplementedError + + def _create_label(self, array): + raise NotImplementedError + + @property + def _bands(self): + return self.label['IsisCube']['Core']['Dimensions']['Bands'] + + @property + def _lines(self): + return self.label['IsisCube']['Core']['Dimensions']['Lines'] + + @property + def _samples(self): + return self.label['IsisCube']['Core']['Dimensions']['Samples'] + + @property + def _format(self): + return self.label['IsisCube']['Core']['Format'] + + @property + def _start_byte(self): + return self.label['IsisCube']['Core']['StartByte'] - 1 + + @property + def _dtype(self): + return self._pixel_type.newbyteorder(self._byte_order) + + @property + def base(self): + """An additive factor by which to offset pixel DN.""" + return self.label['IsisCube']['Core']['Pixels']['Base'] + + @property + def multiplier(self): + """A multiplicative factor by which to scale pixel DN.""" + return self.label['IsisCube']['Core']['Pixels']['Multiplier'] + + @property + def tile_lines(self): + """Number of lines per tile.""" + if self.format != 'Tile': + return None + return self.label['IsisCube']['Core']['TileLines'] + + @property + def tile_samples(self): + """Number of samples per tile.""" + if self.format != 'Tile': + return None + return self.label['IsisCube']['Core']['TileSamples'] + + @property + def tile_shape(self): + """Shape of tiles.""" + if self.format != 'Tile': + return None + return (self.tile_lines, self.tile_samples) + + @property + def _byte_order(self): + return self.BYTE_ORDERS[self._pixels_group['ByteOrder']] + + @property + def _pixels_group(self): + return self.label['IsisCube']['Core']['Pixels'] + + @property + def _pixel_type(self): + return self.PIXEL_TYPES[self._pixels_group['Type']] + + @property + def specials(self): + """Return the special pixel values""" + pixel_type = self._pixels_group['Type'] + return self.SPECIAL_PIXELS[pixel_type] + + @property + def data_filename(self): + """Return detached filename else None.""" + return self.label['IsisCube']['Core'].get('^Core') + + def apply_scaling(self, copy=True): + """Scale pixel values to there true DN. + + Parameters + ---------- + copy: bool [True] + Whether to apply the scaling to a copy of the pixel data + and leave the original unaffected + + Returns + ------- + Numpy Array + A scaled version of the pixel data + + """ + if copy: + return self.multiplier * self.data + self.base + + if self.multiplier != 1: + self.data *= self.multiplier + + if self.base != 0: + self.data += self.base + + return self.data + + def apply_numpy_specials(self, copy=True): + """Convert isis special pixel values to numpy special pixel values. + + ======= ======= + Isis Numpy + ======= ======= + Null nan + Lrs -inf + Lis -inf + His inf + Hrs inf + ======= ======= + + Parameters + ---------- + copy : bool [True] + Whether to apply the new special values to a copy of the + pixel data and leave the original unaffected + + Returns + ------- + Numpy Array + A numpy array with special values converted to numpy's nan, inf, + and -inf + """ + if copy: + data = self.data.astype(numpy.float64) + + elif self.data.dtype != numpy.float64: + data = self.data = self.data.astype(numpy.float64) + + else: + data = self.data + + data[data == self.specials['Null']] = numpy.nan + data[data < self.specials['Min']] = numpy.NINF + data[data > self.specials['Max']] = numpy.inf + + return data + + def specials_mask(self): + """Create a pixel map for special pixels. + + Returns + ------- + An array where the value is `False` if the pixel is special + and `True` otherwise + """ + mask = self.data >= self.specials['Min'] + mask &= self.data <= self.specials['Max'] + return mask + + def get_image_array(self): + """Create an array for use in making an image. + + Creates a linear stretch of the image and scales it to between `0` and + `255`. `Null`, `Lis` and `Lrs` pixels are set to `0`. `His` and `Hrs` + pixels are set to `255`. + + Usage:: + + from planetaryimage import CubeFile + from PIL import Image + + # Read in the image and create the image data + image = CubeFile.open('test.cub') + data = image.get_image_array() + + # Save the first band to a new file + Image.fromarray(data[0]).save('test.png') + + Returns + ------- + A uint8 array of pixel values. + """ + specials_mask = self.specials_mask() + data = self.data.copy() + + data[specials_mask] -= data[specials_mask].min() + data[specials_mask] *= 255 / data[specials_mask].max() + + data[data == self.specials['His']] = 255 + data[data == self.specials['Hrs']] = 255 + + return data.astype(numpy.uint8) + + @property + def _decoder(self): + if self.format == 'BandSequential': + return BandSequentialDecoder(self.dtype, self.shape) + + if self.format == 'Tile': + return TileDecoder(self.dtype, self.shape, self.tile_shape) + + raise ValueError('Unkown format (%s)' % self.format) diff --git a/hirise_blender/planetaryimage/decoders.py b/hirise_blender/planetaryimage/decoders.py new file mode 100644 index 0000000..5044147 --- /dev/null +++ b/hirise_blender/planetaryimage/decoders.py @@ -0,0 +1,49 @@ +import numpy +#from ..six.moves import range + + +class BandSequentialDecoder(object): + def __init__(self, dtype, shape, compression=None): + self.dtype = dtype + self.shape = shape + self.sample_bytes = dtype.itemsize + self.compression = compression + + @property + def size(self): + return numpy.product(self.shape) + + def decode(self, stream): + if self.compression: + data = numpy.fromstring(stream.read(self.size*self.sample_bytes), self.dtype) + else: + data = numpy.fromfile(stream, self.dtype, self.size) + return data.reshape(self.shape) + + +class TileDecoder(object): + def __init__(self, dtype, shape, tile_shape): + self.dtype = dtype + self.shape = shape + self.tile_shape = tile_shape + + def decode(self, stream): + bands, lines, samples = self.shape + tile_lines, tile_samples = self.tile_shape + tile_size = tile_lines * tile_samples + data = numpy.empty(self.shape, dtype=self.dtype) + + for band in data: + for line in range(0, lines, tile_lines): + for sample in range(0, samples, tile_samples): + sample_end = sample + tile_samples + line_end = line + tile_lines + chunk = band[line:line_end, sample:sample_end] + + tile = numpy.fromfile(stream, self.dtype, tile_size) + tile = tile.reshape((tile_lines, tile_samples)) + + chunk_lines, chunk_samples = chunk.shape + chunk[:] = tile[:chunk_lines, :chunk_samples] + + return data diff --git a/hirise_blender/planetaryimage/image.py b/hirise_blender/planetaryimage/image.py new file mode 100644 index 0000000..e395b05 --- /dev/null +++ b/hirise_blender/planetaryimage/image.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +import os +import gzip +import bz2 +from ..six import string_types # not found +from ..pvl import load # not found +import numpy + + +class PlanetaryImage(object): + """A generic image reader. Parent object for PDS3Image and CubeFile + + Parameters + ---------- + + stream + file object to read as an image file + + filename : string + an optional filename to attach to the object + + compression : string + an optional string that indicate the compression type 'bz2' or 'gz' + + Attributes + ---------- + compression : string + Compression type (i.e. 'gz', 'bz2', or None). + + data : numpy array + A numpy array representing the image. + + filename : string + The filename given. + + label : pvl module + The image's label in dictionary form. + + Examples + -------- + >>> from planetaryimage import PDS3Image + >>> testfile = 'tests/mission_data/2p129641989eth0361p2600r8m1.img' + >>> image = PDS3Image.open(testfile) + >>> # Examples of attributes + >>> image.bands + 1 + >>> image.lines + 64 + >>> image.samples + 64 + >>> str(image.format) + 'BAND_SEQUENTIAL' + >>> image.data_filename + >>> image.dtype + dtype('>i2') + >>> image.start_byte + 34304 + >>> image.shape + (1, 64, 64) + >>> image.size + 4096 + + See https://planetaryimage.readthedocs.io/en/latest/usage.html to see how + to open images to view them and make manipulations. + + """ + + @classmethod + def open(cls, filename): + """ Read an image file from disk + + Parameters + ---------- + filename : string + Name of file to read as an image file. This file may be gzip + (``.gz``) or bzip2 (``.bz2``) compressed. + """ + if filename.endswith('.gz'): + fp = gzip.open(filename, 'rb') + try: + return cls(fp, filename, compression='gz') + finally: + fp.close() + elif filename.endswith('.bz2'): + fp = bz2.BZ2File(filename, 'rb') + try: + return cls(fp, filename, compression='bz2') + finally: + fp.close() + else: + with open(filename, 'rb') as fp: + return cls(fp, filename) + + def __init__(self, stream_string_or_array, filename=None, compression=None): + """ + Create an Image object. + """ + + if isinstance(stream_string_or_array, string_types): + error_msg = ( + 'A file like object is expected for stream. ' + 'Use %s.open(filename) to open a image file.' + ) + raise TypeError(error_msg % type(self).__name__) + if isinstance(stream_string_or_array, numpy.ndarray): + self.filename = None + self.compression = None + self.data = stream_string_or_array + self.label = self._create_label(stream_string_or_array) + else: + #: The filename if given, otherwise none. + self.filename = filename + + self.compression = compression + + # TODO: rename to header and add footer? + #: The parsed label header in dictionary form. + self.label = self._load_label(stream_string_or_array) + + #: A numpy array representing the image + self.data = self._load_data(stream_string_or_array) + + def __repr__(self): + # TODO: pick a better repr + return self.filename + + def save(self, file_to_write=None, overwrite=False): + self._save(file_to_write, overwrite) + + @property + def image(self): + """An Image like array of ``self.data`` convenient for image processing tasks + + * 2D array for single band, grayscale image data + * 3D array for three band, RGB image data + + Enables working with ``self.data`` as if it were a PIL image. + + See https://planetaryimage.readthedocs.io/en/latest/usage.html to see + how to open images to view them and make manipulations. + + """ + if self.bands == 1: + return self.data.squeeze() + elif self.bands == 3: + return numpy.dstack(self.data) + # TODO: what about multiband images with 2, and 4+ bands? + + @property + def bands(self): + """Number of image bands.""" + return self._bands + + @property + def lines(self): + """Number of lines per band.""" + return self._lines + + @property + def samples(self): + """Number of samples per line.""" + return self._samples + + @property + def format(self): + """Image format.""" + return self._format + + _data_filename = None + + @property + def data_filename(self): + """Return detached filename else None.""" + return self._data_filename + + @property + def dtype(self): + """Pixel data type.""" + return self._dtype + + @property + def start_byte(self): + """Index of the start of the image data (zero indexed).""" + return self._start_byte + + @property + def shape(self): + """Tuple of images bands, lines and samples.""" + return (self.bands, self.lines, self.samples) + + @property + def size(self): + """Total number of pixels""" + return self.bands * self.lines * self.samples + + def _load_label(self, stream): + return load(stream) + + def _load_data(self, stream): + if self.data_filename is not None: + return self._load_detached_data() + + stream.seek(self.start_byte) + return self._decode(stream) + + def create_label(self, array): + self._create_label(array) + + def _decode(self, stream): + return self._decoder.decode(stream) + + def _load_detached_data(self): + dirpath = os.path.dirname(self.filename) + filename = os.path.abspath(os.path.join(dirpath, self.data_filename)) + + with open(filename, 'rb') as stream: + return self._decode(stream) diff --git a/hirise_blender/planetaryimage/pds3image.py b/hirise_blender/planetaryimage/pds3image.py new file mode 100644 index 0000000..e53fe14 --- /dev/null +++ b/hirise_blender/planetaryimage/pds3image.py @@ -0,0 +1,349 @@ +# -*- coding: utf-8 -*- +import numpy +from ..six import string_types, integer_types +import os +from ..pvl import dump, dumps, encoder, load, PVLModule, Units +import collections + +from .image import PlanetaryImage +from .decoders import BandSequentialDecoder + + +class Pointer(collections.namedtuple('Pointer', ['filename', 'bytes'])): + @staticmethod + def _parse_bytes(value, record_bytes): + if isinstance(value, integer_types): + return (value - 1) * record_bytes + + if isinstance(value, Units) and value.units == 'BYTES': + return value.value + + raise ValueError('Unsupported pointer type') + + @classmethod + def parse(cls, value, record_bytes): + """Parses the pointer label. + + Parameters + ---------- + pointer_data + Supported values for `pointer_data` are:: + + ^PTR = nnn + ^PTR = nnn + ^PTR = "filename" + ^PTR = ("filename") + ^PTR = ("filename", nnn) + ^PTR = ("filename", nnn ) + + record_bytes + Record multiplier value + + Returns + ------- + Pointer object + """ + if isinstance(value, string_types): + return cls(value, 0) + + if isinstance(value, list): + if len(value) == 1: + return cls(value[0], 0) + + if len(value) == 2: + return cls(value[0], cls._parse_bytes(value[1], record_bytes)) + + raise ValueError('Unsupported pointer type') + + return cls(None, cls._parse_bytes(value, record_bytes)) + + +class PDS3Image(PlanetaryImage): + """A PDS3 image reader. + + Examples + -------- + >>> from planetaryimage import PDS3Image + >>> testfile = 'tests/mission_data/2p129641989eth0361p2600r8m1.img' + >>> image = PDS3Image.open(testfile) + >>> # Examples of PDS3Image Attributes + >>> image.dtype + dtype('>i2') + >>> image.record_bytes + 128 + >>> image.data_filename + + """ + + SAMPLE_TYPES = { + 'MSB_INTEGER': '>i', + 'INTEGER': '>i', + 'MAC_INTEGER': '>i', + 'SUN_INTEGER': '>i', + + 'MSB_UNSIGNED_INTEGER': '>u', + 'UNSIGNED_INTEGER': '>u', + 'MAC_UNSIGNED_INTEGER': '>u', + 'SUN_UNSIGNED_INTEGER': '>u', + + 'LSB_INTEGER': 'f', + 'FLOAT': '>f', + 'REAL': '>f', + 'MAC_REAL': '>f', + 'SUN_REAL': '>f', + + 'IEEE_COMPLEX': '>c', + 'COMPLEX': '>c', + 'MAC_COMPLEX': '>c', + 'SUN_COMPLEX': '>c', + + 'PC_REAL': 'S', + 'LSB_BIT_STRING': 'i': 'MSB_INTEGER', + '>u': 'MSB_UNSIGNED_INTEGER', + 'f': 'IEEE_REAL', + '>c': 'IEEE_COMPLEX', + 'S': 'MSB_BIT_STRING', + ' 1 and self._format != 'BAND_SEQUENTIAL'): + raise NotImplementedError + else: + self.data.tofile(stream, format='%' + self.dtype.kind) + stream.close() + + def _create_label(self, array): + """Create sample PDS3 label for NumPy Array. + It is called by 'image.py' to create PDS3Image object + from Numpy Array. + + Returns + ------- + PVLModule label for the given NumPy array. + + Usage: self.label = _create_label(array) + + """ + if len(array.shape) == 3: + bands = array.shape[0] + lines = array.shape[1] + line_samples = array.shape[2] + else: + bands = 1 + lines = array.shape[0] + line_samples = array.shape[1] + record_bytes = line_samples * array.itemsize + label_module = PVLModule([ + ('PDS_VERSION_ID', 'PDS3'), + ('RECORD_TYPE', 'FIXED_LENGTH'), + ('RECORD_BYTES', record_bytes), + ('LABEL_RECORDS', 1), + ('^IMAGE', 1), + ('IMAGE', + {'BANDS': bands, + 'LINES': lines, + 'LINE_SAMPLES': line_samples, + 'MAXIMUM': 0, + 'MEAN': 0, + 'MEDIAN': 0, + 'MINIMUM': 0, + 'SAMPLE_BITS': array.itemsize * 8, + 'SAMPLE_TYPE': 'MSB_INTEGER', + 'STANDARD_DEVIATION': 0}) + ]) + return self._update_label(label_module, array) + + def _update_label(self, label, array): + """Update PDS3 label for NumPy Array. + It is called by '_create_label' to update label values + such as, + - ^IMAGE, RECORD_BYTES + - STANDARD_DEVIATION + - MAXIMUM, MINIMUM + - MEDIAN, MEAN + + Returns + ------- + Update label module for the NumPy array. + + Usage: self.label = self._update_label(label, array) + + """ + maximum = float(numpy.max(array)) + mean = float(numpy.mean(array)) + median = float(numpy.median(array)) + minimum = float(numpy.min(array)) + stdev = float(numpy.std(array, ddof=1)) + + encoder = encoder.PDSLabelEncoder + serial_label = dumps(label, cls=encoder) + label_sz = len(serial_label) + image_pointer = int(label_sz / label['RECORD_BYTES']) + 1 + + label['^IMAGE'] = image_pointer + 1 + label['LABEL_RECORDS'] = image_pointer + label['IMAGE']['MEAN'] = mean + label['IMAGE']['MAXIMUM'] = maximum + label['IMAGE']['MEDIAN'] = median + label['IMAGE']['MINIMUM'] = minimum + label['IMAGE']['STANDARD_DEVIATION'] = stdev + + return label + + @property + def _bands(self): + try: + if len(self.data.shape) == 3: + return self.data.shape[0] + else: + return 1 + except AttributeError: + return self.label['IMAGE'].get('BANDS', 1) + + @property + def _lines(self): + try: + if len(self.data.shape) == 3: + return self.data.shape[1] + else: + return self.data.shape[0] + except AttributeError: + return self.label['IMAGE']['LINES'] + + @property + def _samples(self): + try: + if len(self.data.shape) == 3: + return self.data.shape[2] + else: + return self.data.shape[1] + except AttributeError: + return self.label['IMAGE']['LINE_SAMPLES'] + + @property + def _format(self): + return self.label['IMAGE'].get('BAND_STORAGE_TYPE', 'BAND_SEQUENTIAL') + + @property + def _start_byte(self): + return self._image_pointer.bytes + + @property + def _data_filename(self): + return self._image_pointer.filename + + @property + def _dtype(self): + return self._pixel_type.newbyteorder(self._byte_order) + + @property + def record_bytes(self): + """Number of bytes for fixed length records.""" + return self.label.get('RECORD_BYTES', 0) + + @property + def _image_pointer(self): + return Pointer.parse(self.label['^IMAGE'], self.record_bytes) + + @property + def _sample_type(self): + sample_type = self.label['IMAGE']['SAMPLE_TYPE'] + try: + return self.SAMPLE_TYPES[sample_type] + except KeyError: + raise ValueError('Unsupported sample type: %r' % sample_type) + + @property + def _sample_bytes(self): + # get bytes to match NumPy dtype expressions + try: + return self.data.itemsize + except AttributeError: + return int(self.label['IMAGE']['SAMPLE_BITS'] / 8) + + # FIXME: This dtype overrides the Image.dtype right? Then whats the point + # of _dtype above here ^^, should we just rename this one _dtype and remove + # the other one? + @property + def dtype(self): + """Pixel data type.""" + try: + return self.data.dtype + except AttributeError: + return numpy.dtype('%s%d' % (self._sample_type, self._sample_bytes)) + + @property + def _decoder(self): + if self.format == 'BAND_SEQUENTIAL': + return BandSequentialDecoder( + self.dtype, self.shape, self.compression + ) + raise ValueError('Unkown format (%s)' % self.format) diff --git a/hirise_blender/planetaryimage/specialpixels.py b/hirise_blender/planetaryimage/specialpixels.py new file mode 100644 index 0000000..cfbb8f3 --- /dev/null +++ b/hirise_blender/planetaryimage/specialpixels.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +""" Constants for Isis Special Pixels. + + Min: The minimum valid value for a pixel. + Null: Pixel has no data available. + Lis: Pixel was lower bound saturated on the instrument. + His: Pixel was higher bound saturated on the instrument. + Lrs: Pixel was lower bound saturated during a computation. + Hrs: Pixel was higher bound saturated during a computation. + Max: The maximum valid value for a pixel. +""" + +import numpy + +__all__ = ['SPECIAL_PIXELS'] + + +def _make_num(num, dtype): + return numpy.fromstring(num, dtype=dtype)[0] + + +SPECIAL_PIXELS = { + + 'UnsignedByte': { + 'Min': 1, + 'Null': 0, + 'Lrs': 0, + 'Lis': 0, + 'His': 255, + 'Hrs': 255, + 'Max': 254 + }, + + 'UnsignedWord': { + 'Min': 3, + 'Null': 0, + 'Lrs': 1, + 'Lis': 2, + 'His': 65534, + 'Hrs': 65535, + 'Max': 65522 + }, + + 'SignedWord': { + 'Min': -32752, + 'Null': -32768, + 'Lrs': -32767, + 'Lis': -32766, + 'His': -32765, + 'Hrs': -32764, + 'Max': 32767 + }, + + 'SignedInteger': { + 'Min': -8388614, + 'Null': -8388613, + 'Lrs': -8388612, + 'Lis': -8388611, + 'His': -8388610, + 'Hrs': -8388609, + 'Max': 2147483647 + }, + + 'Real': { + 'Min': _make_num(b'\xFF\x7F\xFF\xFA', '>f4'), + 'Null': _make_num(b'\xFF\x7F\xFF\xFB', '>f4'), + 'Lrs': _make_num(b'\xFF\x7F\xFF\xFC', '>f4'), + 'Lis': _make_num(b'\xFF\x7F\xFF\xFD', '>f4'), + 'His': _make_num(b'\xFF\x7F\xFF\xFE', '>f4'), + 'Hrs': _make_num(b'\xFF\x7F\xFF\xFF', '>f4'), + 'Max': numpy.finfo('f4').max + }, + + 'Double': { + 'Min': _make_num(b'\xFF\xEF\xFF\xFF\xFF\xFF\xFF\xFA', '>f8'), + 'Null': _make_num(b'\xFF\xEF\xFF\xFF\xFF\xFF\xFF\xFB', '>f8'), + 'Lrs': _make_num(b'\xFF\xEF\xFF\xFF\xFF\xFF\xFF\xFC', '>f8'), + 'Lis': _make_num(b'\xFF\xEF\xFF\xFF\xFF\xFF\xFF\xFD', '>f8'), + 'His': _make_num(b'\xFF\xEF\xFF\xFF\xFF\xFF\xFF\xFE', '>f8'), + 'Hrs': _make_num(b'\xFF\xEF\xFF\xFF\xFF\xFF\xFF\xFF', '>f8'), + 'Max': numpy.finfo('f8').max + } +} diff --git a/hirise_blender/pvl/__init__.py b/hirise_blender/pvl/__init__.py new file mode 100644 index 0000000..8b71efd --- /dev/null +++ b/hirise_blender/pvl/__init__.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +"""Python implementation of PVL (Parameter Value Language).""" + +# 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 + +from .encoder import PDSLabelEncoder, PVLEncoder +from .parser import PVLParser, OmniParser +from .collections import ( + PVLModule, + PVLGroup, + PVLObject, + Quantity, + Units, +) + +__author__ = "The pvl Developers" +__email__ = "rbeyer@rossbeyer.net" +__version__ = "1.3.2" +__all__ = [ + "load", + "loads", + "dump", + "dumps", + "PVLModule", + "PVLGroup", + "PVLObject", + "Quantity", + "Units", +] + + +def load( + path, + parser=None, + grammar=None, + decoder=None, + encoding=None, + **kwargs +): + """Returns a Python object from parsing the file at *path*. + + :param path: an :class:`os.PathLike` which presumably has a + PVL Module in it to parse. + :param parser: defaults to :class:`pvl.parser.OmniParser()`. + :param grammar: defaults to :class:`pvl.grammar.OmniGrammar()`. + :param decoder: defaults to :class:`pvl.decoder.OmniDecoder()`. + :param encoding: defaults to None, and has the same meaning as + for :py:func:`open()`. + :param ``**kwargs``: the keyword arguments that will be passed + to :func:`loads()` and are described there. + + If *path* is not an :class:`os.PathLike`, it will be assumed to be an + already-opened file object, and ``.read()`` will be applied + to extract the text. + + If the :class:`os.PathLike` or file object contains some bytes + decodable as text, followed by some that is not (e.g. an ISIS + cube file), that's fine, this function will just extract the + decodable text. + """ + return loads( + get_text_from(path, encoding=encoding), + parser=parser, + grammar=grammar, + decoder=decoder, + **kwargs + ) + + +def get_text_from(path, encoding=None) -> str: + try: + p = Path(path) + return p.read_text(encoding=encoding) + except UnicodeDecodeError: + # This may be the result of an ISIS cube file (or anything else) + # where the first set of bytes might be decodable, but once the + # image data starts, they won't be, and the above tidy function + # fails. So open the file as a bytestream, and read until + # we can't decode. We don't want to just run the .read_bytes() + # method of Path, because this could be a giant file. + with open(path, mode="rb") as f: + return decode_by_char(f) + except TypeError: + # Not an os.PathLike, maybe it is an already-opened file object + if path.readable(): + position = path.tell() + try: + s = path.read() + if isinstance(s, bytes): + # Oh, it was opened in 'b' mode, need to rewind and + # decode. Since the 'catch' below already does that, + # we'll just emit a ... contrived ... UnicodeDecodeError + # so we don't have to double-write the code: + raise UnicodeDecodeError( + "utf_8", + "dummy".encode(), + 0, + 1, + "file object in byte mode", + ) + except UnicodeDecodeError: + # All of the bytes weren't decodeable, maybe the initial + # sequence is (as above)? + path.seek(position) # Reset after the previous .read(): + s = decode_by_char(path) + + else: + # Not a path, not an already-opened file. + raise TypeError( + "Expected an os.PathLike or an already-opened " + "file object, but did not get either." + ) + return s + + +def decode_by_char(f: io.RawIOBase) -> str: + """Returns a ``str`` decoded from the characters in *f*. + + :param f: is expected to be a file object which has been + opened in binary mode ('rb') or just read mode ('r'). + + The *f* stream will have one character or byte at a time read from it, + and will attempt to decode each to a string and accumulate + those individual strings together. Once the end of the file is found + or an element can no longer be decoded as UTF, the accumulated string will + be returned. + """ + s = "" + try: + for elem in iter(lambda: f.read(1), b""): + if isinstance(elem, str): + if elem == "": + break + s += elem + else: + s += elem.decode() + + except UnicodeError: + # Expecting this to mean that we got to the end of decodable + # bytes, so we're all done, and pass through to return s. + pass + + return s + + +def loadu(url, parser=None, grammar=None, decoder=None, **kwargs): + """Returns a Python object from parsing *url*. + + :param url: this will be passed to :func:`urllib.request.urlopen` + and can be a string or a :class:`urllib.request.Request` object. + :param parser: defaults to :class:`pvl.parser.OmniParser()`. + :param grammar: defaults to :class:`pvl.grammar.OmniGrammar()`. + :param decoder: defaults to :class:`pvl.decoder.OmniDecoder()`. + :param ``**kwargs``: the keyword arguments that will be passed + to :func:`urllib.request.urlopen` and to :func:`loads()`. + + The ``**kwargs`` will first be scanned for arguments that + can be given to :func:`urllib.request.urlopen`. If any are + found, they are extracted and used. All remaining elements + will be passed on as keyword arguments to :func:`loads()`. + + Note that *url* can be any URL that :func:`urllib.request.urlopen` + takes. Certainly http and https URLs, but also file, ftp, rsync, + sftp and more! + """ + + # Peel off the args for urlopen: + url_args = dict() + for a in inspect.signature(urllib.request.urlopen).parameters.keys(): + if a in kwargs: + url_args[a] = kwargs.pop(a) + + # The object returned from urlopen will always have a .read() + # function that returns bytes, so: + with urllib.request.urlopen(url, **url_args) as resp: + s = decode_by_char(resp) + + return loads(s, parser=parser, grammar=grammar, decoder=decoder, **kwargs) + + +def loads(s: str, parser=None, grammar=None, decoder=None, **kwargs): + """Deserialize the string, *s*, as a Python object. + + :param s: contains some PVL to parse. + :param parser: defaults to :class:`pvl.parser.OmniParser()`. + :param grammar: defaults to :class:`pvl.grammar.OmniGrammar()`. + :param decoder: defaults to :class:`pvl.decoder.OmniDecoder()`. + :param ``**kwargs``: the keyword arguments to pass to the *parser* class + if *parser* is none. + """ + # decoder = __create_decoder(cls, strict, grammar=grammar, **kwargs) + # return decoder.decode(s) + + if isinstance(s, bytes): + # Someone passed us an old-style bytes sequence. Although it isn't + # a string, we can deal with it: + s = s.decode() + + if parser is None: + parser = OmniParser(grammar=grammar, decoder=decoder, **kwargs) + elif not isinstance(parser, PVLParser): + 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, **kwargs) + elif not isinstance(encoder, PVLEncoder): + raise TypeError("The encoder must be an instance of pvl.PVLEncoder.") + + return encoder.encode(module) diff --git a/hirise_blender/pvl/__pycache__/__init__.cpython-310.pyc b/hirise_blender/pvl/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb73bdf5795f14b6cf295f982cf4ac85db3c8fd2 GIT binary patch literal 7658 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 literal 0 HcmV?d00001 diff --git a/hirise_blender/pvl/__pycache__/decoder.cpython-310.pyc b/hirise_blender/pvl/__pycache__/decoder.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc4bb0766539a4a1a8c9ef85c937fbdd33f7ad5c GIT binary patch literal 16552 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 cWo8caIfL(%@ zM7AJX%=i80^krsuDKHLGl?A4!`*ioY{O7;_|2fkg9xkQuxAOb1HZT9ZRO-L;BKeoc z#q;=he~g2js-#5aXoJD#r0mff5x37U?KIk+b*tqsSj}$drsMjp&b;;7s=v_fSlw%jPSf|SR>$v_ zJm2N5o2=8b{Dp?kH|DzTlI3@;hV|N;pSHYZr`eioH5-1b+o_h`2mDi)D`t>xTx$lU z4!ULgE6Z)CVg&gMcyy`J#%bnO!|l9p;Mqz#D59RvNV`D+r!TtQmF1xD*_B4eZ~3d0 zOi+^2#?n&54N9N6fG70%YZtDB=R+@Fc{#j51KHW`b*F>M`7YWe806jXd>7vRbmN-S z)&)&=Jo(4YJ&%t!ii48^Dy4`tvfiovfTS&$->|4Ld&z7?S=>7dDh~J=L13l z;7-fw_*M)G3lJoL9ktRpo&9*mPvF27fO*PjX^<_*?ph+*huP=4&l{U}Ib-vlwck=bR-EG$E z-%Gjs08csGd;Y{icgZapal@P5I5 z*glN=yNjlMLGJU;5#3h7e#AbCyJOOdX+LUDp_M)QYTB;gYFuB<*pK0Auf95FAIH^% zT}5mAq%32fuur0FQhKmomz}atqwIi`9n@uK?8i~|fUfBi_7k{z&^d%>9$K{YGf&!2 zy_4B6Y7gu3!@B&F_E{-EB2Pc0OP{u%K^>0>9QQeS!1h`DIg}jLZG6gp9#@azoq~Sj zoc#hyri5fK3NN^*SYpG3$Vf27YYoq_EigcT!LeLmFj=5noIii%!j9 zHJcu2vjrZ}@y|w}qLgy1&dSm?$U+R5;`n8vnq(`oZX zR)bb8Fs+p(G;z$jwcvCtkxd@Kq6C)WlAMi(tHkXJ`8ua835C;rfV$PP{e_+T+*|;z zG*OG7pgfH3K=!hSQLo!TH%s&Pg~m;%zrm+Y_O;lUpLd;k5z%$9)w60avn~*s1Jw*w zP4zNlJ|-H)!abt~HBb^&O?lP}?QZkBbs5}0B$taF+d3CFXI=3dzC#E`6-_(U`KopC z@`d_~uYcx^*Q{qD54)9PRs(MekmsPEU_8Y6ZfDB3242uuN1$I&Xes!@(ew=(1FhRm zyS3B;Ebnv3ntHVKV-rygx*JnZPl@~na#XCw9I95B1JSC+sWIge2N8`q91A4%hxWPS z+~UNZr3%3*&>EObe~7=!ZQ!(R0U^}ffo;w8p}NY;vU~qA&Q}&4%0Ru6UNy79;^_DcZAJP5*eibv*{*GCClHtI-veY4it8D-1>l zFg~GMAym>SiCEFb#P2$JR`I8}9I$xtLl@)l;2O-`G%RL%z9Y;L3Clt{HOA^wVz} z@e)2Tb8C3Ao?px1SXj&ASX|5FSn`MbGHt+( z^gL@BUQ68_*-RNJf7e>UA6?vy+CINlz@4#9#@}P7H;lIp+$-vPb8AK18<%^~SKc;y zxV}4%&)8;a(=bw>OLfLFDSyxE*9_E(diU7bwc@+5Bi9Nz=HJWmty?L7uRnpYWEc1O zlXL9remezoqHsCL3cC%mqNnzzWS}MA4<=D#$0zq~dD2G> z)^zX13eAZvMgoXUj6~pG!<`o?2+{}J0ritg`Es&{(>=3NtsDsQnu)qZgrL-l$Ui7v zyxnw`X*{^QcrUNzyKCXfeVB(MJRIiX5gw@DyF{j-q|K0);{{{&*3xp<^+VHyWoXA+ zxh4ea5~qW_hEtH6cb91m6eHRQasU{M(hzk)cCOuR__cxyyO=hPOIsosh7#ept!AC$ z^uAXLGM>|(bEnv$86GSiq|ee_={Wlw3?+aPl#^2vn?CL&pM>Gmf>lzlU&58gLt(gJ zl#G&DG73h;#tnj8S39?reKuKf>AKl(KN@5vSH#W)6D%S^MU!nM4`6_WUdw6 zYS=~-lo-YZ1e zFRxqe)-|``t`hTzrpvBF271i$y1c7EOqT{URgK29`5YJgRZE7d%i&iH_fdSgM{x); zq_iO40AKDdul6SNNmU52`q|L^U}b?0;NjKDs3pTWIK#(e@`wF2EGb#yc^Rs!S303Nx= z@XXin@ra40X?|d4%Eq`!v?viZeuR>oDw#CQ4|)$K(PIlKxd%{-dk}{v(IouK;e>8B zVe5-H@xj>1vb|v+evvf;Y|ea77$+D$&}PG2&wxRv=gbSKFO98d{WO?!7SDs7gE{i~ z+*&qfjyHrc-p%=Whyc8|ktSoc3t*4Mh6$QJXWJC-p+6#&#>(z#x1gDTEiHFFuLb8p zB=lmwp}3c5FQLXuH_8H(qjqShQRUOd8d$+ucy(Y-P`yRfTbmY-7^(nYRvJ*$D(eyM zdG%_{0H-TguPV8lXeRbi&@4;vCkH1~3ym7yq{Zy_Qev^z}isHhB2A4Gi!Mp>|!myftKJ=0}J?OejOUuqItKlR@gMIq*Tiw z_gwD>uS?HK?wTUNcQNI*HRpDhtQl{HW)>VT5f)=|V7)hNx;Dk?w-R==1Eg;!ZT=Yn z5D2HWq@6ZfdwDO5XqNZc>`^fM@0^qt%OEw`~8i81Km z>bi^0(pK|g%DZP+_l#$IX_&>ADmhUwf-%<-qpyx31Bd9-2u7$mz=VJXK^I7{8`_VD zapF8I8odUU=6b!2R_eF9uI&Z7q}!=4tS-Z!;Z=&F#{_A3Tb0H|jV36; zu#-a`ME_6W{eO;+cL;}(qGgU6qttwi5um>kWFUhdl#2zkw>v=uT6*S`IcY&ACZV(;X1TIrcSxhfxY-2wt0OwivLEs|Gax8n2rF=&O63X+y zSiHBM@r!G)1|Z%Jyk!bC;GMgrwcMuRUGj%)^KKcI2!x)Uo=ow$FEwZGPi=xC-ZpL= zi)#YTXL>&uo z{SKZNw9RFqj00uCyUlWXR5UQtm`rE>-29+i=tEoeW*=)Vpqj1dniizZfbkleDWKYJ zg=#Q@b3Tw3`qV+PU>~~FhCyV$QGx~qWVX$3n(NS|^n2Fed+yU~>ANtIfy`N;sktfW zedAGGT2!TL`OS>f`Y6z~ba#kE0CY|F{>^=%tHQ#Mh^$-CCc@rrkrkqV!Ayv9#S6vH zsa(0yi@nsh?fUp>)5L=24pQ3${UC2TlldveWxvGHjaA z8h}dfNqTAU{t(N-W5FrGFH?9DT7~AzsstXRH5e58z&VMEb{1%zS{!j_p;M@uGpc6P zlJDf;@yXgK;gz)H&Vh=p?uuh9q0owJVh8f?i3+wf{TlF5Hj)qM|9CULX}xsw!SQ1>!Fez^{m> z=C=q4rERAwyM=-j>6`${D2Ba9c|<7wiN>Of#FSMVSJsE92l4$TUngr;KA{HBC=vwY z6D}h1`H-wSyiem^v=K#HWldMoYOTntYtDSDQ_J&|Dti==7O#v;+qMWz+M^ktE0*Dm zp&ni^M}@PG851Vek_pY}$?b8{X0a z&Fc;57&bHu(hl?w13C*ZbkhK4lW{{Uffka3zD@rqsBhD7pG3I<)(r~cduuuM>|);i z1g`j0LDL5ALg!9CacRpFQC*;lY~z;!mHQ)%i1lp~{}E>0M;mea3L{A|l_(V4qUg+g zI8o^E?SMuTd;fP77BodeAt4$(LO5`}Q)BR`ibeDZNz)ZKCX5OWge-eT*&`G|Ooq{_ zzlz==1`w%4al}x3skGJ4Z3C@DY(oWi`@%F)Px!iOgMBBziN)Jxw+nQ-mlZly?ysT< zFrg}F;mlD9%32f%Oar9+K=bzqA|c%%cVnfC!NfEeDoO*Swv47kQL5!pVI8#%lGSxQ zahzmD{k||nujV+^q!B1e1tSM-pZ-pMgPb1NhMXcG^{Mz&_bI4^eCU_$L8#oNm@z)5 zT#d#m(z5suL6*2UY9BtQq)JcuA}GY$0WU5h<{3YLiZ(|EttSWSk`RMbpn zScT1cyJsWer{t2=`y6AZaUNAzUleRq2$JZwwS9L(dxxxv(G%GB^c^Wr)0m!enV-7& z$Ph|#5LWhGK??UGo~>jPNI?HmEq7CrnrgWf`5q*@n|S7n#Ec1?jAlwk2G&Udz9chC zvdbW3&^xddOG0$)Gf;>qbO{TwQ96dCMyvU*!MGev5V(EQaO)at86ZF!lEduX_$bpa z9v%Th98LZ4zy*tz7X@-NRrgi&uab6e;VWWt3aV)smwj;LP-;a5c-*TM2cfx& zC$AHl6xpTp2sG_JZ~E3b?4vYtD*{mf_)l;`7X*bpDQ*#}r-VU4M{}e$h8ICcR7s_r z%Jm_X!KRDK3X285uvo;WGzXe=tGGXuxL-!uvulR?Noipi0-r67s2Xv;3-@32M@g-^ zl~I&@7PaiwwT!`2Z^E-LP76@-*Y9g8uL$GP>8KwE>%83C8!DwpcZ4IX)oZ}*U#;v> zI%gRsl@n@{io(*6q(Ko_(u3y|&UQu4VT&4+AWnh)bz0YG?sbKgI(2 z&!k`MgHoELEO9^_Sw3R+4s9jmqy!0ag5%z{IGIL!;(E4?yQy!a=3unh>30fiaBq>Q zLB^S$JxdlEW`H6N*S*=Yq3U>WtP)Z^QGg6{bKTPX_=hW(% z$E?0!Wk@x9*h8|hd0_FE^6SP=rO*HjFe~QbVeG4_K^N1 z#~K)44vcm8TjWAt1oA4b8I6-uw3jvJ2pFCuYImJ{G{e4EsdM^pI^g ztdP6P$uSAWKsR&aa;1R9rkhTKwFYyYqDk&eZc{sTt*Q>_xnBi{`#Cs9?7q#?AI{vyeH1SjR3au?8L0FJzkY88r&J|2hHQzQ;S0~$1IAoq7OqCEZ7 zeNW%VGIPDc(Jvf7x^(=g@|1w{9Yy~J#nsQD>>m;c97Q49o7jr_9C+WL`Dn=DJmx#7 zL9a7vm~x?jAnJ&?;=7smOn5P1T8R2$xDV(%ndF^Hws(Jk`q)tXUiRoqXOF&m&p6x5 z9=%dMI(N@JdIgebXsOk7yB<=okwKSJ-xX6qQ9@?}lK4E`%)v#lf86fEW$2U5M61g&=*$M%qzN3T&raSC7L3{SCY`f4gU zEEDUjDusPFle%1l_(Pr9bIDKT!P;2MgGsCaN4pG~GiIy2nY+Lb<=>W z1mB9lw0mS&+#~WNQ>E5S9MdW#2Q2bt$j z@{C!DE|Z#66o(*Jk<>Y!UFBhs2RequXmfA!j4l@CwJDv4BM_@mnJ-M!A}CzZW{i7> zm+31o+^KN8)WCu=QvzB|tb<+X+M?%)TBiic1|R!*9w;yrA`+=w!X|-j5wmlN6;yP* zpTeP#${6Nh=Ak72Wxj7_%s)4?<`3bxzGvpmADD&GpA|keHZl3Y_yY$XFb@JT0c^Z0 z-=D<`ktQWACxYQ8J?M@3kjzwacEubrEl?RgI(SN@_n~A4KiD6D1GW529pD?jEuq zdZ$p^Bj=X=FwV#A!_MBt33*=i?}+^fp5LciIBGwN7AL;})v>l8d~;gX^dxp`ej@>|Nl=ZqZ2hIlHgxC_(SQae~<}=l&j#;Olv@SWW`B$8xwvEUaTjzuK!< zs3BezE&m(sC1gxlO)SesGA9z4nOJj+iKbYd3-2}3jrjt(Fnlf+`nd{o$aUJvSKfrp z3n#bS#|u-);m0dncEc6EY*WHcQWt&NTwf(&E4 z3`Wli;QqM=pBvgo<~+XQD%893s-72>tb0sGBYt>z=P;00yUf94ii6IbL*5*+{DFLOgUU>5SeVyUq}AZokfuxj+$P5#H&To63^0d+xoF6)$YZ5T(N2F8QEpKLGna#5C|Gdi zVO7#x%W->OWDrDyORyCY6#z%i2bNyRLNLjoC8G4boLD2SLb)>EVcP9k3qs{djLCfp z2SgJ&P{99K9$|q3pJtHXA)xxZLUGZ%?+vI^9NZ*^m zRbe=Xa6J?RQ*rAIl4o-idjtPUf7;su&tSDBus_?k6a>15>{!bU~b$60anG;s~SbGUd7U3<&upp3Mkh9h#(a) z{RqxNOm9%50+Cm$p5X%UpOP^@<+z^dBq10|oM;dLG#k8e}`g8IA{$PA>$ zvJ=KXCVfLSf1Agg91A!Wziqs(Qkf-A>7L>n zYS)HF9^H&}Ir{A#ptcU$6V_mzoDR`%Lx+`eLq!l}GBL>nDVg{$gkep2Kwjh*ii-;i zD5Hk~2(k-iyjqR$vE%A7wOlb=Xe4EznCMmdOX{@lwZ(&bSY~kB*O#g-ahL&BR+p}I z+o~wh`X!)_N{fekZ2<-r)N>bjw+vSVg#z(Mh1M|kvvIFyxQ1GmKS{67VwkJ63%CZV z8s3tXQHsPr8}3$dYXOc9l{*xT!=g`4*0rl>?bb?SdoDrBUxwLu5gf$8s5ej)GZT>|@G|Jo$OZg1_J>v;N ziYn@(r8FAh=R7`M8wceo2CY2KXgnMiNJJLLnKDs}bX7<>X$g03?85R&2olnhoj#aC zy8k2c29|eif(@&?b_V6AZKMp@IsKv0nr{TK5JHNIyd8oyGn{u#A6EoFp>NT2-^QI0 z&B{4NPkqm`9&nL1P|HSAfQ1!>^KEUQCKw%1da0olCcPFWMB6?zkC~{K!1z;DS?kka zzB(3XEFcX@?d*WIL(c^1l~pNXi&rM-HRdrEpRNj*2lXL++qyV62P>wl_hey+>rB-; zkJUPEx1H!rOnsscE`c#=`LG=f1-6zX{mZ&1_6r(Q1V?3g>pm(X4Hs);H33J2jS6g9 z@_N~fWHV^DLp+ju#?&RHtI!afy$pdpjT2?&(UPx?D$!JT!cnRmqla-X7;1U4odAV; zC`D*f)l4Yc{U*Mm6Ty-`XfTZ2ReP{>$1lOG4I6h6qLx6p;WD88UR;y_I? zanPMaL8D~0c|1u;xlIezwhB-`7Up#L2`R1~EV3wRzE@hDwn;L>|}7a{yjuG*1=9(|v_DJeV<%ZgjF-w_=A-5$kQXfdaAHb#cu~AAThc@$jp>(Bo2*!QW*d;`JCP(@cyG5TNxQ&<1!S(n^q;bcN|Y;Hj!4^Dd26G3vf>6 z)oM<=fGm_Of{T)R%5+n_m1pWHf-+3uF1VkORPN$p$u8|@W7d&WS~7$BxDGO7MARqz_*j8LSp5gOn2`Hp9qa*)rRFPEhC>Mbbfl(-b*`m!eZlA zvi0aKWqb;ja;@Z2*Tpt7y_17A^>t1b)<8vwhH%ybQq5wvR7Rqx0$elW{!<>rY`lZB zq>?O2IIhk617;(i{1qY^y~kx_I2B~y17tW6;UqOtoYl~kX~Dx^`e&sP?sUNOa<+E> zeZQ!T+Nwsju%msbMo~(16I3IM_fxPkVXi~#1)6!g{Itqj)ee&kkU{6&A$9TdJ0imt z&>_)>vPUbQ5ke@CDx)Yd-@U0V6^I$3AEau=tRR4#VX}6c>yDNcpoJL-w8j1X*D$6* z;PS?QAaF}K8HMwMU@K>Od$$9c-v=+kDO4#W;6)VyWcP5Nb^gdU*7<{e4zQE_L>2Vn z0-#tDAwxL254m9W>N~)eQX!@>a%~k_i8Fuu6Y{l1x)sFF@I81G!-QejBk;7uRWoT# zvW?kBe=UG}@yD2lG6TYRIUz{}*Zma~3{DB3{5?(yLknEo%~h>X34SyRu>1$* za-Y_xF>4;Bp_!DA4aY*MLK^7Q}EdjnF1_n z=Q?$47zy{X!~+Q?Wtv6UtxUQEUc0}F4ncP!rHQ`l{xhBluq5Pio0kK;fH(d)MAtH6 zv<&29CBWo1=qfg(qy_ZBgMp|BZGRCLYX;Y~!*5!qJc-fJHvs`CLP&!^Db*ey>dj^f zlIIZosFD)FO(=K={#8VD?_$l2XwPcRd(O^YPrFZ{);>iEt8}uc;{xg^Ks_rmJB1LB zuOX4J;l+Dc$tG&iZ|?+qC@M{*(SYu``UH+2IzW2Li!ukk5W!!vp5K@~PtF512yeEr zNeMQfqplS15#=KNWC=n^oPjV(SzoLq;YDv{4vVuv5L!u|s!g%VCLCnkMl2E!x zN?=lGJE24ME|aSv1E1QB-hNEdCsm88TOx&IM3JOZyD`W^iBX=#W(+k^&uiw%lfCDt zMn&!*Aqi~bfK-aX_tYR%s*M*i)al-#N^I1N_N5mZU=-+e^`7xqFMaa4)0JY7^Ls7P zGl{E$x(pr$#TDd@&Jq49k*vr;ATWScGjtBYUA>Il*HG~~$|A$B<$SJJ#^x;x?s?Yt z3J(JW!0LZM0-#JyioJ4n6b5h^`W3eLk$>C^qJVXt6G#WzN*?S{A6-!Nkv7=0cLxW> zCw%iR#2)E_3xD0GXfnWAG=p+vw4l_aqX=w&jM~;+;5OHk**VZt#YbFGOoW0m5pL{5 zos20${nznxUX@h*98V)99R@{NJzW{@{nX9|L5%z2R$(v;=(^4NDZy_j!40}rWJ6qF zvG!~TUJfi4NwBD1@r!SHGbg4i$G;?tqzqBdUNZLH7y?(Z@r5n=L2eGFuw2LUu*`H!xi%NW)bWHEo zlolrj!*_ugNwuw33`dQX$b-%riJDKQHMUOVuo;!g$B6I~83qMltIjhpkrat_CP`<8 z7Hg7i;Qk#pe&EOC7)T6=LQiE8kls{`;%ht;1ylshCNF6usmy{qJQFoZB(+GOud-}| z2a=tPCK%RXC2mF>hL*%RBZMkJrtjh5|7Bp*3{6Q&z?h=Q_&DkK<#Kt?ivy^LBxO{9@5Q6bJLM zy?D_cc1li>f%pw$t{|cOEVdqIz#l<=XIKaP8PqQ$gg+*U0K?8MY^AHh`lI$k(gMT! zMFj2njwvPk?8zu-zu!Ka`iv$5z1-mfr{QW(Cm}+geF)lnVbJ#OBFX zZn@lNh2qOXK+?f}Lv%HW$Q__ahRDw_{HD?fwnniN3|cvOzktX&C1+CpP|ix=WTr3B zjCJ3xHKSv7Gt#{FKx;pv+K*D{2(a&*W`j|F)CNw7o*m~Jhjt5*eU(Cu#2DOW<4V$_ z%@(%DMI8PmY>ELVCKjl{=)j)bV5i6ml3U-X# zdOmlUgHCL(Ks^gPD7b;GYPmuXT)aj7B-#m;Tss0_h$hFRCB`b^R{C=sfMu8tzpfU} z``x+M5v+zg=$h6|uIz3|G#^Rt)}r1#^&MA%u#w0?Yh|G}% zbEl1{ZuEBKm&D$!LA6>0L6#8%Z& zt6Mg&Ld&{3YoY75pdI_rBhX4hDH44*s$yE{P0t+eVvJM8~hE?wWtnhoR80v0V<0*5nn{y7<&dVg$k&yqMTxc+1(W&sp@V& zjH%?;@p}<1Y)~{`@AYRS%_5YgdZ}$X4*hWLFmO2PQD{Wr7UUhP!z^p^#@r{trdgO; zt%v5KIzn~^^i+GjX?k}SOXun7Br8et%l4nn`wBWKU z?rt8$5TqU&?Aw7u2SW++5B6+B3&E~|$-}loPq1=P!$oPgc=<37hk1zl;{Hco{wE%O zn}^@wVZe@L?|u?MQ_DtosYIxp_Ta~QDzc{kG*{-9vdGq%9FUuFCKQz2l%zuiMG$0N z_;66fhPvw1{WR|PZ5otA0q62OK3)w6xKUEFZ`C8%e+upscwVzUG7;d+pXNCXM`ZV4 z(;yh5h$p3QaJK>3s05{tFs%)x<_$RR<1_9lD*nT7ML|liQt2G!*q}J*69WnVIA6W;warqPsXoYd2pj;4-F&JiF) zT!m-YhHMkw#Rh=A|vqXK$SGL9^U53cE+;v573s`~v)SMe)}$ z+*p$S+1T}Fh-MK>#<4RL{A@@}K|%`BT2i57jZ--glZVWQ-et=cc#Syrm<$MCf))#4 zLK~KZB`Ul8faNQdD%`AIv=7_~V}#m7AR@qP#{Fm!4%FDF-=P3zr$hjQU%ueL@zp|m zOaYA0$ac7wD8q^JuyS;XjtefTENABN?ZAVgYUsU%7P*?hPa$X$X|H-d%>UKZ$N;q> zt+g*m0@Z!QoPK?lUJkvV8zV@ef~iNQbSVhX{sF^?i1KKXCJ!b-3l>0uHW9ED3N&;O zYi5UWA>FHM%}~0*FjA1wu;2|JiO`f~(P;E!;a04u-ZP47V~Dc*UrA}gP-9{xi85)W zj@cL0Tj*7JNrd;$@g*|gAg}eH(AT#kF>mMWV>R!Ql<7!-qd+#6n!t7v$OOAHn&##$ zIQtP+7cka!M}3!zC1Z>U1QUaVuAmTPrOSP^4ud=1UFw%`F+l5Js@K!rE}{9fyIZGy zsf`9_6un}vH2B2fA^a4QF|)Go{-Y{!43w9Je{kwRvF!f#oS%m8oL2>G5kuDg z95>&QM6=3`z1T6+xLXpxdfp%6Zm`^$pWhqkiTgUAvooOM?8GHSEqIAbLO2Kob~PQ44l)v*NZsV#OBO2DYF)PTAz zZqhTUJfh(P$%>|1Rj0~?f7N-$0K|J5J`3XmMk6MatW}t3+izjBqhuYz_|R-4O!b6`;NSH2bt27~-*WoVuUMYB;l)R8YRC_c2>VKHmW3 z!6|hrB3?l5FCZ2_{`iMH9!{3#qT5dPCy-vq420LNT(}UXIjIJQ)yf8hw>OossRyl#`h#VF$3&xO}#EuCq`u^a~-+Jxb>*rp*ID7H+D}Qk3J=GWBVC)#cnBW|5i_(Z+ zBZ?}21Kzcy00lxtOo2a~15{{sW%u29(C9<-J~$2n4YGZ`Sm}1lYl0-8o8srV?Bo{p zFLQ@d>y-7V^@IpLjs(7az(Vpm89i)V!`d3`q|%`IuUF`x1Dv9SWePFu+-~64cxd59 zWF~x73G`66;jc#06NHUc1+$Rsjxa{lb(z%LdTo-GaC$1)r1$T|(h#-VzGodd1(3qM zNG)(37U!{M@G}gG)!|lAXs+T`LzrM zqL9wqDW}JwQb2h)IH)`%w4FXnj#gY0PJbR3NYLUc3~o#^2{i)2L8$yVTTIJNRgzk@ zZbEHGexa!CI@g0H`6iP}%?39j3sv(0wH(Buyw~J7R4@C)uU=S3ZX#Yd&52_km&69O z${<#gch)D-2W&V=M=#i*xJ{xNrYad~xF5lVs<}l}4KVkkx==0Ui|Vi9LtLma_4K|Y zI=W)|k-^cQXh`tN10R#xO;VNm`}FQ`_Z6{$iLy%brZt?20?WOSOamVWauJ7m(}NhUSx~|mj9(YUPR)IqGDt`q7yS0<_>tVhJ&1wR zIwql^LBBQdB3k>o(t|z z;H{F%n?kmlsd7j3URLGC&q9O@CXc`sqG_D2)x`SKP^d)nf*>ay2i=wV1&exs3Y{t) z1Ftgk2<8R}D&+YI!EzaTvQ~?cS)@`yP#b;cN>>7kk^RAS6RLrzDQcVT#i}$a3M)Bv zy82}G%rQEu8rG@DVRx~ecpKq_TS?-D9cYiztmK3BSsJmu$uWpB45JOULevX%&~wg4 zmss}n1^gX<_0{7SF3i64(%DyEJ$vO!^$I@GGq5e?nG0KhalzNm8fNv zlp~x;O^@od;&Ngx09Z>1VW>PUSjWhbVbU}-Lx2$c4ly!77r)!HpNb=ca9ybX&4u{U zsw9WGe-q8S7jU?&@@oDyPJ0&p2Gs&2Xr!HpajSA4)J0UiAJ1CVov|Ky72*$=}QEY>e zS62|!Xgm4`_0-xgy0FxaA0o~vkX4qAa4&(#2Fo~5_uumnLqOQtB+GsahbPhi1NpyX z)Kro&itv2of0|PmIP!^5A|L!j-j62tk3Tl@3^HtnA1F&rO;pnN7j2R8IEuH=uHg^Z zZQUY6vRLxaSxbTZu>3`pX5M%bQ+6)iU4pg=gIYA67~Yfo1_>;{?4r2`lGbY$L3cte z{F(arS-Fq+f!vw}Iq>!wOEyoX14p?`rh)pfE6-H1{RF! z2BgG^MI}V-)XSls7G`_Qdga{b z&mF_yU;m7f!qqBVM`QwtCWN;M0c%}IKsndqZ*@;MSFXX=Jf{0bH21s8S_7S|j|6hIwYSODs@ z*kQP0j}N$eq!U*pl@~yS&LF;){v3lmo9kSIUm0R z2-eSpNWmO-D#}*k*4XhF=uMXnthOBZ)f?<1I%F;(8e!Ns z;hP|o6mDX$bM3}W%z?sgp(71Hom3G0ZV((*{3UX!Il1WvLG+^fdG(LF>j!}LG4A@& z(Lbl040m609|A*aQjH7$cpk;OpU1^Vi+5wOo!ZsIh<5cbFT4K$^*|gAV^F{3jdcnou7e9V(bz6_jEytK32@C8qj)jQygjUUoQJ(UOz=?Pfni6rkH?PE z;P>%}!}ci~L>lZ5B|!K+@qL$1RUT3`i=6ol6u6(mAsE8SPQ7E4r+#1rvPJ#QER%25 zFT2W*X#)8@`T0%NFXr^`7R>M<03GGoZXRe2$q&my zHIxmTB;ix!l5{yC|3ngUj6vZU6;MNr1jDZ4fc4E~tb~#u@8M5&PC~^WHx8sni>a^h O-{=)Np67A=)c*#UmffiU literal 0 HcmV?d00001 diff --git a/hirise_blender/pvl/__pycache__/exceptions.cpython-310.pyc b/hirise_blender/pvl/__pycache__/exceptions.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76ada5cb8303b2e847ada0b621327d752544f430 GIT binary patch literal 2972 zcmZ`*OK%%D5avF#lGc*rI&I>lZNQ|j$W82?gQ8842D!NIBS8*AfyHV`*_*6(RdTns zh0Vz>0`xBw1?*#PJ@yy$N9?IUPyP$Jq~CBQ%T8R{9C66uaQMxGn{~TBLo5FIHu|g0 z*uTV_ZcQ+E(e(gCus#!(w1h3~p(`BW9=Lr+EDH~FO=*i3<~`9C{z0SfK4fA+bPky4 z$i}|Y_rQ0>BKSqw1m6OGK`en^GJIRS^N95>PHy`Tb|X2?<1E#Dm?@rbOa6VR!ja6S z;!nb)kbEN^s8H>#`IV!KLFJ4y9oevO)@baY-$&O=Af=VFlI>e7xWHwGgY57(i?Q!g zqdwHS7?~r%hB;q_(p4U79&(IyuHtlyM_HPOaViCmQ!FG|x+Rs*(Q1KPJWK^&g(+q$ znH{G>?vl9CTJM-j^K5929>!^y7%kB@E*a4su>r^4;!5U)0t%!GYGcA=zEA0#&-Y}* z+LU5&{&{P@S%18ZTj9UZa#$p_DByjv!IK(NzOyY;z9adwG}}4vWTVDdedcr^l4s9k zdW#Q=obQBbe%@w2jA8cK>#iC07~Wt#t8x;V_H0F*A}_vVs)3(Z_t&?xkz5y%Op+ps z;`OaKU*C>ZtYt939wF76?D&^tU7p0!+IX*Ond1Rv7b-QWU-A`&_%S-G9Fawp16@@^ z=b_3Yho+~E#$?HTbiE8xvO@$@X_wZKBWx2?&gAbOPx&k@MgzDLo=y-qn%^AkA&QDL zr*MK(&Bf^MHhxc8ph(Dqfj20qH3WQh!7Sv@i3~|WQY;H3f5q| z?5RbJBlI(k6jReTK_u&2s4KRx`pyeBL~RkyL96cyS2PY-zwv^(tnVRVy~^K^yHY(+ zDpTQA1jwUe5G6=m0@#!VvvdltEJ?B*0x1;2Bu|*|vC76$<+0Rv>WmoatvmcNGOS2Z zZAM|9Bf}*f!bei;aI1C`aID2BS8}qZx@RA$)GqEmu)2^#{x z^gtJ2BMUl$YISE*bzrw@fgE1|d1Y}Ep2x4zCe1r_7ED?|!~ipC@|#o|`J~n3sl?#M zE9*wDYjWL`6%(0NL!ppSwvUrIuiU3uoPsG3AXO^};xx{KpmGo)Cip9_b{3@sTmcsG zIFdcTaI&ItvVUwH^N9P!es+Cg9fItb>8 zrB<-{HoCqB(y=<$CF`2AY^~U~<-N9FFS?#p|M}MB;ygpvX7)!^N@tD6Cq;~GgHMfL z@k>5;(A}Xsv>&mT)@HAvu0!U$i}ft^AuYX81$>w;MG?s$xQk^|2CiC@_s-Zgljt=G z8PC0!8gVXg$p!%R9(RWU}@0v=~s4Ee^N3=BMX7a_ol%KpZl&?D8 z!joQ?5WKbZ7Y5#TsO&9|NvGQeaf-7sg!4&(%$70M3E3!Ab)k##7;m+@nus2V1c zFl9VpqHmQJIS~VgL4uS%MAvl8qD5gcH~%C|YW1G_m^ObxZat^!{!rkvAVM z*4s0ThbkNK*=BB1Nb0rCo_p?X<{>-B=yRC0L8i^_;M$7jHWB9(Kt10efv?S6RZg7d zbtR$at@7t@kBP6UeewZoa4?EaA$M|W)%vmy`L75WO(o8?mKRpKOYNoW@3#H}af9qu literal 0 HcmV?d00001 diff --git a/hirise_blender/pvl/__pycache__/grammar.cpython-310.pyc b/hirise_blender/pvl/__pycache__/grammar.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d072ac2eff415a8b97773dd1d31351333753e79 GIT binary patch literal 8156 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 e(rPeo9`RuZYdbMDOS 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 K@IOEP+W!JacH*)C literal 0 HcmV?d00001 diff --git a/hirise_blender/pvl/__pycache__/parser.cpython-310.pyc b/hirise_blender/pvl/__pycache__/parser.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ea09c7c1ae18b04754614b415fa2c40546b8307 GIT binary patch literal 26379 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 literal 0 HcmV?d00001 diff --git a/hirise_blender/pvl/collections.py b/hirise_blender/pvl/collections.py new file mode 100644 index 0000000..4687f2b --- /dev/null +++ b/hirise_blender/pvl/collections.py @@ -0,0 +1,704 @@ +# -*- coding: utf-8 -*- +"""Parameter Value Language container datatypes providing enhancements +to Python general purpose built-in containers. + +To enable efficient operations on parsed PVL text, we need an object +that acts as both a dict-like Mapping container and a list-like +Sequence container, essentially an ordered multi-dict. There is +no existing object or even an Abstract Base Class in the Python +Standard Library for such an object. So we define the +MutableMappingSequence ABC here, which is (as the name implies) an +abstract base class that implements both the Python MutableMapping +and Mutable Sequence ABCs. We also provide two implementations, the +OrderedMultiDict, and the newer PVLMultiDict. + +Additionally, for PVL Values which also have an associated PVL Units +Expression, they need to be returned as a quantity object which contains +both a notion of a value and the units for that value. Again, there +is no fundamental Python type for a quantity, so we define the Quantity +class (formerly the Units class). +""" +# 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 pprint +import warnings +from abc import abstractmethod +from collections import namedtuple, abc + + +class MutableMappingSequence(abc.MutableMapping, abc.MutableSequence): + """ABC for a mutable object that has both mapping and + sequence characteristics. + + Must implement `.getall(k)` and `.popall(k)` since a MutableMappingSequence + can have many values for a single key, while `.get(k)` and + `.pop(k)` return and operate on a single value, the *all* + versions return and operate on all values in the MutableMappingSequence + with the key `k`. + + Furthermore, `.pop()` without an argument should function as the + MutableSequence pop() function and pop the last value when considering + the MutableMappingSequence in a list-like manner. + """ + + @abstractmethod + def append(self, key, value): + pass + + @abstractmethod + def getall(self, key): + pass + + @abstractmethod + def popall(self, key): + pass + + +dict_setitem = dict.__setitem__ +dict_getitem = dict.__getitem__ +dict_delitem = dict.__delitem__ +dict_contains = dict.__contains__ +dict_clear = dict.clear + + +class MappingView(object): + def __init__(self, mapping): + self._mapping = mapping + + def __len__(self): + return len(self._mapping) + + def __repr__(self): + return "{!s}({!r})".format(type(self).__name__, self._mapping) + + +class KeysView(MappingView): + def __contains__(self, key): + return key in self._mapping + + def __iter__(self): + for key, _ in self._mapping: + yield key + + def __getitem__(self, index): + return self._mapping[index][0] + + def __repr__(self): + keys = [key for key, _ in self._mapping] + return "{!s}({!r})".format(type(self).__name__, keys) + + def index(self, key): + keys = [k for k, _ in self._mapping] + return keys.index(key) + + +class ItemsView(MappingView): + def __contains__(self, item): + key, value = item + return value in self._mapping.getlist(key) + + def __iter__(self): + for item in self._mapping: + yield item + + def __getitem__(self, index): + return self._mapping[index] + + def index(self, item): + items = [i for i in self._mapping] + return items.index(item) + + +class ValuesView(MappingView): + def __contains__(self, value): + for _, v in self._mapping: + if v == value: + return True + return False + + def __iter__(self): + for _, value in self._mapping: + yield value + + def __getitem__(self, index): + return self._mapping[index][1] + + def __repr__(self): + values = [value for _, value in self._mapping] + return "{!s}({!r})".format(type(self).__name__, values) + + def index(self, value): + values = [val for _, val in self._mapping] + return values.index(value) + + +class OrderedMultiDict(dict, MutableMappingSequence): + """A ``dict`` like container. + + This container preserves the original ordering as well as + allows multiple values for the same key. It provides similar + semantics to a ``list`` of ``tuples`` but with ``dict`` style + access. + + Using ``__setitem__`` syntax overwrites all fields with the + same key and ``__getitem__`` will return the first value with + the key. + """ + + def __init__(self, *args, **kwargs): + self.__items = [] + self.extend(*args, **kwargs) + + def __setitem__(self, key, value): + if key not in self: + return self.append(key, value) + + dict_setitem(self, key, [value]) + iteritems = iter(self.__items) + + for index, (old_key, old_value) in enumerate(iteritems): + if old_key == key: + # replace first occurrence + self.__items[index] = (key, value) + break + + tail = [item for item in iteritems if item[0] != key] + self.__items[index + 1:] = tail + + def __getitem__(self, key): + if isinstance(key, (int, slice)): + return self.__items[key] + return dict_getitem(self, key)[0] + + def __delitem__(self, key): + dict_delitem(self, key) + self.__items = [item for item in self.__items if item[0] != key] + + def __iter__(self): + return iter(self.__items) + + def __len__(self): + return len(self.__items) + + def __eq__(self, other): + if not isinstance(other, type(self)): + return False + + if len(self) != len(other): + return False + + items1 = self.items() + items2 = other.items() + + for ((key1, value1), (key2, value2)) in zip(items1, items2): + if key1 != key2: + return False + + if value1 != value2: + return False + + return True + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + if not self.__items: + return "{!s}([])".format(type(self).__name__) + + lines = [] + for item in self.__items: + for line in pprint.pformat(item).splitlines(): + lines.append(" " + line) + + return "{!s}([\n{!s}\n])".format(type(self).__name__, "\n".join(lines)) + + get = abc.MutableMapping.get + update = abc.MutableMapping.update + + def keys(self): + return KeysView(self) + + def values(self): + return ValuesView(self) + + def items(self): + return ItemsView(self) + + def clear(self): + dict_clear(self) + self.__items = [] + + def discard(self, key): + + warnings.warn( + "The discard(k) function is deprecated in favor of .popall(k), " + "please begin using it, as .discard(k) may be removed in the " + "next major patch.", + PendingDeprecationWarning, + ) + + try: + del self[key] + except KeyError: + pass + + def append(self, key, value): + """Adds a (name, value) pair, doesn't overwrite the value if + it already exists. + """ + self.__items.append((key, value)) + + try: + dict_getitem(self, key).append(value) + except KeyError: + dict_setitem(self, key, [value]) + + def extend(self, *args, **kwargs): + """Add key value pairs for an iterable.""" + if len(args) > 1: + raise TypeError(f"expected at most 1 arguments, got {len(args)}") + + iterable = args[0] if args else None + if iterable: + if isinstance(iterable, abc.Mapping) or hasattr(iterable, "items"): + for key, value in iterable.items(): + self.append(key, value) + else: + for key, value in iterable: + self.append(key, value) + + for key, value in kwargs.items(): + self.append(key, value) + + def getall(self, key) -> abc.Sequence: + """Returns a list of all the values for a named field. + Returns KeyError if the key doesn't exist. + """ + return list(dict_getitem(self, key)) + + def getlist(self, key) -> abc.Sequence: + """Returns a list of all the values for the named field. + Returns an empty list if the key doesn't exist. + """ + warnings.warn( + "The getlist() function is deprecated in favor of .getall(), " + "please begin using it, as .getlist() may be removed in the " + "next major patch.", + PendingDeprecationWarning, + ) + + try: + return self.getall(key) + except KeyError: + return [] + + # Turns out that this super-class function, even though it doesn't have + # a concept of multiple keys, clears out multiple elements with the key, + # probably because of how __delitem__ is defined: + popall = abc.MutableMapping.pop + + def pop(self, *args, **kwargs): + """Removes all items with the specified *key*.""" + + if len(args) == 0 and len(kwargs) == 0: + if not self: + raise KeyError( + "pop(): {!s} ".format(type(self).__name__) + "is empty" + ) + + key, _ = item = self.__items.pop() + values = dict_getitem(self, key) + values.pop() + + if not values: + dict_delitem(self, key) + + return item + + warnings.warn( + "The pop(k) function removes " + "all keys with value k to remain backwards compatible with the " + "pvl 0.x architecture, this concept of " + "operations for .pop(k) may change in future versions. " + "Consider using .popall(k) instead.", + FutureWarning, + ) + + return self.popall(*args, *kwargs) + + def popitem(self): + + warnings.warn( + "The popitem() function removes " + "and returns the last key, value pair to remain backwards " + "compatible with the pvl 0.x architecture, this concept of " + "operations for .popitem() may change in future versions. " + "Consider using the list-like .pop(), without an argument instead.", + FutureWarning, + ) + return self.pop() + + def copy(self): + return type(self)(self) + + def insert(self, index: int, *args) -> None: + """Inserts at the index given by *index*. + + The first positional argument will always be taken as the + *index* for insertion. + + If three arguments are given, the second will be taken + as the *key*, and the third as the *value* to insert. + + If only two arguments are given, the second must be a sequence. + + If it is a sequence of pairs (such that every item in the sequence is + itself a sequence of length two), that sequence will be inserted + as key, value pairs. + + If it happens to be a sequence of two items (the first of which is + not a sequence), the first will be taken as the *key* and the + second the *value* to insert. + """ + + if not isinstance(index, int): + raise TypeError( + "The first positional argument to pvl.MultiDict.insert()" + "must be an int." + ) + + kvlist = _insert_arg_helper(args) + + for (key, value) in kvlist: + self.__items.insert(index, (key, value)) + index += 1 + + # Make sure indexing works with the new item + if key in self: + value_list = [val for k, val in self.__items if k == key] + dict_setitem(self, key, value_list) + else: + dict_setitem(self, key, [value]) + + return + + 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: + raise KeyError(str(key)) + + idxs = list() + for idx, k in enumerate(self.keys()): + if key == k: + idxs.append(idx) + + try: + return idxs[instance] + except IndexError: + raise IndexError( + f"There are only {len(idxs)} elements with the key {key}, " + f"the provided index ({instance}) is out of bounds." + ) + + def insert_after(self, key, new_item: abc.Iterable, instance=0): + """Insert an item after a key""" + index = self.key_index(key, instance) + self.insert(index + 1, new_item) + + def insert_before(self, key, new_item: abc.Iterable, instance=0): + """Insert an item before a key""" + index = self.key_index(key, instance) + self.insert(index, new_item) + + +def _insert_arg_helper(args): + # Helper function to un-mangle the many and various ways that + # key, value pairs could be provided to the .insert() functions. + # Takes all of them, and returns a list of key, value pairs, even + # if there is only one. + if len(args) == 1: + if not isinstance(args, (abc.Sequence, abc.Mapping)): + raise TypeError( + "If a single argument is provided to the second positional " + "argument of insert(), it must have a Sequence or Mapping " + f"interface. Instead it was {type(args)}: {args}" + ) + + if isinstance(args[0], abc.Mapping): + return list(args[0].items()) + + else: + if len(args[0]) == 2 and ( + isinstance(args[0][0], str) + or not isinstance(args[0][0], abc.Sequence) + ): + kvlist = (args[0],) + else: + for pair in args[0]: + msg = ( + "One of the elements in the sequence passed to the " + "second argument of insert() " + ) + if not isinstance(pair, abc.Sequence): + raise TypeError( + msg + f"was not itself a sequence, it is: {pair}" + ) + if not len(pair) == 2: + raise TypeError( + msg + f"was not a pair of values, it is: {pair}" + ) + + kvlist = args[0] + + elif len(args) == 2: + kvlist = (args,) + else: + raise TypeError( + f"insert() takes 2 or 3 positional arguments ({len(args)} given)." + ) + + return kvlist + + +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 + + class PVLMultiDict(MultiDict, MutableMappingSequence): + """This is a new class that may be implemented as the default + structure to be returned from the pvl loaders in the future (replacing + OrderedMultiDict). + + Here is a summary of the differences: + + * OrderedMultiDict.getall('k') where k is not in the structure returns + an empty list, PVLMultiDict.getall('k') properly returns a KeyError. + * The .items(), .keys(), and .values() are proper iterators + and don't return sequences like OrderedMultiDict did. + * Calling list() on an OrderedMultiDict returns a list of tuples, which + is like calling list() on the results of a dict.items() iterator. + Calling list() on a PVLMultiDict returns just a list of keys, + which is semantically identical to calling list() on a dict. + * OrderedMultiDict.pop(k) removed all keys that matched k, + PVLMultiDict.pop(k) just removes the first occurrence. + PVLMultiDict.popall(k) would pop all. + * OrderedMultiDict.popitem() removes the last item from the underlying + list, PVLMultiDict.popitem() removes an arbitrary key, value pair, + semantically identical to .popitem() on a dict. + * OrderedMultiDict.__repr__() and .__str__() return identical strings, + PVLMultiDict provides a .__str__() that is pretty-printed similar + to OrderedMultiDict, but also a .__repr__() with a more compact + representation. + * Equality is different: OrderedMultiDict has an isinstance() + check in the __eq__() operator, which I don't think was right, + since equality is about values, not about type. PVLMultiDict + has a value-based notion of equality. So an empty PVLGroup and an + empty PVLObject derived from PVLMultiDict could test equal, + but would fail an isinstance() check. + """ + + # Also evaluated the boltons.OrderedMultiDict, but its semantics were + # too different #52 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __getitem__(self, key): + # Allow list-like access of the underlying structure + if isinstance(key, (int, slice)): + return list(self.items())[key] + return super().__getitem__(key) + + def __repr__(self): + if len(self) == 0: + return f"{self.__class__.__name__}()" + + return ( + f"{self.__class__.__name__}(" + str(list(self.items())) + ")" + ) + + def __str__(self): + if len(self) == 0: + return self.__repr__() + + lines = [] + for item in self.items(): + for line in pprint.pformat(item).splitlines(): + lines.append(" " + line) + + return f"{self.__class__.__name__}([\n" + "\n".join(lines) + "\n])" + + def key_index(self, key, ith: int = 0) -> int: + """Returns the index of the item in the underlying list + implementation that is the *ith* value of that *key*. + + Effectively creates a list of all indexes that match *key*, and then + returns the original index of the *ith* element of that list. The + *ith* integer can be any positive or negative integer and follows + the rules for list indexes. + """ + if key not in self: + raise KeyError(str(key)) + idxs = list() + for idx, (k, v) in enumerate(self.items()): + if key == k: + idxs.append(idx) + + try: + return idxs[ith] + except IndexError: + raise IndexError( + f"There are only {len(idxs)} elements with the key {key}, " + f"the provided index ({ith}) is out of bounds." + ) + + def _insert_item( + self, key, new_item: abc.Iterable, instance: int, is_after: bool + ): + """Insert a new item before or after another item.""" + index = self.key_index(key, instance) + index = index + 1 if is_after else index + + if isinstance(new_item, abc.Mapping): + tuple_iter = tuple(new_item.items()) + else: + tuple_iter = new_item + self.insert(index, tuple_iter) + + def insert(self, index: int, *args) -> None: + """Inserts at the index given by *index*. + + The first positional argument will always be taken as the + *index* for insertion. + + If three arguments are given, the second will be taken + as the *key*, and the third as the *value* to insert. + + If only two arguments are given, the second must be a sequence. + + If it is a sequence of pairs (such that every item in the sequence + is itself a sequence of length two), that sequence will be inserted + as key, value pairs. + + If it happens to be a sequence of two items (the first of which is + not a sequence), the first will be taken as the *key* and the + second the *value* to insert. + """ + if not isinstance(index, int): + raise TypeError( + "The first positional argument to pvl.MultiDict.insert()" + "must be an int." + ) + + kvlist = _insert_arg_helper(args) + + for (key, value) in kvlist: + identity = self._title(key) + self._impl._items.insert( + index, (identity, self._key(key), value) + ) + self._impl.incr_version() + index += 1 + return + + def insert_after(self, key, new_item, instance=0): + """Insert an item after a key""" + self._insert_item(key, new_item, instance, True) + + def insert_before(self, key, new_item, instance=0): + """Insert an item before a key""" + self._insert_item(key, new_item, instance, False) + + def pop(self, *args, **kwargs): + """Returns a two-tuple or a single value, depending on how it is + called. + + If no arguments are given, it removes and returns the last key, + value pair (list-like behavior). + + If a *key* is given, the first instance of key is found and its + value is removed and returned. If *default* is not given and + *key* is not in the dictionary, a KeyError is raised, otherwise + *default* is returned (dict-like behavior). + """ + if len(args) == 0 and len(kwargs) == 0: + i, k, v = self._impl._items.pop() + self._impl.incr_version() + return i, v + else: + return super().pop(*args, **kwargs) + + def append(self, key, value): + # Not sure why super() decided to go with the set-like add() instead + # of the more appropriate list-like append(). Fixed it for them. + self.add(key, value) + + # New versions based on PVLMultiDict + class PVLModuleNew(PVLMultiDict): + pass + + class PVLAggregationNew(PVLMultiDict): + pass + + class PVLGroupNew(PVLAggregationNew): + pass + + class PVLObjectNew(PVLAggregationNew): + pass + + +except ImportError: + warnings.warn( + "The multidict library is not present, so the new PVLMultiDict " + "cannot be used. At this time, it is completely optional, and doesn't " + "impact the use of pvl.", + ImportWarning, + ) + + +class PVLModule(OrderedMultiDict): + pass + + +class PVLAggregation(OrderedMultiDict): + pass + + +class PVLGroup(PVLAggregation): + pass + + +class PVLObject(PVLAggregation): + pass + + +class Quantity(namedtuple("Quantity", ["value", "units"])): + """A simple collections.namedtuple object to contain + a value and units parameter. + + If you need more comprehensive units handling, you + may want to use the astropy.units.Quantity object, + the pint.Quantity object, or some other 3rd party + object. Please see the documentation on :doc:`quantities` + for how to use 3rd party Quantity objects with pvl. + """ + + def __int__(self): + return int(self.value) + + def __float__(self): + return float(self.value) + + +class Units(Quantity): + warnings.warn( + "The pvl.collections.Units object is deprecated, and may be removed at " + "the next major patch. Please use pvl.collections.Quantity instead.", + PendingDeprecationWarning, + ) diff --git a/hirise_blender/pvl/decoder.py b/hirise_blender/pvl/decoder.py new file mode 100644 index 0000000..c98dd86 --- /dev/null +++ b/hirise_blender/pvl/decoder.py @@ -0,0 +1,552 @@ +# -*- coding: utf-8 -*- +"""Parameter Value Language decoder. + +The definition of PVL used in this module is based on the Consultive +Committee for Space Data Systems, and their Parameter Value +Language Specification (CCSD0006 and CCSD0008), CCSDS 6441.0-B-2, +referred to as the Blue Book with a date of June 2000. + +A decoder deals with converting strings given to it (typically +by the parser) to the appropriate Python type. +""" +# 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 re +from datetime import datetime, timedelta, timezone +from decimal import InvalidOperation +from itertools import repeat, chain +from warnings import warn + +from .grammar import PVLGrammar, ODLGrammar, PDSGrammar +from .collections import Quantity +from .exceptions import QuantityError + + +def for_try_except(exception, function, *iterable): + """Return the result of the first successful application of *function* + to an element of *iterable*. If the *function* raises an Exception + of type *exception*, it will continue to the next item of *iterable*. + If there are no successful applications an Exception of type + *exception* will be raised. + + If additional *iterable* arguments are passed, *function* must + take that many arguments and is applied to the items from + all iterables in parallel (like ``map()``). With multiple iterables, + the iterator stops when the shortest iterable is exhausted. + """ + for tup in zip(*iterable): + try: + return function(*tup) + except exception: + pass + + raise exception + + +class PVLDecoder(object): + """A decoder based on the rules in the CCSDS-641.0-B-2 'Blue Book' + which defines the PVL language. + + :param grammar: defaults to a :class:`pvl.grammar.PVLGrammar`, but can + be any object that implements the :class:`pvl.grammar` interface. + + :param quantity_cls: defaults to :class:`pvl.collections.Quantity`, but + could be any class object that takes two arguments, where the + first is the value, and the second is the units value. + + :param real_cls: defaults to :class:`float`, but could be any class object + that can be constructed from a `str` object. + """ + + def __init__(self, grammar=None, quantity_cls=None, real_cls=None): + self.errors = [] + + if grammar is None: + self.grammar = PVLGrammar() + elif isinstance(grammar, PVLGrammar): + self.grammar = grammar + else: + raise Exception + + if quantity_cls is None: + self.quantity_cls = Quantity + else: + self.quantity_cls = quantity_cls + + if real_cls is None: + self.real_cls = float + else: + self.real_cls = real_cls + + def decode(self, value: str): + """Returns a Python object based on *value*.""" + return self.decode_simple_value(value) + + def decode_simple_value(self, value: str): + """Returns a Python object based on *value*, assuming + that *value* can be decoded as a PVL Simple Value:: + + ::= ( | | ) + """ + if value.casefold() == self.grammar.none_keyword.casefold(): + return None + + if value.casefold() == self.grammar.true_keyword.casefold(): + return True + + if value.casefold() == self.grammar.false_keyword.casefold(): + return False + + for d in ( + self.decode_quoted_string, + self.decode_non_decimal, + self.decode_decimal, + self.decode_datetime, + ): + try: + return d(value) + except ValueError: + pass + + return self.decode_unquoted_string(value) + + def decode_unquoted_string(self, value: str) -> str: + """Returns a Python ``str`` if *value* can be decoded + as an unquoted string, based on this decoder's grammar. + Raises a ValueError otherwise. + """ + for coll in ( + ("a comment", chain.from_iterable(self.grammar.comments)), + ("some whitespace", self.grammar.whitespace), + ("a special character", self.grammar.reserved_characters), + ): + for item in coll[1]: + if item in value: + raise ValueError( + "Expected a Simple Value, but encountered " + f'{coll[0]} in "{self}": "{item}".' + ) + + agg_keywords = self.grammar.aggregation_keywords.items() + for kw in chain.from_iterable(agg_keywords): + if kw.casefold() == value.casefold(): + raise ValueError( + "Expected a Simple Value, but encountered " + f'an aggregation keyword: "{value}".' + ) + + for es in self.grammar.end_statements: + if es.casefold() == value.casefold(): + raise ValueError( + "Expected a Simple Value, but encountered " + f'an End-Statement: "{value}".' + ) + + # This try block is going to look illogical. But the decode + # rules for Unquoted Strings spell out the things that they + # cannot be, so if it *can* be a datetime, then it *can't* be + # an Unquoted String, which is why we raise if it succeeds, + # and pass if it fails: + try: + self.decode_datetime(value) + raise ValueError + except ValueError: + pass + + return str(value) + + def decode_quoted_string(self, value: str) -> str: + """Returns a Python ``str`` if *value* begins and ends + with matching quote characters based on this decoder's + grammar. Raises ValueError otherwise. + """ + for q in self.grammar.quotes: + if value.startswith(q) and value.endswith(q) and len(value) > 1: + return str(value[1:-1]) + raise ValueError(f'The object "{value}" is not a PVL Quoted String.') + + def decode_decimal(self, value: str): + """Returns a Python ``int`` or ``self.real_cls`` object, as appropriate + based on *value*. Raises a ValueError otherwise. + """ + # Returns int or real_cls + try: + return int(value, base=10) + except ValueError: + try: + return self.real_cls(str(value)) + except InvalidOperation as err: + raise ValueError from err + + def decode_non_decimal(self, value: str) -> int: + """Returns a Python ``int`` as decoded from *value* + on the assumption that *value* conforms to a + non-decimal integer value as defined by this decoder's + grammar, raises ValueError otherwise. + """ + # Non-Decimal (Binary, Hex, and Octal) + for nd_re in ( + self.grammar.binary_re, + self.grammar.octal_re, + self.grammar.hex_re, + ): + match = nd_re.fullmatch(value) + if match is not None: + d = match.groupdict("") + return int(d["sign"] + d["non_decimal"], base=int(d["radix"])) + raise ValueError + + def decode_datetime(self, value: str): # noqa: C901 + """Takes a string and attempts to convert it to the appropriate + Python ``datetime`` ``time``, ``date``, or ``datetime`` + type based on this decoder's grammar, or in one case, a ``str``. + + The PVL standard allows for the seconds value to range + from zero to 60, so that the 60 can accommodate leap + seconds. However, the Python ``datetime`` classes don't + support second values for more than 59 seconds. + + If a time with 60 seconds is encountered, it will not be + returned as a datetime object (since that is not representable + via Python datetime objects), but simply as a string. + + The user can then then try and use the ``time`` module + to parse this string into a ``time.struct_time``. We + chose not to do this with pvl because ``time.struct_time`` + is a full *datetime* like object, even if it parsed + only a *time* like object, the year, month, and day + values in the ``time.struct_time`` would default, which + could be misleading. + + Alternately, the pvl.grammar.PVLGrammar class contains + two regexes: ``leap_second_Ymd_re`` and ``leap_second_Yj_re`` + which could be used along with the ``re.match`` object's + ``groupdict()`` function to extract the string representations + of the various numerical values, cast them to the appropriate + numerical types, and do something useful with them. + """ + try: + # datetime.date objects will always be naive, so just return: + return for_try_except( + ValueError, + datetime.strptime, + repeat(value), + self.grammar.date_formats, + ).date() + except ValueError: + # datetime.time and datetime.datetime might be either: + d = None + try: + d = for_try_except( + ValueError, + datetime.strptime, + repeat(value), + self.grammar.time_formats, + ).time() + except ValueError: + try: + d = for_try_except( + ValueError, + datetime.strptime, + repeat(value), + self.grammar.datetime_formats, + ) + except ValueError: + pass + if d is not None: + if d.utcoffset() is None: + if value.endswith("Z"): + return d.replace(tzinfo=timezone.utc) + elif self.grammar.default_timezone is not None: + return d.replace(tzinfo=self.grammar.default_timezone) + return d + + # if we can regex a 60-second time, return str + if self.is_leap_seconds(value): + return str(value) + else: + raise ValueError + + def is_leap_seconds(self, value: str) -> bool: + """Returns True if *value* is a time that matches the + grammar's definition of a leap seconds time (a time string with + a value of 60 for the seconds value). False otherwise.""" + for r in ( + self.grammar.leap_second_Ymd_re, + self.grammar.leap_second_Yj_re, + ): + if r is not None and r.fullmatch(value) is not None: + return True + else: + return False + + def decode_quantity(self, value, unit): + """Returns a Python object that represents a value with + an associated unit, based on the values provided via + *value* and *unit*. This function creates an object + based on the decoder's *quantity_cls*. + """ + try: + return self.quantity_cls(value, str(unit)) + except ValueError as err: + raise QuantityError(err) + + +class ODLDecoder(PVLDecoder): + """A decoder based on the rules in the PDS3 Standards Reference + (version 3.8, 27 Feb 2009) Chapter 12: Object Description + Language Specification and Usage. + + Extends PVLDecoder, and if *grammar* is not specified, it will + default to an ODLGrammar() object. + """ + + def __init__(self, grammar=None, quantity_cls=None, real_cls=None): + self.errors = [] + + if grammar is None: + grammar = ODLGrammar() + + super().__init__( + grammar=grammar, + quantity_cls=quantity_cls, + real_cls=real_cls + ) + + def decode_datetime(self, value: str): + """Extends parent function to also deal with datetimes + and times with a time zone offset. + + If it cannot, it will raise a ValueError. + """ + + try: + return super().decode_datetime(value) + except ValueError: + # if there is a +HH:MM or a -HH:MM suffix that + # can be stripped, then we're in business. + # Otherwise ... + match = re.fullmatch( + r"(?P
.+?)" # the part before the sign + r"(?P[+-])" # required sign + r"(?P0?[0-9]|1[0-2])" # 0 to 12 + fr"(?:{self.grammar._M_frag})?", # Minutes + value, + ) + if match is not None: + gd = match.groupdict(default=0) + dt = super().decode_datetime(gd["dt"]) + offset = timedelta( + hours=int(gd["hour"]), minutes=int(gd["minute"]) + ) + if gd["sign"] == "-": + offset = -1 * offset + return dt.replace(tzinfo=timezone(offset)) + raise ValueError + + def decode_non_decimal(self, value: str) -> int: + """Extends parent function by allowing the wider variety of + radix values that ODL permits over PVL. + """ + match = self.grammar.nondecimal_re.fullmatch(value) + if match is not None: + d = match.groupdict("") + return int(d["sign"] + d["non_decimal"], base=int(d["radix"])) + raise ValueError + + def decode_quoted_string(self, value: str) -> str: + """Extends parent function because the + ODL specification allows for a dash (-) line continuation + character that results in the dash, the line end, and any + leading whitespace on the next line to be removed. It also + allows for a sequence of format effectors surrounded by + spacing characters to be collapsed to a single space. + """ + s = super().decode_quoted_string(value) + + # Deal with dash (-) continuation: + # sp = ''.join(self.grammar.spacing_characters) + fe = "".join(self.grammar.format_effectors) + ws = "".join(self.grammar.whitespace) + nodash = re.sub(fr"-[{fe}][{ws}]*", "", s) + + # Originally thought that only format effectors surrounded + # by whitespace was to be collapsed + # foo = re.sub(fr'[{sp}]*[{fe}]+[{sp}]*', ' ', nodash) + + # But really it collapses all whitespace and strips lead and trail. + return re.sub(fr"[{ws}]+", " ", nodash.strip(ws)) + + def decode_unquoted_string(self, value: str) -> str: + """Extends parent function to provide the extra enforcement that only + ODL Identifier text may be unquoted as a value. + """ + s = super().decode_unquoted_string(value) + + if self.is_identifier(s): + return s + else: + raise ValueError( + f"Only text that qualifies as an ODL Identifier may be " + f"unquoted as a value, and '{s}' is not." + ) + + @staticmethod + def is_identifier(value): + """Returns true if *value* is an ODL Identifier, false otherwise. + + An ODL Identifier is composed of letters, digits, and underscores. + The first character must be a letter, and the last must not + be an underscore. + """ + if isinstance(value, str): + if len(value) == 0: + return False + + try: + # Ensure we're dealing with ASCII + value.encode(encoding="ascii") + + # value can't start with a letter or end with an underbar + if not value[0].isalpha() or value.endswith("_"): + return False + + for c in value: + if not (c.isalpha() or c.isdigit() or c == "_"): + return False + else: + return True + + except UnicodeError: + return False + else: + return False + + +class PDSLabelDecoder(ODLDecoder): + """A decoder based on the rules in the PDS3 Standards Reference + (version 3.8, 27 Feb 2009) Chapter 12: Object Description + Language Specification and Usage. + + Extends ODLDecoder, and if *grammar* is not specified, it will + default to a PDS3Grammar() object. + """ + + def __init__(self, grammar=None, quantity_cls=None): + self.errors = [] + + if grammar is None: + super().__init__(grammar=PDSGrammar(), quantity_cls=quantity_cls) + else: + super().__init__(grammar=grammar, quantity_cls=quantity_cls) + + def decode_datetime(self, value: str): + """Overrides parent function since PDS3 forbids a timezone + specification, and times with a precision more than miliseconds. + + If it cannot decode properly, it will raise a ValueError. + """ + + t = super(ODLDecoder, self).decode_datetime(value) + + if ( + hasattr(t, "microsecond") + and t.microsecond != round(t.microsecond / 1000) * 1000 + ): + raise ValueError( + f"The PDS specification does not allow time values with" + f"precision greater than miliseconds, and this has " + f"microsecond precision: {t}." + ) + + return t + + +class OmniDecoder(ODLDecoder): + """A permissive decoder that attempts to parse all forms of + "PVL" that are thrown at it. + + Extends ODLDecoder. + """ + + def decode_non_decimal(self, value: str) -> int: + """Extends parent function by allowing a plus or + minus sign to be in two different positions + in a non-decimal number, since PVL has one + specification, and ODL has another. + """ + # Non-Decimal with a variety of radix values and sign + # positions. + match = self.grammar.nondecimal_re.fullmatch(value) + if match is not None: + d = match.groupdict("") + if "second_sign" in d: + if d["sign"] != "" and d["second_sign"] != "": + raise ValueError( + f'The non-decimal value, "{value}", ' "has two signs." + ) + elif d["sign"] != "": + sign = d["sign"] + else: + sign = d["second_sign"] + else: + sign = d["sign"] + + return int(sign + d["non_decimal"], base=int(d["radix"])) + raise ValueError + + def decode_datetime(self, value: str): + """Returns an appropriate Python datetime time, date, or datetime + object by using the 3rd party dateutil library (if present) + to parse an ISO 8601 datetime string in *value*. If it cannot, + or the dateutil library is not present, it will raise a + ValueError. + """ + + try: + return super().decode_datetime(value) + except ValueError: + try: + from dateutil.parser import isoparser + + isop = isoparser() + + if len(value) > 3 and value[-2] == "+" and value[-1].isdigit(): + # This technically means that we accept slightly more + # formats than ISO 8601 strings, since under that + # specification, two digits after the '+' are required + # for an hour offset, but ODL doesn't have this + # requirement. If we find only one digit, we'll + # just assume it means an hour and insert a zero so + # that it can be parsed. + tokens = value.rpartition("+") + value = tokens[0] + "+0" + tokens[-1] + + try: + return isop.parse_isodate(value) + except ValueError: + try: + return isop.parse_isotime(value) + except ValueError: + return isop.isoparse(value) + + except ImportError: + warn( + "The dateutil library is not present, so more " + "exotic date and time formats beyond the PVL/ODL " + "set cannot be parsed.", + ImportWarning, + ) + + raise ValueError + + def decode_unquoted_string(self, value: str) -> str: + """Overrides parent function since the ODLDecoder has a more narrow + definition of what is allowable as an unquoted string than the + PVLDecoder does. + """ + return super(ODLDecoder, self).decode_unquoted_string(value) diff --git a/hirise_blender/pvl/encoder.py b/hirise_blender/pvl/encoder.py new file mode 100644 index 0000000..2a77ac3 --- /dev/null +++ b/hirise_blender/pvl/encoder.py @@ -0,0 +1,1178 @@ +# -*- coding: utf-8 -*- +"""Parameter Value Langage encoder. + +An encoder deals with converting Python objects into +string values that conform to a PVL specification. +""" + +# Copyright 2015, 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 datetime +import re +import textwrap + +from collections import abc, namedtuple +from decimal import Decimal +from warnings import warn + +from .collections import PVLObject, PVLGroup, Quantity +from .grammar import PVLGrammar, ODLGrammar, PDSGrammar, ISISGrammar +from .token import Token +from .decoder import PVLDecoder, ODLDecoder, PDSLabelDecoder + + +class QuantTup(namedtuple("QuantTup", ["cls", "value_prop", "units_prop"])): + """ + This class is just a convenient namedtuple for internally keeping track + of quantity classes that encoders can deal with. In general, users + should not be instantiating this, instead use your encoder's + add_quantity_cls() function. + """ + + +class PVLEncoder(object): + """An encoder based on the rules in the CCSDS-641.0-B-2 'Blue Book' + which defines the PVL language. + + :param grammar: A pvl.grammar object, if None or not specified, it will + be set to the grammar parameter of *decoder* (if + *decoder* is not None) or will default to PVLGrammar(). + :param grammar: defaults to pvl.grammar.PVLGrammar(). + :param decoder: defaults to pvl.decoder.PVLDecoder(). + :param indent: specifies the number of spaces that will be used to + indent each level of the PVL document, when Groups or Objects + are encountered, defaults to 2. + :param width: specifies the number of characters in width that each + line should have, defaults to 80. + :param aggregation_end: when True the encoder will print the value + of the aggregation's Block Name in the End Aggregation Statement + (e.g. END_GROUP = foo), and when false, it won't (e.g. END_GROUP). + Defaults to True. + :param end_delimiter: when True the encoder will print the grammar's + delimiter (e.g. ';' for PVL) after each statement, when False + it won't. Defaults to True. + :param newline: is the string that will be placed at the end of each + 'line' of output (and counts against *width*), defaults to '\\\\n'. + :param group_class: must this class will be tested against with + isinstance() to determine if various elements of the dict-like + passed to encode() should be encoded as a PVL Group or PVL Object, + defaults to PVLGroup. + :param object_class: must be a class that can take a *group_class* + object in its constructor (essentially converting a *group_class* + to an *object_class*), otherwise will raise TypeError. Defaults + to PVLObject. + """ + + def __init__( + self, + grammar=None, + decoder=None, + indent: int = 2, + width: int = 80, + aggregation_end: bool = True, + end_delimiter: bool = True, + newline: str = "\n", + group_class=PVLGroup, + object_class=PVLObject, + ): + + if grammar is None: + if decoder is not None: + self.grammar = decoder.grammar + else: + self.grammar = PVLGrammar() + elif isinstance(grammar, PVLGrammar): + self.grammar = grammar + else: + raise Exception + + if decoder is None: + self.decoder = PVLDecoder(self.grammar) + elif isinstance(decoder, PVLDecoder): + self.decoder = decoder + else: + raise Exception + + self.indent = indent + self.width = width + self.end_delimiter = end_delimiter + self.aggregation_end = aggregation_end + self.newline = newline + + # This list of 3-tuples *always* has our own pvl quantity object, + # and should *only* be added to with self.add_quantity_cls(). + self.quantities = [QuantTup(Quantity, "value", "units")] + self._import_quantities() + + if issubclass(group_class, abc.Mapping): + self.grpcls = group_class + else: + raise TypeError("The group_class must be a Mapping type.") + + if issubclass(object_class, abc.Mapping): + self.objcls = object_class + else: + raise TypeError("The object_class must be a Mapping type.") + + try: + self.objcls(self.grpcls()) + except TypeError: + raise TypeError( + f"The object_class type ({object_class}) cannot be " + f"instantiated with an argument that is of type " + f"group_class ({group_class})." + ) + + # Finally, let's keep track of everything we consider "numerical": + self.numeric_types = (int, float, self.decoder.real_cls, Decimal) + + def _import_quantities(self): + warn_str = ( + "The {} library is not present, so {} objects will " + "not be properly encoded." + ) + try: + from astropy import units as u + + self.add_quantity_cls(u.Quantity, "value", "unit") + except ImportError: + warn( + warn_str.format("astropy", "astropy.units.Quantity"), + ImportWarning, + ) + + try: + from pint import Quantity as q + + self.add_quantity_cls(q, "magnitude", "units") + except ImportError: + warn(warn_str.format("pint", "pint.Quantity"), ImportWarning) + + def add_quantity_cls(self, cls, value_prop: str, units_prop: str): + """Adds a quantity class to the list of possible + quantities that this encoder can handle. + + :param cls: The name of a quantity class that can be tested + with ``isinstance()``. + :param value_prop: A string that is the property name of + *cls* that contains the value or magnitude of the quantity + object. + :param units_prop: A string that is the property name of + *cls* that contains the units element of the quantity + object. + """ + if not isinstance(cls, type): + raise TypeError(f"The cls given ({cls}) is not a Python class.") + + # If a quantity object can't encode "one meter" its probably not + # going to work for us. + test_cls = cls(1, "m") + for prop in (value_prop, units_prop): + if not hasattr(test_cls, prop): + raise AttributeError( + f"The class ({cls}) does not have an " + f" attribute named {prop}." + ) + + self.quantities.append(QuantTup(cls, value_prop, units_prop)) + + def format(self, s: str, level: int = 0) -> str: + """Returns a string derived from *s*, which + has leading space characters equal to + *level* times the number of spaces specified + by this encoder's indent property. + + It uses the textwrap library to wrap long lines. + """ + + prefix = level * (self.indent * " ") + + if len(prefix + s + self.newline) > self.width and "=" in s: + (preq, _, posteq) = s.partition("=") + new_prefix = prefix + preq.strip() + " = " + + lines = textwrap.wrap( + posteq.strip(), + width=(self.width - len(self.newline)), + replace_whitespace=False, + initial_indent=new_prefix, + subsequent_indent=(" " * len(new_prefix)), + break_long_words=False, + break_on_hyphens=False, + ) + return self.newline.join(lines) + else: + return prefix + s + + def encode(self, module: abc.Mapping) -> str: + """Returns a ``str`` formatted as a PVL document based + on the dict-like *module* object + according to the rules of this encoder. + """ + lines = list() + lines.append(self.encode_module(module, 0)) + + end_line = self.grammar.end_statements[0] + if self.end_delimiter: + end_line += self.grammar.delimiters[0] + + lines.append(end_line) + + # Final check to ensure we're sending out the right character set: + s = self.newline.join(lines) + + for i, c in enumerate(s): + if not self.grammar.char_allowed(c): + raise ValueError( + "Encountered a character that was not " + "a valid character according to the " + 'grammar: "{}", it is in: ' + '"{}"'.format(c, s[i - 5, i + 5]) + ) + + return self.newline.join(lines) + + def encode_module(self, module: abc.Mapping, level: int = 0) -> str: + """Returns a ``str`` formatted as a PVL module based + on the dict-like *module* object according to the + rules of this encoder, with an indentation level + of *level*. + """ + lines = list() + + # To align things on the equals sign, just need to normalize + # the non-aggregation key length: + + non_agg_key_lengths = list() + for k, v in module.items(): + if not isinstance(v, abc.Mapping): + non_agg_key_lengths.append(len(k)) + longest_key_len = max(non_agg_key_lengths, default=0) + + for k, v in module.items(): + if isinstance(v, abc.Mapping): + lines.append(self.encode_aggregation_block(k, v, level)) + else: + lines.append( + self.encode_assignment(k, v, level, longest_key_len) + ) + return self.newline.join(lines) + + def encode_aggregation_block( + self, key: str, value: abc.Mapping, level: int = 0 + ) -> str: + """Returns a ``str`` formatted as a PVL Aggregation Block with + *key* as its name, and its contents based on the + dict-like *value* object according to the + rules of this encoder, with an indentation level + of *level*. + """ + lines = list() + + if isinstance(value, self.grpcls): + agg_keywords = self.grammar.group_pref_keywords + elif isinstance(value, abc.Mapping): + agg_keywords = self.grammar.object_pref_keywords + else: + raise ValueError("The value {value} is not dict-like.") + + agg_begin = "{} = {}".format(agg_keywords[0], key) + if self.end_delimiter: + agg_begin += self.grammar.delimiters[0] + lines.append(self.format(agg_begin, level)) + + lines.append(self.encode_module(value, (level + 1))) + + agg_end = "" + if self.aggregation_end: + agg_end += "{} = {}".format(agg_keywords[1], key) + else: + agg_end += agg_keywords[1] + if self.end_delimiter: + agg_end += self.grammar.delimiters[0] + lines.append(self.format(agg_end, level)) + + return self.newline.join(lines) + + def encode_assignment( + self, key: str, value, level: int = 0, key_len: int = None + ) -> str: + """Returns a ``str`` formatted as a PVL Assignment Statement + with *key* as its Parameter Name, and its value based + on *value* object according to the rules of this encoder, + with an indentation level of *level*. It also allows for + an optional *key_len* which indicates the width in characters + that the Assignment Statement should be set to, defaults to + the width of *key*. + """ + if key_len is None: + key_len = len(key) + + s = "" + s += "{} = ".format(key.ljust(key_len)) + + enc_val = self.encode_value(value) + + if enc_val.startswith(self.grammar.quotes): + # deal with quoted lines that need to preserve + # newlines + s = self.format(s, level) + s += enc_val + + if self.end_delimiter: + s += self.grammar.delimiters[0] + + return s + else: + s += enc_val + + if self.end_delimiter: + s += self.grammar.delimiters[0] + + return self.format(s, level) + + def encode_value(self, value) -> str: + """Returns a ``str`` formatted as a PVL Value based + on the *value* object according to the rules of this encoder. + """ + try: + return self.encode_quantity(value) + except ValueError: + return self.encode_simple_value(value) + + def encode_quantity(self, value) -> str: + """Returns a ``str`` formatted as a PVL Value followed by + a PVL Units Expression if the *value* object can be + encoded this way, otherwise raise ValueError.""" + for (cls, v_prop, u_prop) in self.quantities: + if isinstance(value, cls): + return self.encode_value_units( + getattr(value, v_prop), getattr(value, u_prop) + ) + + raise ValueError( + f"The value object {value} could not be " + "encoded as a PVL Value followed by a PVL " + f"Units Expression, it is of type {type(value)}" + ) + + def encode_value_units(self, value, units) -> str: + """Returns a ``str`` formatted as a PVL Value from *value* + followed by a PVL Units Expressions from *units*.""" + value_str = self.encode_simple_value(value) + units_str = self.encode_units(str(units)) + return f"{value_str} {units_str}" + + def encode_simple_value(self, value) -> str: + """Returns a ``str`` formatted as a PVL Simple Value based + on the *value* object according to the rules of this encoder. + """ + if value is None: + return self.grammar.none_keyword + elif isinstance(value, (set, frozenset)): + return self.encode_set(value) + elif isinstance(value, list): + return self.encode_sequence(value) + elif isinstance( + value, (datetime.datetime, datetime.date, datetime.time) + ): + return self.encode_datetype(value) + elif isinstance(value, bool): + if value: + return self.grammar.true_keyword + else: + return self.grammar.false_keyword + elif isinstance(value, self.numeric_types): + return str(value) + elif isinstance(value, str): + return self.encode_string(value) + else: + raise TypeError(f"{value!r} is not serializable.") + + def encode_setseq(self, values: abc.Collection) -> str: + """This function provides shared functionality for + encode_sequence() and encode_set(). + """ + return ", ".join([self.encode_value(v) for v in values]) + + def encode_sequence(self, value: abc.Sequence) -> str: + """Returns a ``str`` formatted as a PVL Sequence based + on the *value* object according to the rules of this encoder. + """ + return "(" + self.encode_setseq(value) + ")" + + def encode_set(self, value: abc.Set) -> str: + """Returns a ``str`` formatted as a PVL Set based + on the *value* object according to the rules of this encoder. + """ + return "{" + self.encode_setseq(value) + "}" + + def encode_datetype(self, value) -> str: + """Returns a ``str`` formatted as a PVL Date/Time based + on the *value* object according to the rules of this encoder. + If *value* is not a datetime date, time, or datetime object, + it will raise TypeError. + """ + if isinstance(value, datetime.datetime): + return self.encode_datetime(value) + elif isinstance(value, datetime.date): + return self.encode_date(value) + elif isinstance(value, datetime.time): + return self.encode_time(value) + else: + raise TypeError(f"{value!r} is not a datetime type.") + + @staticmethod + def encode_date(value: datetime.date) -> str: + """Returns a ``str`` formatted as a PVL Date based + on the *value* object according to the rules of this encoder. + """ + return f"{value:%Y-%m-%d}" + + @staticmethod + def encode_time(value: datetime.time) -> str: + """Returns a ``str`` formatted as a PVL Time based + on the *value* object according to the rules of this encoder. + """ + s = f"{value:%H:%M}" + + if value.microsecond: + s += f":{value:%S.%f}" + elif value.second: + s += f":{value:%S}" + + return s + + def encode_datetime(self, value: datetime.datetime) -> str: + """Returns a ``str`` formatted as a PVL Date/Time based + on the *value* object according to the rules of this encoder. + """ + date = self.encode_date(value) + time = self.encode_time(value) + return date + "T" + time + + def needs_quotes(self, s: str) -> bool: + """Returns true if *s* must be quoted according to this + encoder's grammar, false otherwise. + """ + if any(c in self.grammar.whitespace for c in s): + return True + + if s in self.grammar.reserved_keywords: + return True + + tok = Token(s, grammar=self.grammar, decoder=self.decoder) + return not tok.is_unquoted_string() + + def encode_string(self, value) -> str: + """Returns a ``str`` formatted as a PVL String based + on the *value* object according to the rules of this encoder. + """ + s = str(value) + + if self.needs_quotes(s): + for q in self.grammar.quotes: + if q not in s: + return q + s + q + else: + raise ValueError( + "All of the quote characters, " + f"{self.grammar.quotes}, were in the " + f'string ("{s}"), so it could not be quoted.' + ) + else: + return s + + def encode_units(self, value: str) -> str: + """Returns a ``str`` formatted as a PVL Units Value based + on the *value* object according to the rules of this encoder. + """ + return ( + self.grammar.units_delimiters[0] + + value + + self.grammar.units_delimiters[1] + ) + + +class ODLEncoder(PVLEncoder): + """An encoder based on the rules in the PDS3 Standards Reference + (version 3.8, 27 Feb 2009) Chapter 12: Object Description + Language Specification and Usage for ODL only. This is + almost certainly not what you want. There are very rarely + cases where you'd want to use ODL that you wouldn't also want + to use the PDS Label restrictions, so you probably really want + the PDSLabelEncoder class, not this one. Move along. + + It extends PVLEncoder. + + :param grammar: defaults to pvl.grammar.ODLGrammar(). + :param decoder: defaults to pvl.decoder.ODLDecoder(). + :param end_delimiter: defaults to False. + :param newline: defaults to '\\\\r\\\\n'. + """ + + def __init__( + self, + grammar=None, + decoder=None, + indent=2, + width=80, + aggregation_end=True, + end_delimiter=False, + newline="\r\n", + group_class=PVLGroup, + object_class=PVLObject + ): + + if grammar is None: + grammar = ODLGrammar() + + if decoder is None: + decoder = ODLDecoder(grammar) + + if not callable(getattr(decoder, "is_identifier", None)): + raise TypeError( + f"The decoder for an ODLEncoder() must have the " + f"is_identifier() function, and this does not: {decoder}" + ) + + super().__init__( + grammar, + decoder, + indent, + width, + aggregation_end, + end_delimiter, + newline, + group_class=group_class, + object_class=object_class + ) + + def encode(self, module: abc.Mapping) -> str: + """Extends parent function, but ODL requires that there must be + a spacing or format character after the END statement and this + adds the encoder's ``newline`` sequence. + """ + s = super().encode(module) + return s + self.newline + + def is_scalar(self, value) -> bool: + """Returns a boolean indicating whether the *value* object + qualifies as an ODL 'scalar_value'. + + ODL defines a 'scalar-value' as a numeric_value, a + date_time_string, a text_string_value, or a symbol_value. + + For Python, these correspond to the following: + + * numeric_value: any of self.numeric_types, and Quantity whose value + is one of the self.numeric_types. + * date_time_string: datetime objects + * text_string_value: str + * symbol_value: str + + """ + for quant in self.quantities: + if isinstance(value, quant.cls): + if isinstance( + getattr(value, quant.value_prop), self.numeric_types + ): + return True + + scalar_types = ( + *self.numeric_types, + datetime.date, + datetime.datetime, + datetime.time, + str + ) + if isinstance(value, scalar_types): + return True + + return False + + def is_symbol(self, value) -> bool: + """Returns true if *value* is an ODL Symbol String, false otherwise. + + An ODL Symbol String is enclosed by single quotes + and may not contain any of the following characters: + + 1. The apostrophe, which is reserved as the symbol string delimiter. + 2. ODL Format Effectors + 3. Control characters + + This means that an ODL Symbol String is a subset of the PVL + quoted string, and will be represented in Python as a ``str``. + """ + if isinstance(value, str): + if "'" in value: # Item 1 + return False + + for fe in self.grammar.format_effectors: # Item 2 + if fe in value: + return False + + if len(value) > self.width / 2: + # This means that the string is long and it is very + # likely to get wrapped and have carriage returns, + # and thus "ODL Format Effectors" inserted later. + # Unfortunately, without knowing the width of the + # parameter term, and the current indent level, this + # still may end up being incorrect threshhold. + return False + + if value.isprintable() and len(value) > 0: # Item 3 + return True + else: + return False + + def needs_quotes(self, s: str) -> bool: + """Return true if *s* is an ODL Identifier, false otherwise. + + Overrides parent function. + """ + return not self.decoder.is_identifier(s) + + def is_assignment_statement(self, s) -> bool: + """Returns true if *s* is an ODL Assignment Statement, false otherwise. + + An ODL Assignment Statement is either an + element_identifier or a namespace_identifier + joined to an element_identifier with a colon. + """ + if self.decoder.is_identifier(s): + return True + + (ns, _, el) = s.partition(":") + + if self.decoder.is_identifier(ns) and self.decoder.is_identifier(el): + return True + + return False + + def encode_assignment(self, key, value, level=0, key_len=None) -> str: + """Overrides parent function by restricting the length of + keywords and enforcing that they be ODL Identifiers + and uppercasing their characters. + """ + + if key_len is None: + key_len = len(key) + + if len(key) > 30: + raise ValueError( + "ODL keywords must be 30 characters or less " + f"in length, this one is longer: {key}" + ) + + if ( + key.startswith("^") and self.is_assignment_statement(key[1:]) + ) or self.is_assignment_statement(key): + ident = key.upper() + else: + raise ValueError( + f'The keyword "{key}" is not a valid ODL ' "Identifier." + ) + + s = "{} = ".format(ident.ljust(key_len)) + s += self.encode_value(value) + + if self.end_delimiter: + s += self.grammar.delimiters[0] + + return self.format(s, level) + + def encode_sequence(self, value) -> str: + """Extends parent function, as ODL only allows one- and + two-dimensional sequences of ODL scalar_values. + """ + if len(value) == 0: + raise ValueError("ODL does not allow empty Sequences.") + + for v in value: # check the first dimension (list of elements) + if isinstance(v, list): + for i in v: # check the second dimension (list of lists) + if isinstance(i, list): + # Shouldn't be lists of lists of lists. + raise ValueError( + "ODL only allows one- and two- " + "dimensional Sequences, but " + f"this has more: {value}" + ) + elif not self.is_scalar(i): + raise ValueError( + "ODL only allows scalar_values " + f"within sequences: {v}" + ) + + elif not self.is_scalar(v): + raise ValueError( + "ODL only allows scalar_values within " f"sequences: {v}" + ) + + return super().encode_sequence(value) + + def encode_set(self, values) -> str: + """Extends parent function, ODL only allows sets to contain + scalar values. + """ + + if not all(map(self.is_scalar, values)): + raise ValueError( + f"ODL only allows scalar values in sets: {values}" + ) + + return super().encode_set(values) + + def encode_value(self, value): + """Extends parent function by only allowing Units Expressions for + numeric values. + """ + for quant in self.quantities: + if isinstance(value, quant.cls): + if isinstance( + getattr(value, quant.value_prop), + self.numeric_types + ): + return super().encode_value(value) + else: + raise ValueError( + "Unit expressions are only allowed " + f"following numeric values: {value}" + ) + + return super().encode_value(value) + + def encode_string(self, value): + """Extends parent function by appropriately quoting Symbol + Strings. + """ + if self.decoder.is_identifier(value): + return value + elif self.is_symbol(value): + return "'" + value + "'" + else: + return super().encode_string(value) + + def encode_time(self, value: datetime.time) -> str: + """Extends parent function since ODL allows a time zone offset + from UTC to be included, and otherwise recommends that times + be suffixed with a 'Z' to clearly indicate that they are in UTC. + """ + if value.tzinfo is None: + raise ValueError( + f"ODL cannot output local times, and this time does not " + f"have a timezone offset: {value}" + ) + + t = super().encode_time(value) + + if value.utcoffset() == datetime.timedelta(): + return t + "Z" + else: + td_str = str(value.utcoffset()) + (h, m, s) = td_str.split(":") + if s != "00": + raise ValueError( + "The datetime value had a timezone offset " + f"with seconds values ({value}) which is " + "not allowed in ODL." + ) + if m == "00": + return t + f"+{h:0>2}" + else: + return t + f"+{h:0>2}:{m}" + + return t + + def encode_units(self, value) -> str: + """Overrides parent function since ODL limits what characters + and operators can be present in Units Expressions. + """ + + # if self.is_identifier(value.strip('*/()-')): + if self.decoder.is_identifier(re.sub(r"[\s*/()-]", "", value)): + + if "**" in value: + exponents = re.findall(r"\*\*.+?", value) + for e in exponents: + if re.search(r"\*\*-?\d+", e) is None: + raise ValueError( + "The exponentiation operator (**) in " + f'this Units Expression "{value}" ' + "is not a decimal integer." + ) + + return ( + self.grammar.units_delimiters[0] + + value + + self.grammar.units_delimiters[1] + ) + else: + raise ValueError( + f'The value, "{value}", does not conform to ' + "the specification for an ODL Units Expression." + ) + + +class PDSLabelEncoder(ODLEncoder): + """An encoder based on the rules in the PDS3 Standards Reference + (version 3.8, 27 Feb 2009) Chapter 12: Object Description + Language Specification and Usage and writes out labels that + conform to the PDS 3 standards. + + It extends ODLEncoder. + + You are not allowed to chose *end_delimiter* or *newline* + as the parent class allows, because to be PDS-compliant, + those are fixed choices. However, in some cases, the PDS3 + Standards are asymmetric, allowing for a wider variety of + PVL-text on "read" and a more narrow variety of PVL-text + on "write". The default values of the PDSLabelEncoder enforce + those strict "write" rules, but if you wish to alter them, + but still produce PVL-text that would validate against the PDS3 + standard, you may alter them. + + :param convert_group_to_object: Defaults to True, meaning that + if a GROUP does not conform to the PDS definition of a + GROUP, then it will be written out as an OBJECT. If it is + False, then an exception will be thrown if incompatible + GROUPs are encountered. In PVL and ODL, the OBJECT and GROUP + aggregations are interchangeable, but the PDS applies + restrictions to what can appear in a GROUP. + :param tab_replace: Defaults to 4 and indicates the number of + space characters to replace horizontal tab characters with + (since tabs aren't allowed in PDS labels). If this is set + to zero, tabs will not be replaced with spaces. + :param symbol_single_quotes: Defaults to True, and if a Python `str` + object qualifies as a PVL Symbol String, it will be written to + PVL-text as a single-quoted string. If False, no special + handling is done, and any PVL Symbol String will be treated + as a PVL Text String, which is typically enclosed with double-quotes. + :param time_trailing_z: defaults to True, and suffixes a "Z" to + datetimes and times written to PVL-text as the PDS encoding + standard requires. If False, no trailing "Z" is written. + + """ + + def __init__( + self, + grammar=None, + decoder=None, + 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, + time_trailing_z=True, + ): + + if grammar is None: + grammar = PDSGrammar() + + if decoder is None: + decoder = PDSLabelDecoder(grammar) + + super().__init__( + grammar, + decoder, + indent, + width, + 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 + self.tab_replace = tab_replace + self.symbol_single_quote = symbol_single_quote + self.time_trailing_z = time_trailing_z + + def count_aggs( + self, module: abc.Mapping, obj_count: int = 0, grp_count: int = 0 + ) -> tuple((int, int)): + """Returns the count of OBJECT and GROUP aggregations + that are contained within the *module* as a two-tuple + in that order. + """ + # This currently just counts the values in the passed + # in module, it does not 'recurse' if those aggregations also + # may contain aggregations. + + for k, v in module.items(): + if isinstance(v, abc.Mapping): + if isinstance(v, self.grpcls): + grp_count += 1 + elif isinstance(v, self.objcls): + obj_count += 1 + else: + # We treat other dict-like Python objects as + # PVL Objects for the purposes of this count, + # because that is how they will be encoded. + obj_count += 1 + + return obj_count, grp_count + + def encode(self, module: abc.MutableMapping) -> str: + """Extends the parent function, by adding a restriction. + For PDS, if there are any GROUP elements, there must be at + least one OBJECT element in the label. Behavior here + depends on the value of this encoder's convert_group_to_object + property. + """ + (obj_count, grp_count) = self.count_aggs(module) + + if grp_count > 0 and obj_count < 1: + if self.convert_group_to_object: + for k, v in module.items(): + # First try to convert any GROUPs that would not + # be valid PDS GROUPs. + if isinstance(v, self.grpcls) and not self.is_PDSgroup(v): + module[k] = self.objcls(v) + break + else: + # Then just convert the first GROUP + for k, v in module.items(): + if isinstance(v, self.grpcls): + module[k] = self.objcls(v) + break + else: + raise ValueError( + "Couldn't convert any of the GROUPs " "to OBJECTs." + ) + else: + raise ValueError( + "This module has a GROUP element, but no " + "OBJECT elements, which is not allowed by " + "the PDS. You could set " + "*convert_group_to_object* to *True* on the " + "encoder to try and convert a GROUP " + "to an OBJECT." + ) + + s = super().encode(module) + if self.tab_replace > 0: + return s.replace("\t", (" " * self.tab_replace)) + else: + return s + + def is_PDSgroup(self, group: abc.Mapping) -> bool: + """Returns true if the dict-like *group* qualifies as a PDS Group, + false otherwise. + + PDS applies the following restrictions to GROUPS: + + 1. The GROUP structure may only be used in a data product + label which also contains one or more data OBJECT definitions. + 2. The GROUP statement must contain only attribute assignment + statements, include pointers, or related information pointers + (i.e., no data location pointers). If there are multiple + values, a single statement must be used with either sequence + or set syntax; no attribute assignment statement or pointer + may be repeated. + 3. GROUP statements may not be nested. + 4. GROUP statements may not contain OBJECT definitions. + 5. Only PSDD elements may appear within a GROUP statement. + *PSDD is not defined anywhere in the PDS document, so don't + know how to test for it.* + 6. The keyword contents associated with a specific GROUP + identifier must be identical across all labels of a single data + set (with the exception of the “PARAMETERS” GROUP, as + explained). + + Use of the GROUP structure must be coordinated with the + responsible PDS discipline Node. + + Items 1 & 6 and the final sentence above, can't really be tested + by examining a single group, but must be dealt with in a larger + context. The ODLEncoder.encode_module() handles #1, at least. + You're on your own for the other two issues. + + Item 5: *PSDD* is not defined anywhere in the ODL PDS document, + so don't know how to test for it. + """ + (obj_count, grp_count) = self.count_aggs(group) + + # Items 3 and 4: + if obj_count != 0 or grp_count != 0: + return False + + # Item 2, no data location pointers: + for k, v in group.items(): + if k.startswith("^"): + if isinstance(v, int): + return False + else: + for quant in self.quantities: + if isinstance(v, quant.cls) and isinstance( + getattr(v, quant.value_prop), int + ): + return False + + # Item 2, no repeated keys: + keys = list(group.keys()) + if len(keys) != len(set(keys)): + return False + + return True + + def encode_aggregation_block(self, key, value, level=0): + """Extends parent function because PDS has restrictions on + what may be in a GROUP. + + If the encoder's *convert_group_to_object* parameter is True, + and a GROUP does not conform to the PDS definition of a GROUP, + then it will be written out as an OBJECT. If it is False, + then an exception will be thrown. + """ + + # print('value at top:') + # print(value) + + if isinstance(value, self.grpcls) and not self.is_PDSgroup(value): + if self.convert_group_to_object: + value = self.objcls(value) + else: + raise ValueError( + "This GROUP element is not a valid PDS " + "GROUP. You could set " + "*convert_group_to_object* to *True* on the " + "encoder to try and convert the GROUP" + "to an OBJECT." + ) + + # print('value at bottom:') + # print(value) + + return super().encode_aggregation_block(key, value, level) + + def encode_set(self, values) -> str: + """Extends parent function because PDS only allows symbol values + and integers within sets. + """ + for v in values: + if not self.is_symbol(v) and not isinstance(v, int): + raise ValueError( + "The PDS only allows integers and symbols " + f"in sets: {values}" + ) + + return super().encode_set(values) + + def encode_string(self, value): + """Extends parent function to treat Symbol Strings as Text Strings, + which typically means that they are double-quoted and not + single-quoted. + """ + if self.decoder.is_identifier(value): + return value + elif self.is_symbol(value) and self.symbol_single_quote: + return "'" + value + "'" + else: + return super(ODLEncoder, self).encode_string(value) + + def encode_time(self, value: datetime.time) -> str: + """Overrides parent's encode_time() function because + even though ODL allows for timezones, PDS does not. + + Not in the section on times, but at the end of the PDS + ODL document, in section 12.7.3, para 14, it indicates that + alternate time zones may not be used in a PDS label, only + these: + 1. YYYY-MM-DDTHH:MM:SS.SSS. + 2. YYYY-DDDTHH:MM:SS.SSS. + + """ + s = f"{value:%H:%M}" + + if value.microsecond: + ms = round(value.microsecond / 1000) + if value.microsecond != ms * 1000: + raise ValueError( + f"PDS labels can only encode time values to the milisecond " + f"precision, and this time ({value}) has too much " + f"precision." + ) + else: + s += f":{value:%S}.{ms}" + elif value.second: + s += f":{value:%S}" + + if ( + value.tzinfo is None or + value.tzinfo.utcoffset(None) == datetime.timedelta(0) + ): + if self.time_trailing_z: + return s + "Z" + else: + return s + else: + raise ValueError( + "PDS labels should only have UTC times, but " + f"this time has a timezone: {value}" + ) + + +class ISISEncoder(PVLEncoder): + """An encoder for writing PVL text that can be parsed by the + ISIS PVL text parser. + + The ISIS3 implementation (as of 3.9) of PVL/ODL (like) does not + strictly follow any of the published standards. It was based + on PDS3 ODL from the 1990s, but has several extensions adopted + from existing and prior data sets from ISIS2, PDS, JAXA, ISRO, + ..., and extensions used only within ISIS files (cub, net). This + is one of the reasons using ISIS cube files or PVL text written by + ISIS as an archive format has been strongly discouraged. + + Since there is no specification, only a detailed analysis of + the ISIS software that parses and writes its PVL text would + yield a strategy for parsing it. This encoder is most likely the + least reliable for that reason. We welcome bug reports to help + extend our coverage of this flavor of PVL text. + + :param grammar: defaults to pvl.grammar.ISISGrammar(). + :param decoder: defaults to pvl.decoder.PVLDecoder(). + :param end_delimiter: defaults to False. + :param newline: defaults to '\\\\n'. + """ + + def __init__( + self, + grammar=None, + decoder=None, + indent=2, + width=80, + aggregation_end=True, + end_delimiter=False, + newline="\n", + group_class=PVLGroup, + object_class=PVLObject + ): + + if grammar is None: + grammar = ISISGrammar() + + if decoder is None: + decoder = PVLDecoder(grammar) + + super().__init__( + grammar, + decoder, + indent, + width, + aggregation_end, + end_delimiter, + newline, + group_class=group_class, + object_class=object_class + ) diff --git a/hirise_blender/pvl/exceptions.py b/hirise_blender/pvl/exceptions.py new file mode 100644 index 0000000..9a87feb --- /dev/null +++ b/hirise_blender/pvl/exceptions.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +""" +Exceptions for the Parameter Value Library. +""" + +# Copyright 2019-2020, ``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. + + +def firstpos(sub: str, pos: int): + """On the assumption that *sub* is a substring contained in a longer + string, and *pos* is the index in that longer string of the final + character in sub, returns the position of the first character of + sub in that longer string. + + This is useful in the PVL library when we know the position of the + final character of a token, but want the position of the first + character. + """ + return pos - len(sub) + 1 + + +def linecount(doc: str, end: int, start: int = 0): + """Returns the number of lines (by counting the + number of newline characters \\n, with the first line + being line number one) in the string *doc* between the + positions *start* and *end*. + """ + return doc.count("\n", start, end) + 1 + + +class LexerError(ValueError): + """Subclass of ValueError with the following additional properties: + + msg: The unformatted error message + doc: The PVL text being parsed + pos: The start index in doc where parsing failed + lineno: The line corresponding to pos + colno: The column corresponding to pos + """ + + def __init__(self, msg, doc, pos, lexeme): + self.pos = firstpos(lexeme, pos) + lineno = linecount(doc, self.pos) + colno = self.pos - doc.rfind("\n", 0, self.pos) + # Assemble a context string that consists of whole + # words, using fragments is hard to read. + context_tokens = doc[self.pos - 15: self.pos + 15].split(" ") + context = " ".join(context_tokens[1:-1]) + errmsg = ( + f"{msg}: line {lineno} column {colno} (char {pos}) " + f'near "{context}"' + ) + super().__init__(self, errmsg) + self.msg = msg + self.doc = doc + self.lineno = lineno + self.colno = colno + self.lexeme = lexeme + + def __reduce__(self): + return self.__class__, (self.msg, self.doc, self.pos, self.lexeme) + + +class ParseError(Exception): + """An exception to signal errors in the pvl parser.""" + + def __init__(self, msg, token=None): + super().__init__(self, msg) + self.token = token + + +class QuantityError(Exception): + """A simple exception to distinguish errors from Quantity classes.""" + + pass diff --git a/hirise_blender/pvl/grammar.py b/hirise_blender/pvl/grammar.py new file mode 100755 index 0000000..afa4e53 --- /dev/null +++ b/hirise_blender/pvl/grammar.py @@ -0,0 +1,345 @@ +# -*- coding: utf-8 -*- +"""Describes the language aspects of PVL dialects. + +These grammar objects are not particularly meant to be easily +user-modifiable during running of an external program, which is why +they have no arguments at initiation time, nor are there any methods +or functions to modify them. This is because these grammar objects +are used both for reading and writing PVL-text. As such, objects +like PVLGrammar and ODLGrammar shouldn't be altered, because if +they are, then the PVL-text written out with them wouldn't conform +to the spec. + +Certainly, these objects do have attributes that can be altered, +but unless you've carefully read the code, it isn't recommended. + +Maybe someday we'll add a more user-friendly interface to allow that, +but in the meantime, just leave an Issue on the GitHub repo. +""" + +# Copyright 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 re +from collections import abc +from datetime import timezone + + +class PVLGrammar: + """Describes a PVL grammar for use by the lexer and parser. + + The reference for this grammar is the CCSDS-641.0-B-2 'Blue Book'. + """ + + spacing_characters = (" ", "\t") + format_effectors = ("\n", "\r", "\v", "\f") + + # Tuple of characters to be recognized as PVL White Space + # (used to separate syntactic elements and promote readability, + # but the amount or presence of White Space may not be used to + # provide different meanings). + whitespace = spacing_characters + format_effectors + + # Tuple of characters that may not occur in Parameter Names, + # Unquoted Strings, nor Block Names. + reserved_characters = ( + "&", + "<", + ">", + "'", + "{", + "}", + ",", + "[", + "]", + "=", + "!", + "#", + "(", + ")", + "%", + "+", + '"', + ";", + "~", + "|", + ) + + # If there are any reserved_characters that might start a number, + # they need to be added to numeric_start_chars, otherwise that + # character will get lexed separately from the rest. + # Technically, since '-' isn't in reserved_characters, it isn't needed, + # but it doesn't hurt to keep it here. + numeric_start_chars = ("+", "-") + + delimiters = (";",) + + # Tuple of two-tuples with each two-tuple containing a pair of character + # sequences that enclose a comment. + comments = (("/*", "*/"),) + + # A note on keywords: they should always be compared with + # the str.casefold() function. + # So 'NULL'.casefold(), 'Null'.casefold(), and 'NuLl".casefold() + # all compare equals to none_keyword.casefold(). + none_keyword = "NULL" + true_keyword = "TRUE" + false_keyword = "FALSE" + group_pref_keywords = ("BEGIN_GROUP", "END_GROUP") + group_keywords = {"GROUP": "END_GROUP", "BEGIN_GROUP": "END_GROUP"} + object_pref_keywords = ("BEGIN_OBJECT", "END_OBJECT") + object_keywords = {"OBJECT": "END_OBJECT", "BEGIN_OBJECT": "END_OBJECT"} + aggregation_keywords = dict() + aggregation_keywords.update(group_keywords) + aggregation_keywords.update(object_keywords) + end_statements = ("END",) + reserved_keywords = set(end_statements) + for p in aggregation_keywords.items(): + reserved_keywords |= set(p) + + quotes = ('"', "'") + set_delimiters = ("{", "}") + sequence_delimiters = ("(", ")") + units_delimiters = ("<", ">") + + # [sign]radix#non_decimal_integer# + _s = r"(?P[+-]?)" + nondecimal_pre_re = re.compile(fr"{_s}(?P2|8|16)#") + binary_re = re.compile(fr"{_s}(?P2)#(?P[01]+)#") + octal_re = re.compile(fr"{_s}(?P8)#(?P[0-7]+)#") + hex_re = re.compile(fr"{_s}(?P16)#(?P[0-9A-Fa-f]+)#") + nondecimal_re = re.compile( + fr"{nondecimal_pre_re.pattern}(?P[0-9|A-Fa-f]+)#" + ) + + # The PVL Blue Book says that all PVl Date/Time Values are represented + # in Universal Coordinated Time + default_timezone = timezone.utc + _d_formats = ("%Y-%m-%d", "%Y-%j") + _t_formats = ("%H:%M", "%H:%M:%S", "%H:%M:%S.%f") + date_formats = _d_formats + tuple(x + "Z" for x in _d_formats) + time_formats = _t_formats + tuple(x + "Z" for x in _t_formats) + datetime_formats = list() + for d in _d_formats: + for t in _t_formats: + datetime_formats.append(f"{d}T{t}") + datetime_formats.append(f"{d}T{t}Z") + + # I really didn't want to write these, because it is so easy to + # make a mistake with time regexes, but they're they only way + # to parse times with 60 seconds in them. The above regexes and + # the datetime library are used for all other time parsing. + _H_frag = r"(?P0\d|1\d|2[0-3])" # 00 to 23 + _M_frag = r"(?P[0-5]\d)" # 00 to 59 + _f_frag = r"(\.(?P\d+))" # 1 or more digits + _Y_frag = r"(?P\d{3}[1-9])" # 0001 to 9999 + _m_frag = r"(?P0[1-9]|1[0-2])" # 01 to 12 + _d_frag = r"(?P0[1-9]|[12]\d|3[01])" # 01 to 31 + _Ymd_frag = fr"{_Y_frag}-{_m_frag}-{_d_frag}" + # 001 to 366: + _j_frag = r"(?P(00[1-9]|0[1-9]\d)|[12]\d{2}|3[0-5]\d|36[0-6])" + _Yj_frag = fr"{_Y_frag}-{_j_frag}" + _time_frag = fr"{_H_frag}:{_M_frag}:60{_f_frag}?Z?" # Only times with 60 s + # _time_frag = fr'{_H_frag}:{_M_frag}]' # Only times with 60 s + leap_second_Ymd_re = re.compile(fr"({_Ymd_frag}T)?{_time_frag}") + leap_second_Yj_re = re.compile(fr"({_Yj_frag}T)?{_time_frag}") + + def char_allowed(self, char): + """Returns true if *char* is allowed in the PVL Character Set. + + This is defined as most of the ISO 8859-1 'latin-1' character + set with some exclusions. + """ + if len(char) != 1: + raise ValueError( + f"This function only takes single characters and it was given " + f"{len(char)} ('{char}')." + ) + + o = ord(char) + + # The vertical tab, ord('\t') = 11, is mistakenly + # shaded on page B-3 of the PVL specification. + if ( + o > 255 + or (0 <= o <= 8) + or + # o == 11 or + (14 <= o <= 31) + or (127 <= o <= 159) + ): + return False + else: + return True + + +class ODLGrammar(PVLGrammar): + """This defines an ODL grammar. + + The reference for this grammar is the PDS3 Standards Reference + (version 3.8, 27 Feb 2009) Chapter 12: Object Description + Language Specification and Usage. + """ + + group_pref_keywords = ("GROUP", "END_GROUP") + object_pref_keywords = ("OBJECT", "END_OBJECT") + + # ODL does not allow times with a seconds value of 60. + leap_second_Ymd_re = None + leap_second_Yj_re = None + + # ODL allows "local" times without a timezone specifier. + default_timezone = None + + # ODL allows the radix to be from 2 to 16, but the optional sign + # must be after the first octothorpe (#). Why ODL thought this was + # an important difference to make from PVL, I have no idea. + # radix#[sign]non_decimal_integer# + nondecimal_pre_re = re.compile(fr"(?P[2-9]|1[0-6])#{PVLGrammar._s}") + nondecimal_re = re.compile( + fr"{nondecimal_pre_re.pattern}(?P[0-9A-Fa-f]+)#" + ) + + def char_allowed(self, char): + """Returns true if *char* is allowed in the ODL Character Set. + + The ODL Character Set is limited to ASCII. This is fewer + characters than PVL, but appears to allow more control + characters to be in quoted strings than PVL does. + """ + super().char_allowed(char) + + try: + char.encode(encoding="ascii") + return True + except UnicodeError: + return False + + +class PDSGrammar(ODLGrammar): + """This defines a PDS3 ODL grammar. + + The reference for this grammar is the PDS3 Standards Reference + (version 3.8, 27 Feb 2009) Chapter 12: Object Description + Language Specification and Usage. + """ + + # The PDS spec only allows allows miliseconds, not microseconds, + # but there is only a %f microseconds time format specifier in + # Python, and no miliseconds format specifier, so dealing with + # only the miliseconds will have to be enforced at the encoder and + # decoder stages. + + # PDSLabels default to UTC: + default_timezone = timezone.utc + + +class ISISGrammar(PVLGrammar): + """This defines the ISIS version of PVL. + + This is valid as of ISIS 3.9, and before, at least. + + The ISIS 'Pvl' object typically writes out parameter + values and keywords in CamelCase (e.g. 'Group', 'End_Group', + 'CenterLatitude', etc.), but it will accept all-uppercase + versions. + + Technically, since the ISIS 'Pvl' object which parses + PVL text into C++ objects for ISIS programs to work with + does not recognize the 'BEGIN_' construction, + this means that ISIS does not parse PVL text that would be + valid according to the PVL, ODL, or PDS3 specs. + """ + + # The other thing that ISIS seems to be doing differently is to + # split any text of all kinds with a dash continuation character. This + # is currently handled in the OmniParser.parse() function. + + # At + # https://astrodiscuss.usgs.gov/t/what-pvl-specification-does-isis-conform-to/ + # + # Stuart Sides, ISIS developer, says: + # The ISIS3 implementation of PVL/ODL (like) does not strictly + # follow any of the published standards. It was based on PDS3 + # ODL from the 1990s, but has several extensions (your example + # of continuation lines) adopted from existing and prior data + # sets from ISIS2, PDS, JAXA, ISRO, ..., and extensions used + # only within ISIS3 files (cub, net). This is one of the + # reasons using ISIS cube files as an archive format has been + # strongly discouraged. So to answer your question, there is + # no published specification for ISIS3 PVL. + + # The ISIS parser (at least <=3.9) doesn't recognize the + # 'BEGIN_' construction, which is why we must + # have a separate grammar object. Since we're at it, we might + # as well use the *_pref_keywords to indicate the CamelCase + # that ISIS folks are expecting. + group_pref_keywords = ("Group", "End_Group") + group_keywords = {"GROUP": "END_GROUP"} + object_pref_keywords = ("Object", "End_Object") + object_keywords = {"OBJECT": "END_OBJECT"} + + # A single-line comment that starts with the octothorpe (#) is not part + # of PVL or ODL, but it is used when ISIS writes out comments. + comments = (("/*", "*/"), ("#", "\n")) + + def __init__(self): + # ISIS allows for + characters in Unquoted String values. + self.reserved_characters = tuple( + self.adjust_reserved_characters(self.reserved_characters) + ) + + @staticmethod + def adjust_reserved_characters(chars: abc.Iterable): + # ISIS allows for + characters in Unquoted String values. + # Removing the plus from the reserved characters allows for + # that, but might lead to other parsing errors, so be on the lookout. + rc = list(chars) + rc.remove("+") + return rc + + +class OmniGrammar(PVLGrammar): + """A broadly permissive grammar. + + This grammar does not follow a specification, but is meant to allow + the broadest possible ingestion of PVL-like text that is found. + + This grammar should not be used to write out Python objects to PVL, + instead please use one of the grammars that follows a published + specification, like the PVLGrammar or the ODLGrammar. + """ + + # Interestingly, a single-line comment that starts with the + # octothorpe (#) is neither part of PVL nor ODL, but people use + # it all the time. + comments = (("/*", "*/"), ("#", "\n")) + + # ODL allows the radix to be from 2 to 16, and allows the sign to be + # 'inside' the octothorpes, so we need to allow for the wide variety + # of radix, and the variational placement of the optional sign: + # [sign]radix#[sign]non_decimal_integer# + _ss = r"(?P[+-]?)" + nondecimal_pre_re = re.compile( + PVLGrammar._s + fr"(?P[2-9]|1[0-6])#{_ss}" + ) + nondecimal_re = re.compile( + nondecimal_pre_re.pattern + r"(?P[0-9A-Fa-f]+)#" + ) + + def __init__(self): + # Handle the fact that ISIS writes out unquoted plus signs. + # See ISISGrammar for details. + # Also add the ASCII NULL ("\0") to the reserved_characters tuple. + self.reserved_characters = tuple( + ISISGrammar.adjust_reserved_characters(self.reserved_characters) + + ["\0", ] + ) + + def char_allowed(self, char): + """Takes all characters, could accept bad things, and the user must + beware.""" + return True diff --git a/hirise_blender/pvl/lexer.py b/hirise_blender/pvl/lexer.py new file mode 100644 index 0000000..8fbba5d --- /dev/null +++ b/hirise_blender/pvl/lexer.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Provides lexer functions for PVL.""" + +# Copyright 2019-2020, ``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. + + +from enum import Enum, auto + +from .grammar import PVLGrammar +from .token import Token +from .decoder import PVLDecoder +from .exceptions import LexerError, firstpos + + +class Preserve(Enum): + FALSE = auto() + COMMENT = auto() + UNIT = auto() + QUOTE = auto() + NONDECIMAL = auto() + + +def lex_preserve(char: str, lexeme: str, preserve: dict) -> tuple((str, dict)): + """Returns a modified *lexeme* string and a modified *preserve* + dict in a two-tuple. The modified *lexeme* will always be + the concatenation of *lexeme* and *char*. + + This is a lexer() helper function that is responsible for + changing the state of the *preserve* dict, if needed. + + If the value for 'end' in *preserve* is the same as *char*, + then the modified *preserve* will have its 'state' value + set to ``Preserve.FALSE`` and its 'end' value set to None, + otherwise second item in the returned tuple will be *preserve* + unchanged. + """ + # print(f'in preserve: char "{char}", lexeme "{lexeme}, p {preserve}"') + if char == preserve["end"]: + return lexeme + char, dict(state=Preserve.FALSE, end=None) + else: + return lexeme + char, preserve + + +def lex_singlechar_comments( + char: str, lexeme: str, preserve: dict, comments: dict +) -> tuple((str, dict)): + """Returns a modified *lexeme* string and a modified *preserve* + dict in a two-tuple. + + This is a lexer() helper function for determining how to modify + *lexeme* and *preserve* based on the single character in *char* + which may or may not be a comment character. + + If the *preserve* 'state' value is Preserve.COMMENT then + the value of lex_preserve() is returned. + + If *char* is among the keys of the *comments* dict, then the + returned *lexeme* will be the concatenation of *lexeme* and + *char*. returned *preserve* dict will have its 'state' value + set to Preserve.COMMENT and its 'end' value set to the value + of *comments[char]*. + + Otherwise return *lexeme* and *preserve* unchanged in the + two-tuple. + """ + if preserve["state"] == Preserve.COMMENT: + return lex_preserve(char, lexeme, preserve) + elif char in comments: + return ( + lexeme + char, + dict(state=Preserve.COMMENT, end=comments[char]), + ) + + return lexeme, preserve + + +def lex_multichar_comments( + char: str, + prev_char: str, + next_char: str, + lexeme: str, + preserve: dict, + comments: tuple(tuple((str, str))) = PVLGrammar().comments, +) -> tuple((str, dict)): + """Returns a modified *lexeme* string and a modified *preserve* + dict in a two-tuple. + + This is a lexer() helper function for determining how to + modify *lexeme* and *preserve* based on the single character + in *char* which may or may not be part of a multi-character + comment character group. + + This function has an internal list of allowed pairs of + multi-character comments that it can deal with, if the + *comments* tuple contains any two-tuples that cannot be + handled, a NotImplementedError will be raised. + + This function will determine whether to append *char* to + *lexeme* or not, and will set the value of the 'state' and + 'end' values of *preserve* appropriately. + """ + # print(f'lex_multichar got these comments: {comments}') + if len(comments) == 0: + raise ValueError("The variable provided to comments is empty.") + + allowed_pairs = (("/*", "*/"),) + for p in comments: + if p not in allowed_pairs: + raise NotImplementedError( + "Can only handle these " + "multicharacter comments: " + f"{allowed_pairs}. To handle " + "others this class must be extended." + ) + + if ("/*", "*/") in comments: + if char == "*": + if prev_char == "/": + return lexeme + "/*", dict(state=Preserve.COMMENT, end="*/") + elif next_char == "/": + return lexeme + "*/", dict(state=Preserve.FALSE, end=None) + else: + return lexeme + "*", preserve + elif char == "/": + # If part of a comment ignore, and let the char == '*' handler + # above deal with it, otherwise add it to the lexeme. + if prev_char != "*" and next_char != "*": + return lexeme + "/", preserve + + return lexeme, preserve + + +def lex_comment( + char: str, + prev_char: str, + next_char: str, + lexeme: str, + preserve: dict, + c_info: dict, +) -> tuple((str, dict)): + """Returns a modified *lexeme* string and a modified *preserve* + dict in a two-tuple. + + This is a lexer() helper function for determining how to + modify *lexeme* and *preserve* based on the single character + in *char* which may or may not be a comment character. + + This function just makes the decision about whether to call + lex_multichar_comments() or lex_singlechar_comments(), and + then returns what they return. + """ + + if char in c_info["multi_chars"]: + return lex_multichar_comments( + char, + prev_char, + next_char, + lexeme, + preserve, + comments=c_info["multi_comments"], + ) + else: + return lex_singlechar_comments( + char, lexeme, preserve, c_info["single_comments"] + ) + + +def _prev_char(s: str, idx: int): + """Returns the character from *s* at the position before *idx* + or None, if *idx* is zero. + """ + if idx <= 0: + return None + else: + return s[idx - 1] + + +def _next_char(s: str, idx: int): + """Returns the character from *s* at the position after *idx* + or None, if *idx* is the last position in *s*. + """ + try: + return s[idx + 1] + except IndexError: + return None + + +def _prepare_comment_tuples(comments: tuple(tuple((str, str)))) -> dict: + """Returns a dict of information based on the contents + of *comments*. + + This is a lexer() helper function to prepare information + for lexer(). + """ + # I initially tried to avoid this function, if you + # don't pre-compute this stuff, you end up re-computing + # it every time you pass into the lex_comment() function, + # which seemed excessive. + d = dict() + m = list() + d["single_comments"] = dict() + d["multi_chars"] = set() + for pair in comments: + if len(pair[0]) == 1: + d["single_comments"][pair[0]] = pair[1] + else: + m.append(pair) + for p in pair: + d["multi_chars"] |= set(p) + + d["chars"] = set(d["single_comments"].keys()) + d["chars"] |= d["multi_chars"] + d["multi_comments"] = tuple(m) + + # print(d) + return d + + +def lex_char( + char: str, + prev_char: str, + next_char: str, + lexeme: str, + preserve: dict, + g: PVLGrammar, + c_info: dict, +) -> tuple((str, dict)): + """Returns a modified *lexeme* string and a modified *preserve* + dict in a two-tuple. + + This is the main lexer() helper function for determining how + to modify (or not) *lexeme* and *preserve* based on the + single character in *char* and the other values passed into + this function. + """ + + # When we are 'in' a comment or a units expression, + # we want those to consume everything, regardless. + # So we must handle the 'preserve' states first, + # and then after that we can check to see if the char + # should put us into one of those states. + + # print(f'lex_char start: char "{char}", lexeme "{lexeme}", "{preserve}"') + + if preserve["state"] != Preserve.FALSE: + if preserve["state"] == Preserve.COMMENT: + (lexeme, preserve) = lex_comment( + char, prev_char, next_char, lexeme, preserve, c_info + ) + elif preserve["state"] in ( + Preserve.UNIT, + Preserve.QUOTE, + Preserve.NONDECIMAL, + ): + (lexeme, preserve) = lex_preserve(char, lexeme, preserve) + else: + raise ValueError( + "{} is not a ".format(preserve["state"]) + + "recognized preservation state." + ) + elif ( + char == "#" + and g.nondecimal_pre_re.fullmatch(lexeme + char) is not None + ): + lexeme += char + preserve = dict(state=Preserve.NONDECIMAL, end="#") + elif char in c_info["chars"]: + (lexeme, preserve) = lex_comment( + char, prev_char, next_char, lexeme, preserve, c_info + ) + elif char in g.units_delimiters[0]: + lexeme += char + preserve = dict(state=Preserve.UNIT, end=g.units_delimiters[1]) + elif char in g.quotes: + lexeme += char + preserve = dict(state=Preserve.QUOTE, end=char) + else: + if char not in g.whitespace: + lexeme += char # adding a char each time + + # print(f'lex_char end: char "{char}", lexeme "{lexeme}", "{preserve}"') + return lexeme, preserve + + +def lex_continue( + char: str, + next_char: str, + lexeme: str, + token: Token, + preserve: dict, + g: PVLGrammar, +) -> bool: + """Return True if accumulation of *lexeme* should continue based + on the values passed into this function, false otherwise. + + This is a lexer() helper function. + """ + + if next_char is None: + return False + + if not g.char_allowed(next_char): + return False + + if preserve["state"] != Preserve.FALSE: + return True + + # Since Numeric objects can begin with a reserved + # character, the reserved characters may split up + # the lexeme. + if ( + char in g.numeric_start_chars + and Token(char + next_char, grammar=g).is_numeric() + ): + return True + + # Since Non Decimal Numerics can have reserved characters in them. + if g.nondecimal_pre_re.fullmatch(lexeme + next_char) is not None: + return True + + # Since the numeric signs could be in the reserved characters, + # make sure we can parse scientific notation correctly: + if ( + char.lower() == "e" + and next_char in g.numeric_start_chars + and Token(lexeme + next_char + "2", grammar=g).is_numeric() + ): + return True + + # Some datetimes can have trailing numeric tz offsets, + # if the decoder allows it, this means there could be + # a '+' that splits the lexeme that we don't want. + if next_char in g.numeric_start_chars and token.is_datetime(): + return True + + return False + + +def lexer(s: str, g=PVLGrammar(), d=PVLDecoder()): + """This is a generator function that returns pvl.Token objects + based on the passed in string, *s*, when the generator's + next() is called. + + A call to send(*t*) will 'return' the value *t* to the + generator, which will be yielded upon calling next(). + This allows a user to 'peek' at the next token, but return it + if they don't like what they see. + + *g* is expected to be an instance of pvl.grammar, and *d* an + instance of pvl.decoder. The lexer will perform differently, + given different values of *g* and *d*. + """ + c_info = _prepare_comment_tuples(g.comments) + # print(c_info) + + lexeme = "" + preserve = dict(state=Preserve.FALSE, end=None) + for i, char in enumerate(s): + if not g.char_allowed(char): + raise LexerError( + f'The character "{char}" (ord: {ord(char)}) ' + " is not allowed by the grammar.", + s, + i, + lexeme, + ) + + prev_char = _prev_char(s, i) + next_char = _next_char(s, i) + + # print(repr(f'lexeme at top: ->{lexeme}<-, char: {char}, ' + # f'prev: {prev_char}, next: {next_char}, ' + # f'{preserve}')) + + (lexeme, preserve) = lex_char( + char, prev_char, next_char, lexeme, preserve, g, c_info + ) + + # print(repr(f' at bot: ->{lexeme}<-, ' + # f' ' + # f'{preserve}')) + + # Now having dealt with char, decide whether to + # go on continue accumulating the lexeme, or yield it. + + if lexeme == "": + continue + + try: + # The ``while t is not None: yield None; t = yield(t)`` + # construction below allows a user of the lexer to + # yield a token, not like what they see, and then use + # the generator's send() function to put the token + # back into the generator. + # + # The first ``yield None`` in there allows the call to + # send() on this generator to return None, and keep the + # value of *t* ready for the next call of next() on the + # generator. This is the magic that allows a user to + # 'return' a token to the generator. + tok = Token(lexeme, grammar=g, decoder=d, pos=firstpos(lexeme, i)) + + if lex_continue(char, next_char, lexeme, tok, preserve, g): + # Any lexeme state that we want to just allow + # to run around again and don't want to get + # caught by the clause in the elif, should + # test true via lex_continue() + continue + + elif ( + next_char is None + or not g.char_allowed(next_char) + or next_char in g.whitespace + or next_char in g.reserved_characters + or s.startswith(tuple(p[0] for p in g.comments), i + 1) + or lexeme.endswith(tuple(p[1] for p in g.comments)) + or lexeme in g.reserved_characters + or tok.is_quoted_string() + ): + # print(f'yielding {tok}') + t = yield tok + while t is not None: + yield None + t = yield t + lexeme = "" + else: + continue + + except ValueError as err: + raise LexerError(err, s, i, lexeme) diff --git a/hirise_blender/pvl/new.py b/hirise_blender/pvl/new.py new file mode 100755 index 0000000..4d6ac9b --- /dev/null +++ b/hirise_blender/pvl/new.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +"""Python implementation of PVL (Parameter Value Language), with upcoming +features. + +If you currently use:: + + import pvl + +you can change to:: + + import pvl.new as pvl + +And then use all of the pvl functions as you usually would. You +will also need to have the 3rd party multidict library +(https://github.com/aio-libs/multidict, conda installable) installed. +But then, any objects that are returned by the load functions will +be the new PVLMultiDict objects. +""" + +# 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 + +__all__ = [ + "PVLModuleNew", + "PVLGroupNew", + "PVLObjectNew", +] + + +def load(path, parser=None, grammar=None, decoder=None, **kwargs): + """Returns a Python object from parsing the file at *path*. + + :param path: an :class:`os.PathLike` which presumably has a + PVL Module in it to parse. + :param parser: defaults to :class:`pvl.parser.OmniParser()`. + :param grammar: defaults to :class:`pvl.grammar.OmniGrammar()`. + :param decoder: defaults to :class:`pvl.decoder.OmniDecoder()`. + :param ``**kwargs``: the keyword arguments that will be passed + to :func:`loads()` and are described there. + + If *path* is not an :class:`os.PathLike`, it will be assumed to be an + already-opened file object, and ``.read()`` will be applied + to extract the text. + + If the :class:`os.PathLike` or file object contains some bytes + decodable as text, followed by some that is not (e.g. an ISIS + cube file), that's fine, this function will just extract the + decodable text. + """ + return loads( + get_text_from(path), + parser=parser, + grammar=grammar, + decoder=decoder, + **kwargs + ) + + +def loadu(url, parser=None, grammar=None, decoder=None, **kwargs): + """Returns a Python object from parsing *url*. + + :param url: this will be passed to :func:`urllib.request.urlopen` + and can be a string or a :class:`urllib.request.Request` object. + :param parser: defaults to :class:`pvl.parser.OmniParser()`. + :param grammar: defaults to :class:`pvl.grammar.OmniGrammar()`. + :param decoder: defaults to :class:`pvl.decoder.OmniDecoder()`. + :param ``**kwargs``: the keyword arguments that will be passed + to :func:`urllib.request.urlopen` and to :func:`loads()`. + + The ``**kwargs`` will first be scanned for arguments that + can be given to :func:`urllib.request.urlopen`. If any are + found, they are extracted and used. All remaining elements + will be passed on as keyword arguments to :func:`loads()`. + + Note that *url* can be any URL that :func:`urllib.request.urlopen` + takes. Certainly http and https URLs, but also file, ftp, rsync, + sftp and more! + """ + + # Peel off the args for urlopen: + url_args = dict() + for a in inspect.signature(urllib.request.urlopen).parameters.keys(): + if a in kwargs: + url_args[a] = kwargs.pop(a) + + # The object returned from urlopen will always have a .read() + # function that returns bytes, so: + with urllib.request.urlopen(url, **url_args) as resp: + s = decode_by_char(resp) + + return loads(s, parser=parser, grammar=grammar, decoder=decoder, **kwargs) + + +def loads(s: str, parser=None, grammar=None, decoder=None, **kwargs): + """Deserialize the string, *s*, as a Python object. + + :param s: contains some PVL to parse. + :param parser: defaults to :class:`pvl.parser.OmniParser() which will + return the new PVLMultiDict-derived objects`. + :param grammar: defaults to :class:`pvl.grammar.OmniGrammar()`. + :param decoder: defaults to :class:`pvl.decoder.OmniDecoder()`. + :param ``**kwargs``: the keyword arguments to pass to the *parser* class + if *parser* is none. + """ + if isinstance(s, bytes): + # Someone passed us an old-style bytes sequence. Although it isn't + # a string, we can deal with it: + s = s.decode() + + if parser is None: + parser = OmniParser( + grammar=grammar, + decoder=decoder, + module_class=PVLModuleNew, + group_class=PVLGroupNew, + object_class=PVLObjectNew, + **kwargs + ) + elif not isinstance(parser, PVLParser): + 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/hirise_blender/pvl/parser.py b/hirise_blender/pvl/parser.py new file mode 100644 index 0000000..c7f312a --- /dev/null +++ b/hirise_blender/pvl/parser.py @@ -0,0 +1,955 @@ +# -*- coding: utf-8 -*- +"""Parameter Value Language parser. + +The definition of PVL used in this module is based on the Consultive +Committee for Space Data Systems, and their Parameter Value +Language Specification (CCSD0006 and CCSD0008), CCSDS 6441.0-B-2, +referred to as the Blue Book with a date of June 2000. + +Some of the documention in this module represents the structure +diagrams from the Blue Book for parsing PVL in a Backus–Naur +form. + +So Figure 1-1 from the Blue Book would be represented as : + + ::= ( [ + | ] )* + +Finally, the Blue Book defines as a possibly empty collection +of white space characters or comments: + + ::= ( | )* + +However, to help remember that could be empty, we will typically +always show it as *. + +Likewise the is defined as: + + ::= * [ ';' | ] + +However, since all elements are optional, we will typically +show it as []. + +The parser deals with managing the tokens that come out of the lexer. +Once the parser gets to a state where it has something that needs to +be converted to a Python object and returned, it uses the decoder to +make that conversion. + +Throughout this module, various parser functions will take a *tokens: +collections.abc.Generator* parameter. In all cases, *tokens* is +expected to be a *generator iterator* which provides ``pvl.token.Token`` +objects. It should allow for a generated object to be 'returned' +via the generator's send() function. When parsing the first object +from *tokens*, if an unexpected object is encountered, it will +'return' the object to *tokens*, and raise a ``ValueError``, so +that ``try``-``except`` blocks can be used, and the *generator +iterator* is left in a good state. However, if a parsing anomaly +is discovered deeper in parsing a PVL sequence, then a ``ValueError`` +will be thrown into the *tokens* generator iterator (via .throw()). +""" + +# Copyright 2015, 2017, 2019-2020, ``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 collections.abc as abc +import re + +from .collections import MutableMappingSequence, PVLModule, PVLGroup, PVLObject +from .token import Token +from .grammar import PVLGrammar, OmniGrammar +from .decoder import PVLDecoder, OmniDecoder +from .lexer import lexer as Lexer +from .exceptions import LexerError, ParseError, linecount + + +class EmptyValueAtLine(str): + """Empty string to be used as a placeholder for a parameter without + a value. + + When a label contains a parameter without a value, it is normally + considered a broken label in PVL. To allow parsing to continue, + we can rectify the broken parameter-value pair by setting the + value to have a value of EmptyValueAtLine, which is an empty + string (and can be treated as such) with some additional properties. + + The argument *lineno* should be the line number of the error from + the original document, which will be available as a property. + + Examples:: + >>> from pvl.parser import EmptyValueAtLine + >>> EV1 = EmptyValueAtLine(1) + >>> EV1 + EmptyValueAtLine(1 does not have a value. Treat as an empty string) + >>> EV1.lineno + 1 + >>> print(EV1) + + + >>> EV1 + 'foo' + 'foo' + >>> # Can be turned into an integer and float as 0: + >>> int(EV1) + 0 + >>> float(EV1) + 0.0 + """ + + def __new__(cls, lineno, *args, **kwargs): + self = super(EmptyValueAtLine, cls).__new__(cls, "") + self.lineno = lineno + return self + + def __int__(self): + return 0 + + def __float__(self): + return 0.0 + + def __repr__(self): + return ( + "{}({} does not ".format(type(self).__name__, self.lineno) + + "have a value. Treat as an empty string)" + ) + + +class PVLParser(object): + """A parser based on the rules in the CCSDS-641.0-B-2 'Blue Book' + which defines the PVL language. + + :param grammar: A pvl.grammar object, if None or not specified, it will + be set to the grammar parameter of *decoder* (if + *decoder* is not None) or will default to + :class:`pvl.grammar.OmniGrammar()`. + :param decoder: defaults to :class:`pvl.decoder.OmniDecoder()`. + :param lexer_fn: must be a lexer function that takes a ``str``, + a grammar, and a decoder, as :func:`pvl.lexer.lexer()` does, + which is the default if none is given. + :param module_class: must be a subclass of PVLModule, and is the type + of object that will be returned from this parser's :func:`parse()` + function. + :param group_class: must be a subclass of PVLGroup, and is the type + that will be used to hold PVL elements when a PVL Group is + encountered during parsing, and must be able to be added to + via an ``.append()`` function which should take a two-tuple + of name and value. + :param object_class: must be a subclass of PVLObject, and is the type + that will be used to hold PVL elements when a PVL Object is + encountered during parsing, otherwise similar to *group_class*. + """ + + def __init__( + self, + grammar=None, + decoder=None, + lexer_fn=None, + module_class=PVLModule, + group_class=PVLGroup, + object_class=PVLObject, + ): + + self.errors = [] + self.doc = "" + + if lexer_fn is None: + self.lexer = Lexer + else: + self.lexer = lexer_fn + + if grammar is None: + if decoder is not None: + self.grammar = decoder.grammar + else: + self.grammar = OmniGrammar() + elif isinstance(grammar, PVLGrammar): + self.grammar = grammar + else: + raise TypeError("The grammar must be an instance of pvl.grammar.") + + if decoder is None: + self.decoder = OmniDecoder(grammar=self.grammar) + elif isinstance(decoder, PVLDecoder): + self.decoder = decoder + else: + raise TypeError( + "The decode must be an instance of pvl.PVLDecoder." + ) + + if issubclass(module_class, MutableMappingSequence): + self.modcls = module_class + else: + raise TypeError( + "The module_class must be a " + "pvl.collections.MutableMappingSequence." + ) + + if issubclass(group_class, MutableMappingSequence): + self.grpcls = group_class + else: + raise TypeError( + "The group_class must be a " + "pvl.collections.MutableMappingSequence." + ) + + if issubclass(object_class, MutableMappingSequence): + self.objcls = object_class + else: + raise TypeError( + "The object_class must be a " + "pvl.collections.MutableMappingSequence." + ) + + def parse(self, s: str): + """Converts the string, *s* to a PVLModule.""" + self.doc = s + tokens = self.lexer(s, g=self.grammar, d=self.decoder) + module = self.parse_module(tokens) + module.errors = sorted(self.errors) + return module + + def aggregation_cls(self, begin: str): + """Returns an initiated object of the group_class or object_class + as specified on this parser's creation, according to the value + of *begin*. If *begin* does not match the Group or Object + keywords for this parser's grammar, then it will raise a + ValueError. + """ + begin_fold = begin.casefold() + for gk in self.grammar.group_keywords.keys(): + if begin_fold == gk.casefold(): + return self.grpcls() + + for ok in self.grammar.object_keywords.keys(): + if begin_fold == ok.casefold(): + return self.objcls() + + raise ValueError( + f'The value "{begin}" did not match a Begin ' + "Aggregation Statement." + ) + + def parse_module(self, tokens: abc.Generator): + """Parses the tokens for a PVL Module. + + ::= + ( | * | )* + [] + + """ + m = self.modcls() + + parsing = True + while parsing: + # print(f'top of while parsing: {m}') + parsing = False + for p in ( + self.parse_aggregation_block, + self.parse_assignment_statement, + self.parse_end_statement, + ): + try: + self.parse_WSC_until(None, tokens) + # t = next(tokens) + # print(f'next token: {t}, {t.pos}') + # tokens.send(t) + parsed = p(tokens) + # print(f'parsed: {parsed}') + if parsed is None: # because parse_end_statement returned + return m + else: + m.append(*parsed) + parsing = True + except LexerError: + raise + except ValueError: + pass + try: + (m, keep_parsing) = self.parse_module_post_hook(m, tokens) + if keep_parsing: + parsing = True + else: + return m + except Exception: + pass + + # print(f'got to bottom: {m}') + t = next(tokens) + tokens.throw( + ValueError, + "Expecting an Aggregation Block, an Assignment " + "Statement, or an End Statement, but found " + f'"{t}" ', + ) + + def parse_module_post_hook( + self, module: MutableMappingSequence, tokens: abc.Generator + ): + """This function is meant to be overridden by subclasses + that may want to perform some extra processing if + 'normal' parse_module() operations fail to complete. + See OmniParser for an example. + + This function shall return a two-tuple, with the first item + being the *module* (altered by processing or unaltered), and + the second item being a boolean that will signal whether + the tokens should continue to be parsed to accumulate more + elements into the returned *module*, or whether the + *module* is in a good state and should be returned by + parse_module(). + + If the operations within this function are unsuccessful, + it should raise an exception (any exception descended from + Exception), which will result in the operation of parse_module() + as if it were not overridden. + """ + raise Exception + + def parse_aggregation_block(self, tokens: abc.Generator): # noqa: C901 + """Parses the tokens for an Aggregation Block, and returns + the modcls object that is the result of the parsing and + decoding. + + ::= + (* (Assignment-Statement | Aggregation-Block) *)+ + + + The Begin-Aggregation-Statement Name must match the Block-Name + in the paired End-Aggregation-Statement if a Block-Name is + present in the End-Aggregation-Statement. + """ + (begin, block_name) = self.parse_begin_aggregation_statement(tokens) + + agg = self.aggregation_cls(begin) + + while True: + self.parse_WSC_until(None, tokens) + try: + agg.append(*self.parse_aggregation_block(tokens)) + except LexerError: + raise + except ValueError: + try: + agg.append(*self.parse_assignment_statement(tokens)) + # print(f'agg: {agg}') + # t = next(tokens) + # print(f'next token is: {t}') + # tokens.send(t) + except LexerError: + raise + except ValueError: + # t = next(tokens) + # print(f'parsing agg block, next token is: {t}') + # tokens.send(t) + try: + self.parse_end_aggregation(begin, block_name, tokens) + break + except LexerError: + raise + except ValueError as ve: + try: + (agg, keep_parsing) = self.parse_module_post_hook( + agg, tokens + ) + if not keep_parsing: + raise ve + except Exception: + raise ve + + return block_name, agg + + def parse_around_equals(self, tokens: abc.Generator) -> None: + """Parses white space and comments on either side + of an equals sign. + + *tokens* is expected to be a *generator iterator* which + provides ``pvl.token`` objects. + + This is shared functionality for Begin Aggregation Statements + and Assignment Statements. It basically covers parsing + anything that has a syntax diagram like this: + + * '=' * + + """ + if not self.parse_WSC_until("=", tokens): + try: + t = next(tokens) + tokens.send(t) + raise ValueError(f'Expecting "=", got: {t}') + except StopIteration: + raise ParseError('Expecting "=", but ran out of tokens.') + + self.parse_WSC_until(None, tokens) + return + + def parse_begin_aggregation_statement( + self, tokens: abc.Generator + ) -> tuple: + """Parses the tokens for a Begin Aggregation Statement, and returns + the name Block Name as a ``str``. + + ::= + * '=' * + [] + + Where ::= + + """ + try: + begin = next(tokens) + if not begin.is_begin_aggregation(): + tokens.send(begin) + raise ValueError( + "Expecting a Begin-Aggegation-Statement, but " + f"found: {begin}" + ) + except StopIteration: + raise ValueError( + "Ran out of tokens before starting to parse " + "a Begin-Aggegation-Statement." + ) + + try: + self.parse_around_equals(tokens) + except ValueError: + tokens.throw( + ValueError, f'Expecting an equals sign after "{begin}" ' + ) + + block_name = next(tokens) + if not block_name.is_parameter_name(): + tokens.throw( + ValueError, + f'Expecting a Block-Name after "{begin} =" ' + f'but found: "{block_name}"', + ) + + self.parse_statement_delimiter(tokens) + + return begin, str(block_name) + + def parse_end_aggregation( + self, begin_agg: str, block_name: str, tokens: abc.Generator + ) -> None: + """Parses the tokens for an End Aggregation Statement. + + ::= + [* '=' * + ] [] + + Where ::= + + """ + end_agg = next(tokens) + + # Need to do a little song and dance to case-independently + # match the keys: + for k in self.grammar.aggregation_keywords.keys(): + if k.casefold() == begin_agg.casefold(): + truecase_begin = k + break + if ( + end_agg.casefold() + != self.grammar.aggregation_keywords[truecase_begin].casefold() + ): + tokens.send(end_agg) + raise ValueError( + "Expecting an End-Aggegation-Statement that " + "matched the Begin-Aggregation_Statement, " + f'"{begin_agg}" but found: {end_agg}' + ) + + try: + self.parse_around_equals(tokens) + except (ParseError, ValueError): # No equals statement, which is fine. + self.parse_statement_delimiter(tokens) + return None + + t = next(tokens) + if t != block_name: + tokens.send(t) + tokens.throw( + ValueError, + f'Expecting a Block-Name after "{end_agg} =" ' + f'that matches "{block_name}", but found: ' + f'"{t}"', + ) + + self.parse_statement_delimiter(tokens) + + return None + + def parse_end_statement(self, tokens: abc.Generator) -> None: + """Parses the tokens for an End Statement. + + ::= "END" ( * | [] ) + + """ + try: + end = next(tokens) + if not end.is_end_statement(): + tokens.send(end) + raise ValueError( + "Expecting an End Statement, like " + f'"{self.grammar.end_statements}" but found ' + f'"{end}"' + ) + + # The following commented code was originally put in place to deal + # with the possible future situation of being able to process + # the possible comment after an end-statement. + # In practice, an edge case was discovered (Issue 104) where "data" + # after an END statement *all* properly converted to UTF with no + # whitespace characters. So this request for the next token + # resulted in lexing more than 100 million "valid characters" + # and did not return in a prompt manner. If we ever enable + # processing of comments, we'll have to figure out how to handle + # this case. An alternate to removing this code is to leave it + # but put in a limit on the size that a lexeme can grow to, + # but that implies an additional if-statement for each character. + # This is the better solution for now. + # try: + # t = next(tokens) + # if t.is_WSC(): + # # maybe process comment + # return + # else: + # tokens.send(t) + # return + # except LexerError: + # pass + except StopIteration: + pass + + return + + def parse_assignment_statement(self, tokens: abc.Generator) -> tuple: + """Parses the tokens for an Assignment Statement. + + The returned two-tuple contains the Parameter Name in the + first element, and the Value in the second. + + ::= * '=' * + [] + + """ + try: + t = next(tokens) + if t.is_parameter_name(): + parameter_name = str(t) + else: + tokens.send(t) + raise ValueError( + "Expecting a Parameter Name, but " f'found: "{t}"' + ) + except StopIteration: + raise ValueError( + "Ran out of tokens before starting to parse " + "an Assignment-Statement." + ) + + self.parse_around_equals(tokens) + + try: + # print(f'parameter name: {parameter_name}') + value = self.parse_value(tokens) + except StopIteration: + raise ParseError( + "Ran out of tokens to parse after the equals " + "sign in an Assignment-Statement: " + f'"{parameter_name} =".', + t, + ) + + self.parse_statement_delimiter(tokens) + + return parameter_name, value + + @staticmethod + def parse_WSC_until(token: str, tokens: abc.Generator) -> bool: + """Consumes objects from *tokens*, if the object's *.is_WSC()* + function returns *True*, it will continue until *token* is + encountered and will return *True*. If it encounters an object + that does not meet these conditions, it will 'return' that + object to *tokens* and will return *False*. + + *tokens* is expected to be a *generator iterator* which + provides ``pvl.token`` objects. + """ + for t in tokens: + if t == token: + return True + elif t.is_WSC(): + # If there's a comment, could parse here. + pass + else: + tokens.send(t) + return False + + def _parse_set_seq(self, delimiters, tokens: abc.Generator) -> list: + """The internal parsing of PVL Sets and Sequences are very + similar, and this function provides that shared logic. + + *delimiters* are a two-tuple containing the start and end + characters for the PVL Set or Sequence. + """ + t = next(tokens) + if t != delimiters[0]: + tokens.send(t) + raise ValueError( + f'Expecting a begin delimiter "{delimiters[0]}" ' + f'but found: "{t}"' + ) + set_seq = list() + # Initial WSC and/or empty + if self.parse_WSC_until(delimiters[1], tokens): + return set_seq + + # First item: + set_seq.append(self.parse_value(tokens)) + if self.parse_WSC_until(delimiters[1], tokens): + return set_seq + + # Remaining items, if any + for t in tokens: + # print(f'in loop, t: {t}, set_seq: {set_seq}') + if t == ",": + self.parse_WSC_until(None, tokens) # consume WSC after ',' + set_seq.append(self.parse_value(tokens)) + if self.parse_WSC_until(delimiters[1], tokens): + return set_seq + else: + tokens.send(t) + tokens.throw( + ValueError, + "While parsing, expected a comma (,)" f'but found: "{t}"', + ) + + def parse_set(self, tokens: abc.Generator) -> frozenset: + """Parses a PVL Set. + + ::= "{" * + [ * ( "," * * )* ] + "}" + + Returns the decoded as a Python ``frozenset``. The PVL + specification doesn't seem to indicate that a PVL Set + has distinct values (like a Python ``set``), only that the + ordering of the values is unimportant. For now, we will + implement PVL Sets as Python ``frozenset`` objects. + + They are returned as ``frozenset`` objects because PVL Sets + can contain as their elements other PVL Sets, but since Python + ``set`` objects are non-hashable, they cannot be members of a set, + however, ``frozenset`` objects can. + """ + return frozenset( + self._parse_set_seq(self.grammar.set_delimiters, tokens) + ) + + def parse_sequence(self, tokens: abc.Generator) -> list: + """Parses a PVL Sequence. + + ::= "(" * + [ * ( "," * * )* ] + ")" + + Returns the decoded as a Python ``list``. + """ + return self._parse_set_seq(self.grammar.sequence_delimiters, tokens) + + @staticmethod + def parse_statement_delimiter(tokens: abc.Generator) -> bool: + """Parses the tokens for a Statement Delimiter. + + *tokens* is expected to be a *generator iterator* which + provides ``pvl.token`` objects. + + ::= * + ( | | ';' | ) + + Although the above structure comes from Figure 2-4 + of the Blue Book, the and + elements are redundant with the presence of [WSC]* + so it can be simplified to: + + ::= * [ ';' | ] + + Typically written []. + """ + for t in tokens: + if t.is_WSC(): + # If there's a comment, could parse here. + pass + elif t.is_delimiter(): + return True + else: + tokens.send(t) # Put the next token back into the generator + return False + + def parse_value(self, tokens: abc.Generator): + """Parses PVL Values. + + ::= ( | | ) + [* ] + + Returns the decoded as an appropriate Python object. + """ + value = None + + try: + t = next(tokens) + value = self.decoder.decode_simple_value(t) + except ValueError: + tokens.send(t) + for p in ( + self.parse_set, + self.parse_sequence, + self.parse_value_post_hook, + ): + try: + value = p(tokens) + break + except LexerError: + # A LexerError is a subclass of ValueError, but + # if we get a LexerError, that's a problem and + # we need to raise it, and not let it pass. + raise + except ValueError: + # Getting a ValueError is a normal consequence of + # one of the parsing strategies not working, + # this pass allows us to go to the next one. + pass + else: + tokens.throw( + ValueError, + "Was expecting a Simple Value, or the " + "beginning of a Set or Sequence, but " + f'found: "{t}"', + ) + + # print(f'in parse_value, value is: {value}') + self.parse_WSC_until(None, tokens) + try: + return self.parse_units(value, tokens) + except (ValueError, StopIteration): + return value + + def parse_value_post_hook(self, tokens): + """This function is meant to be overridden by subclasses + that may want to perform some extra processing if + 'normal' parse_value() operations fail to yield a value. + See OmniParser for an example. + + This function shall return an appropriate Python value, + similar to what parse_value() would return. + + If the operations within this function are unsuccessful, + it should raise a ValueError which will result in the + operation of parse_value() as if it were not overridden. + """ + raise ValueError + + def parse_units(self, value, tokens: abc.Generator) -> str: + """Parses PVL Units Expression. + + ::= "<" [] + [] ">" + + and + + ::= + [ [ | ]* + ] + + Returns the *value* and the as a ``Units()`` + object. + """ + t = next(tokens) + + if not t.startswith(self.grammar.units_delimiters[0]): + tokens.send(t) + raise ValueError( + "Was expecting the start units delimiter, " + + '"{}" '.format(self.grammar.units_delimiters[0]) + + f'but found "{t}"' + ) + + if not t.endswith(self.grammar.units_delimiters[1]): + tokens.send(t) + raise ValueError( + "Was expecting the end units delimiter, " + + '"{}" '.format(self.grammar.units_delimiters[1]) + + f'at the end, but found "{t}"' + ) + + delim_strip = t.strip("".join(self.grammar.units_delimiters)) + + units_value = delim_strip.strip("".join(self.grammar.whitespace)) + + for d in self.grammar.units_delimiters: + if d in units_value: + tokens.throw( + ValueError, + "Was expecting a units character, but found a " + f'unit delimiter, "{d}" instead.', + ) + + return self.decoder.decode_quantity(value, units_value) + + +class ODLParser(PVLParser): + """A parser based on the rules in the PDS3 Standards Reference + (version 3.8, 27 Feb 2009) Chapter 12: Object Description + Language Specification and Usage. + + It extends PVLParser. + """ + + def parse_set(self, tokens: abc.Generator) -> set: + """Overrides the parent function to return + the decoded as a Python ``set``. + + The ODL specification only allows scalar_values in Sets, + since ODL Sets cannot contain other ODL Sets, an ODL Set + can be represented as a Python ``set`` (unlike PVL Sets, + which must be represented as a Python ``frozenset`` objects). + """ + return set(self._parse_set_seq(self.grammar.set_delimiters, tokens)) + + def parse_units(self, value, tokens: abc.Generator) -> str: + """Extends the parent function, since ODL only allows units + on numeric values, any others will result in a ValueError. + """ + + if isinstance(value, int) or isinstance(value, float): + return super().parse_units(value, tokens) + + else: + raise ValueError( + "ODL Units Expressions can only follow " "numeric values." + ) + + +class OmniParser(PVLParser): + """A permissive PVL/ODL/ISIS label parser that attempts to parse + all forms of "PVL" that are thrown at it. + """ + + def _empty_value(self, pos): + eq_pos = self.doc.rfind("=", 0, pos) + lc = linecount(self.doc, eq_pos) + self.errors.append(lc) + return EmptyValueAtLine(lc) + + def parse(self, s: str): + """Extends the parent function. + + If *any* line ends with a dash (-) followed by a carriage + return, form-feed, or newline, plus one or more whitespace + characters on the following line, then those characters, and + all whitespace characters that begin the next line will + be removed. + """ + nodash = re.sub(r"-[\n\r\f]\s*", "", s) + self.doc = nodash + + return super().parse(nodash) + + def parse_module_post_hook( + self, module: MutableMappingSequence, tokens: abc.Generator + ): + """Overrides the parent function to allow for more + permissive parsing. If an Assignment-Statement + is blank, then the value will be assigned an + EmptyValueAtLine object. + """ + # It enables this by checking to see if the next thing is an + # '=' which means there was an empty assignment at the previous + # equals sign, and then unwinding the stack to give the + # previous assignment the EmptyValueAtLine() object and trying + # to continue parsing. + + # print('in hook') + try: + t = next(tokens) + if t == "=" and len(module) != 0: + (last_k, last_v) = module[-1] + last_token = Token( + last_v, grammar=self.grammar, decoder=self.decoder + ) + if last_token.is_parameter_name(): + # Fix the previous entry + module.pop() + module.append(last_k, self._empty_value(t.pos)) + # Now use last_token as the parameter name + # for the next assignment, and we must + # reproduce the last part of parse-assignment: + try: + # print(f'parameter name: {last_token}') + self.parse_WSC_until(None, tokens) + value = self.parse_value(tokens) + self.parse_statement_delimiter(tokens) + module.append(str(last_token), value) + except StopIteration: + module.append( + str(last_token), self._empty_value(t.pos + 1) + ) + return module, False # return through parse_module() + else: + tokens.send(t) + else: + # The next token isn't an equals sign or the module is + # empty, so we want return the token and signal + # parse_module() that it should ignore us. + tokens.send(t) + raise Exception + + # Peeking at the next token gives us the opportunity to + # see if we're at the end of tokens, which we want to handle. + t = next(tokens) + tokens.send(t) + return module, True # keep parsing + except StopIteration: + # If we're out of tokens, that's okay. + return module, False # return through parse_module() + + def parse_assignment_statement(self, tokens: abc.Generator) -> tuple: + """Extends the parent function to allow for more + permissive parsing. If an Assignment-Statement + is blank, then the value will be assigned an + EmptyValueAtLine object. + """ + try: + return super().parse_assignment_statement(tokens) + except ParseError as err: + if err.token is not None: + after_eq = self.doc.find("=", err.token.pos) + 1 + return str(err.token), self._empty_value(after_eq) + else: + raise + + def parse_value_post_hook(self, tokens: abc.Generator): + """Overrides the parent function to allow for more + permissive parsing. + + If the next token is a reserved word or delimiter, + then it is returned to the *tokens* and an + EmptyValueAtLine object is returned as the value. + """ + + t = next(tokens) + # print(f't: {t}') + truecase_reserved = [ + x.casefold() for x in self.grammar.reserved_keywords + ] + trucase_delim = [x.casefold() for x in self.grammar.delimiters] + if t.casefold() in (truecase_reserved + trucase_delim): + # print(f'kw: {kw}') + # if kw.casefold() == t.casefold(): + # print('match') + tokens.send(t) + return self._empty_value(t.pos) + else: + raise ValueError diff --git a/hirise_blender/pvl/pvl_translate.py b/hirise_blender/pvl/pvl_translate.py new file mode 100644 index 0000000..9a786ed --- /dev/null +++ b/hirise_blender/pvl/pvl_translate.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +"""A program for converting PVL text to a specific PVL dialect. + +The ``pvl_translate`` program will read a file with PVL text (any +of the kinds of files that :func:`pvl.load` reads) or STDIN and +will convert that PVL text to a particular PVL dialect. It is not +particularly robust, and if it cannot make simple conversions, it +will raise errors. +""" + +# 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 json +import os +import sys + +import pvl +from .encoder import PVLEncoder, ODLEncoder, ISISEncoder, PDSLabelEncoder + + +class Writer(object): + """Base class for writers. Descendents must implement dump(). + """ + + def dump(self, dictlike: dict, outfile: os.PathLike): + raise Exception + + +class PVLWriter(Writer): + def __init__(self, encoder): + self.encoder = encoder + + def dump(self, dictlike: dict, outfile: os.PathLike): + return pvl.dump(dictlike, outfile, encoder=self.encoder) + + +class JSONWriter(Writer): + def dump(self, dictlike: dict, outfile: os.PathLike): + return json.dump(dictlike, outfile) + + +formats = dict( + PDS3=PVLWriter(PDSLabelEncoder()), + ODL=PVLWriter(ODLEncoder()), + ISIS=PVLWriter(ISISEncoder()), + PVL=PVLWriter(PVLEncoder()), + JSON=JSONWriter(), +) + + +def arg_parser(formats): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "-of", + "--output_format", + required=True, + choices=formats.keys(), + help="Select the format to create the new file as.", + ) + parser.add_argument( + "infile", + nargs="?", + type=argparse.FileType("r"), + default=sys.stdin, + help="file containing PVL text to translate, " "defaults to STDIN.", + ) + parser.add_argument( + "outfile", + nargs="?", + type=argparse.FileType("w"), + default=sys.stdout, + help="file to write translated PVL to, defaults " "to STDOUT.", + ) + parser.add_argument("--version", action="version", version=pvl.__version__) + return parser + + +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 diff --git a/hirise_blender/pvl/pvl_validate.py b/hirise_blender/pvl/pvl_validate.py new file mode 100644 index 0000000..96b9cbb --- /dev/null +++ b/hirise_blender/pvl/pvl_validate.py @@ -0,0 +1,253 @@ +# -*- 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), + ), +) + + +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 + + +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 + + +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 + + +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) + + +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) + + +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) diff --git a/hirise_blender/pvl/token.py b/hirise_blender/pvl/token.py new file mode 100644 index 0000000..7478554 --- /dev/null +++ b/hirise_blender/pvl/token.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- + +# Copyright 2019-2020, ``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. + + +from .decoder import PVLDecoder +from .grammar import PVLGrammar + + +class Token(str): + """A PVL-aware string. + + :var content: A string that is the Token text. + + :var grammar: A pvl.grammar object, if None or not specified, it will + be set to the grammar parameter of *decoder* (if + *decoder* is not None) or will default to PVLGrammar(). + + :var decoder: A pvl.decoder object, defaults to + PVLDecoder(grammar=*grammar*). + + :var pos: Integer that describes the starting position of this + Token in the source string, defaults to zero. + """ + + def __new__(cls, content, grammar=None, decoder=None, pos=0): + return str.__new__(cls, content) + + def __init__(self, content, grammar=None, decoder=None, pos=0): + if grammar is None: + if decoder is not None: + self.grammar = decoder.grammar + else: + self.grammar = PVLGrammar() + elif isinstance(grammar, PVLGrammar): + self.grammar = grammar + else: + raise TypeError("The grammar object is not of type PVLGrammar.") + + if decoder is None: + self.decoder = PVLDecoder(grammar=self.grammar) + elif isinstance(decoder, PVLDecoder): + self.decoder = decoder + else: + raise TypeError("The decoder object is not of type PVLDecoder.") + + self.pos = pos + + def __repr__(self): + return f"{self.__class__.__name__}('{self}', " f"'{self.grammar}')" + + def __index__(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 float(self.decoder.decode_decimal(str(self))) + + def split(self, sep=None, maxsplit=-1) -> list: + """Extends ``str.split()`` that calling split() on a Token + returns a list of Tokens. + """ + str_list = super().split(sep, maxsplit) + tkn_list = list() + for t in str_list: + tkn_list.append( + Token(t, grammar=self.grammar, decoder=self.decoder) + ) + return tkn_list + + def replace(self, *args): + """Extends ``str.replace()`` to return a Token.""" + return Token( + super().replace(*args), grammar=self.grammar, decoder=self.decoder + ) + + def lstrip(self, chars=None): + """Extends ``str.lstrip()`` to strip whitespace according + to the definition of whitespace in the Token's grammar + instead of the default Python whitespace definition. + """ + return self._strip(super().lstrip, chars) + + def rstrip(self, chars=None): + """Extends ``str.rstrip()`` to strip whitespace according + to the definition of whitespace in the Token's grammar + instead of the default Python whitespace definition. + """ + return self._strip(super().rstrip, chars) + + def strip(self, chars=None): + """Extends ``str.strip()`` to strip whitespace according + to the definition of whitespace in the Token's grammar + instead of the default Python whitespace definition. + """ + return self._strip(super().strip, chars) + + def _strip(self, strip_func, chars=None): + # Shared functionality for the various strip functions. + if chars is None: + chars = "".join(self.grammar.whitespace) + return Token( + strip_func(chars), grammar=self.grammar, decoder=self.decoder + ) + + def isspace(self) -> bool: + """Overrides ``str.isspace()`` to be the same as Token's + is_space() function, so that we don't get inconsisent + behavior if someone forgets an underbar. + """ + # So that we don't get inconsisent behavior + # if someone forgets an underbar. + return self.is_space() + + def is_space(self) -> bool: + """Return true if the Token contains whitespace according + to the definition of whitespace in the Token's grammar + and there is at least one character, false otherwise. + """ + if len(self) == 0: + return False + + return all(c in self.grammar.whitespace for c in self) + + def is_WSC(self) -> bool: + """Return true if the Token is white space characters or comments + according to the Token's grammar, false otherwise. + """ + if self.is_comment(): + return True + + if self.is_space(): + return True + + for ws in reversed(self.grammar.whitespace): + temp = self.replace(ws, " ") + + return all(t.is_comment() for t in temp.split()) + + def is_comment(self) -> bool: + """Return true if the Token is a comment according to the + Token's grammar (defined as beginning and ending with + comment delimieters), false otherwise. + """ + for pair in self.grammar.comments: + if self.startswith(pair[0]) and self.endswith(pair[1]): + return True + return False + + def is_quote(self) -> bool: + """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 + else: + return False + + def is_quoted_string(self) -> bool: + """Return true if the Token can be converted to a quoted + string by the Token's decoder, false otherwise. + """ + try: + self.decoder.decode_quoted_string(self) + return True + except ValueError: + return False + + def is_delimiter(self) -> bool: + """Return true if the Token is a delimiter character + (e.g. the ';' in PVL) according to the Token's grammar, + false otherwise. + """ + if self in self.grammar.delimiters: + return True + return False + + def is_begin_aggregation(self) -> bool: + """Return true if the Token is a begin aggregation + keyword (e.g. 'BEGIN_GROUP' in PVL) according to + the Token's grammar, false otherwise. + """ + for k in self.grammar.aggregation_keywords.keys(): + if self.casefold() == k.casefold(): + return True + return False + + def is_unquoted_string(self) -> bool: + """Return false if the Token has any + reserved characters, comment characters, whitespace + characters or could be interpreted as a number, + date, or time according to the Token's grammar, + true otherwise. + """ + for char in self.grammar.reserved_characters: + if char in self: + return False + + for pair in self.grammar.comments: + if pair[0] in self: + return False + if pair[1] in self: + return False + + if self.is_numeric() or self.is_datetime(): + return False + + for char in self.grammar.whitespace: + if char in self: + return False + + return True + + def is_string(self) -> bool: + """Return true if either the Token's is_quoted_string() + or is_unquoted_string() return true, false otherwise. + """ + if self.is_quoted_string() or self.is_unquoted_string(): + return True + return False + + def is_parameter_name(self) -> bool: + """Return true if the Token is an unquoted string that + isn't a reserved_keyword according to the Token's + grammar, false otherwise. + """ + for word in self.grammar.reserved_keywords: + if word.casefold() == self.casefold(): + return False + + return self.is_unquoted_string() + + def is_end_statement(self) -> bool: + """Return true if the Token matches an end statement + from its grammar, false otherwise. + """ + for e in self.grammar.end_statements: + if e.casefold() == self.casefold(): + return True + return False + + def isnumeric(self) -> bool: + """Overrides ``str.isnumeric()`` to be the same as Token's + is_numeric() function, so that we don't get inconsisent behavior + if someone forgets an underbar. + """ + return self.is_numeric() + + def is_numeric(self) -> bool: + """Return true if the Token's is_decimal() or is_non_decimal() + functions return true, false otherwise. + """ + if self.is_decimal() or self.is_non_decimal(): + return True + + return False + + def is_decimal(self) -> bool: + """Return true if the Token's decoder can convert the Token + to a decimal value, false otherwise. + """ + try: + self.decoder.decode_decimal(self) + return True + except ValueError: + return False + + def is_non_decimal(self) -> bool: + """Return true if the Token's decoder can convert the Token + to a numeric non-decimal value, false otherwise. + """ + try: + self.decoder.decode_non_decimal(self) + return True + except ValueError: + return False + + # Took these out, since some grammars allow a much wider + # range of radix values. + # + # def is_binary(self) -> bool: + # if self.grammar.binary_re.fullmatch(self) is None: + # return False + # else: + # return True + + # def is_octal(self) -> bool: + # if self.grammar.octal_re.fullmatch(self) is None: + # return False + # else: + # return True + + # def is_hex(self) -> bool: + # if self.grammar.hex_re.fullmatch(self) is None: + # return False + # else: + # return True + + def is_datetime(self) -> bool: + """Return true if the Token's decoder can convert the Token + to a datetime, false otherwise. + + Separate is_date() or is_time() functions aren't needed, + since PVL parsing doesn't distinguish between them. + If a user needs that distinction the decoder's + decode_datetime(self) function should return a datetime + time, date, or datetime object, as appropriate, and + a user can use isinstance() to check. + """ + try: + self.decoder.decode_datetime(self) + return True + except ValueError: + return False + + def is_simple_value(self) -> bool: + """Return true if the Token's decoder can convert the Token + to a 'simple value', however the decoder defines that, false + otherwise. + """ + try: + self.decoder.decode_simple_value(self) + return True + except ValueError: + return False diff --git a/hirise_blender/six/__init__.py b/hirise_blender/six/__init__.py new file mode 100644 index 0000000..9fed1ef --- /dev/null +++ b/hirise_blender/six/__init__.py @@ -0,0 +1,3 @@ +from .six import string_types, integer_types + +__all__ = ["string_types", "integer_types"] \ No newline at end of file diff --git a/hirise_blender/six/__pycache__/__init__.cpython-310.pyc b/hirise_blender/six/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c2b13e9516ed36aacbf4f5377f21d2ed2c6a6f4 GIT binary patch literal 251 zcmZ9GO=`n15QQb#ZAyrt2j~U7i7pUIa)5M`RlONfYmf)zA0!##BjrlkmMnUOt~#QN zhCY}#e8b~0V^ysME8dRnJMYg7_fiadp76qEgpp44vXQ-NloUpp!m6e)uW zN}CWp4Hn`uVMrAX5iR;(^tsO$Y;Sd$ia?jYOIiV(oddWE|CnRhz7M(^d(@^y=jOI0 pZ7J%GJOzXghl4@?AGuWn&_EGjJ1yyH*N6PKh^*M literal 0 HcmV?d00001 diff --git a/hirise_blender/six/__pycache__/six.cpython-310.pyc b/hirise_blender/six/__pycache__/six.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87452d057358cf67619b18850568cf3b74dcdb35 GIT binary patch literal 27584 zcmc(Hd3+qlb>?)>iNRn9fw#_~D2jwc4nstg_a}RBF<8dLv&N zzMS!?)Z1*tXC{+QpPSQfa`U5>v%>9%dzm``ccq(mR=KO4HNao%tgG*J*BcD?@==>O zZhGPhKr7uVftMG&tIl7odM;Snb^BFz!9uEQ?}@2im3uCxa_%(?ai`EqyBplXLTtgZ zVvoe?eTkTR?d&sFjQ=pzW3hVj41Imfq>VGcg zY*Lr00lcqANh{SVl(b4p+6@0{wFdq*?q-y{1^%^a9sKK1^Ff5%fROd-a)ewi7&pRy zg}M^{D}(`rUJOG8frR#Pbsau0B))obvl60 zsIzJ|fLp4i-WCn)Q_ki4d5SB|55#T0RJcTpQEx~R{ur)L=gU8)qhj}J%Im*`pKx=|LJa1KeaZd zemaQz8TkJf{68D`KLr2(UK=xR5cYHG!|LY)_#^5U)JFsO7u7GR|3lch34MN*`q-rX zqOCvZ-UNR7W%Vly_L#LhhPay!#hzU6>~MFw+ua=#yWAZMam*<a4A5^z&~HW1Z%5GYM9}X>&~HT0??uq>N6;Tc&;j+Kpo~9^pif57A4Slo2&qr1 zPc0--pFfVm|0II^2>Nsc{b>Y!CMxmIBIwT}=r1DZFAX%d-nq@a4V2CbrFXX}{bKyo zgP`SY?rzYMl-><`f(I5X^;hbzFQ6|#$-hyb1tmW#l)N4F{hazdc>>TEBIp&=4Pj$( z=MMLdsHEGwN_sm=y2HIaDCu^T^hGJ@Z`I!gC4EVK86|yLO1e{K?7v3|UlE?VQT;;) z{M8QlRg@j#e60ii#}4?PI`F^V0snIc{4X8wYaQ^vcEJDE0e_s%5xy} zIeXka5m(&V!4-*9FM%uexOWCzai_ZtrLQDk;JpgZYQr0V*5FzD9M%rCRybs@dn4d= z>O*kXlOOTEyvuh*m+#8ZHL??5suyMKckc?yxJ$~|7?g1xa@`b_aXoySrHsvi z1av^g=>d!$%*8^bEix-wY7n?LK&m>3H)*jI@Q|B(F{#V&ZF3I6EjowcZg=j6dy{hy z+#SxnaCbUK>H~Q160DmA>lVSfRj_so)@{y_R>FOgb04I@en^4$znGkT-pGMSdpn*x zQ09G*1ue*e_c?C@_EG0(2lfv~*mnYZ53r90*sSNzAZ#z5eFz^$dmnI)!FLy){Rnx` zy~a6SzXrYoAr9Vb?GR##$rcLcJ>(L$^lqWu-69(vXIjcCeNR64_&aF zlL8$E^pHSz1A17XdjLHm(7k{j73c_{$3WMV-h`Y;y(1{!!TU{k?gRWd-uL4<3it`U zS>gkLpG5la0(=ZPlmhrcz)uD6alqyJ4Zt{oC(QLE;4s&R0Ef9g47fAbM>=wSv?JHY zm@C@N8amSMO)hKrcmT78PXzFjfS(kY?fWo%tZxZ*AHn-6JY~QYS2?ciUq+o#cl7)i z!bdJx6XQ+I)o39Cxj1j24Z*d(5?el@_Rey-!9j#c=l@YGRf;x+J0 z1DtP1v{u6>vd}4R3he_ay0NxIUd|awEz`K2OUtW0@x#rH)~r#-YvXY z=W)Pq#``p$D&RT1&)}H=JP+p}M|?yVDVMcCN|+j$!A58|1{(|XQwo~dI`pnd=k zbD2ZP(=Ku5xeF6HI&SuZ2z$$90{Pt|@o!~(J%vzWKZDYqjmmje#jzJ!a2Hg3CWaJG z0b>ErbHG~Y%u@|RMo(+)E;!F2{%sM?CifY@Z$ZFO*{SIAMn|-;L*;sMkC2eixp117_F{;&}pt zC1j2Fmc9=Z`2H6&I_18P(1L~8d;Jr(^M2<8*g1b-!J62nu7n($(YE^m$g0}{7`X2H z_hK(xw_i-3`r_m7fBXYXfBu7R@dEln#ZSbx{E!ukwQhZ=S*=x@Ro5$wHgw^P23#L2 zR2s@HG{?)$f}W{+g+{$_Y_>U$*Umz@t_r(ep~_*<@>i5cyhd%N>6WTf(+%AmZf!p3 z)+fqSRitp6uJ({-&qxroe_AprHuTuOR%%Ca$F0SiUa^tWD{*FL>jYsseqy40x?H_@ z@#0%6@OJ%}U_YK$SHW>(4&)if#%Q)d(KuT+nDDJfD`;KxBg|(AkH`F#1vGoO#&Y?y zV=u>Ew)}+W)<$&?0>pW3&-U@gl)GJ3+*)m>QmJkqt2VcfS9R5MOCvS6u3Wv?>s8Ng z$5Wi1_0y#ia7v|p$nRCwI$^bX9xVo;!>DGaRH~P!+)~NUmP%6%HB%#;E0s>olxrqN z_l31-*GuOZmVvVe&#P=uCYG}$q?Uy5;#%|7b)rrk+6*AJ8JmMdVa1x(ggqBqA49iUt=&h2*$ReYTMOE4 z&geS%wmI$wp@s4&*iQm#Wv_`wG>e0A(%En-=p6TEJwI(S@!0#M_~Lr!Lwtkx5Nj_4 zx3iYMiuK3S^<(|_W)Hbu900(J89oYFVLt-qV&`KQV)OB)MW#Rp zV5+uat!y(sXH6s+!b$g$*c=93BGydKUSmY5$0X!TOkaac(8^3nyO<_UyMEep!A4Cj zwf57>tXhArU&5|cl#2q)r#%=58P%v37=To2|daDnhbN~Q5mJ`W-~Dey&) z-+Q!i+EqtoMi{Pyc`-l2>@+^!&2X9+8yF+@d3z2L4n|5`ubzvm*bEvqXU)gw;uy~f z1xnCr5qgkX zbvzz@{h)IOPu9xXtyLXOX!9Fq;qXT|8tj~UIM^qw17Wy&X}S@f|%Pg>OXI z8HQgAXHkIx$92thq#d6jJRZ{o3~qyhpx%NFRtJs1?84||Yg3W9W>J0`JR{tD9eg2Y zCWM32!SwAXPnT;mOJrIbph@bt1A$_nY-aQwbt z@Nkhrnl{(&B#IDfE1()4Q*116XL|bM((M(5cKzak&)WhZ?9%4g zq~9xp%W!NL(b>byl#j=uvY27f=W)&^U973M0aM>jM+T&@9T$9Epd(G|=E4Pp%go%5 zk>6U?QJv+vFRa%P(=k(u5M%Z{<~q|v)GKxfi_h*D!!Rt9!&+*<38QP(3bQ)FtT_3M zfwP`WGTB7zCHs^OK9b6E-o`3v7jM!Wx*dJRZt}BxYE`dUX-rM;djWn~B?n@y&E0KL zRWMO;PpwfY*SviI!ZcWlF9RE5;wX{RWD@ltRdJc{lMM0}m&m1?+2JH9n9{6O34j4?e7jY+zX_nK7lg3mlK!zdg zI48p(Ld&uKdytbz8{q_;!Ze(*QdR;&O3F4VDZC9tM4uTb`N8rP+WLuZA`fNd}LSCvjw&mP5^a;)4OH!`iO zTfdzx;CG-fg^0p5VhToLr8L_J-~zW89O|Cz$Er=Q;LVH}ihISZJ>z+VxpQ$ z5gzD;V~3CJx@l)&w94&^BsbfFV&O=$;EgwCYN{~eVhcEh!mHTkNN#7Us7Sa8EZ!pN z%C%a-J&WmwqW5WR_OK(sW^pvY+a(E?EC!q{he&nGTxPdX#e7-9J}zV|+sGY#ll8`# zda$yFGnumjZ4bg^_q3S9xL*7e^XFWW!W!GUYB@T$`JWXEe@w$2)IE5Nh+t#Oor4%f z-WT~qS!*HRuEpN#l+7xGPkpNhwm*b4Qt?oDqTu56jLy_#FX#E05L{GcES}sPneO{p zmMolWt+5gpTZQE--%hZEz+`J7FjuRtTBGLNLg~YZ&}?G3iMW>m?WtQ#nDjmbWEq`P?vV6TSCCWxS0`IBR@g*Us5k}Zcx-O z085CK#j1sMYk6XIBv@9K7GkaItXkG=c{tUzD;V_fN+5$-9 z9r(xERb^#cS1;SI5&Du|%^~9uFLN=vypi8n-I6CT@G{UH$l{zO zoWTi|>-Bli4T?l;Xt2!KtsjvH|M z`Ia4nQ9QL^9Ot4yN{^R45pyU@FUNW84WLU6KMfoalKa!GYnJ7=HtrItk>iAFc>vr9 zn(edE47TnJNHyH3yJk5x^gA_E)vglawev23D$x?_D(J-YC99v|u%UYOZ&-PDT{eN) zVEH8~#JzOoIa8a;PoT>~Cb#v&2sdj{Apa-WL5oJQ-&fIY8OmETZ~q28Oq#O)p7HF7OHAP%s7CheUo04Mu`?p}5kmLT z8K!fLj`W8RZ8@M>9R&jKQ|=4W8!mGbQD`Ek5sVH=eQH6)frhtq@S)d zrf0P~M#4?cLLWPZJ926i;6T8jIQj8ra~iW!p!pO7rXDR$y? zq!gM2<>d^8f)d*)J8+!du*_!ltRFvFJ=+RHiU{e5Zch5~W^=M5x;g1w7N*7wL(#VB z`MoD6BTp-g$Jh(v+c~Pb@{=bg@oMJ@PrEIcnHst>-e{nUe9(7@{vqRUMXl+?>P$N| z)u;z_khJtKrUYmI5UZWGIoa+LKi8aW_nHHr@Iz^|fpMNf9Nw)kw8+qQ&re}3!Jr*! zoMkRYjqhONY%A*fHiok%%(*$~b@UJ0-PwyyZbsL@!;oDunYUIN?M<;OFhDU=Q9^8M-mjXqs8i@9prk0)%2mnA3N5s-~wa;l$Wuyqp?? zm~e6o5rfeC9HJH^Ls+5=Jx;>E1WKaS^y^+)w-nj zn9$E6Xr^xRGH?(sf=X$+HUp7urf$TRTmYEh{?ONKltyNo5OP?QTj%}+f)%sJM6751 zjKED)phut?j%%=o5N0rrmlgZ+f~Iu>}-hAD>6@z$qHt23^ojB8d@D4)6O-E0v>@*Bq;s3 zTdqwx>kf=~OAW3r=etzSfkcE+!*>n24zrqvbGDMBqFNF0Uk z$Kz2e)hW183*;uxr<4r~<+MO?KpBA&fO-T<0?G=M0@SP0DuZ*!oa%x8DC@(9M9c>l zi#@F)L!#p=l$QuL1>P+fSU7Y^07Z7lsqsJQ-9I-jNUc{;Da=`w}@ zk=y4zROyewvf@W*KVd_0XkT_ri>k#dU}&)L{A%bmT5dn}Ai&Ti^BiGsP?n&2!7r&00N6~|rH!nB1psU59p_1Dq3 znaa4RJGT}#X`cUYLN!eLHWT_eI6N^>P`$z0iAOkM4!O9&y;=&AHpkIkA+K%s^K{Ch z3-iE>_BZs`g8ZoagN_n8(*_t2RF|ALXEaT65lopF5DvZ|ic^N5wIc1hYK;*nK;cPO zr%KGmA;4Vxoa7p0WAuxXkKZQ|VMYO7kkgVLVQ)!~a5Pcbe}QMXVu3-URj6NJ`D0g? zoIUoJ)oC7Dm8irmLj>B0oU~}#VYa!n=Oa$!QD{@_p`J6wg)qLQB@`|52IIqB6=db& zcsxElReJ_id@?hS(|lS#osK;W%WE8z%LE{Fgw;T)#50mtfepRk9eDZ4N!oud?!=xw zV~_;TPV0SJ*@dj2{q(w6>k61Y)u?6dFoOy)u>pve>$85MIXex5Q`lAO29B02y#=Wk z7mx`vx{>*_E$b}Zvm9{{CY>OAIv!k02S|P}9mf7@q%l)hi}PQ_BZeSOc!OHvn^2CN zb?82p#8Sf_;jko~1=LZBtyd$~k5_6-=(IXGm0<{SP9^AWb9u>p^h zBc{^-iV)okghlO<3g3Zjg;XhPN!^oSp5`bL2a!DP2m4%T+fw>T_6&A{uzbNm;z@Y) z^+*R+U;~%!rP7{>?4GbXm> z7C&~`VHB!Y18DVyr4^Y3*9jIcbVTpyKv}=!IBN|ehL{h{F*JRxUH9NDm)7%~n8eN> z2Q55#!f|lJ^I!%vTL`!-Lg{KHX+eT>A(fjM#&Aqou9x#=W3h|}@CnY~I z>CSpD+rEV(pUE>Fl{JeNaftCSD|ed%F5&>?pl^2&hQE-`5+W~0GKm;xd8^(iZw&z;%8LYDHZMj@etUV5e9Pv= zi2Apemk?_iw$O|?73Sq!#iSPY9;v>NIg5V3ef zTQhHGW}IX7HpI8=eFy!1$Q3xl9>Vh6Jj{DluP{qzzyu#J<1UwX+#Q3I;j9e8#CNpZ zgl5BY)|kNI22`}f3c~%%mSo`fcBJ?7-Rb^c8YYzap|EL7a*EbDPUpt{hqmk&V8kjG zK#Ad&{lpWk#1r`W`I&lEL|NQ9Do?wZi6Og42sdNXHS7kLqpi^M0;(V@|A4im5^=V$c_&V2ew?j*66pGNBw8Y0EGdNsV$p>W zeJj$SMe&iDQQxXAW{)f_UeAi2`{k&ZJxJmNJaUASx$sCUcMugSK>rQNxz@_@1TXY; zHJugWocaKsXxVGR@@kF?pUs6Y6sv5#3rH~0H;mC{$d`@zn4f?JA&$N$&+xsd^16t6 z{w!^|5%v0pP-r066;e+XTq900kl@)eg_TuGZ4b$W(TZ#?HNlC%uytR3g%e| ztj_|6TbOw}yv1hoo(Rs}2jM}@E!2E9WmzhVc)4qWH&(E{LikGf;`8=f;`LMz zas{p=z&;Z5fE8#H60Iu_gGRVsXYTmoysO;S@DzvjcGRMEHR)I|TV5WC$tKq;Z7sku zrQ(%vX^uve(4Sy?gEOBg+RlU0tTWT4;{!rP1Et9b#bE)UyUC;3v3+ zE?hx^(Jl6q9f-}5!rlUN$ztEgZfI#KjU6aAX}8EqxwRm-YR$TP$?_yxFgwtfFV~<3 zMK78B&}zfStkGQTSqN$XgnW>9$#_n-U7^Kf%}VQ~_W!^EBdg>C$P+3#*nl-FNFV*U zp0kt{cswq(7#x_|aFxKy#Pa$PWQa;6$xCL*WdT1`LYZwZ-1#3QyH@*ospQ*z?pH_YI8L4SZ zpPtL0)+|4!=jPJ$$tH}{<<@=v346Y0J~h{)-+{Y=2@WW{Q+%I#8c1_g%;D_0kL?VJUPK=COsi+-u{!cb(#l5j&qI7Iwq-JaRaX*-$4{y4%Q1=yq0hBqlG=tmIv4UTmxow69=Sy(697c;i~dM?iKp67MG)k4FL zTW^x{-Kc~+eX?~W+``lhZh7P03Wse0pE4*H!iwUae(Op&mqck@SFK~{RaF6UG|%q} zSb^orJ5=Ef(|=nAeJlH#RT#|p@#)62nE*i^g?+LRN64TbV!L~&(>N@Lzdddp8DU%C z4rcX!MCt={*lH(t26i{l4vF-oA7}UzaG<`@IOqre)T(vZK#7K4-^plMvmYZQJJ^(= z%GXK0VUQ2^IyphwiCm6^#I#9*cEaXem>DkW2{-21nl`FJ|xs5_b;#| z=KnEl9?w8W4#gKHk_AjYr*Ll`>GJalj!>pLA2;c%7?&J%r*OHD_h08C6Jv{1~Yu6f(Py3EmSVb!=t5Dw8a9LP#3M4ub>O9Cg?k{F0pU`@;mhSY;B58$2heG*X1bL70Pk}PwvkZE6BI&7qW@B z6~W+z@+)+Lu-Qwj?iJ2P3F-}-YBSx3%R>vak2*RAc}Wd@Ci3d zKD=sACiU7TIO-hcZ!Wm!ILOwV!;n+)Jy43Ai_Is_VW9*!;!-=>jG+c{QbWdEUm?l- ze5usX)iJSd#YqOnmdJNG=rMSw4@TSgI1F{AK1{0ROAZE=Ur%7KUCB(cyd7e~^;N{p z2e`@vLd}#z@fShb7z@ zQH}+jv(AGvK`AWLW|L%bKuu%cTjntj_%dpaKFfx+=m<|Ty{3)N1S^Rd7av?{ZTnU# z7G=N8ZPt0xsaN>GD7B>sQ(|}7eow?6PqGF)3*y~YyLXkPpGG#$3hZ8Snh6z?`Iv^} zf^Mc_S+gKCZ;tSr=`cx0InobiOB0pzD9a%M*ChM<^)pD&HRwnKV3^Y%suSB^381h+ zL85i*5RY{V%EdJyoM z6FvAo1cy&@m|rXLY7L(M$q|;!$!>X4yErzKZ-`*2=h>E@5TpBXQkvqE2nVu~=|}0k zj1DDi<|rdWh8|ha>5=hx8iolF09csQW6DI(%cjFS__D?#*jq-35;00PP3AftH<|so zaVTfNOXrd4LbCBKfS*Vgg|CUl9sHNvkJq3(svo4n09?iM+_x1&*h4~@yCeIixs zGCPEW8tdwfgde6u;o5LSG+p_9Ze7l=O>WP!!?>}__R>lr2bR$9MygA9)hLkJISbhM zVdsEhfNsGl9R?Y?2OYK=9fmzb&|xWkt;D1eb1h;rQH)1&BQZ%4BS9V@vS%K+wz z&izo>xt?^b(EtBWhc4oXb{}#YQ@jS*2-u^5pMD2uh!HQ-tcd^kh|W$k;NQtk;Z{f& zK}!AIfc%un#dPDl7`I5~$p*a!{(3LFjB-^j$Bht;7Why`*DoRIrMvFtpzF9<)V=5o ze!RhqE0A6`j2LapjB$|tr-(+}wC%s{YkycueoQF1jqBlX zmS1xSr@f+N`A}!rF4){e*T2Ux?F4;!k~TNfG=qv#O98v&7H^>p1D*O9v>0Ul0#~W6 zSk74|M0Hm}%GB*|u~BEvuLWYeg41KUJKWK@hMbTY5m-{vRzupm`1M_DIIX^Bjraeh4$C#2tpoW$-4%MqF< zI>m!Pgy_iT%W^*W1M=x3UV0y$!N>L>ENeDU_Ho(;GD>9zhwaab+=w;Y7H^8W* zVYrC?6c+1l15t&rXPK@RXhh9zWZ>Dge0o#(MN1@(N znY--rO+`PBI}v{01E~LKt#QU^cJjt{kjAz%_=cwD{!Bh-Xg-Q_HN9I&n5?nPlmc&O zD8EIchv-oF(_f+U6rDYEICJak=x|qM){!HGX!#}=Nt~;@uC$1WTjwXA64()2oGq+m=4pBlQ%Giw;1AusDS~Uh8iZOY2xPh4C8RZjG!I{_R$%jLyI9z10S(} z@w5Es08~ym?{ik-9oQI2In)d}D>?$DdScoYTFXSWyo_k=P*!$zS5Q>v?(|{RY2TtZ z(Ah|b#t44SYz=9uhQ5UDWlV3TvxUx9I@{pDFl<~yPXu)$-xx+SxSX6iVcz(XN@dc? zU?lD2IKfvGuvxFr;~7URSj?yzVQ*F7`00ZjW=9^-`-pdt&JdkL zbTpj?ad#8qnWY>o>K%l3(%HpW3YKz;BZpr40Y*^UsH<@Nm5&@haO~LOV}hE!Stin$6o^&9AWkA=__%C3~6u_xS<)%lVb|m-4IZ zzssz)|2DJ6{^I3p?N>7E>@Q^3+n>)~Zh!9bE9}o^ueARrdzJmy*{kip%3fptWwv1d zMRtSz=h<;^PvODeH&hD~*D|@s3o7r3J-^ku-|9Wb-P3zrHq;8k@ z$Fq0Zzna}+|4MeR{mZF+685qDe*2g52kc)QIB0(~J7oVt_K^LN?BT@E=kAr0*NYjU zAt?Q{jZd%eQ!M@QY|2<}FX!gb155vj&u6X}fJJf2&ZF-V7yAK1QMh?A^LvQ5J8#(kw4!w~> zZ{*M$IrK&jy^%w2OjpiO*yxRP+JC|T8=E&m`Vw=CHO5$h}{wa<@yFZuBt;rxfjb6ogTXl=1ORusMX^hj?vlvo% lgAS-~Jh!fY0ROeQm5J2}DK(QAu== (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + +if PY34: + from importlib.util import spec_from_loader +else: + spec_from_loader = None + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def find_spec(self, fullname, path, target=None): + if fullname in self.known_modules: + return spec_from_loader(fullname, self) + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + + def create_module(self, spec): + return self.load_module(spec.name) + + def exec_module(self, module): + pass + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("getoutput", "commands", "subprocess"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections", "IterableUserDict", "UserDict"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("splitvalue", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), + MovedAttribute("parse_http_list", "urllib2", "urllib.request"), + MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + del io + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" + _assertNotRegex = "assertNotRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +def assertNotRegex(self, *args, **kwargs): + return getattr(self, _assertNotRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + try: + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None + tb = None + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + try: + raise tp, value, tb + finally: + tb = None +""") + + +if sys.version_info[:2] > (3,): + exec_("""def raise_from(value, from_value): + try: + raise value from from_value + finally: + value = None +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + # This does exactly the same what the :func:`py3:functools.update_wrapper` + # function does on Python versions after 3.2. It sets the ``__wrapped__`` + # attribute on ``wrapper`` object and it doesn't raise an error if any of + # the attributes mentioned in ``assigned`` and ``updated`` are missing on + # ``wrapped`` object. + def _update_wrapper(wrapper, wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + for attr in assigned: + try: + value = getattr(wrapped, attr) + except AttributeError: + continue + else: + setattr(wrapper, attr, value) + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr, {})) + wrapper.__wrapped__ = wrapped + return wrapper + _update_wrapper.__doc__ = functools.update_wrapper.__doc__ + + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + return functools.partial(_update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + wraps.__doc__ = functools.wraps.__doc__ + +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + + def __new__(cls, name, this_bases, d): + if sys.version_info[:2] >= (3, 7): + # This version introduced PEP 560 that requires a bit + # of extra care (we mimic what is done by __build_class__). + resolved_bases = types.resolve_bases(bases) + if resolved_bases is not bases: + d['__orig_bases__'] = bases + else: + resolved_bases = bases + return meta(name, resolved_bases, d) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + if hasattr(cls, '__qualname__'): + orig_vars['__qualname__'] = cls.__qualname__ + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def ensure_binary(s, encoding='utf-8', errors='strict'): + """Coerce **s** to six.binary_type. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> encoded to `bytes` + - `bytes` -> `bytes` + """ + if isinstance(s, binary_type): + return s + if isinstance(s, text_type): + return s.encode(encoding, errors) + raise TypeError("not expecting type '%s'" % type(s)) + + +def ensure_str(s, encoding='utf-8', errors='strict'): + """Coerce *s* to `str`. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + # Optimization: Fast return for the common case. + if type(s) is str: + return s + if PY2 and isinstance(s, text_type): + return s.encode(encoding, errors) + elif PY3 and isinstance(s, binary_type): + return s.decode(encoding, errors) + elif not isinstance(s, (text_type, binary_type)): + raise TypeError("not expecting type '%s'" % type(s)) + return s + + +def ensure_text(s, encoding='utf-8', errors='strict'): + """Coerce *s* to six.text_type. + + For Python 2: + - `unicode` -> `unicode` + - `str` -> `unicode` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + if isinstance(s, binary_type): + return s.decode(encoding, errors) + elif isinstance(s, text_type): + return s + else: + raise TypeError("not expecting type '%s'" % type(s)) + + +def python_2_unicode_compatible(klass): + """ + A class decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..d11fc0e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,180 @@ +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "numpy" +version = "1.23.4" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.1.3" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "89f4bd34fda9d9f711e38dacb160780ecc3a847c867c3a83a4580cfa683e55ea" + +[metadata.files] +attrs = [ + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +numpy = [ + {file = "numpy-1.23.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:95d79ada05005f6f4f337d3bb9de8a7774f259341c70bc88047a1f7b96a4bcb2"}, + {file = "numpy-1.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:926db372bc4ac1edf81cfb6c59e2a881606b409ddc0d0920b988174b2e2a767f"}, + {file = "numpy-1.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c237129f0e732885c9a6076a537e974160482eab8f10db6292e92154d4c67d71"}, + {file = "numpy-1.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8365b942f9c1a7d0f0dc974747d99dd0a0cdfc5949a33119caf05cb314682d3"}, + {file = "numpy-1.23.4-cp310-cp310-win32.whl", hash = "sha256:2341f4ab6dba0834b685cce16dad5f9b6606ea8a00e6da154f5dbded70fdc4dd"}, + {file = "numpy-1.23.4-cp310-cp310-win_amd64.whl", hash = "sha256:d331afac87c92373826af83d2b2b435f57b17a5c74e6268b79355b970626e329"}, + {file = "numpy-1.23.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:488a66cb667359534bc70028d653ba1cf307bae88eab5929cd707c761ff037db"}, + {file = "numpy-1.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce03305dd694c4873b9429274fd41fc7eb4e0e4dea07e0af97a933b079a5814f"}, + {file = "numpy-1.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8981d9b5619569899666170c7c9748920f4a5005bf79c72c07d08c8a035757b0"}, + {file = "numpy-1.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a70a7d3ce4c0e9284e92285cba91a4a3f5214d87ee0e95928f3614a256a1488"}, + {file = "numpy-1.23.4-cp311-cp311-win32.whl", hash = "sha256:5e13030f8793e9ee42f9c7d5777465a560eb78fa7e11b1c053427f2ccab90c79"}, + {file = "numpy-1.23.4-cp311-cp311-win_amd64.whl", hash = "sha256:7607b598217745cc40f751da38ffd03512d33ec06f3523fb0b5f82e09f6f676d"}, + {file = "numpy-1.23.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ab46e4e7ec63c8a5e6dbf5c1b9e1c92ba23a7ebecc86c336cb7bf3bd2fb10e5"}, + {file = "numpy-1.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8aae2fb3180940011b4862b2dd3756616841c53db9734b27bb93813cd79fce6"}, + {file = "numpy-1.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c053d7557a8f022ec823196d242464b6955a7e7e5015b719e76003f63f82d0f"}, + {file = "numpy-1.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0882323e0ca4245eb0a3d0a74f88ce581cc33aedcfa396e415e5bba7bf05f68"}, + {file = "numpy-1.23.4-cp38-cp38-win32.whl", hash = "sha256:dada341ebb79619fe00a291185bba370c9803b1e1d7051610e01ed809ef3a4ba"}, + {file = "numpy-1.23.4-cp38-cp38-win_amd64.whl", hash = "sha256:0fe563fc8ed9dc4474cbf70742673fc4391d70f4363f917599a7fa99f042d5a8"}, + {file = "numpy-1.23.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c67b833dbccefe97cdd3f52798d430b9d3430396af7cdb2a0c32954c3ef73894"}, + {file = "numpy-1.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f76025acc8e2114bb664294a07ede0727aa75d63a06d2fae96bf29a81747e4a7"}, + {file = "numpy-1.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12ac457b63ec8ded85d85c1e17d85efd3c2b0967ca39560b307a35a6703a4735"}, + {file = "numpy-1.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95de7dc7dc47a312f6feddd3da2500826defdccbc41608d0031276a24181a2c0"}, + {file = "numpy-1.23.4-cp39-cp39-win32.whl", hash = "sha256:f2f390aa4da44454db40a1f0201401f9036e8d578a25f01a6e237cea238337ef"}, + {file = "numpy-1.23.4-cp39-cp39-win_amd64.whl", hash = "sha256:f260da502d7441a45695199b4e7fd8ca87db659ba1c78f2bbf31f934fe76ae0e"}, + {file = "numpy-1.23.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61be02e3bf810b60ab74e81d6d0d36246dbfb644a462458bb53b595791251911"}, + {file = "numpy-1.23.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:296d17aed51161dbad3c67ed6d164e51fcd18dbcd5dd4f9d0a9c6055dce30810"}, + {file = "numpy-1.23.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4d52914c88b4930dafb6c48ba5115a96cbab40f45740239d9f4159c4ba779962"}, + {file = "numpy-1.23.4.tar.gz", hash = "sha256:ed2cc92af0efad20198638c69bb0fc2870a58dabfba6eb722c933b48556c686c"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, + {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d2119aa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "hirise-blender" +version = "0.1.0" +description = "" +authors = ["Daniel Cellucci "] +readme = "README.md" +packages = [{include = "hirise_blender"}] + +[tool.poetry.dependencies] +python = "^3.10" +numpy = "^1.23.4" +pytest = "^7.1.3" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api"