33 Commits
0.0.2 ... 0.1.0

Author SHA1 Message Date
aa4ca29c22 Release version 0.1.0
Update package URL to the new echemdata repo
2019-06-02 13:26:20 +02:00
e71076bda3 Merge pull request #30 from chatcannon/test-and-fix-Arbin
Fix Arbin reading code
2019-06-02 13:18:32 +02:00
0ea049e279 Merge branch 'master' into test-and-fix-Arbin 2019-05-16 17:29:38 +02:00
a4cf8c1420 Merge pull request #32 from chatcannon/flake8
Add code style checking with flake8
2019-05-16 17:25:36 +02:00
aab135391a Change max-line-length to 100 and refactor all longer lines 2019-05-15 07:09:11 +02:00
8abab57c06 Fixed some more flake8 warnings 2019-05-12 09:20:33 +02:00
3440047dc2 Fixed flake8 warning about lambda assignment 2019-05-12 09:15:20 +02:00
d137bfccef Add flake8 to tox.ini 2019-05-12 09:14:57 +02:00
ed43de1326 Fix flake8 warnings on comments style 2019-05-12 09:13:01 +02:00
1c8335289a Add flake8 configuration 2019-05-12 09:12:40 +02:00
6787a7ec03 Merge branch 'master' into test-and-fix-Arbin 2019-05-05 08:14:43 +02:00
e5aada3a85 Merge branch 'master' into flake8 2019-05-05 08:12:52 +02:00
a41b40c7a4 Merge pull request #26 from bcolsen/dupli_cols
[PR] Biologic file with duplicate columns
2019-05-04 08:38:36 +02:00
2a36713b06 Fixed some flake8 warnings in res2sqlite.py 2019-05-03 20:37:27 +02:00
f2b62265b9 Fixed some flake8 warnings in BioLogic.py 2019-05-03 20:30:12 +02:00
61e2ac8f57 Added unit tests for the VMPdata_dtype_from_colIDs function 2019-05-03 19:46:45 +02:00
c401aca741 Get rid of flags2_dict as flags2 doesn't actually exist 2019-05-03 19:15:34 +02:00
1f57e48602 Refactor the VMPdata_dtype_from_colIDs function 2019-05-03 19:12:18 +02:00
6b0f8b6d37 Add a VMPdata_colID_flags_map dict 2019-05-03 18:47:02 +02:00
56a321f8e3 Formatting 2019-05-03 18:39:42 +02:00
d991cd496e Merge branch 'master' into dupli_cols 2019-05-03 18:28:38 +02:00
531cfc6a42 Merge pull request #24 from bcolsen/loop_module
[PR] Add parsing of loop index modules
2019-04-12 07:51:15 +02:00
bcolsen
ef1ea9a2f4 Default loop to none and trim the trailing zeros 2019-04-11 17:30:05 -06:00
bcolsen
b3c5f36e11 flag fix 2019-04-11 11:59:18 -06:00
bcolsen
d1d53e97fa biologic file with duplicate columns 2019-04-09 17:48:38 -06:00
bcolsen
4ba61aa5d8 added parsing of loop_modules 2019-04-09 17:23:16 -06:00
4381b02242 Add .pytest_cache to Travis cache 2019-04-03 08:16:55 +02:00
846a5b3149 Catch FileNotFoundError from Popen and re-raise a more helpful message 2019-04-03 08:14:05 +02:00
6a8fbe71a4 Add some tests for the res2sqlite command-line tool
Check that the --help option works even if mdbtools is not installed
2019-04-02 23:11:37 +02:00
557e755f03 Move Popen call outside the try/finally block
Ensure that all variables used in the except and finally blocks
are always defined - fixes #23

In Python 3, Popen objects can be used as contextmanagers, but not
in Python 2.7
2019-04-02 23:09:53 +02:00
a1b73867ea Add a test that a sensible error is raised when MDBTools is not found
This is the error that happens in issue #23
2019-04-02 23:07:12 +02:00
5530a7a8ff Add a simple test for loading Arbin .res files 2019-04-02 23:06:23 +02:00
d6d6bf1ac7 Use a pytest fixture to locate the testdata directory 2019-04-02 21:34:34 +02:00
10 changed files with 348 additions and 194 deletions

2
.flake8 Normal file
View File

@@ -0,0 +1,2 @@
# This file will be ignored - see http://flake8.pycqa.org/en/2.6.0/config.html#per-project
# Edit the [flake8] section in tox.ini instead

View File

@@ -3,6 +3,7 @@ language: python
cache: cache:
directories: directories:
- .tox - .tox
- .pytest_cache
- tests/testdata - tests/testdata
python: python:
- "2.7" - "2.7"

View File

