sofar 1.0.0__py2.py3-none-any.whl → 1.1.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sofar/__init__.py CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
  __author__ = """The pyfar developers"""
6
6
  __email__ = 'info@pyfar.org'
7
- __version__ = '1.0.0'
7
+ __version__ = '1.1.1'
8
8
 
9
9
  from .sofa import Sofa
10
10
 
11
- from .io import read_sofa, write_sofa
11
+ from .io import read_sofa, read_sofa_as_netcdf, write_sofa
12
12
 
13
13
  from .utils import (list_conventions,
14
14
  equals,
@@ -21,6 +21,7 @@ __all__ = ['Sofa',
21
21
  'update_conventions',
22
22
  'list_conventions',
23
23
  'read_sofa',
24
+ 'read_sofa_as_netcdf',
24
25
  'write_sofa',
25
26
  'equals',
26
27
  'version']
sofar/io.py CHANGED
@@ -3,7 +3,8 @@ import os
3
3
  import numpy as np
4
4
  from netCDF4 import Dataset, chartostring, stringtochar
5
5
  import warnings
6
- import packaging
6
+ import pathlib
7
+ from packaging.version import parse
7
8
  import sofar as sf
8
9
  from .utils import _verify_convention_and_version, _atleast_nd
9
10
 
@@ -18,8 +19,7 @@ def read_sofa(filename, verify=True, verbose=True):
18
19
  Parameters
19
20
  ----------
20
21
  filename : str
21
- The filename. '.sofa' is appended to the filename, if it is not
22
- explicitly given.
22
+ The full path to the sofa data.
23
23
  verify : bool, optional
24
24
  Verify and update the SOFA object by calling :py:func:`~Sofa.verify`.
25
25
  This helps to find potential errors in the default values and is thus
@@ -32,7 +32,7 @@ def read_sofa(filename, verify=True, verbose=True):
32
32
  Returns
33
33
  -------
34
34
  sofa : Sofa
35
- The SOFA object filled with the default values of the convention.
35
+ Object containing the data from `filename`.
36
36
 
37
37
  Notes
38
38
  -----
@@ -52,9 +52,63 @@ def read_sofa(filename, verify=True, verbose=True):
52
52
  will be a scalar inside SOFA objects after reading from disk.
53
53
  """
54
54
 
55
+ return _read_netcdf(filename, verify, verbose, mode="sofa")
56
+
57
+
58
+ def read_sofa_as_netcdf(filename):
59
+ """
60
+ Read corrupted SOFA data from disk.
61
+
62
+ .. note::
63
+ `read_sofa_as_netcdf` is intended to read and fix corrupted SOFA data
64
+ that could not be read by :py:func:`~read_sofa`. The recommend workflow
65
+ is
66
+
67
+ - Try to read the data with `read_sofa` and ``verify=True``
68
+ - If this fails, try the above with ``verify=False``
69
+ - If this fails, use `read_sofa_as_netcdf`
70
+
71
+ The SOFA object returned by `read_sofa_as_netcdf` may not work
72
+ correctly before the issues with the data were fixed, i.e., before the
73
+ data are in agreement with the SOFA standard AES-69.
74
+
75
+ Numeric data is returned as floats or numpy float arrays unless they have
76
+ missing data, in which case they are returned as numpy masked arrays.
77
+
78
+ Parameters
79
+ ----------
80
+ filename : str
81
+ The full path to the NetCDF data.
82
+
83
+ Returns
84
+ -------
85
+ sofa : Sofa
86
+ Object containing the data from `filename`.
87
+
88
+ Notes
89
+ -----
90
+
91
+ 1. Missing dimensions are appended when writing the SOFA object to disk.
92
+ E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
93
+ of shape (1, 2, 1) because the SOFA standard AES69-2020 defines it as a
94
+ three dimensional array with the dimensions (`M: measurements`,
95
+ `R: receivers`, `N: samples`)
96
+ 2. When reading data from a SOFA file, array data is always returned as
97
+ numpy arrays and singleton trailing dimensions are discarded (numpy
98
+ default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
99
+ after writing and reading to and from disk.
100
+ 3. One dimensional arrays with only one element will be converted to scalar
101
+ values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
102
+ (1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
103
+ will be a scalar inside SOFA objects after reading from disk.
104
+ """
105
+ return _read_netcdf(filename, False, False, mode="netcdf")
106
+
107
+
108
+ def _read_netcdf(filename, verify, verbose, mode):
109
+
55
110
  # check the filename
56
- if not filename.endswith('.sofa'):
57
- raise ValueError("Filename must end with .sofa")
111
+ filename = pathlib.Path(filename).with_suffix('.sofa')
58
112
  if not os.path.isfile(filename):
59
113
  raise ValueError(f"{filename} does not exist")
60
114
 
@@ -68,29 +122,25 @@ def read_sofa(filename, verify=True, verbose=True):
68
122
  # open new NETCDF4 file for reading
69
123
  with Dataset(filename, "r", format="NETCDF4") as file:
70
124
 
71
- # get convention name and version
72
- convention = getattr(file, "SOFAConventions")
73
- all_attr.append("GLOBAL_SOFAConventions")
74
- version_in = getattr(file, "SOFAConventionsVersion")
75
- all_attr.append("GLOBAL_SOFAConventionsVersion")
125
+ if mode == "sofa":
126
+ # get convention name and version
127
+ convention = getattr(file, "SOFAConventions")
128
+ version = getattr(file, "SOFAConventionsVersion")
76
129
 
77
- # check if convention and version exist
78
- version_out = _verify_convention_and_version(
79
- version_in, version_in, convention)
130
+ # check if convention and version exist
131
+ _verify_convention_and_version(version, convention)
80
132
 
81
- # get SOFA object with default values
82
- sofa = sf.Sofa(convention, version=version_out, verify=verify)
133
+ # get SOFA object with default values
134
+ sofa = sf.Sofa(convention, version=version, verify=verify)
135
+ else:
136
+ sofa = sf.Sofa(None)
83
137
 
84
138
  # allow writing read only attributes
85
- sofa._protected = False
139
+ sofa.protected = False
86
140
 
87
141
  # load global attributes
88
142
  for attr in file.ncattrs():
89
143
 
90
- if attr in ["SOFAConventionsVersion", "SOFAConventions"]:
91
- # convention and version were already set above
92
- continue
93
-
94
144
  value = getattr(file, attr)
95
145
  all_attr.append(f"GLOBAL_{attr}")
96
146
 
@@ -98,7 +148,7 @@ def read_sofa(filename, verify=True, verbose=True):
98
148
  sofa._add_custom_api_entry(
99
149
  f"GLOBAL_{attr}", value, None, None, "attribute")
100
150
  custom.append(f"GLOBAL_{attr}")
101
- sofa._protected = False
151
+ sofa.protected = False
102
152
  else:
103
153
  setattr(sofa, f"GLOBAL_{attr}", value)
104
154
 
@@ -117,7 +167,7 @@ def read_sofa(filename, verify=True, verbose=True):
117
167
  sofa._add_custom_api_entry(var.replace(".", "_"), value, None,
118
168
  dimensions, dtype)
119
169
  custom.append(var.replace(".", "_"))
120
- sofa._protected = False
170
+ sofa.protected = False
121
171
 
122
172
  # load variable attributes
123
173
  for attr in [a for a in file[var].ncattrs() if a not in skip]:
@@ -130,7 +180,7 @@ def read_sofa(filename, verify=True, verbose=True):
130
180
  var.replace(".", "_") + "_" + attr, value, None,
131
181
  None, "attribute")
132
182
  custom.append(var.replace(".", "_") + "_" + attr)
133
- sofa._protected = False
183
+ sofa.protected = False
134
184
  else:
135
185
  setattr(sofa, var.replace(".", "_") + "_" + attr, value)
136
186
 
@@ -142,7 +192,7 @@ def read_sofa(filename, verify=True, verbose=True):
142
192
  delattr(sofa, attr)
143
193
 
144
194
  # do not allow writing read only attributes any more
145
- sofa._protected = True
195
+ sofa.protected = True
146
196
 
147
197
  # notice about custom entries
148
198
  if custom and verbose:
@@ -206,20 +256,23 @@ def _write_sofa(filename: str, sofa: sf.Sofa, compression=4, verify=True):
206
256
  """
207
257
 
208
258
  # check the filename
209
- if not filename.endswith('.sofa'):
210
- raise ValueError("Filename must end with .sofa")
211
-
212
- # check if the latest version is used for writing and warn otherwise
213
- # if case required for writing SOFA test data that violates the conventions
214
- if sofa.GLOBAL_SOFAConventions != "invalid-value":
215
- latest = sf.Sofa(sofa.GLOBAL_SOFAConventions)
216
- latest = latest.GLOBAL_SOFAConventionsVersion
217
- current = sofa.GLOBAL_SOFAConventionsVersion
218
-
219
- if packaging.version.parse(current) < packaging.version.parse(latest):
220
- warnings.warn(("Writing SOFA object with outdated Convention "
221
- f"version {current}. Use version='latest' to write "
222
- f"data with version {latest}."))
259
+ filename = pathlib.Path(filename).with_suffix('.sofa')
260
+
261
+ if verify:
262
+ # check if the latest version is used for writing and warn otherwise
263
+ # if case required for writing SOFA test data that violates the
264
+ # conventions
265
+ if sofa.GLOBAL_SOFAConventions != "invalid-value":
266
+ latest = sf.Sofa(sofa.GLOBAL_SOFAConventions)
267
+ latest = latest.GLOBAL_SOFAConventionsVersion
268
+ current = sofa.GLOBAL_SOFAConventionsVersion
269
+
270
+ if parse(current) < parse(latest):
271
+ warnings.warn((
272
+ "Writing SOFA object with outdated Convention "
273
+ f"version {current}. It is recommend to upgrade "
274
+ " data with Sofa.upgrade_convention() before "
275
+ "writing to disk if possible."))
223
276
 
224
277
  # setting the netCDF compression parameter
225
278
  use_zlib = compression != 0
sofar/sofa.py CHANGED
@@ -89,35 +89,43 @@ class Sofa():
89
89
  """See class docstring"""
90
90
 
91
91
  # get convention
92
- self._convention = self._load_convention(convention, version)
92
+ if convention is not None:
93
+ self._convention = self._load_convention(convention, version)
93
94
 
94
- # update read only attributes
95
- self._read_only_attr = [
96
- key for key in self._convention.keys()
97
- if self._read_only(self._convention[key]["flags"])]
95
+ # update read only attributes
96
+ self._read_only_attr = [
97
+ key for key in self._convention.keys()
98
+ if self._read_only(self._convention[key]["flags"])]
98
99
 
99
- # add attributes with default values
100
- self._convention_to_sofa(mandatory)
100
+ # add attributes with default values
101
+ self._convention_to_sofa(mandatory)
101
102
 
102
- # add and update the API
103
- # (mandatory=False can not be verified because some conventions have
104
- # default values that have optional variables as dependencies)
105
- if verify and not mandatory:
106
- self.verify(mode="read")
103
+ # add and update the API
104
+ # (mandatory=False can not be verified because some conventions
105
+ # have default values that have optional variables as dependencies)
106
+ if verify and not mandatory:
107
+ self.verify(mode="read")
107
108
 
108
- self._protected = True
109
+ self.protected = True
110
+ else:
111
+ verify = False
112
+ self._convention = {}
109
113
 
110
114
  def __setattr__(self, name: str, value):
111
115
  # don't allow new attributes to be added outside the class
112
- if self._protected and not hasattr(self, name):
116
+ if self.protected and not hasattr(self, name):
113
117
  raise TypeError(f"{name} is an invalid attribute")
114
118
 
115
119
  # don't allow setting read only attributes
116
- if name in self._read_only_attr and self._protected:
117
- raise TypeError(f"{name} is a read only attribute")
120
+ if name in self._read_only_attr and self.protected:
121
+ raise TypeError((
122
+ f"{name} is a read only attribute. Iy you know what you are "
123
+ "doing, you can set Sofa.protected = False to write read "
124
+ "only data (e.g., to repair corrupted SOFA data)."))
118
125
 
119
126
  # convert to numpy array or scalar
120
- if not isinstance(value, (str, dict, np.ndarray)):
127
+ if not isinstance(value, (str, dict, np.ndarray)) \
128
+ and name != "protected":
121
129
  value = np.atleast_2d(value)
122
130
  if value.size == 1:
123
131
  value = value.flatten()[0]
@@ -129,7 +137,7 @@ class Sofa():
129
137
  if not hasattr(self, name):
130
138
  raise TypeError(f"{name} is not an attribute")
131
139
  # delete anything if not frozen, delete non mandatory
132
- if not self._protected or \
140
+ if not self.protected or \
133
141
  not self._mandatory(self._convention[name]["flags"]):
134
142
  super().__delattr__(name)
135
143
 
@@ -287,9 +295,17 @@ class Sofa():
287
295
  attribute will be printed.
288
296
  """
289
297
 
298
+ # warn for upcoming deprecation
299
+ warnings.warn((
300
+ 'Sofa.info() will be deprecated in sofar 1.3.0 The conventions are'
301
+ ' now documented at '
302
+ 'https://sofar.readthedocs.io/en/stable/resources/conventions.html'), # noqa
303
+ UserWarning)
304
+
290
305
  # update the private attribute `_convention` to make sure the required
291
306
  # meta data is in place
292
- self._update_convention(version="match")
307
+ if not hasattr(self, "_convention"):
308
+ self._reset_convention()
293
309
 
294
310
  # list of all attributes
295
311
  keys = [k for k in self.__dict__.keys() if not k.startswith("_")]
@@ -461,13 +477,13 @@ class Sofa():
461
477
  """
462
478
 
463
479
  # initialize
464
- self._update_convention(version="match")
480
+ self._reset_convention()
465
481
  added = "Added the following missing data with their default values:\n"
466
482
 
467
483
  # current data
468
484
  keys = [key for key in self.__dict__.keys() if not key.startswith("_")]
469
485
 
470
- self._protected = False
486
+ self.protected = False
471
487
 
472
488
  # loop data in convention
473
489
  for key in self._convention.keys():
@@ -480,7 +496,7 @@ class Sofa():
480
496
  added += f"- {key} "
481
497
  added += f"({'mandatory' if is_mandatory else 'optional'})\n"
482
498
 
483
- self._protected = True
499
+ self.protected = True
484
500
 
485
501
  if verbose:
486
502
  if "-" in added:
@@ -510,7 +526,7 @@ class Sofa():
510
526
 
511
527
  dimensions : str
512
528
  The shape of the new entry as a string. See
513
- ``self.info('dimensions')``.
529
+ :py:func:`~Sofa.list_dimensions`.
514
530
 
515
531
  Examples
516
532
  --------
@@ -565,9 +581,10 @@ class Sofa():
565
581
  """
566
582
  Delete variable or attribute from SOFA object.
567
583
 
568
- Note that mandatory data can not be deleted. Call
569
- :py:func:`Sofa.info("optional") <sofar.sofar.Sofa.info>` to list all
570
- optional variables and attributes.
584
+ Note that mandatory data can not be deleted. Check the
585
+ `sofar documentation
586
+ <https://sofar.readthedocs.io/en/stable/resources/conventions.html>`_
587
+ for a complete list of optional variables and attributes.
571
588
 
572
589
  Parameters
573
590
  ----------
@@ -576,6 +593,26 @@ class Sofa():
576
593
  """
577
594
  delattr(self, name)
578
595
 
596
+ @property
597
+ def protected(self):
598
+ """
599
+ If Sofa.protected is ``True``, read only data can not be changed. Only
600
+ change this to ``False`` if you know what you are doing, e.g., if you
601
+ need to repair corrupted SOFA data.
602
+ """
603
+ return self._protected
604
+
605
+ @protected.setter
606
+ def protected(self, value: bool):
607
+ """
608
+ If Sofa.protected is ``True``, read only data can not be changed. Only
609
+ change this to ``False`` if you know what you are doing, e.g., if you
610
+ need to repair corrupted SOFA data.
611
+ """
612
+ if not isinstance(value, bool):
613
+ raise ValueError("Sofa.protected can only be True or False")
614
+ self._protected = value
615
+
579
616
  def _add_entry(self, name, value, dtype, dimensions):
580
617
  """
581
618
  Add custom data to a SOFA object. See add_variable and add_attribute
@@ -633,7 +670,7 @@ class Sofa():
633
670
  double, string, or attribute
634
671
  """
635
672
  # create custom API if it not exists
636
- self._protected = False
673
+ self.protected = False
637
674
 
638
675
  # lower case letters to indicate custom dimensions
639
676
  if dimensions is not None:
@@ -652,11 +689,11 @@ class Sofa():
652
689
  "type": dtype,
653
690
  "default": None,
654
691
  "comment": ""}
655
- self._update_convention(version="match")
692
+ self._convention[key] = self._custom[key]
656
693
 
657
694
  # add attribute to object
658
695
  setattr(self, key, value)
659
- self._protected = True
696
+ self.protected = True
660
697
 
661
698
  def upgrade_convention(self, target=None, verify=True):
662
699
  """
@@ -687,7 +724,7 @@ class Sofa():
687
724
  """
688
725
 
689
726
  # check input ---------------------------------------------------------
690
- self._update_convention(version="match")
727
+ self._reset_convention()
691
728
 
692
729
  # get deprecations and information about Sofa object
693
730
  _, _, deprecations, upgrade = self._verification_rules()
@@ -763,7 +800,7 @@ class Sofa():
763
800
  "GLOBAL_Version",
764
801
  "GLOBAL_DataType"]
765
802
 
766
- self._protected = False
803
+ self.protected = False
767
804
  for key in keys:
768
805
  setattr(self, key, self._convention[key]["default"])
769
806
 
@@ -813,7 +850,7 @@ class Sofa():
813
850
 
814
851
  # check for missing mandatory data
815
852
  self.add_missing(True, False)
816
- self._protected = True
853
+ self.protected = True
817
854
 
818
855
  # display general message
819
856
  if upgrade["message"] is not None:
@@ -908,7 +945,7 @@ class Sofa():
908
945
 
909
946
  # ---------------------------------------------------------------------
910
947
  # 0. update the convention
911
- self._update_convention("match")
948
+ self._reset_convention()
912
949
 
913
950
  # ---------------------------------------------------------------------
914
951
  # 1. check if the mandatory attributes are contained
@@ -921,9 +958,9 @@ class Sofa():
921
958
 
922
959
  if issue_handling != "raise":
923
960
  # add missing data with default value
924
- self._protected = False
961
+ self.protected = False
925
962
  setattr(self, key, self._convention[key]["default"])
926
- self._protected = True
963
+ self.protected = True
927
964
 
928
965
  # prepare to raise warning
929
966
  missing += "- " + key + "\n"
@@ -1104,10 +1141,10 @@ class Sofa():
1104
1141
  # 4. Get dimensions (E, R, M, N, S, c, I, and custom)
1105
1142
 
1106
1143
  # initialize required API fields
1107
- self._protected = False
1144
+ self.protected = False
1108
1145
  self._dimensions = {}
1109
1146
  self._api = {}
1110
- self._protected = True
1147
+ self.protected = True
1111
1148
 
1112
1149
  # get keys for checking the dimensions (all SOFA variables)
1113
1150
  keys = [key for key in self.__dict__.keys()
@@ -1548,49 +1585,36 @@ class Sofa():
1548
1585
  """Return a copy of the SOFA object."""
1549
1586
  return deepcopy(self)
1550
1587
 
1551
- def _update_convention(self, version):
1588
+ def _reset_convention(self):
1552
1589
  """
1553
- Add SOFA convention to SOFA object in private attribute `_convention`.
1554
- If The object already contains a convention, it will be overwritten.
1555
-
1556
- Parameters
1557
- ----------
1558
- version : str
1559
- ``'latest'``
1560
- Use the latest API and upgrade the SOFA file if required.
1561
- ``'match'``
1562
- Match the version of the sofa file.
1563
- str
1564
- Version string, e.g., ``'1.0'``.
1590
+ - Add SOFA convention to SOFA object in private attribute
1591
+ `_convention`. If The object already contains a convention, it will
1592
+ be overwritten.
1593
+ - If the SOFA object contains custom entries, check if any of the
1594
+ custom entries part of the convention. If yes, delete the entry from
1595
+ self._custom
1596
+ - If the SOFA objects contains custom entries, add entries from
1597
+ self._custom to self._convention
1565
1598
  """
1566
1599
 
1567
1600
  # verify convention and version
1568
1601
  c_current = self.GLOBAL_SOFAConventions
1569
1602
  v_current = str(self.GLOBAL_SOFAConventionsVersion)
1570
1603
 
1571
- v_new = _verify_convention_and_version(
1572
- version, v_current, c_current)
1604
+ _verify_convention_and_version(v_current, c_current)
1573
1605
 
1574
1606
  # load and add convention and version
1575
1607
  convention = self._load_convention(
1576
- c_current, v_new)
1608
+ c_current, v_current)
1577
1609
  self._convention = convention
1578
1610
 
1579
- if v_current != v_new:
1580
- self._protected = False
1581
- self.GLOBAL_SOFAConventionsVersion = v_new
1582
- self._protected = True
1583
-
1584
- # feedback in case of up/downgrade
1585
- if float(v_current) < float(v_new):
1586
- warnings.warn(("Upgraded SOFA object from "
1587
- f"version {v_current} to {v_new}"))
1588
- elif float(v_current) > float(v_new):
1589
- warnings.warn(("Downgraded SOFA object from "
1590
- f"version {v_current} to {v_new}"))
1591
-
1592
- # check if custom fields can be added
1593
1611
  if hasattr(self, "_custom"):
1612
+ # check of custom fields can be removed
1613
+ for key in self._convention:
1614
+ if key in self._custom:
1615
+ del self._custom[key]
1616
+
1617
+ # check if custom fields can be added
1594
1618
  for key in self._custom:
1595
1619
  self._convention[key] = self._custom[key]
1596
1620
 
@@ -1684,7 +1708,7 @@ class Sofa():
1684
1708
 
1685
1709
  # write API and date specific fields (some read only)
1686
1710
  now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1687
- self._protected = False
1711
+ self.protected = False
1688
1712
  self.GLOBAL_DateCreated = now
1689
1713
  self.GLOBAL_DateModified = now
1690
1714
  self.GLOBAL_APIName = "sofar SOFA API for Python (pyfar.org)"
@@ -1694,7 +1718,7 @@ class Sofa():
1694
1718
  f"{platform.python_version()} "
1695
1719
  f"[{platform.python_implementation()} - "
1696
1720
  f"{platform.python_compiler()}]")
