From 07608509186d30846e32d6b451cb84d374ce7d33 Mon Sep 17 00:00:00 2001 From: Evan Hathaway Date: Fri, 5 Feb 2021 18:01:48 -0800 Subject: [PATCH 01/20] Update version to develop --- mffpy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mffpy/version.py b/mffpy/version.py index 22049ab..970bce0 100644 --- a/mffpy/version.py +++ b/mffpy/version.py @@ -1 +1 @@ -__version__ = "0.6.2" +__version__ = "0.6.2-develop" From ea821a3831cd92500b3b67b4e52dcf270ad4295f Mon Sep 17 00:00:00 2001 From: Justus Schwabedal Date: Mon, 12 Apr 2021 15:59:55 +0200 Subject: [PATCH 02/20] Add(bin): script to compare two MFFs --- bin/mffdiff.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 bin/mffdiff.py diff --git a/bin/mffdiff.py b/bin/mffdiff.py new file mode 100644 index 0000000..260ceda --- /dev/null +++ b/bin/mffdiff.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +""" +mffdiff.py + +Compare the content of two mff files +""" +from mffpy import Reader + +leftpath = "examples/example_1.mff" +rightpath = "examples/example_3.mff" + +left_mff = Reader(leftpath) +right_mff = Reader(rightpath) + + +def getnested(cls, props): + if props: + val = getattr(cls, props[0]) + return getnested(val, props[1:]) + + return cls + + +def compare(prop): + props = prop.split('.') + status = 'match' + msg = '' + try: + left_value = getnested(left_mff, props) + right_value = getnested(right_mff, props) + left_msg = str(left_value)[:50] + right_msg = str(right_value)[:50] + if left_value != right_value: + msg = f'\t{left_msg} != {right_msg}' + status = 'diff' + + except BaseException: + status = 'error' + + print(f'>>> {status} @ reader_instance.{prop}') + if msg: + print(msg) + + +compare('flavor') +compare('categories.categories') +compare('epochs') +compare('sampling_rates') +compare('durations') +compare('startdatetime') +compare('units') +compare('num_channels') From 4d861f47fbba5b71abd52a4e22724abb2bd55c5b Mon Sep 17 00:00:00 2001 From: Justus Schwabedal Date: Mon, 12 Apr 2021 19:25:37 +0200 Subject: [PATCH 03/20] Add(mffdiff): compare signal1.bin --- bin/mffdiff.py | 52 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/bin/mffdiff.py b/bin/mffdiff.py index 260ceda..9947cf1 100644 --- a/bin/mffdiff.py +++ b/bin/mffdiff.py @@ -4,49 +4,75 @@ Compare the content of two mff files """ +from os.path import join +from subprocess import check_output from mffpy import Reader -leftpath = "examples/example_1.mff" +leftpath = "examples/example_3.mff" rightpath = "examples/example_3.mff" left_mff = Reader(leftpath) right_mff = Reader(rightpath) -def getnested(cls, props): +def getnested(cls, props, callback=None): if props: val = getattr(cls, props[0]) - return getnested(val, props[1:]) + return getnested(val, props[1:], callback) - return cls + return callback(cls) if callback else cls -def compare(prop): +def compare_raw(filename): + leftfile = join(leftpath, filename) + rightfile = join(rightpath, filename) + try: + check_output(['diff', leftfile, rightfile]) + status = 'match' + except BaseException: + status = 'diff' + + print(f">>> {status} @ file '{filename}'") + + +def compare(prop, callback=None, info=''): props = prop.split('.') - status = 'match' - msg = '' try: - left_value = getnested(left_mff, props) - right_value = getnested(right_mff, props) + left_value = getnested(left_mff, props, callback) + right_value = getnested(right_mff, props, callback) left_msg = str(left_value)[:50] right_msg = str(right_value)[:50] if left_value != right_value: msg = f'\t{left_msg} != {right_msg}' status = 'diff' + else: + msg = '' + status = 'match' - except BaseException: + except BaseException as err: status = 'error' + msg = str(err)[:50] + '\n' + left_value = None - print(f'>>> {status} @ reader_instance.{prop}') + print(f">>> {status} @ reader_instance.{prop} " + f"{': ' + info if info else ''}") if msg: print(msg) + return left_value if status == 'match' else None + +print(f"Comparing {leftpath} with {rightpath}") compare('flavor') -compare('categories.categories') -compare('epochs') compare('sampling_rates') compare('durations') compare('startdatetime') compare('units') compare('num_channels') +compare('categories.categories') +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') From ccd5212480910c1d901e537394bce9e189edaa53 Mon Sep 17 00:00:00 2001 From: Justus Schwabedal Date: Mon, 12 Apr 2021 19:25:56 +0200 Subject: [PATCH 04/20] Add(setup): mffdiff.py to package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 875c8b4..afc6ecd 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ name='mffpy', version=__version__, packages=setuptools.find_packages(), - scripts=['./bin/mff2json.py', './bin/mff2mfz.py'], + scripts=['./bin/mff2json.py', './bin/mff2mfz.py', './bin/mffdiff.py'], author='Justus Schwabedal, Wayne Manselle', author_email='jschwabedal@belco.tech, wayne.manselle@belco.tech', maintainer='Evan Hathaway', From d6f4d354af6962c0a5e9022cbe9ebe29c2cb5fc4 Mon Sep 17 00:00:00 2001 From: Justus Schwabedal Date: Mon, 12 Apr 2021 19:31:21 +0200 Subject: [PATCH 05/20] Add(mffdiff): args for files --- bin/mffdiff.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/bin/mffdiff.py b/bin/mffdiff.py index 9947cf1..f526e20 100644 --- a/bin/mffdiff.py +++ b/bin/mffdiff.py @@ -7,12 +7,15 @@ from os.path import join from subprocess import check_output from mffpy import Reader +from argparse import ArgumentParser -leftpath = "examples/example_3.mff" -rightpath = "examples/example_3.mff" +parser = ArgumentParser() +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(leftpath) -right_mff = Reader(rightpath) +left_mff = Reader(args.leftpath) +right_mff = Reader(args.rightpath) def getnested(cls, props, callback=None): @@ -24,8 +27,8 @@ def getnested(cls, props, callback=None): def compare_raw(filename): - leftfile = join(leftpath, filename) - rightfile = join(rightpath, filename) + leftfile = join(args.leftpath, filename) + rightfile = join(args.rightpath, filename) try: check_output(['diff', leftfile, rightfile]) status = 'match' @@ -62,7 +65,7 @@ def compare(prop, callback=None, info=''): return left_value if status == 'match' else None -print(f"Comparing {leftpath} with {rightpath}") +print(f"Comparing {args.leftpath} with {args.rightpath}") compare('flavor') compare('sampling_rates') compare('durations') From 9c1e3701b69b35d83b724150aeb1350c6feafe41 Mon Sep 17 00:00:00 2001 From: Justus Schwabedal Date: Mon, 12 Apr 2021 21:15:34 +0200 Subject: [PATCH 06/20] Add(mffdiff): description --- bin/mffdiff.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/mffdiff.py b/bin/mffdiff.py index f526e20..0d480ff 100644 --- a/bin/mffdiff.py +++ b/bin/mffdiff.py @@ -2,14 +2,16 @@ """ mffdiff.py -Compare the content of two mff files +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 -parser = ArgumentParser() +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() From 673adc471aa1fbe2efa3c5a9086760ec2e4285b4 Mon Sep 17 00:00:00 2001 From: Justus Schwabedal Date: Tue, 13 Apr 2021 15:56:20 +0200 Subject: [PATCH 07/20] Chore: update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bd0869..6d5a6c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- script `mffdiff.py` to compare two MFF files ## [0.6.2] - 2021-02-05 ### Fixed From d61c27c164a57240f619230e8d926d1ee910f787 Mon Sep 17 00:00:00 2001 From: Justus Schwabedal Date: Wed, 14 Apr 2021 16:09:33 +0200 Subject: [PATCH 08/20] Chg(mffdiff): ref. code, add docs --- bin/mffdiff.py | 56 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/bin/mffdiff.py b/bin/mffdiff.py index 0d480ff..7e4f24d 100644 --- a/bin/mffdiff.py +++ b/bin/mffdiff.py @@ -9,6 +9,8 @@ from mffpy import Reader from argparse import ArgumentParser +char_limit = 50 + parser = ArgumentParser(description=""" compare the contents of two MFF files """) @@ -20,15 +22,31 @@ right_mff = Reader(args.rightpath) -def getnested(cls, props, callback=None): +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(cls, props[0]) + val = getattr(instance, props[0]) return getnested(val, props[1:], callback) - return callback(cls) if callback else cls + 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: @@ -40,13 +58,24 @@ def compare_raw(filename): print(f">>> {status} @ file '{filename}'") -def compare(prop, callback=None, info=''): +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)[:50] - right_msg = str(right_value)[:50] + 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' @@ -56,7 +85,7 @@ def compare(prop, callback=None, info=''): except BaseException as err: status = 'error' - msg = str(err)[:50] + '\n' + msg = str(err)[:char_limit] + '\n' left_value = None print(f">>> {status} @ reader_instance.{prop} " @@ -67,14 +96,13 @@ def compare(prop, callback=None, info=''): return left_value if status == 'match' else None +simple_props = ['flavor', 'sampling_rates', 'durations', 'startdatetime', + 'units', 'num_channels', 'categories.categories'] + print(f"Comparing {args.leftpath} with {args.rightpath}") -compare('flavor') -compare('sampling_rates') -compare('durations') -compare('startdatetime') -compare('units') -compare('num_channels') -compare('categories.categories') +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): From 41a90d488825b457a889621e29ce38187ffc2164 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 3 Jun 2021 09:04:27 -0400 Subject: [PATCH 09/20] MAINT: Use FileNotFound rather than ValueError --- .gitignore | 2 ++ mffpy/mffdir.py | 5 +++-- mffpy/reader.py | 2 +- mffpy/tests/conftest.py | 32 ++++++++++++++++++++++++++++++++ mffpy/tests/test_writer.py | 28 +++++++++++----------------- 5 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 mffpy/tests/conftest.py diff --git a/.gitignore b/.gitignore index 8dfe446..4787192 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,8 @@ coverage.xml *.cover .hypothesis/ .pytest_cache/ +test.xml +examples/example_1.mfz # Translations *.mo diff --git a/mffpy/mffdir.py b/mffpy/mffdir.py index 98cda57..17f923d 100644 --- a/mffpy/mffdir.py +++ b/mffpy/mffdir.py @@ -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) diff --git a/mffpy/reader.py b/mffpy/reader.py index fdf8529..7d2a4ff 100644 --- a/mffpy/reader.py +++ b/mffpy/reader.py @@ -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 diff --git a/mffpy/tests/conftest.py b/mffpy/tests/conftest.py new file mode 100644 index 0000000..cc72371 --- /dev/null +++ b/mffpy/tests/conftest.py @@ -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) diff --git a/mffpy/tests/test_writer.py b/mffpy/tests/test_writer.py index 847dd84..27cfe74 100644 --- a/mffpy/tests/test_writer.py +++ b/mffpy/tests/test_writer.py @@ -27,9 +27,6 @@ from ..xml_files import XML -CACHE_DIR = '.cache' - - def test_writer_receives_bad_init_data(): """Test bin writer fails when initialized with non-int sampling rate""" BinWriter(100) @@ -38,20 +35,17 @@ def test_writer_receives_bad_init_data(): assert str(exc_info.value) == "Sampling rate not int. Received 100.0" -def test_writer_doesnt_overwrite(): +def test_writer_doesnt_overwrite(tmpdir): """test that `mffpy.Writer` doesn't overwrite existing files""" - dirname = join(CACHE_DIR, 'testdir.mff') + dirname = join(str(tmpdir), 'testdir.mff') makedirs(dirname, exist_ok=True) - with pytest.raises(AssertionError) as exc_info: + with pytest.raises(AssertionError, match='File.*exists already'): Writer(dirname) - assert str(exc_info.value) == "File '.cache/testdir.mff' exists already" - - rmdir(dirname) -def test_writer_writes(): +def test_writer_writes(tmpdir): """Test `mffpy.Writer` can write binary and xml files""" - dirname = join(CACHE_DIR, 'testdir2.mff') + dirname = join(str(tmpdir), 'testdir2.mff') # create some data and add it to a binary writer device = 'HydroCel GSN 256 1.0' num_samples = 10 @@ -94,9 +88,9 @@ def test_writer_writes(): Clean-up failed of '{dirname}'. Were additional files written?""") -def test_writer_writes_multple_bins(): +def test_writer_writes_multple_bins(tmpdir): """test that `mffpy.Writer` can write multiple binary files""" - dirname = join(CACHE_DIR, 'multiple_bins.mff') + dirname = join(str(tmpdir), 'multiple_bins.mff') device = 'HydroCel GSN 256 1.0' # create some data and add it to binary writers num_samples = 10 @@ -214,9 +208,9 @@ def test_writer_exports_JSON(): raise AssertionError(f"""Clean-up failed of '{filename}'.""") -def test_streaming_writer_receives_bad_init_data(): +def test_streaming_writer_receives_bad_init_data(tmpdir): """Test bin writer fails when initialized with non-int sampling rate""" - dirname = join(CACHE_DIR, 'testdir.mff') + dirname = join(str(tmpdir), 'testdir.mff') makedirs(dirname) StreamingBinWriter(100, mffdir=dirname) with pytest.raises(AssertionError) as exc_info: @@ -225,8 +219,8 @@ def test_streaming_writer_receives_bad_init_data(): rmtree(dirname) -def test_streaming_writer_writes(): - dirname = join(CACHE_DIR, 'testdir3.mff') +def test_streaming_writer_writes(tmpdir): + dirname = join(str(tmpdir), 'testdir3.mff') # create some data and add it to a binary writer device = 'HydroCel GSN 256 1.0' num_samples = 10 From ebe7883bdf583644285a51c9ea65027dc60e7d99 Mon Sep 17 00:00:00 2001 From: ephathaway Date: Thu, 3 Jun 2021 10:30:59 -0700 Subject: [PATCH 10/20] Use tmpdir fixture --- makefile | 1 - mffpy/tests/test_dict2xml.py | 6 +++-- mffpy/tests/test_writer.py | 47 +++--------------------------------- 3 files changed, 8 insertions(+), 46 deletions(-) diff --git a/makefile b/makefile index 820d74b..158c2c2 100644 --- a/makefile +++ b/makefile @@ -12,4 +12,3 @@ test: examples/example_1.mfz clean: -rm examples/example_1.mfz - -rm -rf .cache diff --git a/mffpy/tests/test_dict2xml.py b/mffpy/tests/test_dict2xml.py index 21bf3fa..761e7ce 100644 --- a/mffpy/tests/test_dict2xml.py +++ b/mffpy/tests/test_dict2xml.py @@ -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') diff --git a/mffpy/tests/test_writer.py b/mffpy/tests/test_writer.py index 27cfe74..1ba279e 100644 --- a/mffpy/tests/test_writer.py +++ b/mffpy/tests/test_writer.py @@ -13,9 +13,8 @@ ANY KIND, either express or implied. """ from datetime import datetime -from os import makedirs, rmdir, remove +from os import makedirs from os.path import join -from shutil import rmtree import pytest import json @@ -74,18 +73,6 @@ def test_writer_writes(tmpdir): layout = R.directory.filepointer('sensorLayout') layout = XML.from_file(layout) assert layout.name == device - # cleanup - try: - remove(join(dirname, 'info.xml')) - remove(join(dirname, 'info1.xml')) - remove(join(dirname, 'epochs.xml')) - remove(join(dirname, 'signal1.bin')) - remove(join(dirname, 'coordinates.xml')) - remove(join(dirname, 'sensorLayout.xml')) - rmdir(dirname) - except BaseException: - raise AssertionError(f""" - Clean-up failed of '{dirname}'. Were additional files written?""") def test_writer_writes_multple_bins(tmpdir): @@ -134,20 +121,6 @@ def test_writer_writes_multple_bins(tmpdir): layout = R.directory.filepointer('sensorLayout') layout = XML.from_file(layout) assert layout.name == device - # cleanup - try: - remove(join(dirname, 'info.xml')) - remove(join(dirname, 'info1.xml')) - remove(join(dirname, 'signal1.bin')) - remove(join(dirname, 'info2.xml')) - remove(join(dirname, 'signal2.bin')) - remove(join(dirname, 'epochs.xml')) - remove(join(dirname, 'coordinates.xml')) - remove(join(dirname, 'sensorLayout.xml')) - rmdir(dirname) - except BaseException: - raise AssertionError(f""" - Clean-up failed of '{dirname}'. Were additional files written?""") def test_write_multiple_blocks(): @@ -166,7 +139,7 @@ def test_write_multiple_blocks(): def test_writer_is_compatible_with_egi(): """check that binary writers fail to write EGI-incompatible files""" - filename = join('.cache', 'unimportant-filename.mff') + filename = 'unimportant-filename.mff' bin_writer = BinWriter(sampling_rate=128, data_type='PNSData') writer = Writer(filename) message = "Writing type 'PNSData' to 'signal1.bin' may be " \ @@ -181,8 +154,8 @@ def test_writer_is_compatible_with_egi(): assert str(exc_info.value) == message -def test_writer_exports_JSON(): - filename = 'test1.json' +def test_writer_exports_JSON(tmpdir): + filename = join(str(tmpdir), 'test1.json') # Root tags corresponding to available XMLType sub-classes xml_root_tags = ['fileInfo', 'dataInfo', 'patient', 'sensorLayout', 'coordinates', 'epochs', 'eventTrack', 'categories', @@ -201,11 +174,6 @@ def test_writer_exports_JSON(): with open(filename) as file: data = json.load(file) assert data == content - # cleanup - try: - remove(filename) - except BaseException: - raise AssertionError(f"""Clean-up failed of '{filename}'.""") def test_streaming_writer_receives_bad_init_data(tmpdir): @@ -216,7 +184,6 @@ def test_streaming_writer_receives_bad_init_data(tmpdir): with pytest.raises(AssertionError) as exc_info: StreamingBinWriter(100.0, mffdir=dirname) assert str(exc_info.value) == "Sampling rate not int. Received 100.0" - rmtree(dirname) def test_streaming_writer_writes(tmpdir): @@ -251,9 +218,3 @@ def test_streaming_writer_writes(tmpdir): layout = reader.directory.filepointer('sensorLayout') layout = XML.from_file(layout) assert layout.name == device - # cleanup - try: - rmtree(dirname) - except BaseException: - raise AssertionError(f""" - Clean-up failed of '{dirname}'. Were additional files written?""") From fb29322a5910598472a0d8939ea63c3e9b94c9be Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Sat, 5 Jun 2021 11:44:51 -0400 Subject: [PATCH 11/20] FIX: Remove call to conversion target --- makefile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/makefile b/makefile index 158c2c2..e1b8df7 100644 --- a/makefile +++ b/makefile @@ -3,10 +3,7 @@ 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 From 387de9698812f7e27189ad278633e04c0d78cd85 Mon Sep 17 00:00:00 2001 From: ephathaway Date: Wed, 9 Jun 2021 15:24:56 -0700 Subject: [PATCH 12/20] FIX: install stubs for mypy types --- requirements-dev.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 337e09f..4c0fed7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,3 +3,5 @@ pytest>=5.2.0 pytest-cov>=2.6.1 pre-commit>=2.7.1 flake8==3.8.4 +types-pytz>=0.1.0 +types-deprecated>=0.1.1 From 868b82b8420d776d80fa19c2a10e92500e6af8e1 Mon Sep 17 00:00:00 2001 From: Damian Persico Date: Mon, 20 Sep 2021 15:10:53 -0300 Subject: [PATCH 13/20] feat: add ability to read bad channels from infoN.xml files --- mffpy/xml_files.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mffpy/xml_files.py b/mffpy/xml_files.py index 8d19aea..5452aca 100644 --- a/mffpy/xml_files.py +++ b/mffpy/xml_files.py @@ -289,6 +289,19 @@ def _parse_calibration(self, cali): } return ans + @cached_property + def channels(self) -> List[Dict[str, Any]]: + """returns list of good and bad channels""" + channels = self.findall('channels') + return list(map(self._parse_channels_element, channels)) + + def _parse_channels_element(self, element: ET.Element) -> Dict[str, Any]: + """parses element """ + text = element.text or '' + channels = list(map(int, text.split())) + exclusion = str(element.get('exclusion')) + return {'channels': channels, 'exclusion': exclusion} + @classmethod def content(cls, fileDataType: str, # type: ignore dataTypeProps: dict = None, @@ -354,6 +367,7 @@ def get_content(self): """return info on the associated (data) .bin file""" return { 'generalInformation': self.generalInformation, + 'channels': self.channels, 'filters': self.filters, 'calibrations': self.calibrations } From b055809d69622a9002341e0f3aca71095cc2e169 Mon Sep 17 00:00:00 2001 From: Damian Persico Date: Mon, 20 Sep 2021 15:16:18 -0300 Subject: [PATCH 14/20] test: add unit test for DataInfo.channels property --- examples/example_1.mff/info1.xml | 1 + mffpy/tests/test_xml_files.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/examples/example_1.mff/info1.xml b/examples/example_1.mff/info1.xml index 63b5d6e..5a97ea4 100644 --- a/examples/example_1.mff/info1.xml +++ b/examples/example_1.mff/info1.xml @@ -9,6 +9,7 @@ + 31 47 65 79 0 diff --git a/mffpy/tests/test_xml_files.py b/mffpy/tests/test_xml_files.py index 57c7ee3..c306de6 100644 --- a/mffpy/tests/test_xml_files.py +++ b/mffpy/tests/test_xml_files.py @@ -158,6 +158,16 @@ def test_DataInfo2_generalInfo(field, expected, data_info2): field, val, expected) +@pytest.mark.parametrize("field,expected", [ + ('channels', [31, 47, 65, 79]), + ('exclusion', 'badChannels'), +]) +def test_DataInfo_channels(field, expected, data_info): + val = data_info.channels[0][field] + assert val == expected, "F[%s] = %s [should be %s]" % ( + field, val, expected) + + @pytest.mark.parametrize("field,expected", [ ('beginTime', 0), ('method', 'Hardware'), From aa8daf0229c1d63753e30db50933f9b5e8bf8d58 Mon Sep 17 00:00:00 2001 From: Damian Persico Date: Mon, 20 Sep 2021 15:48:01 -0300 Subject: [PATCH 15/20] docs: update CHANGELOG.md file --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f7d4f..8965e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Ability to read bad channels from infoN.xml files. - script `mffdiff.py` to compare two MFF files ## [0.6.3] - 2021-05-17 From 3f3b27f5cb0344d7017be6cd23d019400572d016 Mon Sep 17 00:00:00 2001 From: Damian Persico Date: Mon, 20 Sep 2021 16:01:00 -0300 Subject: [PATCH 16/20] test: update example_2.json file to include `channels` field --- examples/example_2.json | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/example_2.json b/examples/example_2.json index 60a3e78..e39318d 100644 --- a/examples/example_2.json +++ b/examples/example_2.json @@ -12,6 +12,7 @@ "sensorLayoutName": "HydroCel GSN 256 1.0", "montageName": "HydroCel GSN 256 1.0" }, + "channels": [], "filters": [], "calibrations": {} }, From 16ea6c22d03e200bccaab0a297fa04d3343cde05 Mon Sep 17 00:00:00 2001 From: Ian Brown Date: Wed, 19 Jan 2022 15:26:09 -0800 Subject: [PATCH 17/20] perf(reader): cache _blobs --- mffpy/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mffpy/reader.py b/mffpy/reader.py index 7d2a4ff..bca0b1d 100644 --- a/mffpy/reader.py +++ b/mffpy/reader.py @@ -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 = {} From 383f08e8a134e33530a566aa7203dd80699cb8dd Mon Sep 17 00:00:00 2001 From: Ian Brown Date: Thu, 20 Jan 2022 12:07:18 -0800 Subject: [PATCH 18/20] docs: update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8965e37..989725c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 From 16bef8d5f34668519db09ddeafa028b242d89c49 Mon Sep 17 00:00:00 2001 From: Ian Brown Date: Mon, 24 Jan 2022 09:26:39 -0800 Subject: [PATCH 19/20] chore: bump version to 0.7.0 --- CHANGELOG.md | 5 ++++- mffpy/version.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 989725c..5d6610a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [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 @@ -61,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 diff --git a/mffpy/version.py b/mffpy/version.py index 1debab7..49e0fc1 100644 --- a/mffpy/version.py +++ b/mffpy/version.py @@ -1 +1 @@ -__version__ = "0.6.3-develop" +__version__ = "0.7.0" From 6a1870655fa6fe6f29feca8fe9e7b1cdde1f8bc9 Mon Sep 17 00:00:00 2001 From: Ian Brown Date: Wed, 26 Jan 2022 11:21:58 -0800 Subject: [PATCH 20/20] fix(mffdiff): use mff_flavor instead of flavor --- bin/mffdiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/mffdiff.py b/bin/mffdiff.py index 7e4f24d..ee2602b 100644 --- a/bin/mffdiff.py +++ b/bin/mffdiff.py @@ -96,7 +96,7 @@ def compare(prop: str, callback=None, info: str = ''): return left_value if status == 'match' else None -simple_props = ['flavor', 'sampling_rates', 'durations', 'startdatetime', +simple_props = ['mff_flavor', 'sampling_rates', 'durations', 'startdatetime', 'units', 'num_channels', 'categories.categories'] print(f"Comparing {args.leftpath} with {args.rightpath}")