@@ -9,7 +9,8 @@ import csv
from os import SEEK_SET from os import SEEK_SET
import time import time
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from collections import OrderedDict from collections import defaultdict, OrderedDict
import functools
import numpy as np import numpy as np
@@ -18,7 +19,7 @@ if sys.version_info.major <= 2:
str3 = str str3 = str
from string import maketrans from string import maketrans
else: else:
str3 = lambda b: str(b, encoding='ascii') str3 = functools.partial(str, encoding='ascii')
maketrans = bytes.maketrans maketrans = bytes.maketrans
@@ -71,19 +72,20 @@ def MPTfile(file_or_path):
raise ValueError("Bad first line for EC-Lab file: '%s'" % magic) raise ValueError("Bad first line for EC-Lab file: '%s'" % magic)
# TODO use rb'string' here once Python 2 is no longer supported # TODO use rb'string' here once Python 2 is no longer supported
nb_headers_match = re.match(b'Nb header lines : (\\d+)\\s*$', next(mpt_file)) nb_headers_match = re.match(b'Nb header lines : (\\d+)\\s*$',
next(mpt_file))
nb_headers = int(nb_headers_match.group(1)) nb_headers = int(nb_headers_match.group(1))
if nb_headers < 3: if nb_headers < 3:
raise ValueError("Too few header lines: %d" % nb_headers) raise ValueError("Too few header lines: %d" % nb_headers)
## The 'magic number' line, the 'Nb headers' line and the column headers # The 'magic number' line, the 'Nb headers' line and the column headers
## make three lines. Every additional line is a comment line. # make three lines. Every additional line is a comment line.
comments = [next(mpt_file) for i in range(nb_headers - 3)] comments = [next(mpt_file) for i in range(nb_headers - 3)]
fieldnames = str3(next(mpt_file)).strip().split('\t') fieldnames = str3(next(mpt_file)).strip().split('\t')
record_type = np.dtype(list(map(fieldname_to_dtype, fieldnames))) record_type = np.dtype(list(map(fieldname_to_dtype, fieldnames)))
## Must be able to parse files where commas are used for decimal points # Must be able to parse files where commas are used for decimal points
converter_dict = dict(((i, comma_converter) converter_dict = dict(((i, comma_converter)
for i in range(len(fieldnames)))) for i in range(len(fieldnames))))
mpt_array = np.loadtxt(mpt_file, dtype=record_type, mpt_array = np.loadtxt(mpt_file, dtype=record_type,
@@ -113,8 +115,8 @@ def MPTfileCSV(file_or_path):
if nb_headers < 3: if nb_headers < 3:
raise ValueError("Too few header lines: %d" % nb_headers) raise ValueError("Too few header lines: %d" % nb_headers)
## The 'magic number' line, the 'Nb headers' line and the column headers # The 'magic number' line, the 'Nb headers' line and the column headers
## make three lines. Every additional line is a comment line. # make three lines. Every additional line is a comment line.
comments = [next(mpt_file) for i in range(nb_headers - 3)] comments = [next(mpt_file) for i in range(nb_headers - 3)]
mpt_csv = csv.DictReader(mpt_file, dialect='excel-tab') mpt_csv = csv.DictReader(mpt_file, dialect='excel-tab')
@@ -144,119 +146,105 @@ VMPmodule_hdr = np.dtype([('shortname', 'S10'),
('version', '<u4'), ('version', '<u4'),
('date', 'S8')]) ('date', 'S8')])
# Maps from colID to a tuple defining a numpy dtype
VMPdata_colID_dtype_map = {
4: ('time/s', '<f8'),
5: ('control/V/mA', '<f4'),
6: ('Ewe/V', '<f4'),
7: ('dQ/mA.h', '<f8'),
8: ('I/mA', '<f4'), # 8 is either I or <I> ??
9: ('Ece/V', '<f4'),
11: ('I/mA', '<f8'),
13: ('(Q-Qo)/mA.h', '<f8'),
19: ('control/V', '<f4'),
20: ('control/mA', '<f4'),
23: ('dQ/mA.h', '<f8'), # Same as 7?
24: ('cycle number', '<f8'),
32: ('freq/Hz', '<f4'),
33: ('|Ewe|/V', '<f4'),
34: ('|I|/A', '<f4'),
35: ('Phase(Z)/deg', '<f4'),
36: ('|Z|/Ohm', '<f4'),
37: ('Re(Z)/Ohm', '<f4'),
38: ('-Im(Z)/Ohm', '<f4'),
39: ('I Range', '<u2'),
70: ('P/W', '<f4'),
76: ('<I>/mA', '<f4'),
77: ('<Ewe>/V', '<f4'),
123: ('Energy charge/W.h', '<f8'),
124: ('Energy discharge/W.h', '<f8'),
125: ('Capacitance charge/µF', '<f8'),
126: ('Capacitance discharge/µF', '<f8'),
131: ('Ns', '<u2'),
169: ('Cs/µF', '<f4'),
172: ('Cp/µF', '<f4'),
434: ('(Q-Qo)/C', '<f4'),
435: ('dQ/C', '<f4'),
467: ('Q charge/discharge/mA.h', '<f8'),
468: ('half cycle', '<u4'),
473: ('THD Ewe/%', '<f4'),
474: ('THD I/%', '<f4'),
476: ('NSD Ewe/%', '<f4'),
477: ('NSD I/%', '<f4'),
479: ('NSR Ewe/%', '<f4'),
480: ('NSR I/%', '<f4'),
}
# These column IDs define flags which are all stored packed in a single byte
# The values in the map are (name, bitmask, dtype)
VMPdata_colID_flag_map = {
1: ('mode', 0x03, np.uint8),
2: ('ox/red', 0x04, np.bool_),
3: ('error', 0x08, np.bool_),
21: ('control changes', 0x10, np.bool_),
31: ('Ns changes', 0x20, np.bool_),
65: ('counter inc.', 0x80, np.bool_),
}
def VMPdata_dtype_from_colIDs(colIDs): def VMPdata_dtype_from_colIDs(colIDs):
dtype_dict = OrderedDict() """Get a numpy record type from a list of column ID numbers.
The binary layout of the data in the MPR file is described by the sequence
of column ID numbers in the file header. This function converts that
sequence into a numpy dtype which can then be used to load data from the
file with np.frombuffer().
Some column IDs refer to small values which are packed into a single byte.
The second return value is a dict describing the bit masks with which to
extract these columns from the flags byte.
"""
type_list = []
field_name_counts = defaultdict(int)
flags_dict = OrderedDict() flags_dict = OrderedDict()
flags2_dict = OrderedDict()
for colID in colIDs: for colID in colIDs:
if colID in (1, 2, 3, 21, 31, 65): if colID in VMPdata_colID_flag_map:
dtype_dict['flags'] = 'u1' # Some column IDs represent boolean flags or small integers
if colID == 1: # These are all packed into a single 'flags' byte whose position
flags_dict['mode'] = (np.uint8(0x03), np.uint8) # in the overall record is determined by the position of the first
elif colID == 2: # column ID of flag type. If there are several flags present,
flags_dict['ox/red'] = (np.uint8(0x04), np.bool_) # there is still only one 'flags' int
elif colID == 3: if 'flags' not in field_name_counts:
flags_dict['error'] = (np.uint8(0x08), np.bool_) type_list.append(('flags', 'u1'))
elif colID == 21: field_name_counts['flags'] = 1
flags_dict['control changes'] = (np.uint8(0x10), np.bool_) flag_name, flag_mask, flag_type = VMPdata_colID_flag_map[colID]
elif colID == 31: # TODO what happens if a flag colID has already been seen
flags_dict['Ns changes'] = (np.uint8(0x20), np.bool_) # i.e. if flag_name is already present in flags_dict?
elif colID == 65: # Does it create a second 'flags' byte in the record?
flags_dict['counter inc.'] = (np.uint8(0x80), np.bool_) flags_dict[flag_name] = (np.uint8(flag_mask), flag_type)
elif colID in VMPdata_colID_dtype_map:
field_name, field_type = VMPdata_colID_dtype_map[colID]
field_name_counts[field_name] += 1
count = field_name_counts[field_name]
if count > 1:
unique_field_name = '%s %d' % (field_name, count)
else: else:
raise NotImplementedError("flag %d not implemented" % colID) unique_field_name = field_name
elif colID == 4: type_list.append((unique_field_name, field_type))
dtype_dict['time/s'] = '<f8'
elif colID == 5:
dtype_dict['control/V/mA'] = '<f4'
# 6 is Ewe, 77 is <Ewe>, I don't see the difference
elif colID in (6, 77):
dtype_dict['Ewe/V'] = '<f4'
# Can't see any difference between 7 and 23
elif colID in (7, 23):
dtype_dict['dQ/mA.h'] = '<f8'
# 76 is <I>, 8 is either I or <I> ??
elif colID in (8, 76):
dtype_dict['I/mA'] = '<f4'
elif colID == 9:
dtype_dict['Ece/V'] = '<f4'
elif colID == 11:
dtype_dict['I/mA'] = '<f8'
elif colID == 13:
dtype_dict['(Q-Qo)/mA.h'] = '<f8'
elif colID == 16:
dtype_dict['Analog IN 1/V'] = '<f4'
elif colID == 19:
dtype_dict['control/V'] = '<f4'
elif colID == 20:
dtype_dict['control/mA'] = '<f4'
elif colID == 24:
dtype_dict['cycle number'] = '<f8'
elif colID == 32:
dtype_dict['freq/Hz'] = '<f4'
elif colID == 33:
dtype_dict['|Ewe|/V'] = '<f4'
elif colID == 34:
dtype_dict['|I|/A'] = '<f4'
elif colID == 35:
dtype_dict['Phase(Z)/deg'] = '<f4'
elif colID == 36:
dtype_dict['|Z|/Ohm'] = '<f4'
elif colID == 37:
dtype_dict['Re(Z)/Ohm'] = '<f4'
elif colID == 38:
dtype_dict['-Im(Z)/Ohm'] = '<f4'
elif colID == 39:
dtype_dict['I Range'] = '<u2'
elif colID == 70:
dtype_dict['P/W'] = '<f4'
elif colID == 74:
dtype_dict['Energy/W.h'] = '<f8'
elif colID == 78:
dtype_dict['Cs-2/µf-2'] = '<f4'
elif colID == 123:
dtype_dict['Energy charge/W.h'] = '<f8'
elif colID == 124:
dtype_dict['Energy discharge/W.h'] = '<f8'
elif colID == 125:
dtype_dict['Capacitance charge/µF'] = '<f8'
elif colID == 126:
dtype_dict['Capacitance discharge/µF'] = '<f8'
elif colID == 131:
dtype_dict['Ns'] = '<u2'
elif colID == 169:
dtype_dict['Cs/µF'] = '<f4'
elif colID == 172:
dtype_dict['Cp/µF'] = '<f4'
elif colID == 173:
dtype_dict['Cp-2/µF-2'] = '<f4'
elif colID == 434:
dtype_dict['(Q-Qo)/C'] = '<f4'
elif colID == 435:
dtype_dict['dQ/C'] = '<f4'
elif colID == 467:
dtype_dict['Q charge/discharge/mA.h'] = '<f8'
elif colID == 468:
dtype_dict['half cycle'] = '<u4'
elif colID == 469:
dtype_dict['z cycle'] = '<u4'
elif colID == 473:
dtype_dict['THD Ewe/%'] = '<f4'
elif colID == 476:
dtype_dict['NSD Ewe/%'] = '<f4'
elif colID == 479:
dtype_dict['NSR Ewe/%'] = '<f4'
elif colID == 474:
dtype_dict['THD I/%'] = '<f4'
elif colID == 477:
dtype_dict['NSD I/%'] = '<f4'
elif colID == 480:
dtype_dict['NSR I/%'] = '<f4'
else: else:
print(dtype_dict)
raise NotImplementedError("column type %d not implemented" % colID) raise NotImplementedError("column type %d not implemented" % colID)
return np.dtype(list(dtype_dict.items())), flags_dict, flags2_dict return np.dtype(type_list), flags_dict
def read_VMP_modules(fileobj, read_module_data=True): def read_VMP_modules(fileobj, read_module_data=True):
@@ -270,7 +258,8 @@ def read_VMP_modules(fileobj, read_module_data=True):
if len(module_magic) == 0: # end of file if len(module_magic) == 0: # end of file
break break
elif module_magic != b'MODULE': elif module_magic != b'MODULE':
raise ValueError("Found %r, expecting start of new VMP MODULE" % module_magic) raise ValueError("Found %r, expecting start of new VMP MODULE"
% module_magic)
hdr_bytes = fileobj.read(VMPmodule_hdr.itemsize) hdr_bytes = fileobj.read(VMPmodule_hdr.itemsize)
if len(hdr_bytes) < VMPmodule_hdr.itemsize: if len(hdr_bytes) < VMPmodule_hdr.itemsize:
@@ -294,6 +283,9 @@ def read_VMP_modules(fileobj, read_module_data=True):
fileobj.seek(hdr_dict['offset'] + hdr_dict['length'], SEEK_SET) fileobj.seek(hdr_dict['offset'] + hdr_dict['length'], SEEK_SET)
MPR_MAGIC = b'BIO-LOGIC MODULAR FILE\x1a'.ljust(48) + b'\x00\x00\x00\x00'
class MPRfile: class MPRfile:
"""Bio-Logic .mpr file """Bio-Logic .mpr file
@@ -311,20 +303,20 @@ class MPRfile:
""" """
def __init__(self, file_or_path): def __init__(self, file_or_path):
self.loop_index = None
if isinstance(file_or_path, str): if isinstance(file_or_path, str):
mpr_file = open(file_or_path, 'rb') mpr_file = open(file_or_path, 'rb')
else: else:
mpr_file = file_or_path mpr_file = file_or_path
magic = mpr_file.read(len(MPR_MAGIC))
mpr_magic = b'BIO-LOGIC MODULAR FILE\x1a \x00\x00\x00\x00' if magic != MPR_MAGIC:
magic = mpr_file.read(len(mpr_magic))
if magic != mpr_magic:
raise ValueError('Invalid magic for .mpr file: %s' % magic) raise ValueError('Invalid magic for .mpr file: %s' % magic)
modules = list(read_VMP_modules(mpr_file)) modules = list(read_VMP_modules(mpr_file))
self.modules = modules self.modules = modules
settings_mod, = (m for m in modules if m['shortname'] == b'VMP Set ') settings_mod, = (m for m in modules if m['shortname'] == b'VMP Set ')
data_module, = (m for m in modules if m['shortname'] == b'VMP data ') data_module, = (m for m in modules if m['shortname'] == b'VMP data ')
maybe_loop_module = [m for m in modules if m['shortname'] == b'VMP loop ']
maybe_log_module = [m for m in modules if m['shortname'] == b'VMP LOG '] maybe_log_module = [m for m in modules if m['shortname'] == b'VMP LOG ']
n_data_points = np.frombuffer(data_module['data'][:4], dtype='<u4') n_data_points = np.frombuffer(data_module['data'][:4], dtype='<u4')
@@ -338,7 +330,7 @@ class MPRfile:
elif data_module['version'] == 2: elif data_module['version'] == 2:
column_types = np.frombuffer(data_module['data'][5:], dtype='<u2', column_types = np.frombuffer(data_module['data'][5:], dtype='<u2',
count=n_columns) count=n_columns)
## There is 405 bytes of data before the main array starts # There is 405 bytes of data before the main array starts
remaining_headers = data_module['data'][5 + 2 * n_columns:405] remaining_headers = data_module['data'][5 + 2 * n_columns:405]
main_data = data_module['data'][405:] main_data = data_module['data'][405:]
else: else:
@@ -350,12 +342,12 @@ class MPRfile:
else: else:
assert(not any(remaining_headers)) assert(not any(remaining_headers))
self.dtype, self.flags_dict, self.flags2_dict = VMPdata_dtype_from_colIDs(column_types) self.dtype, self.flags_dict = VMPdata_dtype_from_colIDs(column_types)
self.data = np.frombuffer(main_data, dtype=self.dtype) self.data = np.frombuffer(main_data, dtype=self.dtype)
assert(self.data.shape[0] == n_data_points) assert(self.data.shape[0] == n_data_points)
## No idea what these 'column types' mean or even if they are actually # No idea what these 'column types' mean or even if they are actually
## column types at all # column types at all
self.version = int(data_module['version']) self.version = int(data_module['version'])
self.cols = column_types self.cols = column_types
self.npts = n_data_points self.npts = n_data_points
@@ -366,6 +358,16 @@ class MPRfile:
tm = time.strptime(str3(settings_mod['date']), '%m-%d-%y') tm = time.strptime(str3(settings_mod['date']), '%m-%d-%y')
self.startdate = date(tm.tm_year, tm.tm_mon, tm.tm_mday) self.startdate = date(tm.tm_year, tm.tm_mon, tm.tm_mday)
if maybe_loop_module:
loop_module, = maybe_loop_module
if loop_module['version'] == 0:
self.loop_index = np.fromstring(loop_module['data'][4:],
dtype='<u4')
self.loop_index = np.trim_zeros(self.loop_index, 'b')
else:
raise ValueError("Unrecognised version for data module: %d" %
data_module['version'])
if maybe_log_module: if maybe_log_module:
log_module, = maybe_log_module log_module, = maybe_log_module
try: try:
@@ -374,9 +376,9 @@ class MPRfile:
tm = time.strptime(str3(log_module['date']), '%m-%d-%y') tm = time.strptime(str3(log_module['date']), '%m-%d-%y')
self.enddate = date(tm.tm_year, tm.tm_mon, tm.tm_mday) self.enddate = date(tm.tm_year, tm.tm_mon, tm.tm_mday)
## There is a timestamp at either 465 or 469 bytes # There is a timestamp at either 465 or 469 bytes
## I can't find any reason why it is one or the other in any # I can't find any reason why it is one or the other in any
## given file # given file
ole_timestamp1 = np.frombuffer(log_module['data'][465:], ole_timestamp1 = np.frombuffer(log_module['data'][465:],
dtype='<f8', count=1) dtype='<f8', count=1)
ole_timestamp2 = np.frombuffer(log_module['data'][469:], ole_timestamp2 = np.frombuffer(log_module['data'][469:],
@@ -402,17 +404,14 @@ class MPRfile:
ole_timedelta = timedelta(days=ole_timestamp[0]) ole_timedelta = timedelta(days=ole_timestamp[0])
self.timestamp = ole_base + ole_timedelta self.timestamp = ole_base + ole_timedelta
if self.startdate != self.timestamp.date(): if self.startdate != self.timestamp.date():
raise ValueError("""Date mismatch: raise ValueError("Date mismatch:\n"
Start date: %s + " Start date: %s\n" % self.startdate
End date: %s + " End date: %s\n" % self.enddate
Timestamp: %s""" % (self.startdate, self.enddate, self.timestamp)) + " Timestamp: %s\n" % self.timestamp)
def get_flag(self, flagname): def get_flag(self, flagname):
if flagname in self.flags_dict: if flagname in self.flags_dict:
mask, dtype = self.flags_dict[flagname] mask, dtype = self.flags_dict[flagname]
return np.array(self.data['flags'] & mask, dtype=dtype) return np.array(self.data['flags'] & mask, dtype=dtype)
elif flagname in self.flags2_dict:
mask, dtype = self.flags2_dict[flagname]
return np.array(self.data['flags2'] & mask, dtype=dtype)
else: else:
raise AttributeError("Flag '%s' not present" % flagname) raise AttributeError("Flag '%s' not present" % flagname)

View File

@@ -1 +1,3 @@
from .BioLogic import MPTfile, MPRfile from .BioLogic import MPRfile, MPTfile
__all__ = ['MPRfile', 'MPTfile']

View File

@@ -7,8 +7,8 @@ import csv
import argparse import argparse
## The following scripts are adapted from the result of running # The following scripts are adapted from the result of running
## $ mdb-schema <result.res> oracle # $ mdb-schema <result.res> oracle
mdb_tables = ["Version_Table", "Global_Table", "Resume_Table", mdb_tables = ["Version_Table", "Global_Table", "Resume_Table",
"Channel_Normal_Table", "Channel_Statistic_Table", "Channel_Normal_Table", "Channel_Statistic_Table",
@@ -126,7 +126,8 @@ CREATE TABLE Channel_Statistic_Table
-- Version 1.14 ends here, version 5.23 continues -- Version 1.14 ends here, version 5.23 continues
Charge_Time REAL DEFAULT NULL, Charge_Time REAL DEFAULT NULL,
Discharge_Time REAL DEFAULT NULL, Discharge_Time REAL DEFAULT NULL,
FOREIGN KEY (Test_ID, Data_Point) REFERENCES Channel_Normal_Table (Test_ID, Data_Point) FOREIGN KEY (Test_ID, Data_Point)
REFERENCES Channel_Normal_Table (Test_ID, Data_Point)
); """, ); """,
"Auxiliary_Table": """ "Auxiliary_Table": """
CREATE TABLE Auxiliary_Table CREATE TABLE Auxiliary_Table
@@ -137,7 +138,8 @@ CREATE TABLE Auxiliary_Table
Data_Type INTEGER, Data_Type INTEGER,
X REAL, X REAL,
"dX/dt" REAL, "dX/dt" REAL,
FOREIGN KEY (Test_ID, Data_Point) REFERENCES Channel_Normal_Table (Test_ID, Data_Point) FOREIGN KEY (Test_ID, Data_Point)
REFERENCES Channel_Normal_Table (Test_ID, Data_Point)
); """, ); """,
"Event_Table": """ "Event_Table": """
CREATE TABLE Event_Table CREATE TABLE Event_Table
@@ -220,9 +222,10 @@ CREATE TABLE Smart_Battery_Data_Table
ChargingCurrent REAL DEFAULT NULL, ChargingCurrent REAL DEFAULT NULL,
ChargingVoltage REAL DEFAULT NULL, ChargingVoltage REAL DEFAULT NULL,
ManufacturerData REAL DEFAULT NULL, ManufacturerData REAL DEFAULT NULL,
FOREIGN KEY (Test_ID, Data_Point) REFERENCES Channel_Normal_Table (Test_ID, Data_Point) FOREIGN KEY (Test_ID, Data_Point)
REFERENCES Channel_Normal_Table (Test_ID, Data_Point)
); """, ); """,
## The following tables are not present in version 1.14 # The following tables are not present in version 1.14
'MCell_Aci_Data_Table': """ 'MCell_Aci_Data_Table': """
CREATE TABLE MCell_Aci_Data_Table CREATE TABLE MCell_Aci_Data_Table
( (
@@ -233,7 +236,8 @@ CREATE TABLE MCell_Aci_Data_Table
Phase_Shift REAL, Phase_Shift REAL,
Voltage REAL, Voltage REAL,
Current REAL, Current REAL,
FOREIGN KEY (Test_ID, Data_Point) REFERENCES Channel_Normal_Table (Test_ID, Data_Point) FOREIGN KEY (Test_ID, Data_Point)
REFERENCES Channel_Normal_Table (Test_ID, Data_Point)
);""", );""",
'Aux_Global_Data_Table': """ 'Aux_Global_Data_Table': """
CREATE TABLE Aux_Global_Data_Table CREATE TABLE Aux_Global_Data_Table
@@ -288,7 +292,8 @@ CREATE TABLE Smart_Battery_Clock_Stretch_Table
VCELL3 INTEGER, VCELL3 INTEGER,
VCELL2 INTEGER, VCELL2 INTEGER,
VCELL1 INTEGER, VCELL1 INTEGER,
FOREIGN KEY (Test_ID, Data_Point) REFERENCES Channel_Normal_Table (Test_ID, Data_Point) FOREIGN KEY (Test_ID, Data_Point)
REFERENCES Channel_Normal_Table (Test_ID, Data_Point)
);"""} );"""}
mdb_create_indices = { mdb_create_indices = {
@@ -306,11 +311,14 @@ CREATE TEMPORARY TABLE capacity_helper(
Discharge_Capacity REAL NOT NULL, Discharge_Capacity REAL NOT NULL,
Charge_Energy REAL NOT NULL, Charge_Energy REAL NOT NULL,
Discharge_Energy REAL NOT NULL, Discharge_Energy REAL NOT NULL,
FOREIGN KEY (Test_ID, Cycle_Index) REFERENCES Channel_Normal_Table (Test_ID, Cycle_Index) FOREIGN KEY (Test_ID, Cycle_Index)
REFERENCES Channel_Normal_Table (Test_ID, Cycle_Index)
); );
INSERT INTO capacity_helper INSERT INTO capacity_helper
SELECT Test_ID, Cycle_Index, max(Charge_Capacity), max(Discharge_Capacity), max(Charge_Energy), max(Discharge_Energy) SELECT Test_ID, Cycle_Index,
max(Charge_Capacity), max(Discharge_Capacity),
max(Charge_Energy), max(Discharge_Energy)
FROM Channel_Normal_Table FROM Channel_Normal_Table
GROUP BY Test_ID, Cycle_Index; GROUP BY Test_ID, Cycle_Index;
@@ -328,11 +336,14 @@ CREATE TABLE Capacity_Sum_Table(
Discharge_Capacity_Sum REAL NOT NULL, Discharge_Capacity_Sum REAL NOT NULL,
Charge_Energy_Sum REAL NOT NULL, Charge_Energy_Sum REAL NOT NULL,
Discharge_Energy_Sum REAL NOT NULL, Discharge_Energy_Sum REAL NOT NULL,
FOREIGN KEY (Test_ID, Cycle_Index) REFERENCES Channel_Normal_Table (Test_ID, Cycle_Index) FOREIGN KEY (Test_ID, Cycle_Index)
REFERENCES Channel_Normal_Table (Test_ID, Cycle_Index)
); );
INSERT INTO Capacity_Sum_Table INSERT INTO Capacity_Sum_Table
SELECT a.Test_ID, a.Cycle_Index, total(b.Charge_Capacity), total(b.Discharge_Capacity), total(b.Charge_Energy), total(b.Discharge_Energy) SELECT a.Test_ID, a.Cycle_Index,
total(b.Charge_Capacity), total(b.Discharge_Capacity),
total(b.Charge_Energy), total(b.Discharge_Energy)
FROM capacity_helper AS a LEFT JOIN capacity_helper AS b FROM capacity_helper AS a LEFT JOIN capacity_helper AS b
ON (a.Test_ID = b.Test_ID AND a.Cycle_Index > b.Cycle_Index) ON (a.Test_ID = b.Test_ID AND a.Cycle_Index > b.Cycle_Index)
GROUP BY a.Test_ID, a.Cycle_Index; GROUP BY a.Test_ID, a.Cycle_Index;
@@ -342,30 +353,49 @@ DROP TABLE capacity_helper;
CREATE VIEW IF NOT EXISTS Capacity_View CREATE VIEW IF NOT EXISTS Capacity_View
AS SELECT Test_ID, Data_Point, Test_Time, Step_Time, DateTime, AS SELECT Test_ID, Data_Point, Test_Time, Step_Time, DateTime,
Step_Index, Cycle_Index, Current, Voltage, "dV/dt", Step_Index, Cycle_Index, Current, Voltage, "dV/dt",
Discharge_Capacity + Discharge_Capacity_Sum - Charge_Capacity - Charge_Capacity_Sum AS Net_Capacity, ( (Discharge_Capacity + Discharge_Capacity_Sum)
Discharge_Capacity + Discharge_Capacity_Sum + Charge_Capacity + Charge_Capacity_Sum AS Gross_Capacity, - (Charge_Capacity + Charge_Capacity_Sum) ) AS Net_Capacity,
Discharge_Energy + Discharge_Energy_Sum - Charge_Energy - Charge_Energy_Sum AS Net_Energy, ( (Discharge_Capacity + Discharge_Capacity_Sum)
Discharge_Energy + Discharge_Energy_Sum + Charge_Energy + Charge_Energy_Sum AS Gross_Energy + (Charge_Capacity + Charge_Capacity_Sum) ) AS Gross_Capacity,
( (Discharge_Energy + Discharge_Energy_Sum)
- (Charge_Energy + Charge_Energy_Sum) ) AS Net_Energy,
( (Discharge_Energy + Discharge_Energy_Sum)
+ (Charge_Energy + Charge_Energy_Sum) ) AS Gross_Energy
FROM Channel_Normal_Table NATURAL JOIN Capacity_Sum_Table; FROM Channel_Normal_Table NATURAL JOIN Capacity_Sum_Table;
""" """
def mdb_get_data_text(s3db, filename, table): def mdb_get_data_text(s3db, filename, table):
print("Reading %s..." % table) print("Reading %s..." % table)
insert_pattern = re.compile(
r'INSERT INTO "\w+" \([^)]+?\) VALUES \(("[^"]*"|[^")])+?\);\n',
re.IGNORECASE
)
# TODO after dropping Python 2 support - use Popen as contextmanager
try: try:
mdb_sql = sp.Popen(['mdb-export', '-I', 'postgres', filename, table], mdb_sql = sp.Popen(['mdb-export', '-I', 'postgres', filename, table],
bufsize=-1, stdin=None, stdout=sp.PIPE, bufsize=-1, stdin=None, stdout=sp.PIPE,
universal_newlines=True) universal_newlines=True)
except OSError as e:
if e.errno == 2:
raise RuntimeError('Could not locate the `mdb-export` executable. '
'Check that mdbtools is properly installed.')
else:
raise
try:
# Initialize values to avoid NameError in except clause
mdb_output = ''
insert_match = None
mdb_output = mdb_sql.stdout.read() mdb_output = mdb_sql.stdout.read()
while len(mdb_output) > 0: while len(mdb_output) > 0:
insert_match = re.match(r'INSERT INTO "\w+" \([^)]+?\) VALUES \(("[^"]*"|[^")])+?\);\n', insert_match = insert_pattern.match(mdb_output)
mdb_output, re.IGNORECASE)
s3db.execute(insert_match.group()) s3db.execute(insert_match.group())
mdb_output = mdb_output[insert_match.end():] mdb_output = mdb_output[insert_match.end():]
s3db.commit() s3db.commit()
except: except BaseException:
print("Error while importing %s" % table) print("Error while importing %s" % table)
print("Remaining mdb-export output:", mdb_output) if mdb_output:
print("Remaining mdb-export output:", mdb_output)
if insert_match: if insert_match:
print("insert_re match:", insert_match) print("insert_re match:", insert_match)
raise raise
@@ -375,17 +405,28 @@ def mdb_get_data_text(s3db, filename, table):
def mdb_get_data_numeric(s3db, filename, table): def mdb_get_data_numeric(s3db, filename, table):
print("Reading %s..." % table) print("Reading %s..." % table)
# TODO after dropping Python 2 support - use Popen as contextmanager
try: try:
mdb_sql = sp.Popen(['mdb-export', filename, table], mdb_sql = sp.Popen(['mdb-export', filename, table],
bufsize=-1, stdin=None, stdout=sp.PIPE, bufsize=-1, stdin=None, stdout=sp.PIPE,
universal_newlines=True) universal_newlines=True)
except OSError as e:
if e.errno == 2:
raise RuntimeError('Could not locate the `mdb-export` executable. '
'Check that mdbtools is properly installed.')
else:
raise
try:
mdb_csv = csv.reader(mdb_sql.stdout) mdb_csv = csv.reader(mdb_sql.stdout)
mdb_headers = next(mdb_csv) mdb_headers = next(mdb_csv)
quoted_headers = ['"%s"' % h for h in mdb_headers] quoted_headers = ['"%s"' % h for h in mdb_headers]
joined_headers = ', '.join(quoted_headers) joined_headers = ', '.join(quoted_headers)
joined_placemarks = ', '.join(['?' for h in mdb_headers]) joined_placemarks = ', '.join(['?' for h in mdb_headers])
insert_stmt = 'INSERT INTO "{0}" ({1}) VALUES ({2});'.format(table, insert_stmt = 'INSERT INTO "{0}" ({1}) VALUES ({2});'.format(
joined_headers, joined_placemarks) table,
joined_headers,
joined_placemarks,
)
s3db.executemany(insert_stmt, mdb_csv) s3db.executemany(insert_stmt, mdb_csv)
s3db.commit() s3db.commit()
finally: finally:
@@ -408,7 +449,6 @@ def convert_arbin_to_sqlite(input_file, output_file):
""" """
s3db = sqlite3.connect(output_file) s3db = sqlite3.connect(output_file)
for table in reversed(mdb_tables + mdb_5_23_tables): for table in reversed(mdb_tables + mdb_5_23_tables):
s3db.execute('DROP TABLE IF EXISTS "%s";' % table) s3db.execute('DROP TABLE IF EXISTS "%s";' % table)
@@ -419,7 +459,9 @@ def convert_arbin_to_sqlite(input_file, output_file):
print("Creating indices for %s..." % table) print("Creating indices for %s..." % table)
s3db.executescript(mdb_create_indices[table]) s3db.executescript(mdb_create_indices[table])
if (s3db.execute("SELECT Version_Schema_Field FROM Version_Table;").fetchone()[0] == "Results File 5.23"): csr = s3db.execute("SELECT Version_Schema_Field FROM Version_Table;")
version_text, = csr.fetchone()
if (version_text == "Results File 5.23"):
for table in mdb_5_23_tables: for table in mdb_5_23_tables:
s3db.executescript(mdb_create_scripts[table]) s3db.executescript(mdb_create_scripts[table])
mdb_get_data(input_file, table) mdb_get_data(input_file, table)
@@ -434,7 +476,9 @@ def convert_arbin_to_sqlite(input_file, output_file):
def main(argv=None): def main(argv=None):
parser = argparse.ArgumentParser(description="Convert Arbin .res files to sqlite3 databases using mdb-export") parser = argparse.ArgumentParser(
description="Convert Arbin .res files to sqlite3 databases using mdb-export",
)
parser.add_argument('input_file', type=str) # need file name to pass to sp.Popen parser.add_argument('input_file', type=str) # need file name to pass to sp.Popen
parser.add_argument('output_file', type=str) # need file name to pass to sqlite3.connect parser.add_argument('output_file', type=str) # need file name to pass to sqlite3.connect

