Skip to content

Commit

Permalink
Merge pull request #94 from BEL-Public/release-0.7.0
Browse files Browse the repository at this point in the history
  • Loading branch information
ianbrown9475 authored Jan 27, 2022
2 parents a3fd7d2 + 6a18706 commit 28ccc3a
Show file tree
Hide file tree
Showing 16 changed files with 210 additions and 74 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
test.xml
examples/example_1.mfz

# Translations
*.mo
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.7.0] - 2022-01-24
### Added
- Ability to read bad channels from infoN.xml files.
- script `mffdiff.py` to compare two MFF files

### Changed
- cache `Reader._blobs`

## [0.6.3] - 2021-05-17
### Added
- dependency Deprecated
Expand Down Expand Up @@ -55,7 +63,8 @@ In `Reader.get_physical_samples_from_epoch()`:
- Parse key elements in categories.xml files with `mffpy.xml_files.Categories` class.
- Incorporate `cached_property` dependency into `mffpy` library.

[Unreleased]: https://github.com/bel-public/mffpy/compare/v0.6.3...HEAD
[Unreleased]: https://github.com/bel-public/mffpy/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/bel-public/mffpy/compare/v0.6.3...v0.7.0
[0.6.3]: https://github.com/bel-public/mffpy/compare/v0.6.2...v0.6.3
[0.6.2]: https://github.com/bel-public/mffpy/compare/v0.6.1...v0.6.2
[0.6.1]: https://github.com/bel-public/mffpy/compare/v0.6.0...v0.6.1
Expand Down
111 changes: 111 additions & 0 deletions bin/mffdiff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python
"""
mffdiff.py
Compare the content of two MFF files
"""
from os.path import join
from subprocess import check_output
from mffpy import Reader
from argparse import ArgumentParser

char_limit = 50

parser = ArgumentParser(description="""
compare the contents of two MFF files
""")
parser.add_argument('leftpath', type=str, help="MFF file to compare")
parser.add_argument('rightpath', type=str, help="second MFF file to compare")
args = parser.parse_args()

left_mff = Reader(args.leftpath)
right_mff = Reader(args.rightpath)


def getnested(instance, props, callback=None):
"""get nested property of instance
This is a recursive function to access a nested property, e.g. for
`instance.a.b.c`, props is `['a', 'b', 'c']`. `callback` is optionally
applied to the final value.
Parameters
----------
instance:
instance that has a property `props[0]`
props:
nested property split into a list
callback:
is optionally applied to the output value
"""
if props:
val = getattr(instance, props[0])
return getnested(val, props[1:], callback)

return callback(instance) if callback else instance


def compare_raw(filename):
"""compare file in MFF using `$ diff`"""
leftfile = join(args.leftpath, filename)
rightfile = join(args.rightpath, filename)
try:
check_output(['diff', leftfile, rightfile])
status = 'match'
except BaseException:
status = 'diff'

print(f">>> {status} @ file '{filename}'")


def compare(prop: str, callback=None, info: str = ''):
"""compare property between left_mff and right_mff
Parameters
----------
prop:
string specifying a nested property of `mffpy.Reader` instance
callback:
post processing of the value of `prop`
info:
additional note of the nested property
"""
props = prop.split('.')
try:
left_value = getnested(left_mff, props, callback)
right_value = getnested(right_mff, props, callback)
left_msg = str(left_value)[:char_limit]
right_msg = str(right_value)[:char_limit]
if left_value != right_value:
msg = f'\t{left_msg} != {right_msg}'
status = 'diff'
else:
msg = ''
status = 'match'

except BaseException as err:
status = 'error'
msg = str(err)[:char_limit] + '\n'
left_value = None

print(f">>> {status} @ reader_instance.{prop} "
f"{': ' + info if info else ''}")
if msg:
print(msg)

return left_value if status == 'match' else None


simple_props = ['mff_flavor', 'sampling_rates', 'durations', 'startdatetime',
'units', 'num_channels', 'categories.categories']

print(f"Comparing {args.leftpath} with {args.rightpath}")
for prop in simple_props:
compare(prop)

