diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2836b94a..1f46c326 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -4,3 +4,4 @@ |-----------------|------------------|-------------|------------| | andrewcoughtrie | Andrew Coughtrie | Met Office | 2026-03-05 | | mo-marqh | mark Hedley | Met Office | 2026-03-10 | +| oakleybrunt | Oakley Brunt | Met Office | 2026-03-09 | \ No newline at end of file diff --git a/post-processing/tests/test_cli_tools.py b/post-processing/tests/test_cli_tools.py index 9ecdeb53..4607cd18 100644 --- a/post-processing/tests/test_cli_tools.py +++ b/post-processing/tests/test_cli_tools.py @@ -3,16 +3,25 @@ # The file LICENCE, distributed with this code, contains details of the terms # under which the code may be used. # ------------------------------------------------------------------------------ +""" +Module to hold tests for CLI tools. +""" from pathlib import Path import unittest import subprocess -import sys -class TestCLITools(unittest.TestCase): +class TestCLITools(unittest.TestCase): + """ + Unittest class for testing CLI tools. + """ def setUp(self): + """ + Initialise useful attributes for testing. + """ self.tools_dir = Path(__file__).parent.parent / 'tools' self.test_data_dir = Path(__file__).parent / 'data' + # pylint: disable=line-too-long self.test_data_kgo = ( '| Routine | Total time (s) | Self (s) | Cumul time (s) | No. calls | % time | Time per call (s) |\n' + '| __test_app__ | 6.3465 | 2.5855 | 4.0895 | 1 | 40.9825 | 6.3465 |\n' + @@ -21,29 +30,44 @@ def setUp(self): return super().setUp() def test_summarise_vernier_file(self): + """ + Tests the summarise_vernier python script on a single file and + compares the output to a KGO. + """ result = subprocess.run( [str(self.tools_dir / 'summarise_vernier.py'), str(self.test_data_dir / 'vernier-output-test')], capture_output=True, - text=True + text=True, + check=False ) self.assertEqual(result.returncode, 0) self.assertEqual(self.test_data_kgo, result.stdout) def test_summarise_vernier_dir(self): + """ + Tests the summarise_vernier python script on a directory and compares + the output to a KGO. + """ result = subprocess.run( [str(self.tools_dir / 'summarise_vernier.py'), str(self.test_data_dir / 'vernier-output')], capture_output=True, - text=True + text=True, + check=False ) self.assertEqual(result.returncode, 0) self.assertEqual(self.test_data_kgo, result.stdout) def test_summarise_vernier_noinput(self): + """ + Tests that the correct error is raised when calling the + summarise_vernier python script on an empty file. + """ result = subprocess.run( [str(self.tools_dir / 'summarise_vernier.py')], capture_output=True, - text=True + text=True, + check=False ) - self.assertEqual(result.returncode, 2) \ No newline at end of file + self.assertEqual(result.returncode, 2) diff --git a/post-processing/tests/test_vernier_caliper.py b/post-processing/tests/test_vernier_calliper.py similarity index 78% rename from post-processing/tests/test_vernier_caliper.py rename to post-processing/tests/test_vernier_calliper.py index a69a9225..3f9bf916 100644 --- a/post-processing/tests/test_vernier_caliper.py +++ b/post-processing/tests/test_vernier_calliper.py @@ -3,19 +3,34 @@ # The file LICENCE, distributed with this code, contains details of the terms # under which the code may be used. # ------------------------------------------------------------------------------ +""" +Module for testing the VernierCalliper class. +""" + import unittest from pathlib import Path import sys + +# pylint: disable=wrong-import-position sys.path.append(str(Path(__file__).parent.parent)) from vernier.vernier_data import VernierCalliper -class TestVernierCalliper(unittest.TestCase): +class TestVernierCalliper(unittest.TestCase): + """ + Unittest class for holding tests related to the VernierCalliper class. + """ def setUp(self): + """ + Initialise useful attributes for testing. + """ self.calliper_a = VernierCalliper("test_calliper_a") self.calliper_b = VernierCalliper("test_calliper_b") def test_init(self): + """ + Test that the class initialises as expected (empty lists) + """ self.assertEqual(self.calliper_a.name, "test_calliper_a") self.assertEqual(self.calliper_a.time_percent, []) self.assertEqual(self.calliper_a.cumul_time, []) @@ -24,6 +39,10 @@ def test_init(self): self.assertEqual(self.calliper_a.n_calls, []) def test_reduce(self): + """ + Test that the reduce() method returns the expected results. + """ + # pylint: disable=pointless-statement self.calliper_a.time_percent = [10.0, 20.0] self.calliper_a.cumul_time = [30.0, 40.0] self.calliper_a.self_time = [5.0, 15.0] @@ -40,6 +59,11 @@ def test_reduce(self): self.assertEqual(reduced_data[6], 15.0) def test_compare(self): + """ + Test that the comparison of dataclasses works as expected for the + VernierCalliper class. + """ + # pylint: disable=pointless-statement self.calliper_a.time_percent = [10.0, 20.0] self.calliper_a.cumul_time = [30.0, 40.0] self.calliper_a.self_time = [5.0, 15.0] @@ -56,5 +80,6 @@ def test_compare(self): self.assertFalse(self.calliper_a > self.calliper_b) self.assertFalse(self.calliper_a == self.calliper_b) + if __name__ == '__main__': unittest.main() diff --git a/post-processing/tests/test_vernier_data.py b/post-processing/tests/test_vernier_data.py index c4db62bf..30164698 100644 --- a/post-processing/tests/test_vernier_data.py +++ b/post-processing/tests/test_vernier_data.py @@ -3,22 +3,35 @@ # The file LICENCE, distributed with this code, contains details of the terms # under which the code may be used. # ------------------------------------------------------------------------------ +""" +Module to hold tests for VernierData class and aggregate function. +""" from pathlib import Path from io import StringIO import tempfile import unittest import sys + +# pylint: disable=wrong-import-position sys.path.append(str(Path(__file__).parent.parent)) from vernier import VernierData, VernierDataCollation + class TestVernierData(unittest.TestCase): """ - Tests for the VernierData class and aggregator function + Tests for the VernierData class and aggregator function. """ def setUp(self): + """ + Initialise useful attributes for testing. + """ self.test_data = VernierData() def test_add_empty_calliper(self): + """ + Tests that a VernierCalliper instance is correctly added to the + VernierData instance. + """ self.test_data.add_calliper("test_calliper") self.assertIn("test_calliper", self.test_data.data) self.assertEqual(self.test_data.data["test_calliper"].time_percent, []) @@ -28,6 +41,10 @@ def test_add_empty_calliper(self): self.assertEqual(self.test_data.data["test_calliper"].n_calls, []) def test_filter_calliper(self): + """ + Tests that callipers that do not match the pattern pased to + `VernierData.filter()` are not returned. + """ self.test_data.add_calliper("timestep_calliper") self.test_data.add_calliper("other_calliper") filtered = self.test_data.filter(["timestep"]) @@ -35,11 +52,19 @@ def test_filter_calliper(self): self.assertNotIn("other_calliper", filtered.data) def test_filter_no_match(self): + """ + Test that an error is raised when no callipers are found when calling + the `VernierData.filter()` method. + """ self.test_data.add_calliper("timestep_calliper") with self.assertRaises(ValueError): self.test_data.filter(["nonexistent"]) def test_filter_multiple_matches(self): + """ + Tests that filter correctly returns all callipers with a substring + equal to the pattern passed to `VernierData.filter()` + """ self.test_data.add_calliper("timestep_calliper_1") self.test_data.add_calliper("timestep_calliper_2") filtered = self.test_data.filter(["timestep"]) @@ -47,11 +72,19 @@ def test_filter_multiple_matches(self): self.assertIn("timestep_calliper_2", filtered.data) def test_filter_empty_keys(self): + """ + Test that an error is raised when no filters are passed to the + `VernierData.filter()` method. + """ self.test_data.add_calliper("timestep_calliper") with self.assertRaises(ValueError): self.test_data.filter([]) def test_write_txt_output_file(self): + """ + Test that the formatting of write_txt_ouput is as expected when writing + to a file. + """ self.test_data.add_calliper("test_calliper") self.test_data.data["test_calliper"].time_percent = [10.0, 20.0] self.test_data.data["test_calliper"].cumul_time = [30.0, 40.0] @@ -59,13 +92,19 @@ def test_write_txt_output_file(self): self.test_data.data["test_calliper"].total_time = [25.0, 35.0] self.test_data.data["test_calliper"].n_calls = [2] + # pylint: disable=unspecified-encoding with tempfile.NamedTemporaryFile(delete=False) as tmp_file: self.test_data.write_txt_output(Path(tmp_file.name)) contents = Path(tmp_file.name).read_text().splitlines() + # pylint: disable=line-too-long self.assertEqual("| Routine | Total time (s) | Self (s) | Cumul time (s) | No. calls | % time | Time per call (s) |", contents[0]) self.assertEqual("| test_calliper | 30.0 | 10.0 | 35.0 | 2 | 15.0 | 15.0 |", contents[1]) def test_write_txt_output_terminal(self): + """ + Test that the formatting of write_txt_ouput is as expected when writing + to the terminal. + """ self.test_data.add_calliper("test_calliper") self.test_data.data["test_calliper"].time_percent = [50.0, 40.0] self.test_data.data["test_calliper"].cumul_time = [10.0, 12.0] @@ -78,10 +117,15 @@ def test_write_txt_output_terminal(self): self.test_data.write_txt_output() sys.stdout = sys.__stdout__ + # pylint: disable=line-too-long self.assertEqual("| Routine | Total time (s) | Self (s) | Cumul time (s) | No. calls | % time | Time per call (s) |", write_output.getvalue().splitlines()[0]) self.assertEqual("| test_calliper | 35.0 | 3.5 | 11.0 | 2 | 45.0 | 17.5 |", write_output.getvalue().splitlines()[1]) def test_aggregate(self): + """ + Tests that aggregate returns a single VernierData object with all + expected callipers and data. + """ data1 = VernierData() data1.add_calliper("calliper_a") data1.data["calliper_a"].time_percent = [10.0, 20.0] @@ -101,13 +145,28 @@ def test_aggregate(self): aggregated = VernierData() aggregated.aggregate([data1, data2]) self.assertIn("calliper_a", aggregated.data) - self.assertEqual(aggregated.data["calliper_a"].time_percent, [10.0, 20.0, 15.0, 25.0]) - self.assertEqual(aggregated.data["calliper_a"].cumul_time, [30.0, 40.0, 35.0, 45.0]) - self.assertEqual(aggregated.data["calliper_a"].self_time, [5.0, 15.0, 6.0, 16.0]) - self.assertEqual(aggregated.data["calliper_a"].total_time, [25.0, 35.0, 28.0, 38.0]) - self.assertEqual(aggregated.data["calliper_a"].n_calls, [2, 2, 3, 3]) + self.assertEqual( + aggregated.data["calliper_a"].time_percent, + [10.0, 20.0, 15.0, 25.0]) + self.assertEqual( + aggregated.data["calliper_a"].cumul_time, + [30.0, 40.0, 35.0, 45.0]) + self.assertEqual( + aggregated.data["calliper_a"].self_time, + [5.0, 15.0, 6.0, 16.0]) + self.assertEqual( + aggregated.data["calliper_a"].total_time, + [25.0, 35.0, 28.0, 38.0]) + self.assertEqual( + aggregated.data["calliper_a"].n_calls, + [2, 2, 3, 3]) def test_aggregate_inconsistent(self): + """ + Tests that an error is raised when VernierData objects with mismatched + callipers are passed to aggregate with internal consistency constraints + turned on. + """ data1 = VernierData() data1.add_calliper("calliper_a") data1.data["calliper_a"].time_percent = [10.0, 20.0] @@ -129,6 +188,10 @@ def test_aggregate_inconsistent(self): aggregated.aggregate([data1, data2]) def test_aggregate_inconsistent_ok(self): + """ + Test that combining VernierData objects without forced consistency + of callipers works as expected (both callipers present in output). + """ data1 = VernierData() data1.add_calliper("calliper_a") @@ -141,6 +204,9 @@ def test_aggregate_inconsistent_ok(self): self.assertIn("calliper_b", aggregated.data) def test_get(self): + """ + Test that the get method of the VernierData class works as expected. + """ data1 = VernierData() data1.add_calliper("calliper_a") data1.data["calliper_a"].time_percent = [10.0, 20.0] @@ -156,6 +222,9 @@ class TestVernierCollation(unittest.TestCase): Tests for the VernierData Collation class. """ def _add_data(self): + """ + Setup for testing the VernierCollation object. + """ self.collation = VernierDataCollation() data1 = VernierData() data1.add_calliper("calliper_a") @@ -175,22 +244,38 @@ def _add_data(self): self.collation.add_data('test1', data1) self.collation.add_data('test2', data2) - + def test_add_data(self): + """ + Test that the add_data method adds VernierData objects to the collation + attribute. + """ self._add_data() self.assertEqual(len(self.collation), 2) def test_remove_data(self): + """ + Test that the remove_data method drops the correct VernierData object + from the collation attribute. + """ self._add_data() self.collation.remove_data('test1') self.assertEqual(len(self.collation), 1) def test_get(self): + """ + Test that the get method of VernierCollation returns the expected + VernierData instance. + """ self._add_data() calliper_a = self.collation.get("calliper_a") self.assertEqual(len(calliper_a), 4) def test_internal_consistency(self): + """ + Test that the internal_consistency method of VernierCollation returns + the expected error type and message. + """ self._add_data() data_inc = VernierData() data_inc.add_calliper("calliper_a") @@ -210,7 +295,8 @@ def test_internal_consistency(self): with self.assertRaises(ValueError) as test_exception: self.collation.add_data('test3', data_inc) self.assertEqual(str(test_exception.exception), - "inconsistent callipers in new_vernier_data") + "Inconsistent callipers in new_vernier_data: " + "['calliper_a', 'calliper_b'] detected as unmatched") if __name__ == '__main__': diff --git a/post-processing/tests/test_vernier_reader.py b/post-processing/tests/test_vernier_reader.py index 9723e5cc..66c4da57 100644 --- a/post-processing/tests/test_vernier_reader.py +++ b/post-processing/tests/test_vernier_reader.py @@ -6,9 +6,12 @@ from pathlib import Path import unittest import sys + +# pylint: disable=wrong-import-position sys.path.append(str(Path(__file__).parent.parent)) from vernier.vernier_reader import VernierReader + class TestVernierReader(unittest.TestCase): """ Tests for the VernierReader class diff --git a/post-processing/vernier/__init__.py b/post-processing/vernier/__init__.py index 7d9152bf..ab30defa 100644 --- a/post-processing/vernier/__init__.py +++ b/post-processing/vernier/__init__.py @@ -1,7 +1,11 @@ -from .vernier_data import VernierData -from .vernier_data import VernierCalliper -from .vernier_data import VernierDataCollation -from .vernier_reader import VernierReader +from vernier.vernier_data import VernierData +from vernier.vernier_data import VernierCalliper +from vernier.vernier_data import VernierDataCollation +from vernier.vernier_reader import VernierReader -__all__ = ["VernierData", "VernierReader", - "VernierCalliper", "VernierDataCollation"] +__all__ = [ + "VernierCalliper", + "VernierData", + "VernierDataCollation", + "VernierReader", +] diff --git a/post-processing/vernier/vernier_data.py b/post-processing/vernier/vernier_data.py index 116c10a8..076ffdc5 100644 --- a/post-processing/vernier/vernier_data.py +++ b/post-processing/vernier/vernier_data.py @@ -3,16 +3,23 @@ # The file LICENCE, distributed with this code, contains details of the terms # under which the code may be used. # ------------------------------------------------------------------------------ +""" +Module for storing the VernierData and VernierCalliper classes. +""" from dataclasses import dataclass import sys import numpy as np from pathlib import Path from typing import Optional + @dataclass(order=True) class VernierCalliper(): - """Class to hold data for a single Vernier calliper, including arrays for each metric.""" + """ + Class to hold data for a single Vernier calliper, including arrays for + each metric. + """ total_time: list[float] time_percent: list[float] self_time: list[float] @@ -21,7 +28,13 @@ class VernierCalliper(): name: str def __init__(self, name: str): + """ + Initialise the VernierCalliper instance with lists ready to be filled + with calliper data. + :param str name: The name of the VernierCalliper. + + """ self.name = name self.time_percent = [] self.cumul_time = [] @@ -43,8 +56,13 @@ def __len__(self): return result def reduce(self) -> list: - """Reduces the data for this calliper to a single row of summary data.""" + """Reduces the data for this calliper to a single row of summary data. + :returns: A list containing the aggregate (mean) for each metric of the + VernierCalliper instance. + :rtype: list[str, float, float, float, int, float, float] + + """ return [ self.name.replace('@0', ''), # calliper name round(np.mean(self.total_time), 5), # mean total time across calls @@ -63,29 +81,49 @@ def labels(self): class VernierData(): """ - Class to hold Vernier data from a single instrumented job in a structured way. + Class to hold Vernier data from a single instrumented job in a structured + way. + Provides methods for filtering and outputting the data. """ - def __init__(self): - + """ + Initialises the VernierData instance with an empty dictionary for + storing VernierCalliper objects. + """ self.data = {} - return - - def add_calliper(self, calliper_key: str): - """Adds a new calliper to the data structure, with empty arrays for each metric.""" + """ + Adds a new calliper to the data structure, with empty arrays for each + metric. + :param str calliper_key: The name of the calliper to be added, also + used to access the calliper. + + """ # Create empty data arrays self.data[calliper_key] = VernierCalliper(calliper_key) def filter(self, calliper_keys: list[str]): - """Filters the Vernier data to include only callipers matching the provided keys. - The filtering is done in a glob-like fashion, so an input key of "timestep" - will match any calliper with "timestep" in its name.""" + """ + Filters the Vernier data to include only callipers matching the + provided keys. The filtering is done in a glob-like fashion, so an + input key of "timestep" will match any calliper with "timestep" in + its name. + + :param list[str] calliper_keys: A list of keys to extract from the + callipers owned by the VernierData + instance. + + :returns: A new VernierData object containing only the + filtered callipers. + :rtype: :py:class:`vernier.VernierData` + :raises ValueError: if no callipers are found that match calliper_keys. + + """ filtered_data = VernierData() # Filter data for a given calliper key @@ -94,22 +132,34 @@ def filter(self, calliper_keys: list[str]): filtered_data.data[timer] = self.data[timer] if len(filtered_data.data) == 0: - raise ValueError(f"No callipers found matching the provided keys: {calliper_keys}") + raise ValueError(f"No callipers found matching the provided keys: " + f"{calliper_keys}") return filtered_data - def write_txt_output(self, txt_path: Optional[Path] = None): - """Writes the Vernier data to a text output in a human-readable table format. - If an output path is provided, the table is written to that file. Otherwise, - it is printed to the terminal.""" + """ + Writes the Vernier data to a text output in a human-readable table + format. If an output path is provided, the table is written to that + file. Otherwise, it is printed to the terminal. + :param txt_path: The file path that the text output will be written to. + :type txt_path: :py:class:`pathlib.Path` + + """ txt_table = [] for calliper in self.data.keys(): txt_table.append(self.data[calliper].reduce()) - txt_table = sorted(txt_table, key=lambda x: x[2], reverse=True) # sort by self time, descending + # sort by self time, descending + txt_table = sorted(txt_table, key=lambda x: x[2], reverse=True) - txt_table.insert(0, ["Routine", "Total time (s)", "Self (s)", "Cumul time (s)", "No. calls", "% time", "Time per call (s)"]) + txt_table.insert(0, ["Routine", + "Total time (s)", + "Self (s)", + "Cumul time (s)", + "No. calls", + "% time", + "Time per call (s)"]) max_calliper_len = max([len(line[0]) for line in txt_table]) @@ -136,10 +186,25 @@ def get(self, calliper_key): def aggregate(self, vernier_data_list=None, internal_consistency=True): """ Aggregates a list of VernierData objects into a single VernierData - object, by concatenating the data for each calliper across the input + object by concatenating the data for each calliper across the input objects. - """ + :param vernier_data_list: A list of VernierData objects to combine. + :type vernier_datalist: list[:py:class:`vernier.VernierData`] + + :param bool internal_conistency: If set to True (default), callipers + between all items in the + vernier_data_list must be identical. + + :returns: A single VernierData object containing the data from all + VernierData objects in vernier_data_list. + :rtype: :py:class:`vernier.VernierData` + + :raises ValueError: if internal_consistency is set to True and callipers + between items in vernier_data_list are not + identical. + + """ if vernier_data_list is None: vernier_data_list = [] if internal_consistency: @@ -165,6 +230,7 @@ def aggregate(self, vernier_data_list=None, internal_consistency=True): self.data[calliper].n_calls.extend(vernier_data.data[calliper].n_calls) + class VernierDataCollation(): """ Class to hold an collation of VernierData instances. @@ -173,56 +239,102 @@ class VernierDataCollation(): """ def __init__(self): + """ + Initialise an empty dictionary for storing VernierData objects. + """ self.vernier_data = {} - return def __len__(self): + """ + Gets the number of VernierData objects held by the VernierDataCollation + object. + + :returns: The number of VernierData objects. + :rtype: int + + """ return len(self.vernier_data) def add_data(self, label, vernier_data): + """ + Add a VernierData object to the collation. + + """ if label in self.vernier_data: raise ValueError(f'The label {label} already exists in this ' - 'collation. Please use a different label or ' - 'remove the existing entry.') + f'collation. Please use a different label or ' + f'remove the existing entry.') if not isinstance(vernier_data, VernierData): raise TypeError(f'The provided vernier_data is not a VernierData ' - 'object.') + f'object.') + # Check for consistency self.internal_consistency(vernier_data) + # Add the new data self.vernier_data[label] = vernier_data def remove_data(self, label): + """ + Removes a VernierData object with a matching label from the collation + object. + + :param str label: The label of the VernierData object to be removed. + """ if label not in self.vernier_data: raise ValueError(f'The label {label} does not exist in this ' 'collation.') - discarded = self.vernier_data.pop(label) + + # Drop the VernierData object from the collation. + self.vernier_data.pop(label) def internal_consistency(self, new_vernier_data=None): """ Enforce internal consistency, with the same callipers for all members. + + :param new_vernier_data: A VernierData object being tested for + consistent callipers. + """ # notImplemented enforce consistent sizing of members?? needed? callipers = [] - for k, vdata in self.vernier_data.items(): + # Loop over VernierData objects + for _, vdata in self.vernier_data.items(): + # Get a list of the VernierData's callipers, sorted by name loop_callipers = sorted(list(vdata.data.keys())) + # If no callipers checked yet, set these as the truth callipers if len(callipers) == 0: callipers = loop_callipers else: + # Check callipers against 'truth' if loop_callipers != callipers: - raise ValueError('inconsistent callipers in contents') + # Extract callipers that were mismatched using Python XOR + # Note that this works both ways so all callipers not in the + # others list are included. + mismatched = set(loop_callipers) ^ set(callipers) + raise ValueError(f'Inconsistent callipers in contents: ' + f'{mismatched} detected as unmatched') if new_vernier_data is not None: if not isinstance(new_vernier_data, VernierData): raise TypeError(f'The provided vernier_data is not a ' 'VernierData object.') check_callipers = sorted(list(new_vernier_data.data.keys())) if callipers and check_callipers != callipers: - raise ValueError('inconsistent callipers in new_vernier_data') + # Extract callipers that were mismatched using Python XOR + mismatched = set(check_callipers) ^ set(callipers) + raise ValueError(f'Inconsistent callipers in new_vernier_data: ' + f'{check_callipers} detected as unmatched') def calliper_list(self): - """Return the list of callipers in this collation.""" + """ + Return the list of callipers in this collation. + + :returns: A list of all callipers held by the collation + :rtype: list[str] + + """ result = [] self.internal_consistency() - for k, vdata in self.vernier_data.items(): + for _, vdata in self.vernier_data.items(): result = sorted(list(vdata.data.keys())) break return result @@ -232,12 +344,19 @@ def get(self, calliper_key): Return a VernierCalliper of all the data from all collation members for this calliper_key, or None if it does not exist. + :param str calliper_key: The name of the VernierCalliper to extract + from the VernierData object. + + :returns: A VernierCalliper instance containing the data of all + callipers matching the calliper_key + :rtype: :py:class:`vernier.VernierCalliper` + """ if calliper_key not in self.calliper_list(): return None self.internal_consistency() results = VernierCalliper(calliper_key) - for akey, vdata in self.vernier_data.items(): + for _, vdata in self.vernier_data.items(): results.total_time += vdata.data[calliper_key].total_time results.time_percent += vdata.data[calliper_key].time_percent results.self_time += vdata.data[calliper_key].self_time diff --git a/post-processing/vernier/vernier_reader.py b/post-processing/vernier/vernier_reader.py index ac015e7f..dc27dff6 100644 --- a/post-processing/vernier/vernier_reader.py +++ b/post-processing/vernier/vernier_reader.py @@ -6,23 +6,31 @@ from concurrent import futures from pathlib import Path import os -from .vernier_data import VernierData +from vernier.vernier_data import VernierData + class VernierReader(): - """Class handling the reading of Vernier output files, and converting them into a VernierData object.""" + """ + Class handling the reading of Vernier output files, and converting them + into a VernierData object. + """ def __init__(self, vernier_path: Path): + """ + Initialise the VernierReader instance and set the associated path as + an attribute. + :param vernier_path: Path to the Vernier data file. + :type vernier_path: :py:class:`pathlib.Path` + """ self.path = vernier_path - return - - def _load_from_file(self) -> VernierData: """ - Loads Vernier data from a single file, and returns it as a VernierData object. - """ + Loads Vernier data from a single file, and returns it as a VernierData + object. + """ loaded = VernierData() # Populate data @@ -47,20 +55,27 @@ def _load_from_file(self) -> VernierData: return loaded - def _load_from_directory(self) -> VernierData: - """Loads Vernier data from a directory of files, and returns it as a VernierData object.""" + """ + Loads Vernier data from a directory of files, and returns it as a + VernierData object. - vernier_files = [f for f in os.listdir(self.path) if f.startswith("vernier-output")] + """ + vernier_files = [f for f in os.listdir(self.path) if + f.startswith("vernier-output")] with futures.ThreadPoolExecutor() as pool: - vernier_datasets = list(pool.map(lambda f: VernierReader(self.path / f)._load_from_file(), vernier_files)) + vernier_datasets = list( + pool.map(lambda f: + VernierReader( + self.path / f)._load_from_file(), + vernier_files) + ) result = VernierData() result.aggregate(vernier_datasets) return result - def load(self) -> VernierData: """Generic load routine for Vernier data, aiming to handle both single files and directories of files.""" @@ -72,4 +87,5 @@ def load(self) -> VernierData: return self._load_from_directory() else: - raise ValueError(f"Provided path '{self.path}' is neither a file nor a directory.") + raise ValueError(f"Provided path '{self.path}' is neither a file " + f"nor a directory.")