Skip to content

Commit bd81d40

Browse files
Merge pull request #218 from NeurodataWithoutBorders/debug_none_names
[Bug]: Fix None fields with config
2 parents 803b092 + 9ae5abe commit bd81d40

File tree

7 files changed

+111
-22
lines changed

7 files changed

+111
-22
lines changed

nwbinspector/internal_configs/dandi.inspector_config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ CRITICAL: # All the fields under CRITICAL will be required for dandi validate t
22
- check_subject_exists
33
- check_subject_id_exists
44
BEST_PRACTICE_VIOLATION:
5-
- check_subject_sex # these are planned to be elevated to CRITICAL when requried for DANDI validate
6-
- check_subject_species # these are planned to be elevated to CRITICAL when requried for DANDI validate
7-
- check_subject_age # these are planned to be elevated to CRITICAL when requried for DANDI validate
5+
- check_subject_sex # these are planned to be elevated to CRITICAL when required for DANDI validate
6+
- check_subject_species # these are planned to be elevated to CRITICAL when required for DANDI validate
7+
- check_subject_age # these are planned to be elevated to CRITICAL when required for DANDI validate
88
- check_data_orientation # not 100% accurate, so need to deelevate from CRITICAL to skip it in dandi validate

nwbinspector/nwbinspector.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
class InspectorOutputJSONEncoder(json.JSONEncoder):
3838
"""Custom JSONEncoder for the NWBInspector."""
3939

40-
def default(self, o):
40+
def default(self, o): # noqa D102
4141
if isinstance(o, InspectorMessage):
4242
return o.__dict__
4343
if isinstance(o, Enum):
@@ -53,25 +53,41 @@ def validate_config(config: dict):
5353
jsonschema.validate(instance=config, schema=schema)
5454

5555

56-
def copy_function(function):
56+
def _copy_function(function):
5757
"""
58-
Return a copy of a function so that internal attributes can be adjusted without changing the original function.
58+
Copy the core parts of a given function, excluding wrappers, then return a new function.
5959
60-
Required to ensure our configuration of functions in the registry does not effect the registry itself.
61-
62-
Taken from
60+
Based off of
6361
https://stackoverflow.com/questions/6527633/how-can-i-make-a-deepcopy-of-a-function-in-python/30714299#30714299
6462
"""
65-
if getattr(function, "__wrapped__", False):
66-
function = function.__wrapped__
6763
copied_function = FunctionType(
6864
function.__code__, function.__globals__, function.__name__, function.__defaults__, function.__closure__
6965
)
70-
# in case f was given attrs (note this dict is a shallow copy):
66+
67+
# in case f was given attrs (note this dict is a shallow copy)
7168
copied_function.__dict__.update(function.__dict__)
7269
return copied_function
7370

7471