View File

@@ -9,11 +9,11 @@ with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f:
setup( setup(
name='galvani', name='galvani',
version='0.0.2', version='0.1.0',
description='Open and process battery charger log data files', description='Open and process battery charger log data files',
long_description=readme, long_description=readme,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
url='https://github.com/chatcannon/galvani', url='https://github.com/echemdata/galvani',
author='Chris Kerr', author='Chris Kerr',
author_email='chris.kerr@mykolab.ch', author_email='chris.kerr@mykolab.ch',
license='GPLv3+', license='GPLv3+',
@@ -25,9 +25,11 @@ setup(
'Natural Language :: English', 'Natural Language :: English',
], ],
packages=['galvani'], packages=['galvani'],
entry_points={'console_scripts': [ entry_points={
'console_scripts': [
'res2sqlite = galvani.res2sqlite:main', 'res2sqlite = galvani.res2sqlite:main',
]}, ],
},
install_requires=['numpy'], install_requires=['numpy'],
tests_require=['pytest'], tests_require=['pytest'],
) )

11
tests/conftest.py Normal file
View File

@@ -0,0 +1,11 @@
"""Helpers for pytest tests."""
import os
import pytest
@pytest.fixture(scope='session')
def testdata_dir():
"""Path to the testdata directory."""
return os.path.join(os.path.dirname(__file__), 'testdata')

