Skip to content

Commit 2082f9e

Browse files
Merge branch 'dev' into add_identifier_check_over_files
2 parents 1fef932 + bd81d40 commit 2082f9e

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
@@ -38,7 +38,7 @@
3838
class InspectorOutputJSONEncoder(json.JSONEncoder):
3939
"""Custom JSONEncoder for the NWBInspector."""
4040

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

5656

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

7572

73+
def copy_check(function):
74+
"""
75+
Copy a check function so that internal attributes can be adjusted without changing the original function.
76+
77+
Required to ensure our configuration of functions in the registry does not effect the registry itself.
78+
79+
Also copies the wrapper for auto-parsing ressults,
80+
see https://github.com/NeurodataWithoutBorders/nwbinspector/pull/218 for explanation.
81+
82+
Taken from
83+
https://stackoverflow.com/questions/6527633/how-can-i-make-a-deepcopy-of-a-function-in-python/30714299#30714299
84+
"""
85+
if getattr(function, "__wrapped__", False):
86+
check_function = function.__wrapped__
87+
copied_function = _copy_function(function)
88+
copied_function.__wrapped__ = _copy_function(check_function)
89+
return copied_function
90+
91+
7692
def load_config(filepath_or_keyword: PathType) -> dict:
7793
"""
7894
Load a config dictionary either via keyword search of the internal configs, or an explicit filepath.
@@ -132,7 +148,7 @@ def configure_checks(
132148
checks_out = []
133149
ignore = ignore or []
134150
for check in checks:
135-
mapped_check = copy_function(check)
151+
mapped_check = copy_check(check)
136152
for importance_name, func_names in config.items():
137153
if check.__name__ in func_names:
138154
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
@@ -19,8 +19,10 @@
1919
check_regular_timestamps,
2020
check_data_orientation,
2121
check_timestamps_match_first_dimension,
22+
check_subject_exists,
23+
load_config,
2224
)
23-
from nwbinspector.nwbinspector import inspect_all, inspect_nwb
25+
from nwbinspector import inspect_all, inspect_nwb
2426
from nwbinspector.register_checks import Severity, InspectorMessage, register_check
2527
from nwbinspector.utils import FilePathType, is_module_installed
2628
from nwbinspector.tools import make_minimal_nwbfile
@@ -146,7 +148,7 @@ def assertLogFileContentsEqual(
146148
if ".nwb" in test_line:
147149
# Transform temporary testing path and formatted to hardcoded fake path
148150
str_loc = test_line.find(".nwb")
149-
correction_str = test_line.replace(test_line[5 : str_loc - 8], "./") # noqa E203 (black)
151+
correction_str = test_line.replace(test_line[5 : str_loc - 8], "./") # noqa: E203 (black)
150152
test_file_lines[line_number] = correction_str
151153
self.assertEqual(first=test_file_lines[skip_first_n_lines:-1], second=true_file_lines)
152154

@@ -503,6 +505,71 @@ def test_inspect_nwb_manual_iteration_stop(self):
503505
with self.assertRaises(expected_exception=StopIteration):
504506
next(generator)
505507

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

507574
@pytest.mark.skipif(not HAVE_ROS3 or not HAVE_DANDI, reason="Needs h5py setup with ROS3.")
508575
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)