diff --git a/sofar/sofa.py b/sofar/sofa.py index 3701b4e..4723932 100644 --- a/sofar/sofa.py +++ b/sofar/sofa.py @@ -304,107 +304,6 @@ def _update_dimensions(self): "of wrong type or shape were detected. " "Call Sofa.verify() for more information.")) - def info(self, info="all"): - """ - Print information about the convention of a SOFA object. - - Prints the variable type (attribute, double, string), shape, flags - (mandatory, read only) and comment (if any) for each or selected - entries. - - Parameters - ---------- - info : str - Specifies the kind of information that is printed: - - ``'all'`` ``'mandatory'`` ``'optional'`` ``'read only'`` ``'data'`` - Print the name, type, shape, and flags and comment for all or - selected entries of the SOFA object. ``'data'`` does not show - entries of type attribute. - key - If key is the name of an object attribute, all information for - attribute will be printed. - """ - - # warn for upcoming deprecation - warnings.warn(( - 'Sofa.info() will be deprecated in sofar 1.3.0 The conventions are' - ' now documented at ' - 'https://sofar.readthedocs.io/en/stable/resources/conventions.html'), - UserWarning, stacklevel=1) - - # update the private attribute `_convention` to make sure the required - # meta data is in place - if not hasattr(self, "_convention"): - self._reset_convention() - - # list of all attributes - keys = [k for k in self.__dict__.keys() if not k.startswith("_")] - - # start printing the information - info_str = ( - f"{self.GLOBAL_SOFAConventions} " - F"{self.GLOBAL_SOFAConventionsVersion} " - f"(SOFA version {self.GLOBAL_Version})\n") - info_str += "-" * len(info_str) + "\n" - - if info in ["all", "mandatory", "optional", "read only", "data"]: - - info_str += f"showing {info} entries : type (shape), flags\n\n" - - for key in keys: - - # check if field should be skipped - flags = self._convention[key]["flags"] - if (not self._mandatory(flags) and info == "mandatory") \ - or \ - (self._mandatory(flags) and info == "optional") \ - or \ - (not self._read_only(flags) and info == "read only") \ - or \ - (self._convention[key]['type'] == "attribute" and - info == "data"): - continue - - info_str += f"{key} : {self._convention[key]['type']}" - - if self._convention[key]['dimensions']: - info_str += \ - f" ({self._convention[key]['dimensions'].upper()})" - - if self._mandatory(flags): - info_str += ", mandatory" - else: - info_str += ", optional" - if self._read_only(flags): - info_str += ", read only" - - if self._convention[key]['comment']: - info_str += f"\n {self._convention[key]['comment']}\n" - else: - info_str += "\n" - - elif info in keys: - - for key in [k for k in keys if info in k]: - comment = str(self._convention[key]['comment']) - - info_str += ( - f"{key}\n" - f" type: {self._convention[key]['type']}\n" - f" mandatory: " - f"{self._mandatory(self._convention[key]['flags'])}\n" - f" read only: " - f"{self._read_only(self._convention[key]['flags'])}\n" - f" default: {self._convention[key]['default']}\n" - f" shape: " - f"{str(self._convention[key]['dimensions']).upper()}\n" - f" comment: {comment}\n") - else: - raise ValueError(f"info='{info}' is invalid") - - print(info_str) - def inspect(self, file=None, issue_handling="print"): """ Get information about data inside a SOFA object. @@ -490,6 +389,54 @@ def inspect(self, file=None, issue_handling="print"): # output to console print(info_str) + @property + def convention_status(self): + """ + Get the status of the SOFA convention. + + Returns + ------- + status : str + The status of the SOFA convention + + - ``'current'`` if the convention is up to date. + - ``'deprecated'`` if the convention is outdated. In this case + :py:func:`~upgrade_convention` can be used to upgrade the data to + the latest version of the convention. + - ``'preliminary'`` if the convention is still under development + and not contained in the official SOFA standard, which is + indicated by a version number smaller than 1.0. Note that + preliminary conventions may be subject to change or could be + discarded completely. Data written with preliminary conventions + might thus become invalid in the future. + """ + + status = None + + # get deprecations and information about Sofa object + _, _, deprecations, upgrade = self._verification_rules() + convention = self.GLOBAL_SOFAConventions + version = self.GLOBAL_SOFAConventionsVersion + + # conventions can be completely deprecated or upgradable to a later + # version of the same convention or to a later convention + if convention in deprecations["GLOBAL:SOFAConventions"]: + status = 'deprecated' + elif convention in upgrade: + for from_to in upgrade[convention]["from_to"]: + if version in from_to[0]: + status = 'deprecated' + break + # conventions are preliminary if they are not deprecated and have a + # version number < 1.0 + if status is None and parse(version) < parse('1.0'): + status = 'preliminary' + # if both is not the case, the convention is current. + if status is None: + status = 'current' + + return status + def add_missing(self, mandatory=True, optional=True, verbose=True): """ Add missing data with default values. @@ -759,26 +706,17 @@ def upgrade_convention(self, target=None, verify='auto'): # check input --------------------------------------------------------- self._reset_convention() + status = self.convention_status # get deprecations and information about Sofa object - _, _, deprecations, upgrade = self._verification_rules() + _, _, _, upgrade = self._verification_rules() convention_current = self.GLOBAL_SOFAConventions version_current = self.GLOBAL_SOFAConventionsVersion sofa_version_current = self.GLOBAL_Version - # check if convention is deprecated ----------------------------------- - is_deprecated = False - - if convention_current in deprecations["GLOBAL:SOFAConventions"]: - is_deprecated = True - elif convention_current in upgrade: - for from_to in upgrade[convention_current]["from_to"]: - if version_current in from_to[0]: - is_deprecated = True - break - # check for upgrades -------------------------------------------------- - if is_deprecated: + if status == 'deprecated': + # check if upgrade is available for this convention if convention_current not in upgrade: print((f"Convention {convention_current} v{version_current} is" @@ -979,6 +917,7 @@ def verify(self, issue_handling="raise", mode="write"): # --------------------------------------------------------------------- # 0. update the convention self._reset_convention() + status = self.convention_status # --------------------------------------------------------------------- # 1. check if the mandatory attributes are contained @@ -1389,21 +1328,18 @@ def verify(self, issue_handling="raise", mode="write"): # --------------------------------------------------------------------- # 8. check deprecations # (so far there are only deprecations for the convention) - if self.GLOBAL_SOFAConventions in \ - deprecations["GLOBAL:SOFAConventions"]: - convention = self.GLOBAL_SOFAConventions + if status == 'deprecated': msg = ("Detected deprecations:\n" f"- GLOBAL_SOFAConventions is " - f"{self.GLOBAL_SOFAConventions}, which is deprecated. Use " - "Sofa.upgrade_convention() to upgrade to " - f"{deprecations['GLOBAL:SOFAConventions'][convention]}") + f"{self.GLOBAL_SOFAConventions}, which is deprecated. See " + "Sofa.upgrade_convention() for upgrade possibilities.") if mode == "write": error_msg += msg else: warning_msg += msg # warn if preliminary conventions versions are used - if float(self.GLOBAL_SOFAConventionsVersion) < 1.0: + if status == 'preliminary': warning_msg += ( "\n\nDetected preliminary conventions version " f"{self.GLOBAL_SOFAConventionsVersion}:\n - Upgrade data to " diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index f6251e6..627bfd3 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -1,16 +1,10 @@ import pytest from packaging import version -import re import sofar as sf # deprecate in 1.3.0 ---------------------------------------------------------- -def test_pad_zero_modi(): - with pytest.warns( - UserWarning, - match=re.escape('Sofa.info() will be deprecated in sofar 1.3.0')): - sofa = sf.Sofa('GeneralTF') - sofa.info() +def test_Sofa_info(): if version.parse(sf.__version__) >= version.parse('1.3.0'): sofa = sf.Sofa('GeneralTF') diff --git a/tests/test_io.py b/tests/test_io.py index 6186b4d..f8e93c4 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -12,7 +12,6 @@ import numpy as np import numpy.testing as npt from netCDF4 import Dataset -from packaging.version import parse def test_read_write_sofa(): @@ -166,18 +165,21 @@ def test_roundtrip(mandatory): for name, version in names_versions: print(f"Testing: {name} {version}") - # writing deprecated and proposed conventions is not tested - if name in deprecations["GLOBAL:SOFAConventions"] or \ - parse(version) < parse('1.0'): - sofa = sf.Sofa(name, mandatory, version, verify=False) - # non stable conventions are not verified - if parse(version) >= parse('1.0'): - with pytest.warns(UserWarning, match="deprecations"): - sofa.verify(mode="read") - else: + # create Sofa object without verification. Verification will be done + # when writing below. + sofa = sf.Sofa(name, mandatory, version, verify=False) + status = sofa.convention_status + + if status == 'preliminary': + # don't test anything for preliminary conventions + return + elif status == 'deprecated': + # don't test writing deprecated conventions + with pytest.warns(UserWarning, match="deprecations"): + sofa.verify(mode="read") + elif status == 'current': # test full round-trip for other conventions file = os.path.join(temp_dir.name, name + ".sofa") - sofa = sf.Sofa(name, mandatory, version) sf.write_sofa(file, sofa) sofa_r = sf.read_sofa(file) identical = sf.equals(sofa, sofa_r, verbose=True, exclude="DATE") diff --git a/tests/test_sofa.py b/tests/test_sofa.py index ad0474e..a947938 100644 --- a/tests/test_sofa.py +++ b/tests/test_sofa.py @@ -146,34 +146,6 @@ def test_get_dimension(): size = sofa.get_dimension("Q") -def test_info(capfd): - - sofa = sf.Sofa("SimpleFreeFieldHRIR") - - # test with wrong info string - with pytest.raises( - ValueError, match="info='invalid' is invalid"): - sofa.info("invalid") - - # test with default parameter - sofa.info() - out, _ = capfd.readouterr() - assert "showing all entries" in out - - # test listing all entry names - for info in ["all", "mandatory", "optional", "read only", "data"]: - sofa.info(info) - out, _ = capfd.readouterr() - assert f"showing {info} entries" in out - - # list information for specific entry - sofa.info("ListenerPosition") - out, _ = capfd.readouterr() - assert "ListenerPosition\n type: double" in out - assert "ListenerPosition_Type\n type: attribute" in out - assert "ListenerPosition_Units\n type: attribute" in out - - def test_inspect(capfd): temp_dir = TemporaryDirectory() @@ -197,6 +169,26 @@ def test_inspect(capfd): assert out == "".join(text) +@pytest.mark.parametrize(('sofa', 'status'), [ + # up to date convention + (sf.Sofa('FreeFieldDirectivityTF'), + 'current'), + # convention with outdated version + (sf.Sofa('FreeFieldDirectivityTF', version='1.0', verify=False), + 'deprecated'), + # deprecated convention + (sf.Sofa('GeneralFIRE', verify=False), + 'deprecated'), + # preliminary convention. NOTE: This test will fail if the convention + # becomes standardized. In this case the status will change to 'deprecated' + (sf.Sofa('AnnotatedEmitterAudio', version='0.2', verify=False), + 'preliminary'), +]) +def test_deprecated(sofa, status): + + assert sofa.convention_status == status + + def test_add_entry(): sofa = sf.Sofa("GeneralTF")