56
tests/test_Arbin.py Normal file
View File

@@ -0,0 +1,56 @@
"""Tests for loading Arbin .res files."""
import os
import sqlite3
import subprocess
import pytest
from galvani import res2sqlite
# TODO - change to subprocess.DEVNULL when python 2 support is removed
have_mdbtools = (subprocess.call(['which', 'mdb-export'], stdout=None) == 0)
def test_res2sqlite_help():
"""Test running `res2sqlite --help`.
This should work even when mdbtools is not installed.
"""
help_output = subprocess.check_output(['res2sqlite', '--help'])
assert b'Convert Arbin .res files to sqlite3 databases' in help_output
@pytest.mark.skipif(have_mdbtools, reason='This tests the failure when mdbtools is not installed')
def test_convert_Arbin_no_mdbtools(testdata_dir, tmpdir):
"""Checks that the conversion fails with an appropriate error message."""
res_file = os.path.join(testdata_dir, 'arbin1.res')
sqlite_file = os.path.join(str(tmpdir), 'arbin1.s3db')
with pytest.raises(RuntimeError, match="Could not locate the `mdb-export` executable."):
res2sqlite.convert_arbin_to_sqlite(res_file, sqlite_file)
@pytest.mark.skipif(not have_mdbtools, reason='Reading the Arbin file requires MDBTools')
@pytest.mark.parametrize('basename', ['arbin1'])
def test_convert_Arbin_to_sqlite_function(testdata_dir, tmpdir, basename):
"""Convert an Arbin file to SQLite using the functional interface."""
res_file = os.path.join(testdata_dir, basename + '.res')
sqlite_file = os.path.join(str(tmpdir), basename + '.s3db')
res2sqlite.convert_arbin_to_sqlite(res_file, sqlite_file)
assert os.path.isfile(sqlite_file)
with sqlite3.connect(sqlite_file) as conn:
csr = conn.execute('SELECT * FROM Channel_Normal_Table;')
csr.fetchone()
@pytest.mark.skipif(not have_mdbtools, reason='Reading the Arbin file requires MDBTools')
def test_convert_cmdline(testdata_dir, tmpdir):
"""Checks that the conversion fails with an appropriate error message."""
res_file = os.path.join(testdata_dir, 'arbin1.res')
sqlite_file = os.path.join(str(tmpdir), 'arbin1.s3db')
subprocess.check_call(['res2sqlite', res_file, sqlite_file])
assert os.path.isfile(sqlite_file)
with sqlite3.connect(sqlite_file) as conn:
csr = conn.execute('SELECT * FROM Channel_Normal_Table;')
csr.fetchone()

