Skip to content
178 changes: 57 additions & 121 deletions sofar/sofa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]}")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the error message failed in case a conventio is deprecated but a later version of the same convnetion is available. Simplified the error message to work in all cases

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 "
Expand Down
8 changes: 1 addition & 7 deletions tests/test_deprecations.py
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
24 changes: 13 additions & 11 deletions tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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")
Expand Down
48 changes: 20 additions & 28 deletions tests/test_sofa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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")
Expand Down