From 80360c119e79f2282074aa6bd60bb54fd2d26d18 Mon Sep 17 00:00:00 2001 From: horn Date: Fri, 11 Dec 2020 09:29:41 +0000 Subject: [PATCH 01/10] add tests for opening PathLike objects --- pygac/tests/test_reader.py | 10 ++++++++++ pygac/tests/test_utils.py | 31 ++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/pygac/tests/test_reader.py b/pygac/tests/test_reader.py index cec3451a..c27522c8 100644 --- a/pygac/tests/test_reader.py +++ b/pygac/tests/test_reader.py @@ -23,6 +23,7 @@ import datetime import unittest import sys +import os try: import mock except ImportError: @@ -55,6 +56,15 @@ def test_filename(self): filepath = '/path/to/' + filename + '.gz' self.reader.filename = filepath self.assertEqual(self.reader.filename, filename) + self.reader.filename = None + self.assertIsNone(self.reader.filename) + class TestPath(os.PathLike): + def __init__(self, path): + self.path = str(path) + def __fspath__(self): + return self.path + self.reader.filename = TestPath(filepath) + self.assertEqual(self.reader.filename, filename) @unittest.skipIf(sys.version_info.major < 3, "Skipped in python2!") def test__read_scanlines(self): diff --git a/pygac/tests/test_utils.py b/pygac/tests/test_utils.py index 69d63383..79cbdf1e 100644 --- a/pygac/tests/test_utils.py +++ b/pygac/tests/test_utils.py @@ -28,6 +28,7 @@ import gzip import sys import numpy as np +import os try: from unittest import mock except ImportError: @@ -37,10 +38,6 @@ calculate_sun_earth_distance_correction) -def _raise_OSError(*args, **kwargs): - raise OSError - - class TestUtils(unittest.TestCase): """Test pygac.utils functions""" @@ -68,7 +65,7 @@ def close(self): self.assertTrue(is_file_object(duck)) @mock.patch('pygac.utils.open', mock.mock_open(read_data='file content')) - @mock.patch('pygac.utils.gzip.open', _raise_OSError) + @mock.patch('pygac.utils.gzip.open', mock.MagicMock(side_effect=OSError)) def test_file_opener_1(self): """Test if a file is redirected correctly through file_opener.""" with file_opener('path/to/file') as f: @@ -98,6 +95,30 @@ def test_file_opener_2(self): message = g.read() self.assertEqual(message, gzip_message_decoded) + @unittest.skipIf(sys.version_info.major < 3, "Not supported in python2!") + @mock.patch('pygac.utils.open', mock.MagicMock(side_effect=FileNotFoundError)) + @mock.patch('pygac.utils.gzip.open', mock.MagicMock(side_effect=OSError)) + def test_file_opener_3(self): + """Test file_opener with PathLike object""" + # prepare test + class RawBytes(os.PathLike): + def __init__(self, filename, raw_bytes): + self.filename = str(filename) + self.file_object = io.BytesIO(raw_bytes) + + def __fspath__(self): + return self.filename + + def open(self): + return self.file_object + + filename = '/path/to/file' + file_bytes = b'TestTestTest' + test_pathlike = RawBytes(filename, file_bytes) + with file_opener(test_pathlike) as f: + content = f.read() + self.assertEqual(content, file_bytes) + def test_calculate_sun_earth_distance_correction(self): """Test function for the sun distance corretction.""" corr = calculate_sun_earth_distance_correction(3) From 2c0506b02ace6aae5d12677a2df4befe4ee10638 Mon Sep 17 00:00:00 2001 From: horn Date: Fri, 11 Dec 2020 09:30:38 +0000 Subject: [PATCH 02/10] add capability to read PathLike objects with open method. --- pygac/reader.py | 1 + pygac/utils.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pygac/reader.py b/pygac/reader.py index cfa6b7ce..79bcadbe 100644 --- a/pygac/reader.py +++ b/pygac/reader.py @@ -142,6 +142,7 @@ def filename(self, filepath): if filepath is None: self._filename = None else: + filepath = os.fspath(filepath) match = self.data_set_pattern.search(filepath) if match: self._filename = match.group() diff --git a/pygac/utils.py b/pygac/utils.py index bb4e3c24..14ddda20 100644 --- a/pygac/utils.py +++ b/pygac/utils.py @@ -55,10 +55,17 @@ def _file_opener(file): Args: file - path to file or file object """ + close = False # open file if necessary if is_file_object(file): open_file = file - close = False + elif hasattr(file, 'open'): + # use __enter__ to set the fileobject into context, this + # is needed to access the actual file object in fsspec + try: + open_file = file.open(mode='rb').__enter__() + except TypeError: + open_file = file.open().__enter__() else: open_file = open(file, mode='rb') close = True From 2cf55aca7efe4f7f93e1e334462b067f500a4e15 Mon Sep 17 00:00:00 2001 From: horn Date: Fri, 11 Dec 2020 12:19:20 +0000 Subject: [PATCH 03/10] exit context when opening PathLike objects --- pygac/tests/test_utils.py | 17 +++++++++++++++-- pygac/utils.py | 15 ++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/pygac/tests/test_utils.py b/pygac/tests/test_utils.py index 79cbdf1e..221ae1ad 100644 --- a/pygac/tests/test_utils.py +++ b/pygac/tests/test_utils.py @@ -104,13 +104,13 @@ def test_file_opener_3(self): class RawBytes(os.PathLike): def __init__(self, filename, raw_bytes): self.filename = str(filename) - self.file_object = io.BytesIO(raw_bytes) + self.raw_bytes = raw_bytes def __fspath__(self): return self.filename def open(self): - return self.file_object + return io.BytesIO(self.raw_bytes) filename = '/path/to/file' file_bytes = b'TestTestTest' @@ -118,6 +118,19 @@ def open(self): with file_opener(test_pathlike) as f: content = f.read() self.assertEqual(content, file_bytes) + + # test with lazy loading open method (open only in context) + class RawBytesLazy(RawBytes): + def open(self): + self.lazy_opener_mock = mock.MagicMock() + self.lazy_opener_mock.__enter__.return_value = io.BytesIO(self.raw_bytes) + return self.lazy_opener_mock + + test_pathlike = RawBytesLazy(filename, file_bytes) + with file_opener(test_pathlike) as f: + content = f.read() + self.assertEqual(content, file_bytes) + test_pathlike.lazy_opener_mock.__exit__.assert_called_once_with(None, None, None) def test_calculate_sun_earth_distance_correction(self): """Test function for the sun distance corretction.""" diff --git a/pygac/utils.py b/pygac/utils.py index 14ddda20..2db65c3e 100644 --- a/pygac/utils.py +++ b/pygac/utils.py @@ -56,16 +56,19 @@ def _file_opener(file): file - path to file or file object """ close = False + exit = False # open file if necessary if is_file_object(file): open_file = file elif hasattr(file, 'open'): - # use __enter__ to set the fileobject into context, this - # is needed to access the actual file object in fsspec try: - open_file = file.open(mode='rb').__enter__() + _open_file = file.open(mode='rb') except TypeError: - open_file = file.open().__enter__() + _open_file = file.open() + # use __enter__ to set the fileobject into context, this + # is needed to access the actual file object in fsspec + open_file = _open_file.__enter__() + exit = True else: open_file = open(file, mode='rb') close = True @@ -82,7 +85,9 @@ def _file_opener(file): yield file_object finally: if close: - file_object.close() + open_file.close() + if exit: + _open_file.__exit__(None, None, None) @contextmanager From 414f3666ca45851f7e890246dfc8fd7e944384b2 Mon Sep 17 00:00:00 2001 From: stickler-ci Date: Fri, 11 Dec 2020 12:25:51 +0000 Subject: [PATCH 04/10] Fixing style errors. --- pygac/tests/test_reader.py | 2 ++ pygac/tests/test_utils.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pygac/tests/test_reader.py b/pygac/tests/test_reader.py index c27522c8..7b35e875 100644 --- a/pygac/tests/test_reader.py +++ b/pygac/tests/test_reader.py @@ -58,9 +58,11 @@ def test_filename(self): self.assertEqual(self.reader.filename, filename) self.reader.filename = None self.assertIsNone(self.reader.filename) + class TestPath(os.PathLike): def __init__(self, path): self.path = str(path) + def __fspath__(self): return self.path self.reader.filename = TestPath(filepath) diff --git a/pygac/tests/test_utils.py b/pygac/tests/test_utils.py index 221ae1ad..17a44e46 100644 --- a/pygac/tests/test_utils.py +++ b/pygac/tests/test_utils.py @@ -118,14 +118,14 @@ def open(self): with file_opener(test_pathlike) as f: content = f.read() self.assertEqual(content, file_bytes) - + # test with lazy loading open method (open only in context) class RawBytesLazy(RawBytes): def open(self): self.lazy_opener_mock = mock.MagicMock() self.lazy_opener_mock.__enter__.return_value = io.BytesIO(self.raw_bytes) return self.lazy_opener_mock - + test_pathlike = RawBytesLazy(filename, file_bytes) with file_opener(test_pathlike) as f: content = f.read() From 2a75d3c3966bb6b9959d2662dca9859ce23f68d5 Mon Sep 17 00:00:00 2001 From: horn Date: Fri, 11 Dec 2020 13:14:14 +0000 Subject: [PATCH 05/10] update stickler to read python 3 --- .stickler.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.stickler.yml b/.stickler.yml index 4a6f4e14..e5e6fe74 100644 --- a/.stickler.yml +++ b/.stickler.yml @@ -1,6 +1,7 @@ linters: flake8: fixer: true - max-line-length: 120 + python: 3 + config: setup.cfg fixers: enable: true From e86352f2e2b3d972668b3d3259a3713604dac0d9 Mon Sep 17 00:00:00 2001 From: horn Date: Fri, 11 Dec 2020 13:22:42 +0000 Subject: [PATCH 06/10] set required python version to >=3.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1b685864..f0f00d6f 100644 --- a/setup.py +++ b/setup.py @@ -74,6 +74,6 @@ ('gapfilled_tles', ['gapfilled_tles/TLE_noaa16.txt'])], test_suite="pygac.tests.suite", tests_require=[], - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', + python_requires='>=3.6', zip_safe=False ) From 4769a9408af846d26f8c8a4efc695c944455c486 Mon Sep 17 00:00:00 2001 From: horn Date: Mon, 14 Dec 2020 10:46:06 +0000 Subject: [PATCH 07/10] remove obsolete test. --- pygac/tests/test_utils.py | 26 +-------- pygac/utils.py | 112 +++++++------------------------------- 2 files changed, 22 insertions(+), 116 deletions(-) diff --git a/pygac/tests/test_utils.py b/pygac/tests/test_utils.py index 17a44e46..44473a1e 100644 --- a/pygac/tests/test_utils.py +++ b/pygac/tests/test_utils.py @@ -34,8 +34,7 @@ except ImportError: import mock -from pygac.utils import (is_file_object, file_opener, - calculate_sun_earth_distance_correction) +from pygac.utils import file_opener, calculate_sun_earth_distance_correction class TestUtils(unittest.TestCase): @@ -43,27 +42,6 @@ class TestUtils(unittest.TestCase): longMessage = True - def test_is_file_object(self): - """Test is_file_object function.""" - # true test - with io.BytesIO(b'file content') as fileobj: - self.assertTrue(is_file_object(fileobj)) - # false test - self.assertFalse(is_file_object("test.txt")) - # duck type test - - class Duck(object): - def read(self, n): - return n*b'\00' - - def seekable(self): - return True - - def close(self): - pass - duck = Duck() - self.assertTrue(is_file_object(duck)) - @mock.patch('pygac.utils.open', mock.mock_open(read_data='file content')) @mock.patch('pygac.utils.gzip.open', mock.MagicMock(side_effect=OSError)) def test_file_opener_1(self): @@ -72,7 +50,6 @@ def test_file_opener_1(self): content = f.read() self.assertEqual(content, 'file content') - @unittest.skipIf(sys.version_info.major < 3, "Not supported in python2!") def test_file_opener_2(self): """Test file_opener with file objects and compression""" # prepare test @@ -95,7 +72,6 @@ def test_file_opener_2(self): message = g.read() self.assertEqual(message, gzip_message_decoded) - @unittest.skipIf(sys.version_info.major < 3, "Not supported in python2!") @mock.patch('pygac.utils.open', mock.MagicMock(side_effect=FileNotFoundError)) @mock.patch('pygac.utils.gzip.open', mock.MagicMock(side_effect=OSError)) def test_file_opener_3(self): diff --git a/pygac/utils.py b/pygac/utils.py index 2db65c3e..4bb8f397 100644 --- a/pygac/utils.py +++ b/pygac/utils.py @@ -20,59 +20,19 @@ # along with this program. If not, see . import gzip +import io import logging -import numpy as np import sys -from contextlib import contextmanager - -LOG = logging.getLogger(__name__) +from contextlib import contextmanager, nullcontext -def is_file_object(filename): - """Check if the input is a file object. - - Args: - filename - object to check +import numpy as np - Note: - This method only check if the object implements the - interface of a file object to allow duck types like - gzip.GzipFile instances. - """ - has_close = hasattr(filename, 'close') - has_read = hasattr(filename, 'read') - if hasattr(filename, 'seekable'): - is_seekable = filename.seekable() - else: - is_seekable = False - return has_close and has_read and is_seekable +LOG = logging.getLogger(__name__) -@contextmanager -def _file_opener(file): - """Open a file depending on the input. - - Args: - file - path to file or file object - """ - close = False - exit = False - # open file if necessary - if is_file_object(file): - open_file = file - elif hasattr(file, 'open'): - try: - _open_file = file.open(mode='rb') - except TypeError: - _open_file = file.open() - # use __enter__ to set the fileobject into context, this - # is needed to access the actual file object in fsspec - open_file = _open_file.__enter__() - exit = True - else: - open_file = open(file, mode='rb') - close = True - # check if it is a gzip file +def gzip_inspected(open_file): + """Try to gzip decompress the file object if applicable.""" try: file_object = gzip.open(open_file) file_object.read(1) @@ -80,53 +40,23 @@ def _file_opener(file): file_object = open_file finally: file_object.seek(0) - # provide file_object with the context - try: - yield file_object - finally: - if close: - open_file.close() - if exit: - _open_file.__exit__(None, None, None) - + return file_object @contextmanager -def _file_opener_py2(file): - """Open a file depending on the input. - - Args: - file - path to file - """ - close = True - # check if it is a gzip file - try: - file_object = gzip.open(file) - file_object.read(1) - # Note: in python 2, this is an IOError, but we keep the - # OSError for testing. - except (OSError, IOError): - file_object = open(file, mode='rb') - except TypeError: - # In python 2 gzip.open cannot handle file objects - LOG.debug("Gzip cannot open file objects in python2!") - if is_file_object(file): - file_object = file - close = False - finally: - file_object.seek(0) - # provide file_object with the context - try: - yield file_object - finally: - if close: - file_object.close() - - -if sys.version_info.major < 3: - file_opener = _file_opener_py2 -else: - file_opener = _file_opener - +def file_opener(file): + if isinstance(file, io.IOBase) and file.seekable(): + # avoid closing the file using nullcontext + open_file = nullcontext(file) + elif hasattr(file, 'open'): + try: + open_file = file.open(mode='rb') + except TypeError: + open_file = file.open() + else: + open_file = open(file, mode='rb') + # set open_file into context in case of lazy loding in __enter__ method. + with open_file as file_object: + yield gzip_inspected(file_object) def get_absolute_azimuth_angle_diff(sat_azi, sun_azi): """Calculates absolute azimuth difference angle. """ From c62b8f3e9cf650b0f9e1ef2626f7201c3637dac0 Mon Sep 17 00:00:00 2001 From: horn Date: Mon, 14 Dec 2020 11:39:52 +0000 Subject: [PATCH 08/10] remove unused imports --- pygac/tests/test_utils.py | 13 +++++-------- pygac/utils.py | 1 - 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pygac/tests/test_utils.py b/pygac/tests/test_utils.py index 44473a1e..7bf9c220 100644 --- a/pygac/tests/test_utils.py +++ b/pygac/tests/test_utils.py @@ -23,16 +23,13 @@ """Test pygac.utils module """ -import unittest -import io import gzip -import sys -import numpy as np +import io import os -try: - from unittest import mock -except ImportError: - import mock +import unittest +from unittest import mock + +import numpy as np from pygac.utils import file_opener, calculate_sun_earth_distance_correction diff --git a/pygac/utils.py b/pygac/utils.py index 4bb8f397..614ad498 100644 --- a/pygac/utils.py +++ b/pygac/utils.py @@ -22,7 +22,6 @@ import gzip import io import logging -import sys from contextlib import contextmanager, nullcontext From 74deb240e146304b8a3c95f615208af1c7888aa3 Mon Sep 17 00:00:00 2001 From: horn Date: Mon, 14 Dec 2020 13:42:54 +0000 Subject: [PATCH 09/10] clean code and remove unused mock in file opener test --- pygac/tests/test_utils.py | 6 ++---- pygac/utils.py | 6 ++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pygac/tests/test_utils.py b/pygac/tests/test_utils.py index 7bf9c220..8c632c80 100644 --- a/pygac/tests/test_utils.py +++ b/pygac/tests/test_utils.py @@ -39,13 +39,12 @@ class TestUtils(unittest.TestCase): longMessage = True - @mock.patch('pygac.utils.open', mock.mock_open(read_data='file content')) - @mock.patch('pygac.utils.gzip.open', mock.MagicMock(side_effect=OSError)) + @mock.patch('pygac.utils.open', mock.MagicMock(return_value=io.BytesIO(b'file content'))) def test_file_opener_1(self): """Test if a file is redirected correctly through file_opener.""" with file_opener('path/to/file') as f: content = f.read() - self.assertEqual(content, 'file content') + self.assertEqual(content, b'file content') def test_file_opener_2(self): """Test file_opener with file objects and compression""" @@ -70,7 +69,6 @@ def test_file_opener_2(self): self.assertEqual(message, gzip_message_decoded) @mock.patch('pygac.utils.open', mock.MagicMock(side_effect=FileNotFoundError)) - @mock.patch('pygac.utils.gzip.open', mock.MagicMock(side_effect=OSError)) def test_file_opener_3(self): """Test file_opener with PathLike object""" # prepare test diff --git a/pygac/utils.py b/pygac/utils.py index 614ad498..fd6daccc 100644 --- a/pygac/utils.py +++ b/pygac/utils.py @@ -33,14 +33,15 @@ def gzip_inspected(open_file): """Try to gzip decompress the file object if applicable.""" try: - file_object = gzip.open(open_file) - file_object.read(1) + file_object = gzip.GzipFile(mode='rb', fileobj=open_file) + file_object.read1() except OSError: file_object = open_file finally: file_object.seek(0) return file_object + @contextmanager def file_opener(file): if isinstance(file, io.IOBase) and file.seekable(): @@ -57,6 +58,7 @@ def file_opener(file): with open_file as file_object: yield gzip_inspected(file_object) + def get_absolute_azimuth_angle_diff(sat_azi, sun_azi): """Calculates absolute azimuth difference angle. """ rel_azi = abs(sat_azi - sun_azi) From 166cbbd157b326326acb7b1e65e45af0070b374f Mon Sep 17 00:00:00 2001 From: horn Date: Tue, 15 Dec 2020 16:18:55 +0000 Subject: [PATCH 10/10] fix gzip inspect and correct typo --- pygac/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygac/utils.py b/pygac/utils.py index fd6daccc..76a7fd9e 100644 --- a/pygac/utils.py +++ b/pygac/utils.py @@ -34,7 +34,7 @@ def gzip_inspected(open_file): """Try to gzip decompress the file object if applicable.""" try: file_object = gzip.GzipFile(mode='rb', fileobj=open_file) - file_object.read1() + file_object.read(1) except OSError: file_object = open_file finally: @@ -54,7 +54,7 @@ def file_opener(file): open_file = file.open() else: open_file = open(file, mode='rb') - # set open_file into context in case of lazy loding in __enter__ method. + # set open_file into context in case of lazy loading in __enter__ method. with open_file as file_object: yield gzip_inspected(file_object)