View File

@@ -8,13 +8,11 @@ import numpy as np
from numpy.testing import assert_array_almost_equal, assert_array_equal from numpy.testing import assert_array_almost_equal, assert_array_equal
import pytest import pytest
from galvani import MPTfile, MPRfile from galvani import BioLogic, MPTfile, MPRfile
from galvani.BioLogic import MPTfileCSV, str3 # not exported from galvani.BioLogic import MPTfileCSV, str3 # not exported
testdata_dir = os.path.join(os.path.dirname(__file__), 'testdata')
def test_open_MPT(testdata_dir):
def test_open_MPT():
mpt1, comments = MPTfile(os.path.join(testdata_dir, 'bio_logic1.mpt')) mpt1, comments = MPTfile(os.path.join(testdata_dir, 'bio_logic1.mpt'))
assert comments == [] assert comments == []
assert mpt1.dtype.names == ( assert mpt1.dtype.names == (
@@ -24,12 +22,12 @@ def test_open_MPT():
) )
def test_open_MPT_fails_for_bad_file(): def test_open_MPT_fails_for_bad_file(testdata_dir):
with pytest.raises(ValueError, match='Bad first line'): with pytest.raises(ValueError, match='Bad first line'):
MPTfile(os.path.join(testdata_dir, 'bio_logic1.mpr')) MPTfile(os.path.join(testdata_dir, 'bio_logic1.mpr'))
def test_open_MPT_csv(): def test_open_MPT_csv(testdata_dir):
mpt1, comments = MPTfileCSV(os.path.join(testdata_dir, 'bio_logic1.mpt')) mpt1, comments = MPTfileCSV(os.path.join(testdata_dir, 'bio_logic1.mpt'))
assert comments == [] assert comments == []
assert mpt1.fieldnames == [ assert mpt1.fieldnames == [
@@ -39,11 +37,42 @@ def test_open_MPT_csv():
] ]
def test_open_MPT_csv_fails_for_bad_file(): def test_open_MPT_csv_fails_for_bad_file(testdata_dir):
with pytest.raises((ValueError, UnicodeDecodeError)): with pytest.raises((ValueError, UnicodeDecodeError)):
MPTfileCSV(os.path.join(testdata_dir, 'bio_logic1.mpr')) MPTfileCSV(os.path.join(testdata_dir, 'bio_logic1.mpr'))
def test_colID_map_uniqueness():
"""Check some uniqueness properties of the VMPdata_colID_xyz maps."""
field_colIDs = set(BioLogic.VMPdata_colID_dtype_map.keys())
flag_colIDs = set(BioLogic.VMPdata_colID_flag_map.keys())
field_names = [v[0] for v in BioLogic.VMPdata_colID_dtype_map.values()]
flag_names = [v[0] for v in BioLogic.VMPdata_colID_flag_map.values()]
assert not field_colIDs.intersection(flag_colIDs)
# 'I/mA' and 'dQ/mA.h' are duplicated
# assert len(set(field_names)) == len(field_names)
assert len(set(flag_names)) == len(flag_names)
assert not set(field_names).intersection(flag_names)
@pytest.mark.parametrize('colIDs, expected', [
([1, 2, 3], [('flags', 'u1')]),
([4, 6], [('time/s', '<f8'), ('Ewe/V', '<f4')]),
([1, 4, 21], [('flags', 'u1'), ('time/s', '<f8')]),
([4, 6, 4], [('time/s', '<f8'), ('Ewe/V', '<f4'), ('time/s 2', '<f8')]),
([4, 9999], NotImplementedError),
])
def test_colID_to_dtype(colIDs, expected):
"""Test converting column ID to numpy dtype."""
if isinstance(expected, type) and issubclass(expected, Exception):
with pytest.raises(expected):
BioLogic.VMPdata_dtype_from_colIDs(colIDs)
return
expected_dtype = np.dtype(expected)
dtype, flags_dict = BioLogic.VMPdata_dtype_from_colIDs(colIDs)
assert dtype == expected_dtype
@pytest.mark.parametrize('filename, startdate, enddate', [ @pytest.mark.parametrize('filename, startdate, enddate', [
('bio_logic1.mpr', '2011-10-29', '2011-10-31'), ('bio_logic1.mpr', '2011-10-29', '2011-10-31'),
('bio_logic2.mpr', '2012-09-27', '2012-09-27'), ('bio_logic2.mpr', '2012-09-27', '2012-09-27'),
@@ -55,7 +84,7 @@ def test_open_MPT_csv_fails_for_bad_file():
# C019P-0ppb-A_C01.mpr stores the date in a different format # C019P-0ppb-A_C01.mpr stores the date in a different format
('C019P-0ppb-A_C01.mpr', '2019-03-14', '2019-03-14'), ('C019P-0ppb-A_C01.mpr', '2019-03-14', '2019-03-14'),
]) ])
def test_MPR_dates(filename, startdate, enddate): def test_MPR_dates(testdata_dir, filename, startdate, enddate):
"""Check that the start and end dates in .mpr files are read correctly.""" """Check that the start and end dates in .mpr files are read correctly."""
mpr = MPRfile(os.path.join(testdata_dir, filename)) mpr = MPRfile(os.path.join(testdata_dir, filename))
assert mpr.startdate.strftime('%Y-%m-%d') == startdate assert mpr.startdate.strftime('%Y-%m-%d') == startdate
@@ -65,7 +94,7 @@ def test_MPR_dates(filename, startdate, enddate):
assert not hasattr(mpr, 'enddate') assert not hasattr(mpr, 'enddate')
def test_open_MPR_fails_for_bad_file(): def test_open_MPR_fails_for_bad_file(testdata_dir):
with pytest.raises(ValueError, match='Invalid magic for .mpr file'): with pytest.raises(ValueError, match='Invalid magic for .mpr file'):
MPRfile(os.path.join(testdata_dir, 'arbin1.res')) MPRfile(os.path.join(testdata_dir, 'arbin1.res'))
@@ -98,7 +127,7 @@ def assert_MPR_matches_MPT(mpr, mpt, comments):
assert_array_equal(mpr.get_flag("control changes"), mpt["control changes"]) assert_array_equal(mpr.get_flag("control changes"), mpt["control changes"])
if "Ns changes" in mpt.dtype.fields: if "Ns changes" in mpt.dtype.fields:
assert_array_equal(mpr.get_flag("Ns changes"), mpt["Ns changes"]) assert_array_equal(mpr.get_flag("Ns changes"), mpt["Ns changes"])
## Nothing uses the 0x40 bit of the flags # Nothing uses the 0x40 bit of the flags
assert_array_equal(mpr.get_flag("counter inc."), mpt["counter inc."]) assert_array_equal(mpr.get_flag("counter inc."), mpt["counter inc."])
assert_array_almost_equal(mpr.data["time/s"], assert_array_almost_equal(mpr.data["time/s"],
@@ -134,7 +163,7 @@ def assert_MPR_matches_MPT(mpr, mpt, comments):
'CV_C01', 'CV_C01',
'121_CA_455nm_6V_30min_C01', '121_CA_455nm_6V_30min_C01',
]) ])
def test_MPR_matches_MPT(basename): def test_MPR_matches_MPT(testdata_dir, basename):
"""Check the MPR parser against the MPT parser. """Check the MPR parser against the MPT parser.
Load a binary .mpr file and a text .mpt file which should contain Load a binary .mpr file and a text .mpt file which should contain
@@ -147,7 +176,7 @@ def test_MPR_matches_MPT(basename):
assert_MPR_matches_MPT(mpr, mpt, comments) assert_MPR_matches_MPT(mpr, mpt, comments)
def test_MPR5_matches_MPT5(): def test_MPR5_matches_MPT5(testdata_dir):
mpr = MPRfile(os.path.join(testdata_dir, 'bio_logic5.mpr')) mpr = MPRfile(os.path.join(testdata_dir, 'bio_logic5.mpr'))
mpt, comments = MPTfile((re.sub(b'\tXXX\t', b'\t0\t', line) for line in mpt, comments = MPTfile((re.sub(b'\tXXX\t', b'\t0\t', line) for line in
open(os.path.join(testdata_dir, 'bio_logic5.mpt'), open(os.path.join(testdata_dir, 'bio_logic5.mpt'),
@@ -155,7 +184,7 @@ def test_MPR5_matches_MPT5():
assert_MPR_matches_MPT(mpr, mpt, comments) assert_MPR_matches_MPT(mpr, mpt, comments)
def test_MPR6_matches_MPT6(): def test_MPR6_matches_MPT6(testdata_dir):
mpr = MPRfile(os.path.join(testdata_dir, 'bio_logic6.mpr')) mpr = MPRfile(os.path.join(testdata_dir, 'bio_logic6.mpr'))
mpt, comments = MPTfile(os.path.join(testdata_dir, 'bio_logic6.mpt')) mpt, comments = MPTfile(os.path.join(testdata_dir, 'bio_logic6.mpt'))
mpr.data = mpr.data[:958] # .mpt file is incomplete mpr.data = mpr.data[:958] # .mpt file is incomplete

12
tox.ini
View File

@@ -1,5 +1,13 @@
[tox] [tox]
envlist = py27,py35,py37 envlist = py27,py35,py37
[testenv] [testenv]
deps=pytest deps =
commands=pytest flake8
pytest
commands =
flake8
pytest
[flake8]
exclude = build,dist,*.egg-info,.cache,.git,.tox,__pycache__
max-line-length = 100