epoch_count = compare('epochs.epochs', lambda e: len(e), 'len(..)')
if epoch_count is not None:
for i in range(epoch_count):
compare('epochs.epochs', lambda e: e[i].content, f"[{i}].content")

out = compare_raw('signal1.bin')
1 change: 1 addition & 0 deletions examples/example_1.mff/info1.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</EEG>
</fileDataType>
</generalInformation>
<channels exclusion="badChannels">31 47 65 79</channels>
<filters>
<filter>
<beginTime>0</beginTime>
Expand Down
1 change: 1 addition & 0 deletions examples/example_2.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"sensorLayoutName": "HydroCel GSN 256 1.0",
"montageName": "HydroCel GSN 256 1.0"
},
"channels": [],
"filters": [],
"calibrations": {}
},
Expand Down
6 changes: 1 addition & 5 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ examples/example_1.mfz: examples/example_1.mff
# zip -Z store -r -j ./examples/example_1.mfz ./examples/example_1.mff
-python ./bin/mff2mfz.py ./examples/example_1.mff


# some tests depend on the existence of a zipped version of
# 'examples/example_1.mff/'
test: examples/example_1.mfz
test:
mypy --ignore-missing-imports mffpy
pytest --cov

clean:
-rm examples/example_1.mfz
-rm -rf .cache
5 changes: 3 additions & 2 deletions mffpy/mffdir.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ def filename(self, basename: str) -> str:
if basename in files:
return join(self._mffname, basename) + ext
else:
raise ValueError(f"No file with basename {basename} \
in directory {super().__str__()}.")
raise FileNotFoundError(
f"No file with basename {basename} "
f"in directory {super().__str__()}.")

def __contains__(self, filename: str) -> bool:
return exists(filename)
Expand Down
4 changes: 2 additions & 2 deletions mffpy/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def epochs(self) -> Epochs:
# Attempt to add category names to the `Epoch` objects in `epochs`
try:
categories = self.categories
except (ValueError, AssertionError):
except (FileNotFoundError, AssertionError):
print('categories.xml not found or of wrong type. '
'`Epoch.name` will default to "epoch" for all epochs.')
return epochs
Expand Down Expand Up @@ -208,7 +208,7 @@ def num_channels(self) -> Dict[str, int]:
for fn, bin_file in self._blobs.items()
}

@property
@cached_property
def _blobs(self) -> Dict[str, bin_files.BinFile]:
"""return dictionary of `BinFile` data readers by signal type"""
__blobs = {}
Expand Down
32 changes: 32 additions & 0 deletions mffpy/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Copyright 2019 Brain Electrophysiology Laboratory Company LLC
Licensed under the ApacheLicense, Version 2.0(the "License");
you may not use this module except in compliance with the License.
You may obtain a copy of the License at:
http: // www.apache.org / licenses / LICENSE - 2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied.
"""

from glob import glob
import os.path as op
from zipfile import ZipFile, ZIP_STORED

import pytest


@pytest.fixture(scope='session', autouse=True)
def ensure_mfz():
"""Ensure that the mfz file exists."""
fname = op.join(
op.dirname(__file__), '..', '..', 'examples', 'example_1.mfz')
if not op.isfile(fname):
with ZipFile(fname, mode='w', compression=ZIP_STORED) as zf:
for content_filename in glob(op.join(fname[:-3] + 'mff', '*')):
arc_filename = op.basename(content_filename)
zf.write(content_filename, arcname=arc_filename)
6 changes: 4 additions & 2 deletions mffpy/tests/test_dict2xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied.
"""
from os.path import join

from ..dict2xml import dict2xml, TEXT, ATTR


def test_dict2xml():
def test_dict2xml(tmpdir):
rootname = 'myroot'
content = {
'a': {TEXT: '35', ATTR: {'hello': 'world'}},
'b': [{TEXT: 'b' + str(i+1)} for i in range(2)]
}
elem = dict2xml(content, rootname=rootname)
elem.write('test.xml')
elem.write(join(str(tmpdir), 'test.xml'))
root = elem.getroot()
a = root.find('a')
bs = root.findall('b')
Expand Down
Loading

0 comments on commit 28ccc3a

Please sign in to comment.