72+
def copy_check(function):
73+
"""
74+
Copy a check function so that internal attributes can be adjusted without changing the original function.
75+
76+
Required to ensure our configuration of functions in the registry does not effect the registry itself.
77+
78+
Also copies the wrapper for auto-parsing ressults,
79+
see https://github.com/NeurodataWithoutBorders/nwbinspector/pull/218 for explanation.
80+
81+
Taken from
82+
https://stackoverflow.com/questions/6527633/how-can-i-make-a-deepcopy-of-a-function-in-python/30714299#30714299
83+
"""
84+
if getattr(function, "__wrapped__", False):
85+
check_function = function.__wrapped__
86+
copied_function = _copy_function(function)
87+
copied_function.__wrapped__ = _copy_function(check_function)
88+
return copied_function
89+
90+
7591
def load_config(filepath_or_keyword: PathType) -> dict:
7692
"""
7793
Load a config dictionary either via keyword search of the internal configs, or an explicit filepath.
@@ -131,7 +147,7 @@ def configure_checks(
131147
checks_out = []
132148
ignore = ignore or []
133149
for check in checks:
134-
mapped_check = copy_function(check)
150+
mapped_check = copy_check(check)
135151
for importance_name, func_names in config.items():
136152
if check.__name__ in func_names:
137153
if importance_name == "SKIP":

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ pynwb
22
PyYAML
33
jsonschema
44
packaging
5+
natsort
6+
click
7+
tqdm

setup.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from setuptools import setup, find_packages
2+
from pathlib import Path
23

3-
# Get the long description from the README file
4-
with open("README.md", "r") as f:
4+
root = Path(__file__).parent
5+
with open(root / "README.md", "r") as f:
56
long_description = f.read()
7+
with open(root / "requirements.txt") as f:
8+
install_requires = f.readlines()
69
setup(
710
name="nwbinspector",
811
version="0.4.9",
@@ -14,7 +17,7 @@
1417
packages=find_packages(),
1518
include_package_data=True,
1619
url="https://github.com/NeurodataWithoutBorders/nwbinspector",
17-
install_requires=["pynwb", "natsort", "click", "PyYAML", "jsonschema", "tqdm"],
20+
install_requires=install_requires,
1821
extras_require=dict(dandi=["dandi>=0.39.2"]),
1922
entry_points={"console_scripts": ["nwbinspector=nwbinspector.nwbinspector:inspect_all_cli"]},
2023
license="BSD-3-Clause",

tests/test_check_configuration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
check_timestamps_match_first_dimension,
1010
available_checks,
1111
)
12-
from nwbinspector.nwbinspector import validate_config, configure_checks, copy_function, load_config
12+
from nwbinspector.nwbinspector import validate_config, configure_checks, _copy_function, load_config
1313

1414

1515
class TestCheckConfiguration(TestCase):
@@ -24,7 +24,7 @@ def setUpClass(cls):
2424

2525
def test_safe_check_copy(self):
2626
initial_importance = available_checks[0].importance
27-
changed_check = copy_function(function=available_checks[0])
27+
changed_check = _copy_function(function=available_checks[0])
2828
if initial_importance is Importance.CRITICAL:
2929
changed_importance = Importance.BEST_PRACTICE_SUGGESTION
3030
else:

tests/test_inspector.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
check_regular_timestamps,
1818
check_data_orientation,
1919
check_timestamps_match_first_dimension,
20+
check_subject_exists,
21+
load_config,
2022
)
21-
from nwbinspector.nwbinspector import inspect_all, inspect_nwb
23+
from nwbinspector import inspect_all, inspect_nwb
2224
from nwbinspector.register_checks import Severity, InspectorMessage, register_check
2325
from nwbinspector.utils import FilePathType, is_module_installed
2426
from nwbinspector.tools import make_minimal_nwbfile
@@ -144,7 +146,7 @@ def assertLogFileContentsEqual(
144146
if ".nwb" in test_line:
145147
# Transform temporary testing path and formatted to hardcoded fake path
146148
str_loc = test_line.find(".nwb")
147-
correction_str = test_line.replace(test_line[5 : str_loc - 8], "./")
149+
correction_str = test_line.replace(test_line[5 : str_loc - 8], "./") # noqa: E203 (black)
148150
test_file_lines[line_number] = correction_str
149151
self.assertEqual(first=test_file_lines[skip_first_n_lines:-1], second=true_file_lines)
150152

@@ -501,6 +503,71 @@ def test_inspect_nwb_manual_iteration_stop(self):
501503
with self.assertRaises(expected_exception=StopIteration):
502504
next(generator)
503505

506+
def test_inspect_nwb_dandi_config(self):
507+
config_checks = [check_subject_exists] + self.checks
508+
test_results = list(
509+
inspect_nwb(
510+
nwbfile_path=self.nwbfile_paths[0],
511+
checks=config_checks,
512+
config=load_config(filepath_or_keyword="dandi"),
513+
)
514+
)
515+
true_results = [
516+
InspectorMessage(
517+
message="Subject is missing.",
518+
importance=Importance.BEST_PRACTICE_SUGGESTION,
519+
check_function_name="check_subject_exists",
520+
object_type="NWBFile",
521+
object_name="root",
522+
location="/",
523+
file_path=self.nwbfile_paths[0],
524+
),
525+
InspectorMessage(
526+
message="data is not compressed. Consider enabling compression when writing a dataset.",
527+
importance=Importance.BEST_PRACTICE_SUGGESTION,
528+
check_function_name="check_small_dataset_compression",
529+
object_type="TimeSeries",
530+
object_name="test_time_series_1",
531+
location="/acquisition/test_time_series_1",
532+
file_path=self.nwbfile_paths[0],
533+
),
534+
InspectorMessage(
535+
message=(
536+
"TimeSeries appears to have a constant sampling rate. "
537+
"Consider specifying starting_time=1.2 and rate=2.0 instead of timestamps."
538+
),
539+
importance=Importance.BEST_PRACTICE_VIOLATION,
540+
check_function_name="check_regular_timestamps",
541+
object_type="TimeSeries",
542+
object_name="test_time_series_2",
543+
location="/acquisition/test_time_series_2",
544+
file_path=self.nwbfile_paths[0],
545+
),
546+
InspectorMessage(
547+
message=(
548+
"Data may be in the wrong orientation. "
549+
"Time should be in the first dimension, and is usually the longest dimension. "
550+
"Here, another dimension is longer."
551+
),
552+
importance=Importance.CRITICAL,
553+
check_function_name="check_data_orientation",
554+
object_type="SpatialSeries",
555+
object_name="my_spatial_series",
556+
location="/processing/behavior/Position/my_spatial_series",
557+
file_path=self.nwbfile_paths[0],
558+
),
559+
InspectorMessage(
560+
message="The length of the first dimension of data does not match the length of timestamps.",
561+
importance=Importance.CRITICAL,
562+
check_function_name="check_timestamps_match_first_dimension",
563+
object_type="TimeSeries",
564+
object_name="test_time_series_3",
565+
location="/acquisition/test_time_series_3",
566+
file_path=self.nwbfile_paths[0],
567+
),
568+
]
569+
self.assertCountEqual(first=test_results, second=true_results)
570+
504571

505572
@pytest.mark.skipif(not HAVE_ROS3 or not HAVE_DANDI, reason="Needs h5py setup with ROS3.")
506573
def test_dandiset_streaming():

tests/unit_tests/test_tables.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ def test_check_single_row_pass():
229229

230230
def test_check_single_row_ignore_units():
231231
table = Units(
232-
name="Units", # default name when building through nwbfile
233-
)
232+
name="Units",
233+
) # default name when building through nwbfile
234234
table.add_unit(spike_times=[1, 2, 3])
235235
assert check_single_row(table=table) is None
236236

0 commit comments

Comments
 (0)