1697
- self._protected = True
1721
+ self.protected = True
1698
1722
 
1699
1723
  @staticmethod
1700
1724
  def _get_size_and_shape_of_string_var(value, key):
@@ -1,4 +1,38 @@
1
1
  {
2
+ "FreeFieldDirectivityTF": {
3
+ "from_to": [
4
+ [
5
+ [
6
+ "1.0"
7
+ ],
8
+ [
9
+ "FreeFieldDirectivityTF_1.1"
10
+ ],
11
+ "1"
12
+ ]
13
+ ],
14
+ "1": {
15
+ "move": {
16
+ "EmitterPosition": {
17
+ "target": "EmitterPosition",
18
+ "moveaxis": null,
19
+ "deprecated_dimensions": [
20
+ "IC",
21
+ "MC"
22
+ ]
23
+ },
24
+ "EmitterDescription": {
25
+ "target": "EmitterDescriptions",
26
+ "moveaxis": null,
27
+ "deprecated_dimensions": [
28
+ "IS"
29
+ ]
30
+ }
31
+ },
32
+ "remove": [],
33
+ "message": "Consider to add the optional data 'GLOBAL_EmitterDescription'introduced in convention version 1.1.\nWARNING: Adding 'GLOBAL_EmitterDescription' is required if 'EmitterDescriptions' is contained in the SOFA object."
34
+ }
35
+ },
2
36
  "SimpleFreeFieldHRIR": {
3
37
  "from_to": [
4
38
  [
@@ -333,7 +333,7 @@ def _convention_csv2dict(file: str):
333
333
  return convention
334
334
 
335
335
 
336
- def _check_congruency(save_dir=None):
336
+ def _check_congruency(save_dir=None, branch="master"):
337
337
  """
338
338
  SOFA conventions are stored in two different places - is this a good idea?
339
339
  They should be identical, but let's find out.
@@ -346,9 +346,14 @@ def _check_congruency(save_dir=None):
346
346
  directory to save diverging conventions for further inspections
347
347
  """
348
348
 
349
- urls = ["https://www.sofaconventions.org/conventions/",
350
- ("https://raw.githubusercontent.com/sofacoustics/SOFAtoolbox/"
351
- "master/SOFAtoolbox/conventions/")]
349
+ # urls for checking which conventions exist
350
+ urls_check = ["https://www.sofaconventions.org/conventions/",
351
+ ("https://github.com/sofacoustics/SOFAtoolbox/tree/"
352
+ f"{branch}/SOFAtoolbox/conventions/")]
353
+ # urls for loading the convention files
354
+ urls_load = ["https://www.sofaconventions.org/conventions/",
355
+ ("https://raw.githubusercontent.com/sofacoustics/SOFAtoolbox/"
356
+ f"{branch}/SOFAtoolbox/conventions/")]
352
357
  subdirs = ["sofaconventions", "sofatoolbox"]
353
358
 
354
359
  # check save_dir
@@ -359,21 +364,29 @@ def _check_congruency(save_dir=None):
359
364
  if not os.path.isdir(os.path.join(save_dir, subdir)):
360
365
  os.makedirs(os.path.join(save_dir, subdir))
361
366
 
362
- # get file names of conventions from sofaconventions.org and SOFAtoolbox
363
- url = urls[0]
367
+ # get file names of conventions from sofaconventions.org
368
+ url = urls_check[0]
364
369
  page = requests.get(url).text
365
370
  soup = BeautifulSoup(page, 'html.parser')
366
371
  sofaconventions = [os.path.split(node.get('href'))[1]
367
372
  for node in soup.find_all('a')
368
373
  if node.get('href').endswith(".csv")]
369
374
 
370
- url = ("https://github.com/sofacoustics/SOFAtoolbox/tree/"
371
- "master/SOFAtoolbox/conventions/")
372
- page = requests.get(url).text
373
- soup = BeautifulSoup(page, 'html.parser')
374
- sofatoolbox = [os.path.split(node.get('href'))[1]
375
- for node in soup.find_all('a')
376
- if node.get('href').endswith(".csv")]
375
+ if not sofaconventions:
376
+ raise ValueError(f"Did not find any conventions at {url}")
377
+
378
+ # get file names of conventions from github
379
+ url = urls_check[1]
380
+ page = requests.get(url).json()
381
+ sofatoolbox = []
382
+ for content in page["payload"]["tree"]["items"]:
383
+ if content["contentType"] == "file" and \
384
+ content["path"].startswith("SOFAtoolbox/conventions") and \
385
+ content["name"].endswith("csv"):
386
+ sofatoolbox.append(content["name"])
387
+
388
+ if not sofatoolbox:
389
+ raise ValueError(f"Did not find any conventions at {url}")
377
390
 
378
391
  # check if lists are identical. Remove items not contained in both lists
379
392
  report = ""
@@ -394,7 +407,7 @@ def _check_congruency(save_dir=None):
394
407
  for convention in sofaconventions:
395
408
 
396
409
  # download SOFA convention definitions to package directory
397
- data = [requests.get(url + convention) for url in urls]
410
+ data = [requests.get(url + convention) for url in urls_load]
398
411
  # remove trailing tabs and windows style line breaks
399
412
  data = [d.content.replace(b"\r\n", b"\n").replace(b"\t\n", b"\n")
400
413
  for d in data]
sofar/utils.py CHANGED
@@ -2,7 +2,6 @@ import os
2
2
  import glob
3
3
  import numpy as np
4
4
  import numpy.testing as npt
5
- from packaging.version import parse as version_parse
6
5
  import warnings
7
6
  import sofar as sf
8
7
 
@@ -19,58 +18,39 @@ def version():
19
18
  f"SOFA standard {sofa_conventions}")
20
19
 
21
20
 
22
- def _verify_convention_and_version(version, version_in, convention):
21
+ def _verify_convention_and_version(version, convention):
23
22
  """
24
- Verify if convention and version exist and return version
23
+ Verify if convention and version exist. Raise a Value error if it does not.
25
24
 
26
25
  Parameters
27
26
  ----------
28
27
  version : str
29
- 'latest', 'match', version string (e.g., '1.0')
30
- version_in : str
31
- The version to be checked against
28
+ The version to be checked
32
29
  convention : str
33
30
  The name of the convention to be checked
34
-
35
- Returns
36
- -------
37
- version_out : str
38
- The version to be used depending on `version`, and `version_in`
39
31
  """
40
32
 
41
- # check if the convention exists in sofar
33
+ # check if the convention exists
42
34
  if convention not in _get_conventions("name"):
43
35
  raise ValueError(
44
36
  f"Convention '{convention}' does not exist")
45
37
 
46
38
  name_version = _get_conventions("name_version")
47
39
 
48
- if version == "latest":
49
- # get list of versions as floats
50
- version_out = [float(versions[1]) for versions in name_version
51
- if versions[0] == convention]
52
- # get latest version as string
53
- version_out = str(version_out[np.argmax(version_out)])
54
-
55
- if version_parse(version_out) > version_parse(version_in):
56
- print(("Updated conventions version from "
57
- f"{version_in} to {version_out}"))
58
- else:
59
- # check which version is wanted
60
- match = version_in if version == "match" else version
61
- version_out = None
62
- for versions in name_version:
63
- # check if convention and version match
64
- if versions[0] == convention \
65
- and str(float(versions[1])) == match:
66
- version_out = str(float(versions[1]))
67
-
68
- if version_out is None:
69
- raise ValueError((
70
- f"Version {match} does not exist for convention {convention}. "
71
- "Try to access the data with version='latest'"))
72
-
73
- return version_out
40
+ # check which version is wanted
41
+ version_exists = False
42
+ for versions in name_version:
43
+ # check if convention and version match
44
+ if versions[0] == convention \
45
+ and str(float(versions[1])) == version:
46
+ version_exists = True
47
+
48
+ if not version_exists:
49
+ raise ValueError((
50
+ f"{convention} v{version} is not a valid SOFA Convention."
51
+ "If you are trying to read the data use "
52
+ "sofar.read_sofa_as_netcdf(). Call sofar.list_conventions() for a "
53
+ "list of valid Conventions"))
74
54
 
75
55
 
76
56
  def list_conventions():
@@ -1,17 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sofar
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: Maybe the most complete python package for SOFA files so far
5
5
  Home-page: https://pyfar.org/
6
+ Download-URL: https://pypi.org/project/sofar/
6
7
  Author: The pyfar developers
7
8
  Author-email: info@pyfar.org
8
9
  License: MIT license
9
- Download-URL: https://pypi.org/project/sofar/
10
10
  Project-URL: Bug Tracker, https://github.com/pyfar/sofar/issues
11
11
  Project-URL: Documentation, https://sofar.readthedocs.io/
12
12
  Project-URL: Source Code, https://github.com/pyfar/sofar
13
13
  Keywords: sofar
14
- Platform: UNKNOWN
15
14
  Classifier: Development Status :: 4 - Beta
16
15
  Classifier: Intended Audience :: Science/Research
17
16
  Classifier: License :: OSI Approved :: MIT License
@@ -20,6 +19,7 @@ Classifier: Programming Language :: Python :: 3
20
19
  Classifier: Programming Language :: Python :: 3.8
21
20
  Classifier: Programming Language :: Python :: 3.9
22
21
  Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
23
  Requires-Python: >=3.8
24
24
  License-File: LICENSE
25
25
  License-File: AUTHORS.rst
@@ -85,7 +85,5 @@ format*, Audio Engineering Society, Inc., New York, NY, USA.
85
85
 
86
86
  P. Majdak, F. Zotter, F. Brinkmann, J. De Muynke, M. Mihocic, and M.
87
87
  Noisternig, "Spatially Oriented Format for Acoustics 2.1: Introduction and
88
- Recent Advances,” *J. Audio Eng. Soc.*, vol. 70, no. 7/8, pp. 565584,
88
+ Recent Advances", *J. Audio Eng. Soc.*, vol. 70, no. 7/8, pp. 565-584,
89
89
  Jul. 2022. DOI: https://doi.org/10.17743/jaes.2022.0026
90
-
91
-
@@ -1,11 +1,9 @@
1
- sofar/__init__.py,sha256=TL5IoZifHkTHXakrdkz9DlBPbFc7pdJwMt7QyVFuN24,540
2
- sofar/io.py,sha256=c4DgMzX3vleUAsRVSATXxNj2l1RvnXJmvg6QXg6Bs4Y,13867
3
- sofar/sofa.py,sha256=z34aul9Jp3Yr9ghwWFqHuJsF3BOF4d-j3kbcOe-hIbI,67185
4
- sofar/update_conventions.py,sha256=MqnbR3FX3UD2FC-3XILCergtsO35YH2wYvksTUSSpEI,16193
5
- sofar/utils.py,sha256=QhSHIBwl1xGumABK8_2TkPoY67ZTOaIB9sE-S2PivFM,11527
1
+ sofar/__init__.py,sha256=Jcf2PHRtcQo3gOPJwXrjpYoB613twZJ2faZREU8dosQ,595
2
+ sofar/io.py,sha256=XdkfG6jo4RI8JqUgXEfx2ka06_CfffPJXs0HqeMSri0,15761
3
+ sofar/sofa.py,sha256=QVr0UKr-aqo2K7_PEHtXk9VuvrjE36QgcYIqFJ6KBrc,68255
4
+ sofar/update_conventions.py,sha256=36ba0cAVtKppJCrsEhP2aiv1DGEtxNx06hsUAERDlwM,16753
5
+ sofar/utils.py,sha256=0JwYLLyVu1ZcAfUQKSucywcQPlHB2Ojez7fxIXhuaFw,10754
6
6
  sofar/sofa_conventions/VERSION,sha256=IuQRql5P3qfPUQuJwuh-RX5TvI_Wd4bDdSm8VLNQqds,34
7
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.0.csv,sha256=Ha6178_LxRod-L2UcI7SX3o6Cf1YRFs1zgIpn0kd0rA,5125
8
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.0.json,sha256=GcOb3cbv63C7S4cJ7IVvU7NAAd1h8mA7RHaAFQ_jqz8,12627
9
7
  sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.csv,sha256=Wait37FTh6ROKgpX7-JsVM-ohtKfsdHjxVL5vN7uTTs,5510
10
8
  sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.json,sha256=sm8Pj_Z1n1tW2jYlu00eikR3yCcANEm_Kw5Eq9jrbw4,13138
11
9
  sofar/sofa_conventions/conventions/FreeFieldHRIR_1.0.csv,sha256=C_fc7wHjBLc2krPja33sQaDSZDvlr80GSsgOj904GBY,2479
@@ -38,6 +36,8 @@ sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.csv,sha256=2OzG5t9rYzD
38
36
  sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.json,sha256=5tjPriXZk8WEIr15i41L7hQRwAxzw_gA0nxWkZfe_7g,14346
39
37
  sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.csv,sha256=l7aeLNxbOyXgfQIjCo3QzpRpBonMp9ssfhps4fejHxc,4199
40
38
  sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.json,sha256=gVwyzGni9e3w5wehnxe6Oe5knO2Np5doHYLzRsfrlfc,14491
39
+ sofar/sofa_conventions/conventions/deprecated/FreeFieldDirectivityTF_1.0.csv,sha256=Ha6178_LxRod-L2UcI7SX3o6Cf1YRFs1zgIpn0kd0rA,5125
40
+ sofar/sofa_conventions/conventions/deprecated/FreeFieldDirectivityTF_1.0.json,sha256=GcOb3cbv63C7S4cJ7IVvU7NAAd1h8mA7RHaAFQ_jqz8,12627
41
41
  sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.csv,sha256=2_ed7Y0CGJmCx9V3X7L4gipJ94nLzH7Jl9fXXtI7rLQ,1988
42
42
  sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.json,sha256=y3A1fhyS4XUAIbEGv92pwrYp01HutgIKajI4WmJYYKQ,6603
43
43
  sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.csv,sha256=E_jW15CNWUsp5W9Uw2EbNNyvDxDMFvs-CAELDmYtp0I,2560
@@ -59,16 +59,17 @@ sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.3.json,sha256=ccC
59
59
  sofar/sofa_conventions/rules/deprecations.json,sha256=hyq112XNfojROZsAEbcV7EOLin-xaTiNi4o4WdplS74,453
60
60
  sofar/sofa_conventions/rules/rules.json,sha256=iH8e5UUVRQcGZ_AoULk4A2IiAT1yK7R2CvRQyCMYhYA,22501
61
61
  sofar/sofa_conventions/rules/unit_aliases.json,sha256=QXgOfpe8bnmUybVhSSj3nIUYw50CAVehh7sdFNPFl0k,212
62
- sofar/sofa_conventions/rules/upgrade.json,sha256=T485ei4Tl9fzd7Wat5vtc9ShVxBQ1iXhp5ydt6tJr7c,4054
62
+ sofar/sofa_conventions/rules/upgrade.json,sha256=viHPPqBaPblE-fY8BOuVUraQYrBhckrS3w5XKqHCTIE,5167
63
63
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
- tests/test_io.py,sha256=Yf3p3lwcpnvzUc6hakB-uN0sQWfqjmapkJDtjxWkRvM,11682
65
- tests/test_sofa.py,sha256=0WueGXUb38x3XfjrlkOE52yxsiBWW48VInAXfUQtOEY,11415
66
- tests/test_sofa_upgrade_conventions.py,sha256=rudTFkRnefKv-QdJAAbB2usamoeUimClqQM0y7ClFro,3038
67
- tests/test_sofa_verify.py,sha256=gRk1d603MMXfUEiJwmJfx1-dJPybcV1_4gpPEqRRnyU,15641
68
- tests/test_utils.py,sha256=7Brpk_Cg1qj0Gny-1tWElU-znau5vbD5sDiTPFS-8AM,7930
69
- sofar-1.0.0.dist-info/AUTHORS.rst,sha256=JGcf9PJwl0w14PgTRjI3HXpcwDbkGbUN4mkYoZ8smL8,164
70
- sofar-1.0.0.dist-info/LICENSE,sha256=qdGH_RUPveBBfGYShm6OIBd6bRJdwp3fDCpjMsowmqA,1068
71
- sofar-1.0.0.dist-info/METADATA,sha256=--L3yP_9_SFwsgjKAPXZ2kZ4VMA9GCaQRCnnWlCE8Y4,3141
72
- sofar-1.0.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
73
- sofar-1.0.0.dist-info/top_level.txt,sha256=2JSMmeQ5zeMumFynTJU1oODh09wxSiN3gFpsY-bNsSo,12
74
- sofar-1.0.0.dist-info/RECORD,,
64
+ tests/test_deprecations.py,sha256=coHOEq5XUquYxRge8XKeg005M84wcgSHTEE--CoEar4,584
65
+ tests/test_io.py,sha256=xMBdWP3vdnk_6aYAhg30nEgGvPfYm30a1qouGHIwRNo,11808
66
+ tests/test_sofa.py,sha256=DpiI517T-gLoajy9nQCHT7ouxM40JwirsqkVMFT5cWo,11314
67
+ tests/test_sofa_upgrade_conventions.py,sha256=HqIuvFFub1_HCH6qHTLPcPyBZ2ayDKzmZR7RwnTxo_o,3377
68
+ tests/test_sofa_verify.py,sha256=cleTkFJurJfDEZrnSTfBYe2VqsCZ1VxWr9efNGjWr88,15631
69
+ tests/test_utils.py,sha256=yT290xCaeDW4l3DHUeFGiVmLe9xMguETN1NAGRTJ-X4,8307
70
+ sofar-1.1.1.dist-info/AUTHORS.rst,sha256=JGcf9PJwl0w14PgTRjI3HXpcwDbkGbUN4mkYoZ8smL8,164
71
+ sofar-1.1.1.dist-info/LICENSE,sha256=qdGH_RUPveBBfGYShm6OIBd6bRJdwp3fDCpjMsowmqA,1068
72
+ sofar-1.1.1.dist-info/METADATA,sha256=dzyA462zt5gq3YjCarEozElPtpd-sTUpaE0D1xxb0L8,3168
73
+ sofar-1.1.1.dist-info/WHEEL,sha256=a-zpFRIJzOq5QfuhBzbhiA1eHTzNCJn8OdRvhdNX0Rk,110
74
+ sofar-1.1.1.dist-info/top_level.txt,sha256=2JSMmeQ5zeMumFynTJU1oODh09wxSiN3gFpsY-bNsSo,12
75
+ sofar-1.1.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.38.4)
2
+ Generator: bdist_wheel (0.40.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
@@ -0,0 +1,19 @@
1
+ import pytest
2
+ from packaging import version
3
+ import re
4
+ import sofar as sf
5
+
6
+
7
+ # deprecate in 1.3.0 ----------------------------------------------------------
8
+ def test_pad_zero_modi():
9
+ with pytest.warns(
10
+ UserWarning,
11
+ match=re.escape('Sofa.info() will be deprecated in sofar 1.3.0')):
12
+ sofa = sf.Sofa('GeneralTF')
13
+ sofa.info()
14
+
15
+ if version.parse(sf.__version__) >= version.parse('1.3.0'):
16
+ with pytest.raises(ValueError):
17
+ # remove Sofa.info() from pyfar 1.3.0!
18
+ sofa = sf.Sofa('GeneralTF')
19
+ sofa.info()
tests/test_io.py CHANGED
@@ -6,6 +6,7 @@ from sofar.utils import (_get_conventions,
6
6
  from sofar.io import (_format_value_for_netcdf,
7
7
  _format_value_from_netcdf)
8
8
  import os
9
+ import pathlib
9
10
  from tempfile import TemporaryDirectory
10
11
  import pytest
11
12
  from pytest import raises
@@ -14,7 +15,7 @@ import numpy.testing as npt
14
15
  from netCDF4 import Dataset
15
16
 
16
17
 
17
- def test_read_sofa(capfd):
18
+ def test_read_write_sofa(capfd):
18
19
 
19
20
  temp_dir = TemporaryDirectory()
20
21
  filename = os.path.join(temp_dir.name, "test.sofa")
@@ -25,6 +26,11 @@ def test_read_sofa(capfd):
25
26
  sofa = sf.read_sofa(filename)
26
27
  assert hasattr(sofa, "_api")
27
28
 
29
+ # test with path object
30
+ sf.write_sofa(pathlib.Path(filename), sofa)
31
+ sofa = sf.read_sofa(pathlib.Path(filename))
32
+ assert hasattr(sofa, "_api")
33
+
28
34
  # reading without updating API
29
35
  sofa = sf.read_sofa(filename, verify=False)
30
36
  assert not hasattr(sofa, "_api")
@@ -47,7 +53,7 @@ def test_read_sofa(capfd):
47
53
  with Dataset(filename, "r+", format="NETCDF4") as file:
48
54
  setattr(file, "SOFAConventionsVersion", "0.1")
49
55
  # ValueError when version should be matched
50
- with raises(ValueError, match="Version 0.1 does not exist for"):
56
+ with raises(ValueError, match="v0.1 is not a valid SOFA Convention"):
51
57
  sf.read_sofa(filename)
52
58
 
53
59
  # read file containing a variable with wrong shape
@@ -64,10 +70,6 @@ def test_read_sofa(capfd):
64
70
  # data can be read without updating API
65
71
  sf.read_sofa(filename, verify=False)
66
72
 
67
- # test assertion for wrong filename
68
- with raises(ValueError, match="Filename must end with .sofa"):
69
- sf.read_sofa('sofa.exe')
70
-
71
73
 
72
74
  def test_read_sofa_custom_data():
73
75
  """Test if sofa files with custom data are loaded correctly"""
@@ -83,12 +85,26 @@ def test_read_sofa_custom_data():
83
85
  assert sofa.GLOBAL_Warming == 'critical'
84
86
 
85
87
 
86
- def test_write_sofa_assertion():
87
- """Test assertion for wrong filename ending"""
88
-
89
- sofa = sf.Sofa("SimpleFreeFieldHRIR")
90
- with raises(ValueError, match="Filename must end with .sofa"):
91
- sf.write_sofa("sofa.exe", sofa)
88
+ def test_read_netcdf():
89
+ tmp = TemporaryDirectory()
90
+ files = [os.path.join(tmp.name, "invalid.sofa"),
91
+ os.path.join(tmp.name, "invalid.netcdf")]
92
+
93
+ # create data with invalid SOFA convention and version
94
+ sofa = sf.Sofa("GeneralTF")
95
+ sofa.protected = False
96
+ sofa.GLOBAL_SOFAConventions = "MadeUp"
97
+ sofa.protected = True
98
+
99
+ # test reading
100
+ for file in files:
101
+ # write data
102
+ sf.io._write_sofa(file, sofa, verify=False)
103
+ # can not be read with read_sofa
104
+ with raises(ValueError):
105
+ sf.read_sofa(file)
106
+ sofa_read = sf.read_sofa_as_netcdf(file)
107
+ sf.equals(sofa, sofa_read)
92
108
 
93
109
 
94
110
  def test_write_sofa_outdated_version():
@@ -299,23 +315,15 @@ def test_format_value_from_netcdf():
299
315
 
300
316
  def test_verify_convention_and_version():
301
317
 
302
- # test different possibilities for version
303
- version = _verify_convention_and_version("latest", "1.0", "GeneralTF")
304
- assert version == "2.0"
305
-
306
- version = _verify_convention_and_version("2.0", "1.0", "GeneralTF")
307
- assert version == "2.0"
308
-
309
- version = _verify_convention_and_version("match", "1.0", "GeneralTF")
310
- assert version == "1.0"
318
+ # test with existing convention and version (no error returns None)
319
+ out = _verify_convention_and_version("1.0", "GeneralTF")
320
+ assert out is None
311
321
 
312
322
  # test assertions
313
323
  with raises(ValueError, match="Convention 'Funky' does not exist"):
314
- _verify_convention_and_version("latest", "1.0", "Funky")
315
- with raises(ValueError, match="Version 1.1 does not exist"):
316
- _verify_convention_and_version("match", "1.1", "GeneralTF")
317
- with raises(ValueError, match="Version 1.2 does not exist"):
318
- _verify_convention_and_version("1.2", "1.0", "GeneralTF")
324
+ _verify_convention_and_version("1.0", "Funky")
325
+ with raises(ValueError, match="v1.1 is not a valid SOFA Convention"):
326
+ _verify_convention_and_version("1.1", "GeneralTF")
319
327
 
320
328
 
321
329
  def test_atleast_nd():
tests/test_sofa.py CHANGED
@@ -231,9 +231,9 @@ def test_add_entry():
231
231
  assert "Temperature_Units" not in sofa._custom
232
232
 
233
233
  # test adding missing entry defined in convention
234
- sofa._protected = False
234
+ sofa.protected = False
235
235
  delattr(sofa, "ListenerPosition")
236
- sofa._protected = True
236
+ sofa.protected = True
237
237
  sofa.add_variable("ListenerPosition", [0, 0, 0], "double", "IC")
238
238
  assert "ListenerPosition" not in sofa._custom
239
239
 
@@ -273,10 +273,10 @@ def test_add_missing(
273
273
  opt = "GLOBAL_History"
274
274
 
275
275
  # remove data before adding it again
276
- sofa._protected = False
276
+ sofa.protected = False
277
277
  sofa.delete(man)
278
278
  sofa.delete(opt)
279
- sofa._protected = False
279
+ sofa.protected = False
280
280
 
281
281
  # add missing data
282
282
  sofa.add_missing(mandatory, optional, verbose)
@@ -313,11 +313,6 @@ def test_delete_entry():
313
313
  assert not hasattr(sofa, "SourceManufacturer")
314
314
 
315
315
 
316
- def test__update_conventions():
317
- """Tested in test_sofa_verify.pi::test_version"""
318
- pass
319
-
320
-
321
316
  def test__get_size_and_shape_of_string_var():
322
317
 
323
318
  # test with string
@@ -84,9 +84,18 @@ def test_upgrade_conventions(path, capfd):
84
84
  targets = sofa.upgrade_convention()
85
85
  out, _ = capfd.readouterr()
86
86
 
87
+ # don't verify conventions that might require user action after
88
+ if os.path.basename(path) in ["FreeFieldDirectivityTF_1.0.json"]:
89
+ # FreeFieldDirectivityTF_1.0
90
+ # - optional dependency GLOBAL_EmitterDescription
91
+ # might need to be added
92
+ verify = False
93
+ else:
94
+ verify = True
95
+
87
96
  if targets:
88
97
  for target in targets:
89
- sofa.upgrade_convention(target)
98
+ sofa.upgrade_convention(target, verify=verify)
90
99
  out, _ = capfd.readouterr()
91
100
  assert "Upgrading" in out
92
101
  else:
tests/test_sofa_verify.py CHANGED
@@ -115,9 +115,9 @@ def test_case_insensitivity():
115
115
 
116
116
  # data type (must be case sensitive) --------------------------------------
117
117
  sofa = sf.Sofa("SimpleFreeFieldHRIR")
118
- sofa._protected = False
118
+ sofa.protected = False
119
119
  sofa.GLOBAL_DataType = "fir"
120
- sofa._protected = True
120
+ sofa.protected = True
121
121
  with raises(ValueError, match="GLOBAL_DataType is fir"):
122
122
  sofa.verify()
123
123
 
@@ -153,9 +153,9 @@ def test_missing_default_attributes(capfd):
153
153
 
154
154
  # test missing default attribute
155
155
  sofa = sf.Sofa("GeneralTF")
156
- sofa._protected = False
156
+ sofa.protected = False
157
157
  delattr(sofa, "GLOBAL_Conventions")
158
- sofa._protected = True
158
+ sofa.protected = True
159
159
 
160
160
  # raises error
161
161
  with raises(ValueError, match="Detected missing mandatory data"):
@@ -242,42 +242,42 @@ def test_wrong_name():
242
242
 
243
243
  # attribute with missing variable
244
244
  sofa = sf.Sofa("GeneralTF")
245
- sofa._protected = False
245
+ sofa.protected = False
246
246
  sofa.IR_Type = "pressure"
247
247
  sofa._custom = {"IR_Type": {"default": None,
248
248
  "flags": None,
249
249
  "dimensions": None,
250
250
  "type": "attribute",
251
251
  "comment": ""}}
252
- sofa._protected = True
252
+ sofa.protected = True
253
253
 
254
254
  with raises(ValueError, match="Detected attributes with missing"):
255
255
  sofa.verify()
256
256
 
257
257
  # attribute with no underscore
258
258
  sofa = sf.Sofa("GeneralTF")
259
- sofa._protected = False
259
+ sofa.protected = False
260
260
  sofa.IRType = "pressure"
261
261
  sofa._custom = {"IRType": {"default": None,
262
262
  "flags": None,
263
263
  "dimensions": None,
264
264
  "type": "attribute",
265
265
  "comment": ""}}
266
- sofa._protected = True
266
+ sofa.protected = True
267
267
 
268
268
  with raises(ValueError, match="Detected attribute names with too many"):
269
269
  sofa.verify()
270
270
 
271
271
  # variable with underscore
272
272
  sofa = sf.Sofa("GeneralTF")
273
- sofa._protected = False
273
+ sofa.protected = False
274
274
  sofa.IR_Data = 1
275
275
  sofa._custom = {"IR_Data": {"default": None,
276
276
  "flags": None,
277
277
  "dimensions": "IM",
278
278
  "type": "double",
279
279
  "comment": ""}}
280
- sofa._protected = True
280
+ sofa.protected = True
281
281
 
282
282
  with raises(ValueError, match="Detected variable names with too many"):
283
283
  sofa.verify()
tests/test_utils.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import shutil
2
2
  import sofar as sf
3
3
  from sofar.utils import _get_conventions
4
- from sofar.update_conventions import _compile_conventions
4
+ from sofar.update_conventions import _compile_conventions, _check_congruency
5
5
  import os
6
6
  import json
7
7
  from tempfile import TemporaryDirectory
@@ -9,6 +9,7 @@ import pytest
9
9
  from pytest import raises
10
10
  import numpy as np
11
11
  from copy import deepcopy
12
+ import warnings
12
13
 
13
14
 
14
15
  def test_list_conventions(capfd):
@@ -48,6 +49,19 @@ def test__get_conventions(capfd):
48
49
  _get_conventions(return_type="None")
49
50
 
50
51
 
52
+ @pytest.mark.parametrize('branch', ['master', 'development'])
53
+ def test__congruency(capfd, branch):
54
+ """
55
+ Check if conventions from SOFAToolbox and sofaconventions.org are
56
+ identical.
57
+ """
58
+ out, _ = capfd.readouterr()
59
+ _check_congruency(branch=branch)
60
+ out, _ = capfd.readouterr()
61
+ if out != "":
62
+ warnings.warn(out, Warning)
63
+
64
+
51
65
  def test_update_conventions(capfd):
52
66
 
53
67
  # create temporary directory and copy existing conventions
@@ -146,53 +160,53 @@ def test_equals_global_parameters():
146
160
 
147
161
  # check different number of keys
148
162
  sofa_b = deepcopy(sofa_a)
149
- sofa_b._protected = False
163
+ sofa_b.protected = False
150
164
  delattr(sofa_b, "ReceiverPosition")
151
- sofa_b._protected = True
165
+ sofa_b.protected = True
152
166
  with pytest.warns(UserWarning, match="not identical: sofa_a has"):
153
167
  is_identical = sf.equals(sofa_a, sofa_b)
154
168
  assert not is_identical
155
169
 
156
170
  # check different keys
157
171
  sofa_b = deepcopy(sofa_a)
158
- sofa_b._protected = False
172
+ sofa_b.protected = False
159
173
  sofa_b.PositionReceiver = sofa_b.ReceiverPosition
160
174
  delattr(sofa_b, "ReceiverPosition")
161
- sofa_b._protected = True
175
+ sofa_b.protected = True
162
176
  with pytest.warns(UserWarning, match="not identical: sofa_a and sofa_b"):
163
177
  is_identical = sf.equals(sofa_a, sofa_b)
164
178
  assert not is_identical
165
179
 
166
180
  # check mismatching data types
167
181
  sofa_b = deepcopy(sofa_a)
168
- sofa_b._protected = False
182
+ sofa_b.protected = False
169
183
  sofa_b._convention["ReceiverPosition"]["type"] = "int"
170
- sofa_b._protected = True
184
+ sofa_b.protected = True
171
185
  with pytest.warns(UserWarning, match="not identical: ReceiverPosition"):
172
186
  is_identical = sf.equals(sofa_a, sofa_b)
173
187
  assert not is_identical
174
188
 
175
189
  # check exclude GLOBAL attributes
176
190
  sofa_b = deepcopy(sofa_a)
177
- sofa_b._protected = False
191
+ sofa_b.protected = False
178
192
  delattr(sofa_b, "GLOBAL_Version")
179
- sofa_b._protected = True
193
+ sofa_b.protected = True
180
194
  is_identical = sf.equals(sofa_a, sofa_b, exclude="GLOBAL")
181
195
  assert is_identical
182
196
 
183
197
  # check exclude Date attributes
184
198
  sofa_b = deepcopy(sofa_a)
185
- sofa_b._protected = False
199
+ sofa_b.protected = False
186
200
  delattr(sofa_b, "GLOBAL_DateModified")
187
- sofa_b._protected = True
201
+ sofa_b.protected = True
188
202
  is_identical = sf.equals(sofa_a, sofa_b, exclude="DATE")
189
203
  assert is_identical
190
204
 
191
205
  # check exclude Date attributes
192
206
  sofa_b = deepcopy(sofa_a)
193
- sofa_b._protected = False
207
+ sofa_b.protected = False
194
208
  delattr(sofa_b, "GLOBAL_DateModified")
195
- sofa_b._protected = True
209
+ sofa_b.protected = True
196
210
  is_identical = sf.equals(sofa_a, sofa_b, exclude="ATTR")
197
211
  assert is_identical
198
212
 
@@ -210,14 +224,14 @@ def test_equals_attribute_values(value_a, value_b, attribute, fails):
210
224
 
211
225
  # generate SOFA objects (SimpleHeadphoneIR has string variables)
212
226
  sofa_a = sf.Sofa("SimpleHeadphoneIR")
213
- sofa_a._protected = False
227
+ sofa_a.protected = False
214
228
  sofa_b = deepcopy(sofa_a)
215
229
 
216
230
  # set parameters
217
231
  setattr(sofa_a, attribute, value_a)
218
- sofa_a._protected = True
232
+ sofa_a.protected = True
219
233
  setattr(sofa_b, attribute, value_b)
220
- sofa_b._protected = True
234
+ sofa_b.protected = True
221
235
 
222
236
  # compare
223
237
  if fails:
File without changes