From 04ae55fc8f3e3c687224fd000d33feda94885769 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 30 Sep 2021 11:59:36 -0400 Subject: [PATCH 001/316] Fix documentation of the internal variable 'filebytes'. This variable contains the complete contents of the input annotation file, as a numpy array of pairs of bytes (shape=(N,2), dtype='uint8'). It is neither a str nor a bytes object. --- wfdb/io/annotation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/wfdb/io/annotation.py b/wfdb/io/annotation.py index 7ae619bb..2ef078f6 100644 --- a/wfdb/io/annotation.py +++ b/wfdb/io/annotation.py @@ -1748,8 +1748,8 @@ def load_byte_pairs(record_name, extension, pn_dir): Returns ------- - filebytes : str - The input filestream converted to bytes. + filebytes : ndarray + The input filestream converted to an Nx2 array of unsigned bytes. """ # local file @@ -1769,8 +1769,8 @@ def proc_ann_bytes(filebytes, sampto): Parameters ---------- - filebytes : str - The input filestream converted to bytes. + filebytes : ndarray + The input filestream converted to an Nx2 array of unsigned bytes. sampto : int The maximum sample number for annotations to be returned. @@ -1852,8 +1852,8 @@ def proc_core_fields(filebytes, bpi): Parameters ---------- - filebytes : str - The input filestream converted to bytes. + filebytes : ndarray + The input filestream converted to an Nx2 array of unsigned bytes. bpi : int The index to start the conversion. From 2dd48457f5736ed0841cc37e9886cc08634096a5 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Wed, 15 Sep 2021 17:08:47 -0400 Subject: [PATCH 002/316] rdann: handle multiple consecutive SKIPs. In WFDB-format annotation files, annotation timestamps are represented as an offset from the previous annotation. When this offset is less than 0 or greater than 1023, a SKIP pseudo-annotation is used; when the offset is greater than 2**31 - 1 or less than -2**31, multiple SKIPs must be used. Thus, proc_core_fields must be able to handle an arbitrary number of SKIPs in a row, preceding the actual annotation, and add all of the offsets together to obtain the final timestamp. --- wfdb/io/annotation.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/wfdb/io/annotation.py b/wfdb/io/annotation.py index 2ef078f6..db6b5b9f 100644 --- a/wfdb/io/annotation.py +++ b/wfdb/io/annotation.py @@ -1869,31 +1869,26 @@ def proc_core_fields(filebytes, bpi): The index to start the conversion. """ - label_store = filebytes[bpi, 1] >> 2 + sample_diff = 0 # The current byte pair will contain either the actual d_sample + annotation store value, # or 0 + SKIP. - - # Not a skip - it is the actual sample number + annotation type store value - if label_store != 59: - sample_diff = filebytes[bpi, 0] + 256 * (filebytes[bpi, 1] & 3) - bpi = bpi + 1 - # Skip. Note: Could there be another skip after the first? - else: + while filebytes[bpi, 1] >> 2 == 59: # 4 bytes storing dt - sample_diff = 65536 * filebytes[bpi + 1,0] + 16777216 * filebytes[bpi + 1,1] \ + skip_diff = 65536 * filebytes[bpi + 1,0] + 16777216 * filebytes[bpi + 1,1] \ + filebytes[bpi + 2,0] + 256 * filebytes[bpi + 2,1] # Data type is long integer (stored in two's complement). Range -2**31 to 2**31 - 1 - if sample_diff > 2147483647: - sample_diff = sample_diff - 4294967296 + if skip_diff > 2147483647: + skip_diff = skip_diff - 4294967296 - # After the 4 bytes, the next pair's samp is also added - sample_diff = sample_diff + filebytes[bpi + 3, 0] + 256 * (filebytes[bpi + 3, 1] & 3) + sample_diff += skip_diff + bpi = bpi + 3 - # The label is stored after the 4 bytes. Samples here should be 0. - label_store = filebytes[bpi + 3, 1] >> 2 - bpi = bpi + 4 + # Not a skip - it is the actual sample number + annotation type store value + label_store = filebytes[bpi, 1] >> 2 + sample_diff += filebytes[bpi, 0] + 256 * (filebytes[bpi, 1] & 3) + bpi = bpi + 1 return sample_diff, label_store, bpi From 94449b85fb145a5a6a1dff6b50d10c5401b17027 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 30 Sep 2021 12:08:47 -0400 Subject: [PATCH 003/316] rdann: compute timestamps as int, not a numpy integer. When reading an annotation file in WFDB format, the timestamp (sample number) must be computed by adding up the relative timestamp difference for each annotation. For long records, sample numbers can easily exceed 2**32. The input to proc_core_fields is a numpy array, so if we operate on the byte values with ordinary arithmetic operations, the result will be a numpy integer object with numpy's default precision (i.e., int32 on 32-bit architectures, int64 on 64-bit architectures.) Instead, calculate the result as a Python integer, to avoid architecture-dependent behavior and (possible) silent wrapping. (Furthermore, use left-shift operations instead of multiplying by constants that are hard to remember.) --- wfdb/io/annotation.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/wfdb/io/annotation.py b/wfdb/io/annotation.py index db6b5b9f..bc41e8da 100644 --- a/wfdb/io/annotation.py +++ b/wfdb/io/annotation.py @@ -1875,8 +1875,10 @@ def proc_core_fields(filebytes, bpi): # or 0 + SKIP. while filebytes[bpi, 1] >> 2 == 59: # 4 bytes storing dt - skip_diff = 65536 * filebytes[bpi + 1,0] + 16777216 * filebytes[bpi + 1,1] \ - + filebytes[bpi + 2,0] + 256 * filebytes[bpi + 2,1] + skip_diff = ((int(filebytes[bpi + 1, 0]) << 16) + + (int(filebytes[bpi + 1, 1]) << 24) + + (int(filebytes[bpi + 2, 0]) << 0) + + (int(filebytes[bpi + 2, 1]) << 8)) # Data type is long integer (stored in two's complement). Range -2**31 to 2**31 - 1 if skip_diff > 2147483647: @@ -1887,7 +1889,7 @@ def proc_core_fields(filebytes, bpi): # Not a skip - it is the actual sample number + annotation type store value label_store = filebytes[bpi, 1] >> 2 - sample_diff += filebytes[bpi, 0] + 256 * (filebytes[bpi, 1] & 3) + sample_diff += int(filebytes[bpi, 0] + 256 * (filebytes[bpi, 1] & 3)) bpi = bpi + 1 return sample_diff, label_store, bpi From c098e38b2945da6dce7fd6ea9e759bf4d7d94bfc Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 30 Sep 2021 12:26:23 -0400 Subject: [PATCH 004/316] rdann: store sample as an array of int64. For long records, annotation timestamps (sample numbers) can easily exceed the range of a numpy 'int' on 32-bit architectures. Therefore, store the 'sample' array as 'int64' instead. --- wfdb/io/annotation.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wfdb/io/annotation.py b/wfdb/io/annotation.py index bc41e8da..cf9c9da7 100644 --- a/wfdb/io/annotation.py +++ b/wfdb/io/annotation.py @@ -1653,8 +1653,11 @@ def rdann(record_name, extension, sampfrom=0, sampto=None, shift_samps=False, subtype, chan, num, aux_note) # Convert lists to numpy arrays dtype='int' - (sample, label_store, subtype, - chan, num) = lists_to_int_arrays(sample, label_store, subtype, chan, num) + (label_store, subtype, + chan, num) = lists_to_int_arrays(label_store, subtype, chan, num) + + # Convert sample numbers to a numpy array of 'int64' + sample = np.array(sample, dtype='int64') # Try to get fs from the header file if it is not contained in the # annotation file From 481978e7c7f51f9796d9d95dc0ca8a5d25708eda Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Wed, 15 Sep 2021 17:09:16 -0400 Subject: [PATCH 005/316] wrann: allow intervals larger than 2**31 - 1. If the gap between two consecutive annotation timestamps is greater than 2**31 - 1 ticks, it must be represented as two or more SKIP pseudo-annotations. Handle this correctly in field2bytes() (to actually generate the correct byte sequences) and in Annotation.check_field() (to permit the application to specify such a gap.) (Previously, if there was a gap of exactly 2**31 ticks, this would not be caught by check_field, and field2bytes would incorrectly generate a SKIP of -2**31 instead.) --- wfdb/io/annotation.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/wfdb/io/annotation.py b/wfdb/io/annotation.py index cf9c9da7..041083cd 100644 --- a/wfdb/io/annotation.py +++ b/wfdb/io/annotation.py @@ -466,8 +466,6 @@ def check_field(self, field): raise ValueError("The 'sample' field must only contain non-negative integers") if min(sampdiffs) < 0 : raise ValueError("The 'sample' field must contain monotonically increasing sample numbers") - if max(sampdiffs) > 2147483648: - raise ValueError('WFDB annotation files cannot store sample differences greater than 2**31') elif field == 'label_store': if min(item) < 1 or max(item) > 49: @@ -1370,19 +1368,19 @@ def field2bytes(field, value): # sample difference sd = value[0] - # Add SKIP element if value is too large for single byte - if sd>1023: - # 8 bytes in total: - # - [0, 59>>2] indicates SKIP - # - Next 4 gives sample difference - # - Final 2 give 0 and sym - data_bytes = [0, 236, (sd&16711680)>>16, (sd&4278190080)>>24, sd&255, (sd&65280)>>8, 0, 4*typecode] - # Just need samp and sym - else: - # - First byte stores low 8 bits of samp - # - Second byte stores high 2 bits of samp - # and sym - data_bytes = [sd & 255, ((sd & 768) >> 8) + 4*typecode] + data_bytes = [] + # Add SKIP elements if value is too large + while sd > 0x7fffffff: + data_bytes += [0, 59 << 2, 0xff, 0x7f, 0xff, 0xff] + sd -= 0x7fffffff + if sd > 1023: + data_bytes += [0, 59 << 2, + (sd >> 16) & 255, + (sd >> 24) & 255, + (sd >> 0) & 255, + (sd >> 8) & 255] + sd = 0 + data_bytes += [sd & 255, ((sd & 768) >> 8) + 4 * typecode] elif field == 'num': # First byte stores num From 0dab235621183c89174486517d84489e79018b93 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Wed, 29 Sep 2021 11:54:31 -0400 Subject: [PATCH 006/316] test_annotation: convert to a standard unittest.TestCase. Make the test_annotation class a subclass of unittest.TestCase, allowing it to use standard unit testing utility methods, as well as setup and teardown functions. (nosetests will run "test" class methods automatically even if they are not subclasses of TestCase, but unittest won't.) Rename the class to TestAnnotation for consistency. Make the module executable (invoke unittest.main()) so it can be invoked simply using 'python3 -m tests.test_annotation'. Ensure that temporary files created by the annotation tests will be correctly cleaned up by TestAnnotation.tearDownClass() rather than by the unrelated TestRecord.tearDownClass(). (Presumably this only happened to work previously because "test_record" comes alphabetically after "test_annotation".) --- tests/test_annotation.py | 20 +++++++++++++++++++- tests/test_record.py | 6 +++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/test_annotation.py b/tests/test_annotation.py index 32f39082..4fc33cc3 100644 --- a/tests/test_annotation.py +++ b/tests/test_annotation.py @@ -1,10 +1,13 @@ +import os import re +import unittest import numpy as np import wfdb -class test_annotation(): + +class TestAnnotation(unittest.TestCase): """ Testing read and write of WFDB annotations, including Physionet streaming. @@ -183,3 +186,18 @@ def test_3(self): assert (comp == [True] * 6) assert annotation.__eq__(pn_annotation) assert annotation.__eq__(write_annotation) + + @classmethod + def tearDownClass(cls): + writefiles = [ + '100.atr', + '1003.atr', + '12726.anI', + ] + for file in writefiles: + if os.path.isfile(file): + os.remove(file) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_record.py b/tests/test_record.py index 89fbc941..b71ffba1 100644 --- a/tests/test_record.py +++ b/tests/test_record.py @@ -521,9 +521,9 @@ def test_header_with_non_utf8(self): @classmethod def tearDownClass(cls): "Clean up written files" - writefiles = ['03700181.dat','03700181.hea','100.atr','100.dat', - '100.hea','1003.atr','100_3chan.dat','100_3chan.hea', - '12726.anI','a103l.hea','a103l.mat','s0010_re.dat', + writefiles = ['03700181.dat','03700181.hea','100.dat', + '100.hea','100_3chan.dat','100_3chan.hea', + 'a103l.hea','a103l.mat','s0010_re.dat', 's0010_re.hea','s0010_re.xyz','test01_00s.dat', 'test01_00s.hea','test01_00s_skewframe.hea', 'n8_evoked_raw_95_F1_R9.dat', 'n8_evoked_raw_95_F1_R9.hea'] From aad7b14d09998d8581ab0f5ee3aec0f3d2e3816d Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Wed, 29 Sep 2021 12:35:26 -0400 Subject: [PATCH 007/316] Add test cases for reading/writing huge skips. Check that we can both read and write an annotation file containing a relative offset of more than 2**31 - 1 ticks, which necessitates the use of multiple SKIP pseudo-annotations. --- sample-data/huge.qrs | Bin 0 -> 34 bytes tests/test_annotation.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 sample-data/huge.qrs diff --git a/sample-data/huge.qrs b/sample-data/huge.qrs new file mode 100644 index 0000000000000000000000000000000000000000..f48e8e03e317aeaf3b4ed21a7bb96f5d3c9ab2a3 GIT binary patch literal 34 YcmZR0^S}Q8e+DchcL>W91{MYe0FvPo*Z=?k literal 0 HcmV?d00001 diff --git a/tests/test_annotation.py b/tests/test_annotation.py index 4fc33cc3..c7d0f4d3 100644 --- a/tests/test_annotation.py +++ b/tests/test_annotation.py @@ -187,12 +187,28 @@ def test_3(self): assert annotation.__eq__(pn_annotation) assert annotation.__eq__(write_annotation) + def test_4(self): + """ + Read and write annotations with large time skips + + Annotation file created by: + echo "xxxxxxxxx 10000000000 N 0 0 0" | wrann -r huge -a qrs + """ + annotation = wfdb.rdann('sample-data/huge', 'qrs') + self.assertEqual(annotation.sample[0], 10000000000) + annotation.wrann() + + annotation1 = wfdb.rdann('sample-data/huge', 'qrs') + annotation2 = wfdb.rdann('huge', 'qrs') + self.assertEqual(annotation1, annotation2) + @classmethod def tearDownClass(cls): writefiles = [ '100.atr', '1003.atr', '12726.anI', + 'huge.qrs', ] for file in writefiles: if os.path.isfile(file): From bfa0a37fd13464586cb102354e6a6d74946a6886 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Mon, 1 Nov 2021 16:32:12 -0400 Subject: [PATCH 008/316] field2bytes: rearrange and add comments for clarity. --- wfdb/io/annotation.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/wfdb/io/annotation.py b/wfdb/io/annotation.py index 041083cd..b3d30e1d 100644 --- a/wfdb/io/annotation.py +++ b/wfdb/io/annotation.py @@ -1369,17 +1369,28 @@ def field2bytes(field, value): sd = value[0] data_bytes = [] - # Add SKIP elements if value is too large - while sd > 0x7fffffff: - data_bytes += [0, 59 << 2, 0xff, 0x7f, 0xff, 0xff] - sd -= 0x7fffffff - if sd > 1023: + + # Add SKIP element(s) if the sample difference is too large to + # be stored in the annotation type word. + # + # Each SKIP element consists of three words (6 bytes): + # - Bytes 0-1 contain the SKIP indicator (59 << 10) + # - Bytes 2-3 contain the high 16 bits of the sample difference + # - Bytes 4-5 contain the low 16 bits of the sample difference + # If the total difference exceeds 2**31 - 1, multiple skips must + # be used. + while sd > 1023: + n = min(sd, 0x7fffffff) data_bytes += [0, 59 << 2, - (sd >> 16) & 255, - (sd >> 24) & 255, - (sd >> 0) & 255, - (sd >> 8) & 255] - sd = 0 + (n >> 16) & 255, + (n >> 24) & 255, + (n >> 0) & 255, + (n >> 8) & 255] + sd -= n + + # Annotation type itself is stored as a single word: + # - bits 0 to 9 store the sample difference (0 to 1023) + # - bits 10 to 15 store the type code data_bytes += [sd & 255, ((sd & 768) >> 8) + 4 * typecode] elif field == 'num': From 72df6b70b52896ab72b0bedba83d97083aa70f2c Mon Sep 17 00:00:00 2001 From: Vivek Gopalakrishnan Date: Fri, 14 Jan 2022 15:52:33 -0500 Subject: [PATCH 009/316] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 32382ec9..e30da086 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ # List run-time dependencies here. These will be installed by pip when # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: - # https://packaging.python.org/en/latest/requirements.html + # https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/?highlight=requirements install_requires=[ 'matplotlib>=3.3.4', 'numpy>=1.10.1', From 0a2b75f6de199af4a99064d6a0fea9ec550d075a Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 1 Mar 2022 12:39:52 -0500 Subject: [PATCH 010/316] _np_dtype: eliminate "is True". Writing "if X is True" is almost always a mistake. --- wfdb/io/_signal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index a7f124fb..dbe7d046 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -1891,7 +1891,7 @@ def _np_dtype(bit_res, discrete): if bit_res <= np_res: break - if discrete is True: + if discrete: return 'int' + str(np_res) else: # No float8 dtype From e2ce76b163fb656a823f513338d28c4db2e7cd84 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 1 Mar 2022 12:41:13 -0500 Subject: [PATCH 011/316] check_read_inputs: eliminate "is True" and "is False". Writing "if X is True" is almost always a mistake. --- wfdb/io/record.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wfdb/io/record.py b/wfdb/io/record.py index 077c03b5..bc51cda8 100644 --- a/wfdb/io/record.py +++ b/wfdb/io/record.py @@ -409,12 +409,12 @@ def check_read_inputs(self, sampfrom, sampto, channels, physical, if return_res not in [64, 32, 16, 8]: raise ValueError("return_res must be one of the following: 64, 32, 16, 8") - if physical is True and return_res == 8: + if physical and return_res == 8: raise ValueError("return_res must be one of the following when physical is True: 64, 32, 16") # Cannot expand multiple samples/frame for multi-segment records if isinstance(self, MultiRecord): - if smooth_frames is False: + if not smooth_frames: raise ValueError('This package version cannot expand all samples when reading multi-segment records. Must enable frame smoothing.') From ebd9d40a3b61b049d4d03f3b736fdfc6c6eb50c1 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 1 Mar 2022 12:45:29 -0500 Subject: [PATCH 012/316] convert_dtype: eliminate "is True" and "is False". Writing "if X is True" is almost always a mistake. This change allows rdrecord to be called with "physical=1" ("physical=0" would work previously, but "physical=1" wouldn't) or with "smooth_frames=0" ("smooth_frames=1" would work previously, but "smooth_frames=0" wouldn't.) --- wfdb/io/_signal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index dbe7d046..868d3596 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -698,9 +698,9 @@ def convert_dtype(self, physical, return_res, smooth_frames): N/A """ - if physical is True: + if physical: return_dtype = 'float'+str(return_res) - if smooth_frames is True: + if smooth_frames: current_dtype = self.p_signal.dtype if current_dtype != return_dtype: self.p_signal = self.p_signal.astype(return_dtype, copy=False) @@ -715,7 +715,7 @@ def convert_dtype(self, physical, return_res, smooth_frames): self.p_signal[ch] = self.p_signal[ch].astype(return_dtype, copy=False) else: return_dtype = 'int'+str(return_res) - if smooth_frames is True: + if smooth_frames: current_dtype = self.d_signal.dtype if current_dtype != return_dtype: # Do not allow changing integer dtype to lower value due to over/underflow From 8f4fd87ef2eca3522f63059998f4f36ec9e8090a Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 8 Jul 2021 13:58:24 -0400 Subject: [PATCH 013/316] _rd_dat_signals: always honor the smooth_frames parameter. This function can return signal data in either of two formats: - "smooth" (a two-dimensional array, where x[t,s] is sample t of signal s) - "non-smooth" (a list of one-dimensional arrays, where x[s][t] is sample t of signal s) Previously, _rd_dat_signals would use "smooth" format if 'sum(samps_per_frame) == n_sig', regardless of 'smooth_frames'. This makes little sense, since the caller needs to know what type of return value to expect. Instead, the format should be determined solely by the 'smooth_frames' parameter. (In this case, the only caller of _rd_dat_signals is _rd_segment, and the only caller of _rd_segment is wfdb.io.record.rdrecord.) --- wfdb/io/_signal.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index 868d3596..f7a68b3a 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -1089,7 +1089,7 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, sampto : int The final sample number to be read from the signals. smooth_frames : bool - Whether to smooth channels with multiple samples/frame. + Whether to return the result as a two-dimensional array. no_file : bool, optional Used when using this function with just an array of signal data and no associated file to read the data from. @@ -1101,9 +1101,8 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, ------- signal : ndarray, list The signals read from the dat file(s). A 2d numpy array is - returned if the signals have uniform samples/frame or if - `smooth_frames` is True. Otherwise a list of 1d numpy arrays - is returned. + returned if `smooth_frames` is True. Otherwise a list of 1d + numpy arrays is returned. Notes ----- @@ -1212,7 +1211,7 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, # required for storing the final digital samples. # No extra samples/frame. Obtain original uniform numpy array - if tsamps_per_frame == n_sig: + if smooth_frames and tsamps_per_frame == n_sig: # Reshape into multiple channels signal = sig_data.reshape(-1, n_sig) # Skew the signal From 3e84119d37c66deceaa2359c1a294717ac99075b Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 8 Jul 2021 14:16:29 -0400 Subject: [PATCH 014/316] _rd_segment: always honor the smooth_frames parameter. This function can return signal data in either of two formats: - "smooth" (a two-dimensional array, where x[t,s] is sample t of signal s) - "non-smooth" (a list of one-dimensional arrays, where x[s][t] is sample t of signal s) Previously, _rd_segment would use "smooth" format if 'sum(samps_per_frame) == n_sig', regardless of 'smooth_frames'. This makes little sense, since the caller needs to know what type of return value to expect. Instead, the format should be determined solely by the 'smooth_frames' parameter. (In this case, the only caller of _rd_segment is wfdb.io.record.rdrecord.) --- wfdb/io/_signal.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index f7a68b3a..c660269b 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -900,7 +900,7 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, sampto : int The final sample number to be read from the signals. smooth_frames : bool - Whether to smooth channels with multiple samples/frame. + Whether to return the result as a two-dimensional array. ignore_skew : bool Used when reading records with at least one skewed signal. Specifies whether to apply the skew to align the signals in the @@ -922,9 +922,8 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, ------- signals : ndarray, list The signals read from the dat file(s). A 2d numpy array is - returned if the signals have uniform samples/frame or if - `smooth_frames` is True. Otherwise a list of 1d numpy arrays - is returned. + returned if `smooth_frames` is True. Otherwise a list of 1d + numpy arrays is returned. Notes ----- @@ -996,7 +995,7 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, # Signals with multiple samples/frame are smoothed, or all signals have 1 sample/frame. # Return uniform numpy array - if smooth_frames or sum(samps_per_frame) == n_sig: + if smooth_frames: # Figure out the largest required dtype for the segment to minimize memory usage max_dtype = _np_dtype(_fmt_res(fmt, max_res=True), discrete=True) # Allocate signal array. Minimize dtype From 1b9c90c3e001bcaf96cdad9ce03cd1ccd36fc303 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 8 Jul 2021 14:20:48 -0400 Subject: [PATCH 015/316] rdrecord: handle smooth_frames when maximum spf is 1. rdrecord can return signal data in any of four formats: - by resampling all signals to a uniform rate ("smoothing"), and setting d_signal to a two-dimensional array, where d_signal[t,s] is sample t of signal s - by resampling to a uniform rate, and setting p_signal to a two-dimensional array, where p_signal[t,s] is sample t of signal s converted into physical units - by setting e_d_signal to a list of one-dimensional arrays, where e_d_signal[s][t] is sample t of signal s - by setting e_p_signal to a list of one-dimensional arrays, where e_p_signal[s][t] is sample t of signal s converted into physical units If the selected signals contain multiple samples per frame, the behavior of rdrecord is consistent: - If smooth_frames is True, the selected signals are resampled to the frame rate and stored as d_signal or p_signal. - If smooth_frames is False, the selected signals are stored in their original form as e_d_signal or e_p_signal. However, if each of the selected signals contains only one sample per frame, rdrecord would previously behave inconsistently: - If all signals in the record contain only one sample per frame, and smooth_frames is False and physical is True, the selected signals would be stored as p_signal (not e_p_signal as the caller would expect.) - If all signals in the record contain only one sample per frame, and smooth_frames is False and physical is False, rdrecord would crash with a TypeError in convert_dtype. - If some signals in the record contain multiple samples per frame (but the selected signals don't), rdrecord would crash with an AttributeError in _arrange_fields. Change this behavior so that if smooth_frames is false, rdrecord will always store the signals as e_d_signal or e_p_signal, regardless of the underlying number of samples per frame. --- wfdb/io/record.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/wfdb/io/record.py b/wfdb/io/record.py index bc51cda8..0c772403 100644 --- a/wfdb/io/record.py +++ b/wfdb/io/record.py @@ -3363,10 +3363,9 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None, directly return a WFDB MultiRecord object (False), or to convert it into and return a WFDB Record object (True). smooth_frames : bool, optional - Used when reading records with signals having multiple samples - per frame. Specifies whether to smooth the samples in signals - with more than one sample per frame and return an (MxN) uniform - numpy array as the `d_signal` or `p_signal` field (True), or to + Specifies whether to smooth the samples in signals with more + than one sample per frame and return an (MxN) uniform numpy + array as the `d_signal` or `p_signal` field (True), or to return a list of 1d numpy arrays containing every expanded sample as the `e_d_signal` or `e_p_signal` field (False). ignore_skew : bool, optional @@ -3535,7 +3534,7 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None, return_res=return_res) # Only 1 sample/frame, or frames are smoothed. Return uniform numpy array - if smooth_frames or max([record.samps_per_frame[c] for c in channels]) == 1: + if smooth_frames: # Read signals from the associated dat files that contain # wanted channels record.d_signal = signals From d0173b4683cd9ddbd5504a6385c6b6270d29af46 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 8 Jul 2021 15:21:38 -0400 Subject: [PATCH 016/316] calc_checksum: handle non-smooth partial records. calc_checksum is called in order to calculate checksums of the signal data (d_signal or e_d_signal) in a record. In particular, if rdrecord is used to read only part of a record, it will call calc_checksum to determine the checksums of that part of the record. However, at that time, self.n_sig is still equal to the total number of signals in the input record (not the number of signals stored in d_signal or e_d_signal, which might be different if a subset of channels are selected). Thus, if expanded is true and self.n_sig > len(self.e_d_signal), this would crash. For simplicity, and consistency with the expanded=False case, ignore n_sig and simply calculate the checksums of e_d_signal. --- wfdb/io/_signal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index c660269b..902ce867 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -750,7 +750,7 @@ def calc_checksum(self, expanded=False): """ if expanded: - cs = [int(np.sum(self.e_d_signal[ch]) % 65536) for ch in range(self.n_sig)] + cs = [int(np.sum(s) % 65536) for s in self.e_d_signal] else: cs = np.sum(self.d_signal, 0) % 65536 cs = [int(c) for c in cs] From 7008e47a0fb089ee040b61887e5f637eb4c9ac32 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 8 Jul 2021 16:39:10 -0400 Subject: [PATCH 017/316] Record.__eq__: allow checking equality of lists of arrays. The Record.__eq__ function is currently used in the test suite to check whether two Record objects have the same contents. (This function doesn't actually conform to the standard Python API, but it works for this purpose.) In the case of "expanded"-format records, the e_p_signal and/or e_d_signal attributes are lists of numpy.ndarray objects, not numpy.ndarray objects themselves. In order to compare these for equality we must compare each element separately. --- wfdb/io/record.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wfdb/io/record.py b/wfdb/io/record.py index 0c772403..ab705621 100644 --- a/wfdb/io/record.py +++ b/wfdb/io/record.py @@ -628,6 +628,10 @@ def __eq__(self, other, verbose=False): if type(v1) == np.ndarray: # Necessary for nans np.testing.assert_array_equal(v1, v2) + elif (type(v1) == list and len(v1) == len(v2) + and all(type(e) == np.ndarray for e in v1)): + for (e1, e2) in zip(v1, v2): + np.testing.assert_array_equal(e1, e2) else: if v1 != v2: if verbose: From 68bb86d42305d1fd46b40c47c10fd5f58fefdcdf Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 8 Jul 2021 15:01:14 -0400 Subject: [PATCH 018/316] test_1c: test reading single-frequency record in non-smooth mode. When smooth_frames is False, rdrecord should store the signal data as a list of 1D arrays (each signal at its original sampling frequency) rather than as a "smooth" 2D array. Previously, this would fail if the input record contained only one sample per frame. Modify the existing test case to check that this works. --- tests/test_record.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_record.py b/tests/test_record.py index ccdad08c..dc879a91 100644 --- a/tests/test_record.py +++ b/tests/test_record.py @@ -75,24 +75,30 @@ def test_1b(self): def test_1c(self): """ Format 16, byte offset, selected duration, selected channels, - digital. + digital, expanded format. Target file created with: rdsamp -r sample-data/a103l -f 80 -s 0 1 | cut -f 2- > record-1c """ record = wfdb.rdrecord('sample-data/a103l', - sampfrom=20000, channels=[0, 1], physical=False) - sig = record.d_signal + sampfrom=20000, channels=[0, 1], physical=False, + smooth_frames=False) + # convert expanded to uniform array + sig = np.zeros((record.sig_len, record.n_sig)) + for i in range(record.n_sig): + sig[:,i] = record.e_d_signal[i] + sig_target = np.genfromtxt('tests/target-output/record-1c') # Compare data streaming from Physionet record_pn = wfdb.rdrecord('a103l', pn_dir='challenge-2015/training', sampfrom=20000, channels=[0, 1], - physical=False) + physical=False, smooth_frames=False) # Test file writing - record.wrsamp() - record_write = wfdb.rdrecord('a103l', physical=False) + record.wrsamp(expanded=True) + record_write = wfdb.rdrecord('a103l', physical=False, + smooth_frames=False) assert np.array_equal(sig, sig_target) assert record.__eq__(record_pn) From 19c1eb306424d2972a200b29da01d4c226851439 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Mon, 8 Nov 2021 16:49:30 -0500 Subject: [PATCH 019/316] convert_dtype: don't change p_signal when smooth_frames is false. If physical is true, and smooth_frames is false, and e_p_signal is None, this function should raise an exception. It should not silently modify p_signal instead. The previous code didn't make any sense: p_signal is a numpy array, so you can't change its data type by assigning to a slice of it, and even if that conversion were possible, the slicing was being done along the wrong axis. This reverts a dubious change in commit 725e7a65f90f5e86620b902f4a5ad0734f1bec46, which seems unrelated to the rest of that commit. --- wfdb/io/_signal.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index 868d3596..fc37efbc 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -705,14 +705,9 @@ def convert_dtype(self, physical, return_res, smooth_frames): if current_dtype != return_dtype: self.p_signal = self.p_signal.astype(return_dtype, copy=False) else: - if self.e_p_signal is not None: - for ch in range(self.n_sig): - if self.e_p_signal[ch].dtype != return_dtype: - self.e_p_signal[ch] = self.e_p_signal[ch].astype(return_dtype, copy=False) - else: - for ch in range(self.n_sig): - if self.p_signal[ch].dtype != return_dtype: - self.p_signal[ch] = self.p_signal[ch].astype(return_dtype, copy=False) + for ch in range(self.n_sig): + if self.e_p_signal[ch].dtype != return_dtype: + self.e_p_signal[ch] = self.e_p_signal[ch].astype(return_dtype, copy=False) else: return_dtype = 'int'+str(return_res) if smooth_frames: From 9d77075ebff003222c7edab84c94e6e6289481fc Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 16 Nov 2021 12:58:21 -0500 Subject: [PATCH 020/316] Record.__eq__: replace exact type comparisons with isinstance. When comparing the attributes of two records, if a value is an instance of a class derived from 'np.ndarray' or a class derived from 'list', it should be treated the same way as an 'np.ndarray' or a 'list'. Note that there currently are no such classes defined or used by this package. Note also that in any case, the respective types of the two attributes must be equal in order for the values to be considered equal. --- wfdb/io/record.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wfdb/io/record.py b/wfdb/io/record.py index ab705621..5ffeea80 100644 --- a/wfdb/io/record.py +++ b/wfdb/io/record.py @@ -625,11 +625,11 @@ def __eq__(self, other, verbose=False): print('Mismatch in attribute: %s' % k, v1, v2) return False - if type(v1) == np.ndarray: + if isinstance(v1, np.ndarray): # Necessary for nans np.testing.assert_array_equal(v1, v2) - elif (type(v1) == list and len(v1) == len(v2) - and all(type(e) == np.ndarray for e in v1)): + elif (isinstance(v1, list) and len(v1) == len(v2) + and all(isinstance(e, np.ndarray) for e in v1)): for (e1, e2) in zip(v1, v2): np.testing.assert_array_equal(e1, e2) else: From ca20b1a360b10315f2ec5eab6d2df382497b8583 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 15 Mar 2022 11:31:01 -0400 Subject: [PATCH 021/316] rdrecord: smooth frames by invoking smooth_frames(). To simplify the implementation of _rd_segment and _rd_dat_signals, we want to eliminate the smooth_frames argument, so that the return values of these two functions will always have the same type (a list of numpy arrays.) Therefore, if the application requested frame smoothing, then instead of calling _rd_segment with smooth_frames=True, we will call _rd_segment with smooth_frames=False, and post-process the result by calling Record.smooth_frames. Record.smooth_frames (SignalMixin.smooth_frames) will give a result equivalent to what _rd_segment gives with smooth_frames=True, but there are likely differences in performance: - Record.smooth_frames performs the computation by slicing along the "long" axis and storing the intermediate results in an int64 numpy array. _rd_dat_signals slices along the "short" axis and stores the intermediate results in a Python list. Record.smooth_frames should therefore be faster for large inputs. - Record.smooth_frames only operates on the channels present in e_d_signal, whereas _rd_dat_signals smooths all of the signals in the input file. Record.smooth_frames therefore saves memory and time when reading a subset of channels. - Record.smooth_frames always returns an int64 array, whereas _rd_dat_signals returns an array of the same type as the original data. Record.smooth_frames therefore uses more memory in many cases. (Note that rdrecord will post-process the result in any case, making this change invisible to applications; the issue of increased temporary memory usage can be addressed separately.) - If there are multiple channels in a signal file, then calling _rd_dat_signals with smooth_frames=False requires making an extra copy of each signal that has multiple samples per frame (because of the "reshape(-1)".) (This could be addressed in the future by allowing _rd_segment, or at least _rd_dat_signals, to return a list of *two-dimensional* arrays instead.) In order for this to work correctly, Record.smooth_frames must be called after setting both e_d_signal and samps_per_frame. In particular, it must be done after Record._arrange_fields "rearranges" samps_per_frame according to channels. On the other hand, _arrange_fields is expected to set checksum and init_value in different ways depending on whether the result is to be smoothed. (This use of checksum and init_value is somewhat dubious.) Therefore, smooth_frames is now invoked as part of _arrange_fields, after setting channel-specific metadata and before setting checksum and init_value. _arrange_fields should never be invoked other than by rdrecord; it doesn't make any sense to call this function at other times. Change the signature of this function to reflect the fact that it actively transforms the signal array, and make all arguments mandatory. --- wfdb/io/record.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/wfdb/io/record.py b/wfdb/io/record.py index 5ffeea80..38b8ae39 100644 --- a/wfdb/io/record.py +++ b/wfdb/io/record.py @@ -668,7 +668,7 @@ def wrsamp(self, expanded=False, write_dir=''): self.wr_dats(expanded=expanded, write_dir=write_dir) - def _arrange_fields(self, channels, sampfrom=0, expanded=False): + def _arrange_fields(self, channels, sampfrom, smooth_frames): """ Arrange/edit object fields to reflect user channel and/or signal range input. @@ -677,10 +677,11 @@ def _arrange_fields(self, channels, sampfrom=0, expanded=False): ---------- channels : list List of channel numbers specified. - sampfrom : int, optional + sampfrom : int Starting sample number read. - expanded : bool, optional - Whether the record was read in expanded mode. + smooth_frames : bool + Whether to convert the expanded signal array (e_d_signal) into + a smooth signal array (d_signal). Returns ------- @@ -693,11 +694,11 @@ def _arrange_fields(self, channels, sampfrom=0, expanded=False): setattr(self, field, [item[c] for c in channels]) # Expanded signals - multiple samples per frame. - if expanded: + if not smooth_frames: # Checksum and init_value to be updated if present # unless the whole signal length was input if self.sig_len != int(len(self.e_d_signal[0]) / self.samps_per_frame[0]): - self.checksum = self.calc_checksum(expanded) + self.checksum = self.calc_checksum(True) self.init_value = [s[0] for s in self.e_d_signal] self.n_sig = len(channels) @@ -705,6 +706,9 @@ def _arrange_fields(self, channels, sampfrom=0, expanded=False): # MxN numpy array d_signal else: + self.d_signal = self.smooth_frames('digital') + self.e_d_signal = None + # Checksum and init_value to be updated if present # unless the whole signal length was input if self.sig_len != self.d_signal.shape[0]: @@ -3517,7 +3521,7 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None, no_file = False sig_data = None - signals = _signal._rd_segment( + record.e_d_signal = _signal._rd_segment( file_name=record.file_name, dir_name=dir_name, pn_dir=pn_dir, @@ -3531,7 +3535,7 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None, sampfrom=sampfrom, sampto=sampto, channels=channels, - smooth_frames=smooth_frames, + smooth_frames=False, ignore_skew=ignore_skew, no_file=no_file, sig_data=sig_data, @@ -3539,14 +3543,10 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None, # Only 1 sample/frame, or frames are smoothed. Return uniform numpy array if smooth_frames: - # Read signals from the associated dat files that contain - # wanted channels - record.d_signal = signals - # Arrange/edit the object fields to reflect user channel # and/or signal range input record._arrange_fields(channels=channels, sampfrom=sampfrom, - expanded=False) + smooth_frames=True) if physical: # Perform inplace dac to get physical signal @@ -3554,12 +3554,10 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None, # Return each sample of the signals with multiple samples per frame else: - record.e_d_signal = signals - # Arrange/edit the object fields to reflect user channel # and/or signal range input record._arrange_fields(channels=channels, sampfrom=sampfrom, - expanded=True) + smooth_frames=False) if physical: # Perform dac to get physical signal From b2f39955f0ba42e9f422ddf409dbc0bb772bf226 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 15 Mar 2022 12:46:06 -0400 Subject: [PATCH 022/316] _rd_segment, _rd_dat_signals: remove support for smooth_frames=True. Now, _rd_segment is only ever called with smooth_frames=False (the function previously fulfilled by smooth_frames=True is handled by invoking Record.smooth_frames in Record._arrange_fields.) Accordingly, remove the logic for smoothing frames within _rd_dat_signals and for handling "smoothed" data in _rd_segment. --- wfdb/io/_signal.py | 155 +++++++++++++++------------------------------ 1 file changed, 50 insertions(+), 105 deletions(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index 902ce867..7268c2f1 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -900,7 +900,7 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, sampto : int The final sample number to be read from the signals. smooth_frames : bool - Whether to return the result as a two-dimensional array. + Deprecated. Must be set to False. ignore_skew : bool Used when reading records with at least one skewed signal. Specifies whether to apply the skew to align the signals in the @@ -920,16 +920,14 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, Returns ------- - signals : ndarray, list - The signals read from the dat file(s). A 2d numpy array is - returned if `smooth_frames` is True. Otherwise a list of 1d - numpy arrays is returned. + signals : list + The signals read from the dat file(s). Each signal is returned as a + one-dimensional numpy array. Notes ----- - 'channels', 'sampfrom', 'sampto', 'smooth_frames', and 'ignore_skew' - are user desired input fields. All other parameters are - specifications of the segment. + 'channels', 'sampfrom', 'sampto', and 'ignore_skew' are user desired + input fields. All other parameters are specifications of the segment. """ # Check for valid inputs @@ -993,61 +991,35 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, r_w_channel[fn] = [c - min(datchannel[fn]) for c in w_channel[fn]] out_dat_channel[fn] = [channels.index(c) for c in w_channel[fn]] - # Signals with multiple samples/frame are smoothed, or all signals have 1 sample/frame. - # Return uniform numpy array if smooth_frames: - # Figure out the largest required dtype for the segment to minimize memory usage - max_dtype = _np_dtype(_fmt_res(fmt, max_res=True), discrete=True) - # Allocate signal array. Minimize dtype - signals = np.zeros([sampto-sampfrom, len(channels)], dtype=max_dtype) - - # Read each wanted dat file and store signals - for fn in w_file_name: - datsignals = _rd_dat_signals( - file_name=fn, - dir_name=dir_name, - pn_dir=pn_dir, - fmt=w_fmt[fn], - n_sig=len(datchannel[fn]), - sig_len=sig_len, - byte_offset=w_byte_offset[fn], - samps_per_frame=w_samps_per_frame[fn], - skew=w_skew[fn], - init_value=w_init_value[fn], - sampfrom=sampfrom, - sampto=sampto, - smooth_frames=smooth_frames, - no_file=no_file, - sig_data=sig_data) - signals[:, out_dat_channel[fn]] = datsignals[:, r_w_channel[fn]] + raise ValueError('smooth_frames=True is not supported') # Return each sample in signals with multiple samples/frame, without smoothing. # Return a list of numpy arrays for each signal. - else: - signals = [None] * len(channels) - - for fn in w_file_name: - # Get the list of all signals contained in the dat file - datsignals = _rd_dat_signals( - file_name=fn, - dir_name=dir_name, - pn_dir=pn_dir, - fmt=w_fmt[fn], - n_sig=len(datchannel[fn]), - sig_len=sig_len, - byte_offset=w_byte_offset[fn], - samps_per_frame=w_samps_per_frame[fn], - skew=w_skew[fn], - init_value=w_init_value[fn], - sampfrom=sampfrom, - sampto=sampto, - smooth_frames=smooth_frames, - no_file=no_file, - sig_data=sig_data) - - # Copy over the wanted signals - for cn in range(len(out_dat_channel[fn])): - signals[out_dat_channel[fn][cn]] = datsignals[r_w_channel[fn][cn]] + signals = [None] * len(channels) + + for fn in w_file_name: + # Get the list of all signals contained in the dat file + datsignals = _rd_dat_signals( + file_name=fn, + dir_name=dir_name, + pn_dir=pn_dir, + fmt=w_fmt[fn], + n_sig=len(datchannel[fn]), + sig_len=sig_len, + byte_offset=w_byte_offset[fn], + samps_per_frame=w_samps_per_frame[fn], + skew=w_skew[fn], + init_value=w_init_value[fn], + sampfrom=sampfrom, + sampto=sampto, + smooth_frames=smooth_frames, + no_file=no_file, + sig_data=sig_data) + + # Copy over the wanted signals + for cn in range(len(out_dat_channel[fn])): + signals[out_dat_channel[fn][cn]] = datsignals[r_w_channel[fn][cn]] return signals @@ -1088,7 +1060,7 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, sampto : int The final sample number to be read from the signals. smooth_frames : bool - Whether to return the result as a two-dimensional array. + Deprecated. Must be set to False. no_file : bool, optional Used when using this function with just an array of signal data and no associated file to read the data from. @@ -1099,15 +1071,13 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, Returns ------- signal : ndarray, list - The signals read from the dat file(s). A 2d numpy array is - returned if `smooth_frames` is True. Otherwise a list of 1d - numpy arrays is returned. + The signals read from the dat file(s). Each signal is returned as a + one-dimensional numpy array. Notes ----- - 'channels', 'sampfrom', 'sampto', 'smooth_frames', and 'ignore_skew' - are user desired input fields. All other parameters are - specifications of the segment. + 'channels', 'sampfrom', 'sampto', and 'ignore_skew' are user desired + input fields. All other parameters are specifications of the segment. """ # Check for valid inputs @@ -1209,46 +1179,21 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, # At this point, dtype of sig_data is the minimum integer format # required for storing the final digital samples. - # No extra samples/frame. Obtain original uniform numpy array - if smooth_frames and tsamps_per_frame == n_sig: - # Reshape into multiple channels - signal = sig_data.reshape(-1, n_sig) - # Skew the signal - signal = _skew_sig(signal, skew, n_sig, read_len, fmt, nan_replace) - # Extra frames present to be smoothed. Obtain averaged uniform numpy array - elif smooth_frames: - # Allocate memory for smoothed signal. - signal = np.zeros((int(len(sig_data) / tsamps_per_frame) , n_sig), - dtype=sig_data.dtype) - - # Transfer and average samples - for ch in range(n_sig): - if samps_per_frame[ch] == 1: - signal[:, ch] = sig_data[sum(([0] + samps_per_frame)[:ch + 1])::tsamps_per_frame] - else: - if ch == 0: - startind = 0 - else: - startind = np.sum(samps_per_frame[:ch]) - signal[:,ch] = [np.average(sig_data[ind:ind+samps_per_frame[ch]]) for ind in range(startind,len(sig_data),tsamps_per_frame)] - # Skew the signal - signal = _skew_sig(signal, skew, n_sig, read_len, fmt, nan_replace) + if smooth_frames: + raise ValueError('smooth_frames=True is not supported') - # Extra frames present without wanting smoothing. Return all - # expanded samples. - else: - # List of 1d numpy arrays - signal = [] - # Transfer over samples - sig_frames = sig_data.reshape(-1, tsamps_per_frame) - ch_start = 0 - for ch in range(n_sig): - ch_end = ch_start + samps_per_frame[ch] - ch_signal = sig_frames[:, ch_start:ch_end].reshape(-1) - signal.append(ch_signal) - ch_start = ch_end - # Skew the signal - signal = _skew_sig(signal, skew, n_sig, read_len, fmt, nan_replace, samps_per_frame) + # List of 1d numpy arrays + signal = [] + # Transfer over samples + sig_frames = sig_data.reshape(-1, tsamps_per_frame) + ch_start = 0 + for ch in range(n_sig): + ch_end = ch_start + samps_per_frame[ch] + ch_signal = sig_frames[:, ch_start:ch_end].reshape(-1) + signal.append(ch_signal) + ch_start = ch_end + # Skew the signal + signal = _skew_sig(signal, skew, n_sig, read_len, fmt, nan_replace, samps_per_frame) # Integrity check of signal shape after reading _check_sig_dims(signal, read_len, n_sig, samps_per_frame) From d02c478be6b2ae753eaddb53175cc7d147a328ae Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 15 Mar 2022 12:54:41 -0400 Subject: [PATCH 023/316] _rd_segment, _rd_dat_signals: remove smooth_frames parameter. The smooth_frames parameter has been deprecated, and these functions always return a list of arrays; remove the parameter. Note that _rd_segment is only ever called by rdrecord, and _rd_dat_signals is only ever called by _rd_segment. --- wfdb/io/_signal.py | 16 ++-------------- wfdb/io/record.py | 1 - 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index 7268c2f1..14648c5f 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -866,7 +866,7 @@ def smooth_frames(self, sigtype='physical'): def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, samps_per_frame, skew, init_value, sampfrom, sampto, channels, - smooth_frames, ignore_skew, no_file=False, sig_data=None, return_res=64): + ignore_skew, no_file=False, sig_data=None, return_res=64): """ Read the digital samples from a single segment record's associated dat file(s). @@ -899,8 +899,6 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, The starting sample number to be read from the signals. sampto : int The final sample number to be read from the signals. - smooth_frames : bool - Deprecated. Must be set to False. ignore_skew : bool Used when reading records with at least one skewed signal. Specifies whether to apply the skew to align the signals in the @@ -991,9 +989,6 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, r_w_channel[fn] = [c - min(datchannel[fn]) for c in w_channel[fn]] out_dat_channel[fn] = [channels.index(c) for c in w_channel[fn]] - if smooth_frames: - raise ValueError('smooth_frames=True is not supported') - # Return each sample in signals with multiple samples/frame, without smoothing. # Return a list of numpy arrays for each signal. signals = [None] * len(channels) @@ -1013,7 +1008,6 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, init_value=w_init_value[fn], sampfrom=sampfrom, sampto=sampto, - smooth_frames=smooth_frames, no_file=no_file, sig_data=sig_data) @@ -1026,8 +1020,7 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset, samps_per_frame, skew, init_value, - sampfrom, sampto, smooth_frames, - no_file=False, sig_data=None): + sampfrom, sampto, no_file=False, sig_data=None): """ Read all signals from a WFDB dat file. @@ -1059,8 +1052,6 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, The starting sample number to be read from the signals. sampto : int The final sample number to be read from the signals. - smooth_frames : bool - Deprecated. Must be set to False. no_file : bool, optional Used when using this function with just an array of signal data and no associated file to read the data from. @@ -1179,9 +1170,6 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, # At this point, dtype of sig_data is the minimum integer format # required for storing the final digital samples. - if smooth_frames: - raise ValueError('smooth_frames=True is not supported') - # List of 1d numpy arrays signal = [] # Transfer over samples diff --git a/wfdb/io/record.py b/wfdb/io/record.py index 38b8ae39..9d0ff047 100644 --- a/wfdb/io/record.py +++ b/wfdb/io/record.py @@ -3535,7 +3535,6 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None, sampfrom=sampfrom, sampto=sampto, channels=channels, - smooth_frames=False, ignore_skew=ignore_skew, no_file=no_file, sig_data=sig_data, From dac438f370534d82b536e51ba5bc2d725510cf66 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 17 Mar 2022 14:12:21 -0400 Subject: [PATCH 024/316] Add example annotation files for a multi-frequency record. The annotation files given here may be useful as examples and for testing. They were generated by the following commands (WFDB 10.6.2): gqrs -r 03700181 -o gqrsl (an annotation file with no explicit time resolution - meaning the resolution is assumed to be one tick per WFDB frame, i.e. 1/125 s.) gqrs -H -r 03700181 -o gqrsh (an annotation file with a time resolution of 1/500 s, which, in this case, matches the sampling interval of the signal in question.) sqrs -r 03700181 && mv 03700181.qrs 03700181.sqrs (an annotation file with a time resolution of 1/250 s, which doesn't match either the frame interval or the sampling interval.) --- sample-data/03700181.gqrsh | Bin 0 -> 4242 bytes sample-data/03700181.gqrsl | Bin 0 -> 4062 bytes sample-data/03700181.sqrs | Bin 0 -> 2434 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 sample-data/03700181.gqrsh create mode 100644 sample-data/03700181.gqrsl create mode 100644 sample-data/03700181.sqrs diff --git a/sample-data/03700181.gqrsh b/sample-data/03700181.gqrsh new file mode 100644 index 0000000000000000000000000000000000000000..6390b1930f17bc5fa4b8f4643164b744725b128c GIT binary patch literal 4242 zcmZ{mTW=dx6otp-Oae}PO`1qZfJFswky0T9;ur7}`Ufhg1Vp6ezwwB~MM1m}D)EHi zt))qYM)7!@gm78kS>73&hDNo|?8{nf?{m%`7x#9r?%bI?dhqG}$>IHn2cLZL=)u8f zA57le-!F>i|L9+*DDJ&>_0i{t4<~ycPWBEb`|rHBzkm1ryOX_x3FeOrMHa=|sYu54+T(Lx2$=5t;33;Jd&`ewO5)XE>Zpc~Cq+GBgGWhRCAi6$?U zv;KWW;YKyfy^J1{W1Pr|E}|tE>V(eFvaHe-E_;km##1rlw*>^0E%)aCJ*XiAR_|() z`9YZJ&--mOb6CF>uj+9>%sOD}63mB7ya1VgAI+G?!{yLCC9 zE>QW&VmF8<0~?-Y9QQXofzsq&kbxe1LG>Tc&#wEuiruPj6dlSzx|y|+C;DJk_=2^` zyV$MxhNn}Xl+iDqu}1B55zerA6;^r?7NWd%W#MVAcfvKhuk65v6nK}q>M9I@yS$%P zi6ZIIMoxO~Q|1|e+*-s9!=%*`lMS}$@Ozu4hUHSdQ`edEX)AUt25|a2;pXX{%Rw6xd&kcL`=S!dmm!}C$vQ3a#wmXvTp#`@$O4)i_J|DkkoCR;Vw&Cfo~mNa4Y`=M8T ztMhZI?xeD>Gfu{~P`I zB_*-P?NqQT^4%eB6;&}QsfTC2vC=O%$nhI2EWDkpo}RJyyjz&&?k^Tk3i3dt9`_ao z?y#pf_`HpL*FA^qafN2_t@n(9+4GRS^*kficb@05&|r0KVQ)0;*>}8nxWQ;-rsA7x z1p_f7eG*M9Gua+btg#}A-d^~*tFVSG^4lr4rMrDQXTSmLxuUrzVBmpkm~n`hFQU)& z?nA>o`(T_aXJ_Fj2c(gn@!0-DdZX%>TRM$9y2G0~nPBiMU^io3EVwSk5q9z~RP{g^ zuFfo5dM1nCM?*1PEVN!`uCc7tOcwlb+|HHxd6?1KtUq&BV39@ixWZ09E;4st&-4o{ M@Xa->x@A%P3wovcRR910 literal 0 HcmV?d00001 diff --git a/sample-data/03700181.gqrsl b/sample-data/03700181.gqrsl new file mode 100644 index 0000000000000000000000000000000000000000..edac09d7e0c9faceed0007260113b56abad76e3e GIT binary patch literal 4062 zcmZvdO>PrG5QSr@k%ArPj{p(^DTp;v5+p#%#17&T+<*<@cZCCI;5dX^&0?}+L4E!D zrN#-etm&zr_g+=?bZ@+S^!NRjuip-ze?2^U@#^U4`1SGO^G}D^K5YCd)Bdc?;?M7U znGAPhjcu#hyZVNgD6Cpd`>sV#hD0FOb|WhX&-XPH$8H9uc#{F&szrD4|G971yQmcsy$BB1N)KEw+i|DXAjxx9$KgLL{ z_*KJX$Y{lmHDHluWo$-w6Ggtn!Q)xy98_0Et!pF4v2@0ajyJzOtLxgjY_f?tX54cx?t3NFV-e98St*6 zdqgNJ*QvqY$#B`aMf7FEWre=0cwdwun5O+z#T;U_D8IuZ1Vv1HVy>$70cD62TXRSR{?Mtxu;@5ks9 znddRH?dEZXj>OJyD|CoKmU(|%_9J`Vzp4J+ntv9ERtht3^N8Axr@A+b3DJ>xu<})t z=NxI>2@4#%ts>Qd6&um2K?)U{UNY0u)o3!oww-A9$)fCqdaQ8l$Df^rcQ5{~$Z9~r zE`H@Cuk7H^nS+04vD(1c3& zT%F?`Ue|1uWR_{3w?j?&iyFLrZ`bF|h@ z)0$Y_8@?;YEMSRQdy=&?s7z;-`jOq!63WJhTw?C79oo2l3zOe>8P)quuAakaS+jGZ|pQZ#C594B6->Hgy$0 zHzPhQ#Nu;o`t{w+`ad;d9$92+uj{TOha1da*k#4_`z(B@hru^0^McPfFUzAi@Aw+0 z{b_kppYU+U5UW})uh-bMzc}G_%L-E-Wr;z%xr_7cv^?v7UhDK;MA^7%lyxJEwG(A6 zQZHn6!WA(`?f$M-$68;UJEkYo@HxcO$l7=MiuwA4g+^9Pif`DnBx{V~cqt{4Gu+us zn+$NVa-y5Vyx7Uxix}}pKe5>EH9rc#;wdbAT4Mu)`|qq;#Ur)Ygv4$mmv~~a!gJ7` z^bLK9gNG=06%VYO+hWI;?8Lt)kNdZQab9mUZgXxnG9SbrG!!f&AH~ax#Z$V>FHJB> s6WWBbY&WIe=p#zNSB{WjP~X?>yJ1JMtb3JT*;KNu`qneHqneG4e+&n*Q~&?~ literal 0 HcmV?d00001 diff --git a/sample-data/03700181.sqrs b/sample-data/03700181.sqrs new file mode 100644 index 0000000000000000000000000000000000000000..9d52879fee86ff7f5a7e17eac93d5ba102f473fc GIT binary patch literal 2434 zcmZXUL2eU45Jd;6*KFBZ$^{S-8?F$s=K>J~1%gOokH?N3!!6*7903XH_4C)*;L%K1 zbyfZPRn_g`-HYEB7mNGrPgjetS9iBJ5BJx%pWiHAy?!|izy8$kS*^ox_k3vUHXYwc z=*UwW+p0Zwd93f)Huc?f3m(es#UEKTqJ2b~dfd6J7R=zq6DzsI2kTUeY&JD~616?b za=;E-UgUGXXzR8u>)7pNJI7E<%_7dMtehyG(>k|DjfiuM;A07|RC#Jc&a>(BNR?=^ zH*U@)51ELMN7z%R{f^&U$(&inV9s&&(Wp5_JzlY*=Lusor{4eL>>OVI){-d^J9FHY z%-NJHUo(MhPh^?NZrZNZaE3Eoz{^qlXLFs+^O{Cl$&+JF^|~M zTxUDd+xDa9MJ7+!z|qW!ie`Lz&+1H!$NQ$E(2G@R-0+~w8fDEI?b4=QmHlvJOXRlmTozXl;LJMK3-7V?WzP@FuA`8R?p&7- z2=sYhUl__9PY}pS2Hx24@?S&$eB*Ol#2UGMeQa`qR-$Byf-Ki=eN-Vhr+0KOvXdkQFhdzZ}MR~62 zu@PYdCNF0o*pD@joT?z=V<%umG)PD}E8;pUI+lJar`}Wo-`V#|Rcy|5+xuj<@pP`P z=FFt=loJxHV8UK96R&2M+_#Icr#DU`obXG8*{n1fhJ9>sZ9C>NI?>XR3GAPsLNo>~lhp_tgdN dGIyO@ijM72^3&1p+YwhA_Sjgbleno3!(RoBT*v?b literal 0 HcmV?d00001 From 7db6d0ede5051ef966bc7b7e34b62f4cfef13d3a Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 17 Mar 2022 16:07:47 -0400 Subject: [PATCH 025/316] plot_annotation: show annotation markers atop signals. Display the annotation markers at a higher "z-order" than the signals, so that the markers are visible when they overlap with the signals. --- wfdb/plot/plot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index 99279b53..68d7fac2 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -355,7 +355,8 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, raise Exception('IndexError: try setting shift_samps=True in ' 'the "rdann" function?') - axes[ch].plot(ann_samp[ch] / downsample_factor, y, ann_style[ch]) + axes[ch].plot(ann_samp[ch] / downsample_factor, y, ann_style[ch], + zorder=4) # Plot the annotation symbols if any if ann_sym is not None and ann_sym[ch] is not None: From 0cd04862165d94128c32b3839002d5c54534bab7 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 17 Mar 2022 16:01:57 -0400 Subject: [PATCH 026/316] plot_items: add sampling_freq and ann_freq parameters. An annotation file (represented by a wfdb.Annotation object) may have a "sampling frequency" that differs from the sampling frequency or frequencies of the signals themselves. This will be the case, for example, for annotations generated by programs like sqrs that operate on an upsampled or downsampled copy of the input signals. In any event, when plotting annotations, we need to translate the annotation time into a sample number in order to display it in the correct location. Furthermore, in the future, we want to permit plotting multiple synchronized signals that are sampled at different frequencies. Therefore, to disambiguate between the many possible "sampling frequencies" invvolved, add parameters 'sampling_freq' and 'ann_freq'. Both parameters are optional and default to the value of 'fs'. Either may be a list (one entry per channel), allowing the API to accomodate multi-frequency data. Currently, if 'sampling_freq' is a list, all of its elements must be equal. --- wfdb/plot/plot.py | 175 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 155 insertions(+), 20 deletions(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index 99279b53..ac9aa719 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -8,12 +8,79 @@ from wfdb.io.annotation import Annotation +def _get_sampling_freq(sampling_freq, n_sig, frame_freq): + """ + Convert application-specified sampling frequency to a list. + + Parameters + ---------- + sampling_freq : number or sequence or None + The sampling frequency or frequencies of the signals. If this is a + list, its length must equal `n_sig`. If unset, defaults to + `frame_freq`. + n_sig : int + Number of channels. + frame_freq : number or None + Default sampling frequency (record frame frequency). + + Returns + ------- + sampling_freq : list + The sampling frequency for each channel (a list of length `n_sig`.) + + """ + if sampling_freq is None: + return [frame_freq] * n_sig + elif hasattr(sampling_freq, '__len__'): + if len(sampling_freq) != n_sig: + raise ValueError('length mismatch: n_sig = {}, ' + 'len(sampling_freq) = {}'.format( + n_sig, len(sampling_freq))) + return list(sampling_freq) + else: + return [sampling_freq] * n_sig + + +def _get_ann_freq(ann_freq, n_annot, frame_freq): + """ + Convert application-specified annotation frequency to a list. + + Parameters + ---------- + ann_freq : number or sequence or None + The sampling frequency or frequencies of the annotations. If this + is a list, its length must equal `n_annot`. If unset, defaults to + `frame_freq`. + n_annot : int + Number of channels. + frame_freq : number or None + Default sampling frequency (record frame frequency). + + Returns + ------- + ann_freq : list + The sampling frequency for each channel (a list of length `n_annot`). + + """ + if ann_freq is None: + return [frame_freq] * n_annot + elif hasattr(ann_freq, '__len__'): + if len(ann_freq) != n_annot: + raise ValueError('length mismatch: n_annot = {}, ' + 'len(ann_freq) = {}'.format( + n_annot, len(ann_freq))) + return list(ann_freq) + else: + return [ann_freq] * n_annot + + def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None, time_units='samples', sig_name=None, sig_units=None, xlabel=None, ylabel=None, title=None, sig_style=[''], ann_style=['r*'], ecg_grids=[], figsize=None, sharex=False, sharey=False, return_fig=False, - return_fig_axes=False): + return_fig_axes=False, sampling_freq=None, + ann_freq=None): """ Subplot individual channels of signals and/or annotations. @@ -87,6 +154,14 @@ def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None, 'figsize' argument passed into matplotlib.pyplot's `figure` function. return_fig : bool, optional Whether the figure is to be returned as an output argument. + sampling_freq : number or sequence, optional + The sampling frequency or frequencies of the signals. If this is a + list, it must have the same length as the number of channels. If + unspecified, defaults to `fs`. + ann_freq : number or sequence, optional + The sampling frequency or frequencies of the annotations. If this + is a list, it must have the same length as `ann_samp`. If + unspecified, defaults to `fs`. Returns ------- @@ -113,18 +188,25 @@ def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None, # Figure out number of subplots required sig_len, n_sig, n_annot, n_subplots = get_plot_dims(signal, ann_samp) + # Convert sampling_freq and ann_freq to lists if needed + sampling_freq = _get_sampling_freq(sampling_freq, n_sig, fs) + ann_freq = _get_ann_freq(ann_freq, n_annot, fs) + # Create figure fig, axes = create_figure(n_subplots, sharex, sharey, figsize) if signal is not None: - plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes) + plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes, + sampling_freq=sampling_freq) if ann_samp is not None: plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, - time_units, ann_style, axes) + time_units, ann_style, axes, + sampling_freq=sampling_freq, ann_freq=ann_freq) if ecg_grids: - plot_ecg_grids(ecg_grids, fs, sig_units, time_units, axes) + plot_ecg_grids(ecg_grids, fs, sig_units, time_units, axes, + sampling_freq=sampling_freq) # Add title and axis labels. # First, make sure that xlabel and ylabel inputs are valid @@ -238,7 +320,8 @@ def create_figure(n_subplots, sharex, sharey, figsize): return fig, axes -def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes): +def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes, + sampling_freq=None): """ Plot signal channels. @@ -262,22 +345,39 @@ def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes): will be used for all channels. axes : list The information needed for each subplot. + sampling_freq : number or sequence, optional + The sampling frequency or frequencies of the signals. If this is a + list, it must have the same length as the number of channels. If + unspecified, defaults to `fs`. Returns ------- N/A """ + if n_sig == 0: + return + # Extend signal style if necessary if len(sig_style) == 1: sig_style = n_sig * sig_style + # Convert sampling_freq to a list if needed + sampling_freq = _get_sampling_freq(sampling_freq, n_sig, fs) + + if any(f != sampling_freq[0] for f in sampling_freq): + raise NotImplementedError( + 'multiple sampling frequencies are not supported') + # Figure out time indices if time_units == 'samples': t = np.linspace(0, sig_len-1, sig_len) else: - downsample_factor = {'seconds':fs, 'minutes':fs * 60, - 'hours':fs * 3600} + downsample_factor = { + 'seconds': sampling_freq[0], + 'minutes': sampling_freq[0] * 60, + 'hours': sampling_freq[0] * 3600 + } t = np.linspace(0, sig_len-1, sig_len) / downsample_factor[time_units] # Plot the signals @@ -289,7 +389,7 @@ def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes): def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, - ann_style, axes): + ann_style, axes, sampling_freq=None, ann_freq=None): """ Plot annotations, possibly overlaid on signals. ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, ann_style, axes @@ -320,6 +420,14 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, will be used for all channels. axes : list The information needed for each subplot. + sampling_freq : number or sequence, optional + The sampling frequency or frequencies of the signals. If this is a + list, it must have the same length as the number of channels. If + unspecified, defaults to `fs`. + ann_freq : number or sequence, optional + The sampling frequency or frequencies of the annotations. If this + is a list, it must have the same length as `ann_samp`. If + unspecified, defaults to `fs`. Returns ------- @@ -330,25 +438,45 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, if len(ann_style) == 1: ann_style = n_annot * ann_style - # Figure out downsample factor for time indices - if time_units == 'samples': - downsample_factor = 1 - else: - downsample_factor = {'seconds':float(fs), 'minutes':float(fs)*60, - 'hours':float(fs)*3600}[time_units] + # Convert sampling_freq and ann_freq to lists if needed + sampling_freq = _get_sampling_freq(sampling_freq, n_sig, fs) + ann_freq = _get_ann_freq(ann_freq, n_annot, fs) # Plot the annotations for ch in range(n_annot): + afreq = ann_freq[ch] + if ch < n_sig: + sfreq = sampling_freq[ch] + else: + sfreq = afreq + + # Figure out downsample factor for time indices + if time_units == 'samples': + if afreq is None and sfreq is None: + downsample_factor = 1 + else: + downsample_factor = afreq / sfreq + else: + downsample_factor = { + 'seconds': float(afreq), + 'minutes': float(afreq) * 60, + 'hours': float(afreq) * 3600 + }[time_units] + if ann_samp[ch] is not None and len(ann_samp[ch]): # Figure out the y values to plot on a channel basis # 1 dimensional signals try: if n_sig > ch: + if sfreq == afreq: + index = ann_samp[ch] + else: + index = (sfreq / afreq * ann_samp[ch]).astype('int') if signal.ndim == 1: - y = signal[ann_samp[ch]] + y = signal[index] else: - y = signal[ann_samp[ch], ch] + y = signal[index, ch] else: y = np.zeros(len(ann_samp[ch])) except IndexError: @@ -364,7 +492,7 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, y[i])) -def plot_ecg_grids(ecg_grids, fs, units, time_units, axes): +def plot_ecg_grids(ecg_grids, fs, units, time_units, axes, sampling_freq=None): """ Add ECG grids to the axes. @@ -381,6 +509,10 @@ def plot_ecg_grids(ecg_grids, fs, units, time_units, axes): and 'hours'. axes : list The information needed for each subplot. + sampling_freq : number or sequence, optional + The sampling frequency or frequencies of the signals. If this is a + list, it must have the same length as the number of channels. If + unspecified, defaults to `fs`. Returns ------- @@ -390,6 +522,9 @@ def plot_ecg_grids(ecg_grids, fs, units, time_units, axes): if ecg_grids == 'all': ecg_grids = range(0, len(axes)) + # Convert sampling_freq to a list if needed + sampling_freq = _get_sampling_freq(sampling_freq, len(axes), fs) + for ch in ecg_grids: # Get the initial plot limits auto_xlims = axes[ch].get_xlim() @@ -397,8 +532,8 @@ def plot_ecg_grids(ecg_grids, fs, units, time_units, axes): (major_ticks_x, minor_ticks_x, major_ticks_y, minor_ticks_y) = calc_ecg_grids(auto_ylims[0], auto_ylims[1], - units[ch], fs, auto_xlims[1], - time_units) + units[ch], sampling_freq[ch], + auto_xlims[1], time_units) min_x, max_x = np.min(minor_ticks_x), np.max(minor_ticks_x) min_y, max_y = np.min(minor_ticks_y), np.max(minor_ticks_y) @@ -439,7 +574,7 @@ def calc_ecg_grids(minsig, maxsig, sig_units, fs, maxt, time_units): sig_units : list The units used for plotting each signal. fs : float - The sampling frequency of the record. + The sampling frequency of the signal. maxt : float The max time of the signal. time_units : str From e3813f7bfd6add2c18ea821a2eb86ee46976f4e8 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Thu, 17 Mar 2022 16:02:54 -0400 Subject: [PATCH 027/316] plot_wfdb: handle differing frame/annotation frequencies. An annotation file (represented by a wfdb.Annotation object) may have a "sampling frequency" that differs from the sampling frequency or frequencies of the signals themselves. This will be the case, for example, for annotations generated by programs like sqrs that operate on an upsampled or downsampled copy of the input signals. To handle this case, since record.fs does not equal annotation.fs, we must explicitly specify both sampling_freq and ann_freq when calling plot_items. --- wfdb/plot/plot.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index ac9aa719..67b0c3c3 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -779,12 +779,25 @@ def plot_wfdb(record=None, annotation=None, plot_sym=False, annotation=annotation, plot_sym=plot_sym) + if record: + sampling_freq = record.fs + else: + sampling_freq = None + + if annotation and annotation.fs is not None: + ann_freq = annotation.fs + elif record: + ann_freq = record.fs + else: + ann_freq = None + return plot_items(signal=signal, ann_samp=ann_samp, ann_sym=ann_sym, fs=fs, time_units=time_units, ylabel=ylabel, title=(title or record_name), sig_style=sig_style, sig_units=sig_units, ann_style=ann_style, ecg_grids=ecg_grids, - figsize=figsize, return_fig=return_fig) + figsize=figsize, return_fig=return_fig, + sampling_freq=sampling_freq, ann_freq=ann_freq) def get_wfdb_plot_items(record, annotation, plot_sym): From 6a7b47b8b8ffa1c22a4d63f7302c85b5f1a26c5b Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Fri, 18 Mar 2022 14:50:28 -0400 Subject: [PATCH 028/316] plot_annotation: remove documentation of non-existent parameter. There is no sig_len parameter to plot_annotation. --- wfdb/plot/plot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index 67b0c3c3..a4fcabbc 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -404,8 +404,6 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, The values of the annotation symbol locations. signal : ndarray Tranformed expanded signal into uniform signal. - sig_len : int - The signal length (per channel) of the dat file. n_sig : int The number of signals contained in the dat file. fs : float From 5fb38c142f72cce40f03e360b346fb9c412d5109 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Fri, 18 Mar 2022 14:52:26 -0400 Subject: [PATCH 029/316] plot_wfdb: if plotting digital values, show units as "adu". If we are plotting digital (d_signal) values, then the values on the Y axis are ADC units, not physical units. Don't label the axes as physical units, and don't try to calculate grid lines as if ADC units were physical units. --- wfdb/plot/plot.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index a4fcabbc..565ae69c 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -857,14 +857,19 @@ def get_wfdb_plot_items(record, annotation, plot_sym): if record: if record.p_signal is not None: signal = record.p_signal + physical = True elif record.d_signal is not None: signal = record.d_signal + physical = False else: raise ValueError('The record has no signal to plot') fs = record.fs sig_name = [str(s) for s in record.sig_name] - sig_units = [str(s) for s in record.units] + if physical: + sig_units = [str(s) for s in record.units] + else: + sig_units = ['adu'] * n_sig record_name = 'Record: %s' % record.record_name ylabel = ['/'.join(pair) for pair in zip(sig_name, sig_units)] else: From da3bce5488802b5b477c2a8a25ab724e04eb17c2 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Fri, 18 Mar 2022 15:21:28 -0400 Subject: [PATCH 030/316] New internal function _expand_signal. This function accepts either None, a one-dimensional array, a two-dimensional array, or a list (or other non-numpy sequence) of one-dimensional arrays, and converts the result to list of one-dimensional arrays (where each element represents one channel.) This will be used by various plotting functions to accept "non-smooth" signal data as the 'signal' argument while keeping backward compatibility. --- wfdb/plot/plot.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index 565ae69c..a0ecd66d 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -8,6 +8,43 @@ from wfdb.io.annotation import Annotation +def _expand_channels(signal): + """ + Convert application-specified signal data to a list. + + Parameters + ---------- + signal : 1d or 2d numpy array or list or None + The uniformly sampled signal or signals to be plotted. If signal + is a one-dimensional array, it is assumed to represent a single + channel. If it is a two-dimensional array, axes 0 and 1 must + represent time and channel number respectively. Otherwise it must + be a list of one-dimensional arrays (one for each channel.) + + Returns + ------- + signal : list + A list of one-dimensional arrays (one for each channel.) + + """ + if signal is None: + return [] + elif hasattr(signal, 'ndim'): + if signal.ndim == 1: + return [signal] + elif signal.ndim == 2: + return list(signal.transpose()) + else: + raise ValueError('invalid shape for signal array: {}'.format( + signal.shape)) + else: + signal = list(signal) + if any(s.ndim != 1 for s in signal): + raise ValueError('invalid shape for signal array(s): {}'.format( + [s.shape for s in signal])) + return signal + + def _get_sampling_freq(sampling_freq, n_sig, frame_freq): """ Convert application-specified sampling frequency to a list. From 317c427536443980fb93de8fa62f69a834e21988 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Fri, 18 Mar 2022 15:28:10 -0400 Subject: [PATCH 031/316] plot_annotation: accept non-smooth signal data as input. When plotting signals, in addition to allowing the signal argument to be a one-dimensional or two-dimensional array, allow it to be a list of one-dimensional arrays (so that each channel can have a different length.) If signal is a 1D or 2D array, convert it to a list of arrays using _expand_channels. This allows the later logic to be simplified. --- wfdb/plot/plot.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index a0ecd66d..13bb2b38 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -439,8 +439,12 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, The number of annotations contained in the dat file. ann_sym : list The values of the annotation symbol locations. - signal : ndarray - Tranformed expanded signal into uniform signal. + signal : 1d or 2d numpy array or list + The uniformly sampled signal or signals to be plotted. If signal + is a one-dimensional array, it is assumed to represent a single + channel. If it is a two-dimensional array, axes 0 and 1 must + represent time and channel number respectively. Otherwise it must + be a list of one-dimensional arrays (one for each channel). n_sig : int The number of signals contained in the dat file. fs : float @@ -469,6 +473,9 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, N/A """ + # Convert signal to a list if needed + signal = _expand_channels(signal) + # Extend annotation style if necessary if len(ann_style) == 1: ann_style = n_annot * ann_style @@ -508,10 +515,7 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, index = ann_samp[ch] else: index = (sfreq / afreq * ann_samp[ch]).astype('int') - if signal.ndim == 1: - y = signal[index] - else: - y = signal[index, ch] + y = signal[ch][index] else: y = np.zeros(len(ann_samp[ch])) except IndexError: From 69c93a3ecf02e781c34c4dc315176508671e6659 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Fri, 18 Mar 2022 15:40:11 -0400 Subject: [PATCH 032/316] plot_signal: accept non-smooth signal data as input. When plotting signals, in addition to allowing the signal argument to be a one-dimensional or two-dimensional array, allow it to be a list of one-dimensional arrays (so that each channel can have a different length.) This means that the array of X values (t) must be calculated separately for each channel, as both the sampling frequency and the number of samples may vary. (Here, we don't require that the lengths of the signals, in seconds, must all be the same, although they always will be when plotting a WFDB record.) The sig_len parameter makes no sense if the channels have different lengths; this parameter is now unused, and marked as deprecated. --- wfdb/plot/plot.py | 49 +++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index 13bb2b38..9f3df988 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -364,10 +364,14 @@ def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes, Parameters ---------- - signal : ndarray - Tranformed expanded signal into uniform signal. + signal : 1d or 2d numpy array or list + The uniformly sampled signal or signals to be plotted. If signal + is a one-dimensional array, it is assumed to represent a single + channel. If it is a two-dimensional array, axes 0 and 1 must + represent time and channel number respectively. Otherwise it must + be a list of one-dimensional arrays (one for each channel). sig_len : int - The signal length (per channel) of the dat file. + The signal length (per channel) of the dat file. Deprecated. n_sig : int The number of signals contained in the dat file. fs : float @@ -392,6 +396,8 @@ def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes, N/A """ + # Convert signal to a list if needed + signal = _expand_channels(signal) if n_sig == 0: return @@ -402,27 +408,24 @@ def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes, # Convert sampling_freq to a list if needed sampling_freq = _get_sampling_freq(sampling_freq, n_sig, fs) - if any(f != sampling_freq[0] for f in sampling_freq): - raise NotImplementedError( - 'multiple sampling frequencies are not supported') - - # Figure out time indices - if time_units == 'samples': - t = np.linspace(0, sig_len-1, sig_len) - else: - downsample_factor = { - 'seconds': sampling_freq[0], - 'minutes': sampling_freq[0] * 60, - 'hours': sampling_freq[0] * 3600 - } - t = np.linspace(0, sig_len-1, sig_len) / downsample_factor[time_units] - # Plot the signals - if signal.ndim == 1: - axes[0].plot(t, signal, sig_style[0], zorder=3) - else: - for ch in range(n_sig): - axes[ch].plot(t, signal[:,ch], sig_style[ch], zorder=3) + for ch in range(n_sig): + ch_len = len(signal[ch]) + ch_freq = sampling_freq[ch] + + # Figure out time indices + if time_units == 'samples': + t = np.linspace(0, ch_len-1, ch_len) + else: + downsample_factor = { + 'seconds': ch_freq, + 'minutes': ch_freq * 60, + 'hours': ch_freq * 3600 + } + t = np.linspace(0, ch_len-1, ch_len) + t /= downsample_factor[time_units] + + axes[ch].plot(t, signal[ch], sig_style[ch], zorder=3) def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, From 1dc98edfa9986090185972600027eeae878cde2a Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Fri, 18 Mar 2022 15:44:13 -0400 Subject: [PATCH 033/316] plot_signal: avoid making redundant X-coordinate arrays. In most cases, the same X-coordinate array will be used for more than one channel; the contents of this array only depend on ch_len and ch_freq (and time_units), so we can cache these arrays in a dictionary, saving some (likely small) amount of time and memory when plotting a huge record. --- wfdb/plot/plot.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index 9f3df988..bc2c8e63 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -408,22 +408,28 @@ def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes, # Convert sampling_freq to a list if needed sampling_freq = _get_sampling_freq(sampling_freq, n_sig, fs) + tarrays = {} + # Plot the signals for ch in range(n_sig): ch_len = len(signal[ch]) ch_freq = sampling_freq[ch] # Figure out time indices - if time_units == 'samples': - t = np.linspace(0, ch_len-1, ch_len) - else: - downsample_factor = { - 'seconds': ch_freq, - 'minutes': ch_freq * 60, - 'hours': ch_freq * 3600 - } - t = np.linspace(0, ch_len-1, ch_len) - t /= downsample_factor[time_units] + try: + t = tarrays[ch_len, ch_freq] + except KeyError: + if time_units == 'samples': + t = np.linspace(0, ch_len-1, ch_len) + else: + downsample_factor = { + 'seconds': ch_freq, + 'minutes': ch_freq * 60, + 'hours': ch_freq * 3600 + } + t = np.linspace(0, ch_len-1, ch_len) + t /= downsample_factor[time_units] + tarrays[ch_len, ch_freq] = t axes[ch].plot(t, signal[ch], sig_style[ch], zorder=3) From be23d6ae8dd8a6532d770e2379a56b459bd27029 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Fri, 18 Mar 2022 15:48:30 -0400 Subject: [PATCH 034/316] get_plot_dims: accept non-smooth signal data as input. When plotting signals, in addition to allowing the signal argument to be a one-dimensional or two-dimensional array, allow it to be a list of one-dimensional arrays (so that each channel can have a different length.) Using _expand_channels here, as in plot_annotation, allows the logic to be simplified. The sig_len return value makes no sense if the channels have different lengths; this return value is now marked as deprecated, and will be set to None if the channel lengths differ. --- wfdb/plot/plot.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index bc2c8e63..a81dc688 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -275,10 +275,12 @@ def get_plot_dims(signal, ann_samp): Parameters ---------- - signal : 1d or 2d numpy array, optional - The uniformly sampled signal to be plotted. If signal.ndim is 1, it is - assumed to be a one channel signal. If it is 2, axes 0 and 1, must - represent time and channel number respectively. + signal : 1d or 2d numpy array or list, optional + The uniformly sampled signal or signals to be plotted. If signal + is a one-dimensional array, it is assumed to represent a single + channel. If it is a two-dimensional array, axes 0 and 1 must + represent time and channel number respectively. Otherwise it must + be a list of one-dimensional arrays (one for each channel). ann_samp: list, optional A list of annotation locations to plot, with each list item corresponding to a different channel. List items may be: @@ -297,7 +299,7 @@ def get_plot_dims(signal, ann_samp): Returns ------- sig_len : int - The signal length (per channel) of the dat file. + The signal length (per channel) of the dat file. Deprecated. n_sig : int The number of signals contained in the dat file. n_annot : int @@ -306,13 +308,14 @@ def get_plot_dims(signal, ann_samp): The max between number of signals and annotations. """ - if signal is not None: - if signal.ndim == 1: - sig_len = len(signal) - n_sig = 1 - else: - sig_len = signal.shape[0] - n_sig = signal.shape[1] + # Convert signal to a list if needed + signal = _expand_channels(signal) + + if signal: + n_sig = len(signal) + sig_len = len(signal[0]) + if any(len(s) != sig_len for s in signal): + sig_len = None else: sig_len = 0 n_sig = 0 From 0de1a9df7ca970e839ff407be13584a483410aec Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Fri, 18 Mar 2022 16:10:32 -0400 Subject: [PATCH 035/316] plot_items: accept non-smooth signal data as input. When plotting signals, in addition to allowing the signal argument to be a one-dimensional or two-dimensional array, allow it to be a list of one-dimensional arrays (so that each channel can have a different length.) --- wfdb/plot/plot.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index a81dc688..412f0944 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -123,10 +123,12 @@ def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None, Parameters ---------- - signal : 1d or 2d numpy array, optional - The uniformly sampled signal to be plotted. If signal.ndim is 1, it is - assumed to be a one channel signal. If it is 2, axes 0 and 1, must - represent time and channel number respectively. + signal : 1d or 2d numpy array or list, optional + The uniformly sampled signal or signals to be plotted. If signal + is a one-dimensional array, it is assumed to represent a single + channel. If it is a two-dimensional array, axes 0 and 1 must + represent time and channel number respectively. Otherwise it must + be a list of one-dimensional arrays (one for each channel). ann_samp: list, optional A list of annotation locations to plot, with each list item corresponding to a different channel. List items may be: @@ -222,6 +224,9 @@ def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None, """ import matplotlib.pyplot as plt + # Convert signal to a list if needed + signal = _expand_channels(signal) + # Figure out number of subplots required sig_len, n_sig, n_annot, n_subplots = get_plot_dims(signal, ann_samp) From 576412486a97e3f61a32c211cfbb4dcfb783f788 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Fri, 18 Mar 2022 16:14:39 -0400 Subject: [PATCH 036/316] plot_wfdb: plot non-smooth records. If a record is loaded in "non-smooth" ("expanded") mode, it can contain signals of different lengths sampled at different frequencies. In such a case, we want to plot each signal at its original frequency in order to see the effect of the sampling and the temporal relationships between the signals. plot_items now allows the signal argument to be either a numpy array (as in p_signal or d_signal, loaded when 'smooth_frames=True') or a list of arrays (as in e_p_signal or e_d_signal, loaded when 'smooth_frames=False'); in the latter case, plot_wfdb has to calculate and provide the correct per-channel sampling frequencies. (The annotation file, if any, may have its own sampling frequency, which may differ from the signal sampling frequencies. The *default*, if the annotation file doesn't specify a frequency, is always the record frame frequency (fs), not the sampling frequency of any particular signal.) --- wfdb/plot/plot.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index 412f0944..a1f09334 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -768,8 +768,9 @@ def plot_wfdb(record=None, annotation=None, plot_sym=False, function, while allowing direct input of WFDB objects. If the record object is input, the function will extract from it: - - signal values, from the `p_signal` (priority) or `d_signal` attribute - - sampling frequency, from the `fs` attribute + - signal values, from the `e_p_signal`, `e_d_signal`, `p_signal`, or + `d_signal` attribute (in that order of priority.) + - frame frequency, from the `fs` attribute - signal names, from the `sig_name` attribute - signal units, from the `units` attribute @@ -836,7 +837,10 @@ def plot_wfdb(record=None, annotation=None, plot_sym=False, plot_sym=plot_sym) if record: - sampling_freq = record.fs + if record.e_p_signal is not None or record.e_d_signal is not None: + sampling_freq = [spf * record.fs for spf in record.samps_per_frame] + else: + sampling_freq = record.fs else: sampling_freq = None @@ -913,11 +917,21 @@ def get_wfdb_plot_items(record, annotation, plot_sym): """ # Get record attributes if record: - if record.p_signal is not None: + if record.e_p_signal is not None: + signal = record.e_p_signal + n_sig = len(signal) + physical = True + elif record.e_d_signal is not None: + signal = record.e_d_signal + n_sig = len(signal) + physical = False + elif record.p_signal is not None: signal = record.p_signal + n_sig = signal.shape[1] physical = True elif record.d_signal is not None: signal = record.d_signal + n_sig = signal.shape[1] physical = False else: raise ValueError('The record has no signal to plot') @@ -972,7 +986,7 @@ def get_wfdb_plot_items(record, annotation, plot_sym): # In this case, omit empty middle channels. This function should # already process labels and arrangements before passing into # `plot_items` - sig_chans = set(range(signal.shape[1])) + sig_chans = set(range(n_sig)) all_chans = sorted(sig_chans.union(ann_chans)) # Need to update ylabels and annotation values From 13a894866ec1398200d22c99e1b5329f9e93660a Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 1 Mar 2022 10:20:27 -0500 Subject: [PATCH 037/316] SignalMixin.smooth_frames: consolidate duplicated logic. The logic for physical and digital cases is exactly the same except for the choice of input data and the resulting data type. --- wfdb/io/_signal.py | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index 14648c5f..2509b6e7 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -831,33 +831,26 @@ def smooth_frames(self, sigtype='physical'): tspf = sum(spf) if sigtype == 'physical': - n_sig = len(self.e_p_signal) - sig_len = int(len(self.e_p_signal[0])/spf[0]) - signal = np.zeros((sig_len, n_sig), dtype='float64') - - for ch in range(n_sig): - if spf[ch] == 1: - signal[:, ch] = self.e_p_signal[ch] - else: - for frame in range(spf[ch]): - signal[:, ch] += self.e_p_signal[ch][frame::spf[ch]] - signal[:, ch] = signal[:, ch] / spf[ch] - + expanded_signal = self.e_p_signal + output_dtype = np.dtype('float64') elif sigtype == 'digital': - n_sig = len(self.e_d_signal) - sig_len = int(len(self.e_d_signal[0])/spf[0]) - signal = np.zeros((sig_len, n_sig), dtype='int64') - - for ch in range(n_sig): - if spf[ch] == 1: - signal[:, ch] = self.e_d_signal[ch] - else: - for frame in range(spf[ch]): - signal[:, ch] += self.e_d_signal[ch][frame::spf[ch]] - signal[:, ch] = signal[:, ch] / spf[ch] + expanded_signal = self.e_d_signal + output_dtype = np.dtype('int64') else: raise ValueError("sigtype must be 'physical' or 'digital'") + n_sig = len(expanded_signal) + sig_len = int(len(expanded_signal[0])/spf[0]) + signal = np.zeros((sig_len, n_sig), dtype=output_dtype) + + for ch in range(n_sig): + if spf[ch] == 1: + signal[:, ch] = expanded_signal[ch] + else: + for frame in range(spf[ch]): + signal[:, ch] += expanded_signal[ch][frame::spf[ch]] + signal[:, ch] = signal[:, ch] / spf[ch] + return signal From b09bab41de09253a7d99e99e585a7def7094c03d Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 1 Mar 2022 10:42:07 -0500 Subject: [PATCH 038/316] SignalMixin.smooth_frames: check lengths of input arrays. Ensure that all of the input signals have the expected lengths before trying to do anything else. (If any of the signal lengths were wrong, this would typically have raised a ValueError later on - though not always, since numpy will try to be clever if one of the arguments is an array of length 1.) --- wfdb/io/_signal.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index 2509b6e7..94076fee 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -841,6 +841,12 @@ def smooth_frames(self, sigtype='physical'): n_sig = len(expanded_signal) sig_len = int(len(expanded_signal[0])/spf[0]) + for ch in range(n_sig): + if len(expanded_signal[ch]) != sig_len * spf[ch]: + raise ValueError("length mismatch: signal %d has %d samples," + " expected %dx%d" + % (ch, len(expanded_signal), + sig_len, spf[ch])) signal = np.zeros((sig_len, n_sig), dtype=output_dtype) for ch in range(n_sig): From 6db9ebfd56e9dee75979bc2fe9f3febeb47fb562 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 1 Mar 2022 11:05:13 -0500 Subject: [PATCH 039/316] SignalMixin.smooth_frames: use minimal data type for result. Instead of always returning the result as an int64 or float64 array, select the output type based on the types of the input arrays. The output type should be the smallest type that has the correct "kind" and is able to represent all input values. For example, in digital mode, if the input includes some int8 arrays and some int16 arrays, the result should be an int16 array. In physical mode, if the inputs are all float32, then the result will be float32; otherwise the result will be float64. However, although the output type should generally match the input type, intermediate results may need to be stored as a different type. For example, if the input and output are both int16, and one or more signals have spf > 1 and use the entire 16-bit range, then the sum of N samples will overflow an int16. Previously, it was fine simply to store the intermediate results in the output array itself, because the output array was 64-bit, and no WFDB format has more than 32-bit precision, and spf is (in practice) limited to at most 2**31-1. For simplicity, continue using int64 or float64 as the intermediate type, regardless of the actual input types and spf. At the same time, we can also optimize the calculation slightly by reshaping the input array and using np.sum, avoiding another Python loop. --- wfdb/io/_signal.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index 94076fee..a47fa140 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -830,32 +830,53 @@ def smooth_frames(self, sigtype='physical'): # Total samples per frame tspf = sum(spf) + # The output data type should be the smallest type that can + # represent any input sample value. The intermediate data type + # must be able to represent the sum of spf[ch] sample values. + if sigtype == 'physical': expanded_signal = self.e_p_signal - output_dtype = np.dtype('float64') + intermediate_dtype = np.dtype('float64') + allowed_dtypes = [ + np.dtype('float32'), + np.dtype('float64'), + ] elif sigtype == 'digital': expanded_signal = self.e_d_signal - output_dtype = np.dtype('int64') + intermediate_dtype = np.dtype('int64') + allowed_dtypes = [ + np.dtype('int8'), + np.dtype('int16'), + np.dtype('int32'), + np.dtype('int64'), + ] else: raise ValueError("sigtype must be 'physical' or 'digital'") n_sig = len(expanded_signal) sig_len = int(len(expanded_signal[0])/spf[0]) + input_dtypes = set() for ch in range(n_sig): if len(expanded_signal[ch]) != sig_len * spf[ch]: raise ValueError("length mismatch: signal %d has %d samples," " expected %dx%d" % (ch, len(expanded_signal), sig_len, spf[ch])) + input_dtypes.add(expanded_signal[ch].dtype) + + for output_dtype in allowed_dtypes: + if all(dt <= output_dtype for dt in input_dtypes): + break + signal = np.zeros((sig_len, n_sig), dtype=output_dtype) for ch in range(n_sig): if spf[ch] == 1: signal[:, ch] = expanded_signal[ch] else: - for frame in range(spf[ch]): - signal[:, ch] += expanded_signal[ch][frame::spf[ch]] - signal[:, ch] = signal[:, ch] / spf[ch] + frames = expanded_signal[ch].reshape(-1, spf[ch]) + signal_sum = np.sum(frames, axis=1, dtype=intermediate_dtype) + signal[:, ch] = signal_sum / spf[ch] return signal From 186ae72c1ec39a84745f3151f7b2b8f4bd03087a Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Tue, 1 Mar 2022 09:58:42 -0500 Subject: [PATCH 040/316] SignalMixin.smooth_frames: assorted optimizations. The variable tspf is not used and can be removed. sig_len can be calculated using // rather than / and truncating to an integer (the latter could be incorrect for records longer than 2**53.) The output array can be allocated using np.empty instead of np.zeros since it will be fully initialized by the subsequent loop. Since each frame is independent of the others, the loop can be broken up into blocks (here, arbitrarily chosen as 2**16 frames) to reduce temporary memory usage while still spending most of our CPU time in numpy operations. --- wfdb/io/_signal.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index a47fa140..ededa1b9 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -827,9 +827,6 @@ def smooth_frames(self, sigtype='physical'): if spf[ch] is None: spf[ch] = 1 - # Total samples per frame - tspf = sum(spf) - # The output data type should be the smallest type that can # represent any input sample value. The intermediate data type # must be able to represent the sum of spf[ch] sample values. @@ -854,7 +851,7 @@ def smooth_frames(self, sigtype='physical'): raise ValueError("sigtype must be 'physical' or 'digital'") n_sig = len(expanded_signal) - sig_len = int(len(expanded_signal[0])/spf[0]) + sig_len = len(expanded_signal[0]) // spf[0] input_dtypes = set() for ch in range(n_sig): if len(expanded_signal[ch]) != sig_len * spf[ch]: @@ -868,15 +865,22 @@ def smooth_frames(self, sigtype='physical'): if all(dt <= output_dtype for dt in input_dtypes): break - signal = np.zeros((sig_len, n_sig), dtype=output_dtype) + signal = np.empty((sig_len, n_sig), dtype=output_dtype) + + # Large input arrays will be processed in chunks to avoid the need + # to allocate a single huge temporary array. + CHUNK_SIZE = 65536 for ch in range(n_sig): if spf[ch] == 1: signal[:, ch] = expanded_signal[ch] else: frames = expanded_signal[ch].reshape(-1, spf[ch]) - signal_sum = np.sum(frames, axis=1, dtype=intermediate_dtype) - signal[:, ch] = signal_sum / spf[ch] + for chunk_start in range(0, sig_len, CHUNK_SIZE): + chunk_end = chunk_start + CHUNK_SIZE + signal_sum = np.sum(frames[chunk_start:chunk_end, :], + axis=1, dtype=intermediate_dtype) + signal[chunk_start:chunk_end, ch] = signal_sum / spf[ch] return signal From 4cd1d7b35a841a777b410b3ff102299361303cf5 Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Mon, 21 Mar 2022 13:18:33 -0400 Subject: [PATCH 041/316] TestRecord: test rdrecord with the return_res parameter. --- tests/test_record.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_record.py b/tests/test_record.py index dc879a91..b592ef7c 100644 --- a/tests/test_record.py +++ b/tests/test_record.py @@ -26,19 +26,22 @@ def test_1a(self): Target file created with: rdsamp -r sample-data/test01_00s | cut -f 2- > record-1a """ - record = wfdb.rdrecord('sample-data/test01_00s', physical=False) + record = wfdb.rdrecord('sample-data/test01_00s', + physical=False, return_res=16) sig = record.d_signal sig_target = np.genfromtxt('tests/target-output/record-1a') # Compare data streaming from Physionet - record_pn = wfdb.rdrecord('test01_00s', physical=False, - pn_dir='macecgdb') + record_pn = wfdb.rdrecord('test01_00s', pn_dir='macecgdb', + physical=False, return_res=16) # Test file writing - record_2 = wfdb.rdrecord('sample-data/test01_00s', physical=False) + record_2 = wfdb.rdrecord('sample-data/test01_00s', + physical=False, return_res=16) record_2.sig_name = ['ECG_1', 'ECG_2', 'ECG_3', 'ECG_4'] record_2.wrsamp() - record_write = wfdb.rdrecord('test01_00s', physical=False) + record_write = wfdb.rdrecord('test01_00s', + physical=False, return_res=16) assert np.array_equal(sig, sig_target) assert record.__eq__(record_pn) From f5961688f430c1a4a5418809b2ee48e84c57156c Mon Sep 17 00:00:00 2001 From: Chen Xie Date: Mon, 11 Apr 2022 16:38:50 -0700 Subject: [PATCH 042/316] Convert the README file to markdown. Add badges and citing section --- README.md | 50 +++++++++++++++++++++++++++++++++++++++++++ README.rst | 63 ------------------------------------------------------ 2 files changed, 50 insertions(+), 63 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/README.md b/README.md new file mode 100644 index 00000000..1c6e45cf --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# The WFDB Python Package + +![signals](https://raw.githubusercontent.com/MIT-LCP/wfdb-python/master/demo-img.png) + +[![tests workflow](https://github.com/MIT-LCP/wfdb-python/actions/workflows/run-tests.yml/badge.svg)](https://github.com/MIT-LCP/wfdb-python/actions?query=workflow%3Arun-tests+event%3Apush+branch%3Amaster) +[![PyPI Downloads](https://img.shields.io/pypi/dm/wfdb.svg?label=PyPI%20downloads)](https://pypi.org/project/wfdb/) +[![PhysioNet Project](https://img.shields.io/badge/DOI-10.13026%2Fegpf--2788-blue)](https://doi.org/10.13026/egpf-2788) +[![Supported Python Versions](https://img.shields.io/pypi/pyversions/wfdb.svg)](https://pypi.org/project/wfdb) + +## Introduction + +A Python-native package for reading, writing, processing, and plotting physiologic signal and annotation data. The core I/O functionality is based on the Waveform Database (WFDB) [specifications](https://github.com/wfdb/wfdb-spec/). + +This package is heavily inspired by the original [WFDB Software Package](https://www.physionet.org/content/wfdb/), and initially aimed to replicate many of its command-line APIs. However, the projects are independent, and there is no promise of consistency between the two, beyond each package adhering to the core specifications. + +## Documentation and Usage + +See the [documentation site](http://wfdb.readthedocs.io) for the public APIs. + +See the [demo.ipynb](https://github.com/MIT-LCP/wfdb-python/blob/master/demo.ipynb) notebook file for example use cases. + +## Installation + +The distribution is hosted on pypi at: . To directly install the package from pypi, run from your terminal:: + +```sh +pip install wfdb +``` + +The development version is hosted at: . This repository also contains demo scripts and example data. To install the development version, clone or download the repository, navigate to the base directory, and run: + +```sh +pip install . +``` + +## Development + +The package is to be expanded with physiological signal-processing tools, and general improvements. Development is made for Python 3.6+ only. + +## Contributing + +We welcome community contributions in the form of pull requests. When contributing code, please ensure: + +- [PEP8](https://www.python.org/dev/peps/pep-0008/) style guidelines are followed. +- Documentation is provided. New functions and classes should have numpy/scipy style [docstrings](https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt). +- Unit tests are written for new features that are not covered by [existing tests](https://github.com/MIT-LCP/wfdb-python/tree/master/tests). + +## Citing + +When using this resource, please cite the software [publication](https://physionet.org/content/wfdb-python/) oh PhysioNet. diff --git a/README.rst b/README.rst deleted file mode 100644 index ba834cf1..00000000 --- a/README.rst +++ /dev/null @@ -1,63 +0,0 @@ -wfdb-python -=========== - -.. figure:: https://raw.githubusercontent.com/MIT-LCP/wfdb-python/master/demo-img.png - :alt: wfdb signals - - -Introduction ------------- - -The native Python waveform-database (WFDB) package. A library of tools for reading, writing, and processing WFDB signals and annotations. - -Core components of this package are based on the original WFDB specifications. This package does not contain the exact same functionality as the original WFDB package. It aims to implement as many of its core features as possible, with user-friendly APIs. Additional useful physiological signal-processing tools are added over time. - - -Documentation and Usage ------------------------ - -See the `documentation site`_ for the public APIs. - -See the `demo.ipynb`_ notebook file for more example use cases. - - -Installation ------------- - -The distribution is hosted on pypi at: https://pypi.python.org/pypi/wfdb/. To directly install the package from pypi without needing to explicitly download content, run from your terminal:: - - $ pip install wfdb - -The development version is hosted at: https://github.com/MIT-LCP/wfdb-python. This repository also contains demo scripts and example data. To install the development version, clone or download the repository, navigate to the base directory, and run:: - - $ pip install . - - -Development ------------ - -The development repository is hosted at: https://github.com/MIT-LCP/wfdb-python - -The package is to be expanded with physiological signal-processing tools, and general improvements. Development is made for Python 3.6+ only. - - -Contributing ------------- - -We welcome community contributions in the form of pull requests. When contributing code, please ensure: - -* PEP8_ style guidelines are followed. -* Documentation is provided. New functions and classes should have numpy/scipy style docstrings_. -* Unit tests are written for new features that are not covered by `existing tests`_. - - -.. |Build Status| image:: https://travis-ci.org/MIT-LCP/wfdb-python.svg?branch=master - :target: https://travis-ci.org/MIT-LCP/wfdb-python - -.. _documentation site: http://wfdb.readthedocs.io - -.. _PEP8: https://www.python.org/dev/peps/pep-0008/ -.. _docstrings: https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt -.. _existing tests: https://github.com/MIT-LCP/wfdb-python/tree/master/tests - -.. _demo.ipynb: https://github.com/MIT-LCP/wfdb-python/blob/master/demo.ipynb From 0b1aa8f68b7879fff81f7fa16631e8c1a17b300a Mon Sep 17 00:00:00 2001 From: Benjamin Moody Date: Wed, 13 Apr 2022 13:19:41 -0400 Subject: [PATCH 043/316] plot.plot: simplify wording in documentation. --- wfdb/plot/plot.py | 50 +++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/wfdb/plot/plot.py b/wfdb/plot/plot.py index a1f09334..2010c2f2 100644 --- a/wfdb/plot/plot.py +++ b/wfdb/plot/plot.py @@ -15,11 +15,11 @@ def _expand_channels(signal): Parameters ---------- signal : 1d or 2d numpy array or list or None - The uniformly sampled signal or signals to be plotted. If signal - is a one-dimensional array, it is assumed to represent a single - channel. If it is a two-dimensional array, axes 0 and 1 must - represent time and channel number respectively. Otherwise it must - be a list of one-dimensional arrays (one for each channel.) + The signal or signals to be plotted. If signal is a + one-dimensional array, it is assumed to represent a single channel. + If it is a two-dimensional array, axes 0 and 1 must represent time + and channel number respectively. Otherwise it must be a list of + one-dimensional arrays (one for each channel.) Returns ------- @@ -124,11 +124,11 @@ def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None, Parameters ---------- signal : 1d or 2d numpy array or list, optional - The uniformly sampled signal or signals to be plotted. If signal - is a one-dimensional array, it is assumed to represent a single - channel. If it is a two-dimensional array, axes 0 and 1 must - represent time and channel number respectively. Otherwise it must - be a list of one-dimensional arrays (one for each channel). + The signal or signals to be plotted. If signal is a + one-dimensional array, it is assumed to represent a single channel. + If it is a two-dimensional array, axes 0 and 1 must represent time + and channel number respectively. Otherwise it must be a list of + one-dimensional arrays (one for each channel). ann_samp: list, optional A list of annotation locations to plot, with each list item corresponding to a different channel. List items may be: @@ -281,11 +281,11 @@ def get_plot_dims(signal, ann_samp): Parameters ---------- signal : 1d or 2d numpy array or list, optional - The uniformly sampled signal or signals to be plotted. If signal - is a one-dimensional array, it is assumed to represent a single - channel. If it is a two-dimensional array, axes 0 and 1 must - represent time and channel number respectively. Otherwise it must - be a list of one-dimensional arrays (one for each channel). + The signal or signals to be plotted. If signal is a + one-dimensional array, it is assumed to represent a single channel. + If it is a two-dimensional array, axes 0 and 1 must represent time + and channel number respectively. Otherwise it must be a list of + one-dimensional arrays (one for each channel). ann_samp: list, optional A list of annotation locations to plot, with each list item corresponding to a different channel. List items may be: @@ -373,11 +373,11 @@ def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes, Parameters ---------- signal : 1d or 2d numpy array or list - The uniformly sampled signal or signals to be plotted. If signal - is a one-dimensional array, it is assumed to represent a single - channel. If it is a two-dimensional array, axes 0 and 1 must - represent time and channel number respectively. Otherwise it must - be a list of one-dimensional arrays (one for each channel). + The signal or signals to be plotted. If signal is a + one-dimensional array, it is assumed to represent a single channel. + If it is a two-dimensional array, axes 0 and 1 must represent time + and channel number respectively. Otherwise it must be a list of + one-dimensional arrays (one for each channel). sig_len : int The signal length (per channel) of the dat file. Deprecated. n_sig : int @@ -457,11 +457,11 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, ann_sym : list The values of the annotation symbol locations. signal : 1d or 2d numpy array or list - The uniformly sampled signal or signals to be plotted. If signal - is a one-dimensional array, it is assumed to represent a single - channel. If it is a two-dimensional array, axes 0 and 1 must - represent time and channel number respectively. Otherwise it must - be a list of one-dimensional arrays (one for each channel). + The signal or signals to be plotted. If signal is a + one-dimensional array, it is assumed to represent a single channel. + If it is a two-dimensional array, axes 0 and 1 must represent time + and channel number respectively. Otherwise it must be a list of + one-dimensional arrays (one for each channel). n_sig : int The number of signals contained in the dat file. fs : float From b84a7656e1d5bb36e5e843b1331efeec0d1a722a Mon Sep 17 00:00:00 2001 From: Chen Date: Wed, 13 Apr 2022 11:00:33 -0700 Subject: [PATCH 044/316] Replace references to README.rst with README.md (#359) * Replace references to README.rst with README.md * Update desc content type --- MANIFEST.in | 4 ++-- setup.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 0ff3f716..2f5dfb98 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,11 +2,11 @@ include LICENSE # Include readme file -include README.rst +include README.md # Include the data files # recursive-include data * # If using Python 2.6 or less, then have to include package data, even though # it's already declared in setup.py -# include sample/*.dat \ No newline at end of file +# include sample/*.dat diff --git a/setup.py b/setup.py index e30da086..2549bc8f 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ here = path.abspath(path.dirname(__file__)) # Get the long description from the README file -with open(path.join(here, 'README.rst'), encoding='utf-8') as f: +with open(path.join(here, 'README.md'), encoding='utf-8') as f: long_description = f.read() # Get the version number from the version.py file @@ -28,6 +28,7 @@ description='The WFDB Python Toolbox', long_description=long_description, + long_description_content_type="text/markdown", # The project's main homepage. url='https://github.com/MIT-LCP/wfdb-python', From 994b794a6f994fc3132e106ca23f41bf6758d1d1 Mon Sep 17 00:00:00 2001 From: Chen Date: Tue, 19 Apr 2022 20:53:39 -0700 Subject: [PATCH 045/316] Switch to Poetry for dependency management and package description (#356) * Add poetry configuration file and update project dependencies * Update package description and patch version. * Remove requirements.txt, setup.py, and MANIFEST.in. * Update GH workflows. Set test Python version matrix to 3.7, 3.8, 3.9. * Add instructions for developing with Poetry and downloading dependencies --- .github/workflows/run-tests.yml | 15 +- .gitignore | 3 + MANIFEST.in | 12 - README.md | 36 ++- poetry.lock | 526 ++++++++++++++++++++++++++++++++ pyproject.toml | 24 ++ requirements.txt | 6 - setup.py | 106 ------- wfdb/version.py | 2 +- 9 files changed, 593 insertions(+), 137 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0409c388..e77a93dc 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] - python-version: [3.6, 3.7, 3.8] + python-version: ["3.7", "3.8", "3.9"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -27,11 +27,12 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Run nosetests - run: | - nosetests + python -m pip install --upgrade pip poetry + pip install ".[dev]" + - name: Run tests + run: nosetests + - name: Validate poetry file + run: poetry check test-deb10-i386: runs-on: ubuntu-latest @@ -55,6 +56,6 @@ jobs: # https://github.com/actions/checkout/issues/334 - uses: actions/checkout@v1 - - name: Run nosetests + - name: Run tests run: | nosetests3 diff --git a/.gitignore b/.gitignore index 84200fc5..ebdd3e46 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,6 @@ target/ # OSX .DS_Store .DS_Store + +# pyenv +.python-version diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2f5dfb98..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,12 +0,0 @@ -# Include the license file -include LICENSE - -# Include readme file -include README.md - -# Include the data files -# recursive-include data * - -# If using Python 2.6 or less, then have to include package data, even though -# it's already declared in setup.py -# include sample/*.dat diff --git a/README.md b/README.md index 1c6e45cf..42d83e03 100644 --- a/README.md +++ b/README.md @@ -21,23 +21,28 @@ See the [demo.ipynb](https://github.com/MIT-LCP/wfdb-python/blob/master/demo.ipy ## Installation -The distribution is hosted on pypi at: . To directly install the package from pypi, run from your terminal:: +The distribution is hosted on PyPI at: . The package can be directly installed from PyPI using either pip or poetry: ```sh pip install wfdb +poetry add wfdb ``` The development version is hosted at: . This repository also contains demo scripts and example data. To install the development version, clone or download the repository, navigate to the base directory, and run: ```sh +# Without dev dependencies pip install . -``` +poetry install -## Development +# With dev dependencies +pip install ".[dev]" +poetry install -E dev +``` -The package is to be expanded with physiological signal-processing tools, and general improvements. Development is made for Python 3.6+ only. +See the [note](#package-management) below about dev dependencies. -## Contributing +## Developing We welcome community contributions in the form of pull requests. When contributing code, please ensure: @@ -45,6 +50,27 @@ We welcome community contributions in the form of pull requests. When contributi - Documentation is provided. New functions and classes should have numpy/scipy style [docstrings](https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt). - Unit tests are written for new features that are not covered by [existing tests](https://github.com/MIT-LCP/wfdb-python/tree/master/tests). +### Package Management + +This project uses [poetry](https://python-poetry.org/docs/) for package management and distribution. + +Development dependencies are specified as optional dependencies, and then added to the "dev" extra group in the [pyproject.toml](./pyproject.toml) file. + +```sh +# Do NOT use: poetry add --dev +poetry add --optional +``` + +The `[tool.poetry.dev-dependencies]` attribute is NOT used because of a [limitation](https://github.com/python-poetry/poetry/issues/3514) that prevents these dependencies from being pip installable. Therefore, dev dependencies are not installed when purely running `poetry install`, and the `--no-dev` flag has no meaning in this project. + +Make sure the versions in [version.py](./wfdb/version.py) and [pyproject.toml](./pyproject.toml) are kept in sync. + +To upload a new distribution to PyPI: + +```sh +poetry publish +``` + ## Citing When using this resource, please cite the software [publication](https://physionet.org/content/wfdb-python/) oh PhysioNet. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..706379ee --- /dev/null +++ b/poetry.lock @@ -0,0 +1,526 @@ +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "cycler" +version = "0.11.0" +description = "Composable style cycles" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "fonttools" +version = "4.32.0" +description = "Tools to manipulate font files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=14.0.0)", "xattr"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["scipy", "munkres"] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=14.0.0)"] +woff = ["zopfli (>=0.1.4)", "brotlicffi (>=0.8.0)", "brotli (>=1.0.1)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "kiwisolver" +version = "1.4.2" +description = "A fast implementation of the Cassowary constraint solver" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "matplotlib" +version = "3.5.1" +description = "Python plotting package" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.0.1" +numpy = ">=1.17" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.2.1" +python-dateutil = ">=2.7" +setuptools_scm = ">=4" + +[[package]] +name = "nose" +version = "1.3.7" +description = "nose extends unittest to make testing easier" +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "numpy" +version = "1.21.1" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pandas" +version = "1.1.5" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +numpy = ">=1.15.4" +python-dateutil = ">=2.7.3" +pytz = ">=2017.2" + +[package.extras] +test = ["pytest (>=4.0.2)", "pytest-xdist", "hypothesis (>=3.58)"] + +[[package]] +name = "pillow" +version = "9.1.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "pyparsing" +version = "3.0.8" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2022.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "scipy" +version = "1.6.1" +description = "SciPy: Scientific Library for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +numpy = ">=1.16.5" + +[[package]] +name = "setuptools-scm" +version = "6.4.2" +description = "the blessed package to manage your versions by scm tags" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +packaging = ">=20.0" +tomli = ">=1.0.0" + +[package.extras] +test = ["pytest (>=6.2)", "virtualenv (>20)"] +toml = ["setuptools (>=42)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typing-extensions" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[extras] +dev = ["nose"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "1cfc9fad8f7f149573d271b2f21a0c5ef5d84bfbb7400e5dcdde083310fd33f0" + +[metadata.files] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +cycler = [ + {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, + {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, +] +fonttools = [ + {file = "fonttools-4.32.0-py3-none-any.whl", hash = "sha256:b038d1a0dee0079de7ade57071e2e2aced6e35bd697de244ac62938b2b1628c1"}, + {file = "fonttools-4.32.0.zip", hash = "sha256:59a90de72149893167e3d552ae2402c6874e006b9adc3feaf5f6d706fe20d392"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +kiwisolver = [ + {file = "kiwisolver-1.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e395ece147f0692ca7cdb05a028d31b83b72c369f7b4a2c1798f4b96af1e3d8"}, + {file = "kiwisolver-1.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b7f50a1a25361da3440f07c58cd1d79957c2244209e4f166990e770256b6b0b"}, + {file = "kiwisolver-1.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c032c41ae4c3a321b43a3650e6ecc7406b99ff3e5279f24c9b310f41bc98479"}, + {file = "kiwisolver-1.4.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1dcade8f6fe12a2bb4efe2cbe22116556e3b6899728d3b2a0d3b367db323eacc"}, + {file = "kiwisolver-1.4.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e45e780a74416ef2f173189ef4387e44b5494f45e290bcb1f03735faa6779bf"}, + {file = "kiwisolver-1.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d2bb56309fb75a811d81ed55fbe2208aa77a3a09ff5f546ca95e7bb5fac6eff"}, + {file = "kiwisolver-1.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b2d6c12f2ad5f55104a36a356192cfb680c049fe5e7c1f6620fc37f119cdc2"}, + {file = "kiwisolver-1.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:262c248c60f22c2b547683ad521e8a3db5909c71f679b93876921549107a0c24"}, + {file = "kiwisolver-1.4.2-cp310-cp310-win32.whl", hash = "sha256:1008346a7741620ab9cc6c96e8ad9b46f7a74ce839dbb8805ddf6b119d5fc6c2"}, + {file = "kiwisolver-1.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:6ece2e12e4b57bc5646b354f436416cd2a6f090c1dadcd92b0ca4542190d7190"}, + {file = "kiwisolver-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b978afdb913ca953cf128d57181da2e8798e8b6153be866ae2a9c446c6162f40"}, + {file = "kiwisolver-1.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f88c4b8e449908eeddb3bbd4242bd4dc2c7a15a7aa44bb33df893203f02dc2d"}, + {file = "kiwisolver-1.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e348f1904a4fab4153407f7ccc27e43b2a139752e8acf12e6640ba683093dd96"}, + {file = "kiwisolver-1.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c839bf28e45d7ddad4ae8f986928dbf5a6d42ff79760d54ec8ada8fb263e097c"}, + {file = "kiwisolver-1.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8ae5a071185f1a93777c79a9a1e67ac46544d4607f18d07131eece08d415083a"}, + {file = "kiwisolver-1.4.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c222f91a45da9e01a9bc4f760727ae49050f8e8345c4ff6525495f7a164c8973"}, + {file = "kiwisolver-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:a4e8f072db1d6fb7a7cc05a6dbef8442c93001f4bb604f1081d8c2db3ca97159"}, + {file = "kiwisolver-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:be9a650890fb60393e60aacb65878c4a38bb334720aa5ecb1c13d0dac54dd73b"}, + {file = "kiwisolver-1.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ec2e55bf31b43aabe32089125dca3b46fdfe9f50afbf0756ae11e14c97b80ca"}, + {file = "kiwisolver-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d1078ba770d6165abed3d9a1be1f9e79b61515de1dd00d942fa53bba79f01ae"}, + {file = "kiwisolver-1.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbb5eb4a2ea1ffec26268d49766cafa8f957fe5c1b41ad00733763fae77f9436"}, + {file = "kiwisolver-1.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e6cda72db409eefad6b021e8a4f964965a629f577812afc7860c69df7bdb84a"}, + {file = "kiwisolver-1.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1605c7c38cc6a85212dfd6a641f3905a33412e49f7c003f35f9ac6d71f67720"}, + {file = "kiwisolver-1.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81237957b15469ea9151ec8ca08ce05656090ffabc476a752ef5ad7e2644c526"}, + {file = "kiwisolver-1.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:240009fdf4fa87844f805e23f48995537a8cb8f8c361e35fda6b5ac97fcb906f"}, + {file = "kiwisolver-1.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:240c2d51d098395c012ddbcb9bd7b3ba5de412a1d11840698859f51d0e643c4f"}, + {file = "kiwisolver-1.4.2-cp38-cp38-win32.whl", hash = "sha256:8b6086aa6936865962b2cee0e7aaecf01ab6778ce099288354a7229b4d9f1408"}, + {file = "kiwisolver-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:0d98dca86f77b851350c250f0149aa5852b36572514d20feeadd3c6b1efe38d0"}, + {file = "kiwisolver-1.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:91eb4916271655dfe3a952249cb37a5c00b6ba68b4417ee15af9ba549b5ba61d"}, + {file = "kiwisolver-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa4d97d7d2b2c082e67907c0b8d9f31b85aa5d3ba0d33096b7116f03f8061261"}, + {file = "kiwisolver-1.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:71469b5845b9876b8d3d252e201bef6f47bf7456804d2fbe9a1d6e19e78a1e65"}, + {file = "kiwisolver-1.4.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8ff3033e43e7ca1389ee59fb7ecb8303abb8713c008a1da49b00869e92e3dd7c"}, + {file = "kiwisolver-1.4.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89b57c2984f4464840e4b768affeff6b6809c6150d1166938ade3e22fbe22db8"}, + {file = "kiwisolver-1.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffbdb9a96c536f0405895b5e21ee39ec579cb0ed97bdbd169ae2b55f41d73219"}, + {file = "kiwisolver-1.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a830a03970c462d1a2311c90e05679da56d3bd8e78a4ba9985cb78ef7836c9f"}, + {file = "kiwisolver-1.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f74f2a13af201559e3d32b9ddfc303c94ae63d63d7f4326d06ce6fe67e7a8255"}, + {file = "kiwisolver-1.4.2-cp39-cp39-win32.whl", hash = "sha256:e677cc3626287f343de751e11b1e8a5b915a6ac897e8aecdbc996cd34de753a0"}, + {file = "kiwisolver-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b3e251e5c38ac623c5d786adb21477f018712f8c6fa54781bd38aa1c60b60fc2"}, + {file = "kiwisolver-1.4.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0c380bb5ae20d829c1a5473cfcae64267b73aaa4060adc091f6df1743784aae0"}, + {file = "kiwisolver-1.4.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:484f2a5f0307bc944bc79db235f41048bae4106ffa764168a068d88b644b305d"}, + {file = "kiwisolver-1.4.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e8afdf533b613122e4bbaf3c1e42c2a5e9e2d1dd3a0a017749a7658757cb377"}, + {file = "kiwisolver-1.4.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42f6ef9b640deb6f7d438e0a371aedd8bef6ddfde30683491b2e6f568b4e884e"}, + {file = "kiwisolver-1.4.2.tar.gz", hash = "sha256:7f606d91b8a8816be476513a77fd30abe66227039bd6f8b406c348cb0247dcc9"}, +] +matplotlib = [ + {file = "matplotlib-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:456cc8334f6d1124e8ff856b42d2cc1c84335375a16448189999496549f7182b"}, + {file = "matplotlib-3.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a77906dc2ef9b67407cec0bdbf08e3971141e535db888974a915be5e1e3efc6"}, + {file = "matplotlib-3.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e70ae6475cfd0fad3816dcbf6cac536dc6f100f7474be58d59fa306e6e768a4"}, + {file = "matplotlib-3.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53273c5487d1c19c3bc03b9eb82adaf8456f243b97ed79d09dded747abaf1235"}, + {file = "matplotlib-3.5.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3b6f3fd0d8ca37861c31e9a7cab71a0ef14c639b4c95654ea1dd153158bf0df"}, + {file = "matplotlib-3.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8c87cdaf06fd7b2477f68909838ff4176f105064a72ca9d24d3f2a29f73d393"}, + {file = "matplotlib-3.5.1-cp310-cp310-win32.whl", hash = "sha256:e2f28a07b4f82abb40267864ad7b3a4ed76f1b1663e81c7efc84a9b9248f672f"}, + {file = "matplotlib-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:d70a32ee1f8b55eed3fd4e892f0286df8cccc7e0475c11d33b5d0a148f5c7599"}, + {file = "matplotlib-3.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:68fa30cec89b6139dc559ed6ef226c53fd80396da1919a1b5ef672c911aaa767"}, + {file = "matplotlib-3.5.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3484d8455af3fdb0424eae1789af61f6a79da0c80079125112fd5c1b604218"}, + {file = "matplotlib-3.5.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e293b16cf303fe82995e41700d172a58a15efc5331125d08246b520843ef21ee"}, + {file = "matplotlib-3.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e3520a274a0e054e919f5b3279ee5dbccf5311833819ccf3399dab7c83e90a25"}, + {file = "matplotlib-3.5.1-cp37-cp37m-win32.whl", hash = "sha256:2252bfac85cec7af4a67e494bfccf9080bcba8a0299701eab075f48847cca907"}, + {file = "matplotlib-3.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:abf67e05a1b7f86583f6ebd01f69b693b9c535276f4e943292e444855870a1b8"}, + {file = "matplotlib-3.5.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6c094e4bfecd2fa7f9adffd03d8abceed7157c928c2976899de282f3600f0a3d"}, + {file = "matplotlib-3.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:506b210cc6e66a0d1c2bb765d055f4f6bc2745070fb1129203b67e85bbfa5c18"}, + {file = "matplotlib-3.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b04fc29bcef04d4e2d626af28d9d892be6aba94856cb46ed52bcb219ceac8943"}, + {file = "matplotlib-3.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577ed20ec9a18d6bdedb4616f5e9e957b4c08563a9f985563a31fd5b10564d2a"}, + {file = "matplotlib-3.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e486f60db0cd1c8d68464d9484fd2a94011c1ac8593d765d0211f9daba2bd535"}, + {file = "matplotlib-3.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b71f3a7ca935fc759f2aed7cec06cfe10bc3100fadb5dbd9c435b04e557971e1"}, + {file = "matplotlib-3.5.1-cp38-cp38-win32.whl", hash = "sha256:d24e5bb8028541ce25e59390122f5e48c8506b7e35587e5135efcb6471b4ac6c"}, + {file = "matplotlib-3.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:778d398c4866d8e36ee3bf833779c940b5f57192fa0a549b3ad67bc4c822771b"}, + {file = "matplotlib-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bb1c613908f11bac270bc7494d68b1ef6e7c224b7a4204d5dacf3522a41e2bc3"}, + {file = "matplotlib-3.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:edf5e4e1d5fb22c18820e8586fb867455de3b109c309cb4fce3aaed85d9468d1"}, + {file = "matplotlib-3.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:40e0d7df05e8efe60397c69b467fc8f87a2affeb4d562fe92b72ff8937a2b511"}, + {file = "matplotlib-3.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a350ca685d9f594123f652ba796ee37219bf72c8e0fc4b471473d87121d6d34"}, + {file = "matplotlib-3.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3e66497cd990b1a130e21919b004da2f1dc112132c01ac78011a90a0f9229778"}, + {file = "matplotlib-3.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:87900c67c0f1728e6db17c6809ec05c025c6624dcf96a8020326ea15378fe8e7"}, + {file = "matplotlib-3.5.1-cp39-cp39-win32.whl", hash = "sha256:b8a4fb2a0c5afbe9604f8a91d7d0f27b1832c3e0b5e365f95a13015822b4cd65"}, + {file = "matplotlib-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:fe8d40c434a8e2c68d64c6d6a04e77f21791a93ff6afe0dce169597c110d3079"}, + {file = "matplotlib-3.5.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34a1fc29f8f96e78ec57a5eff5e8d8b53d3298c3be6df61e7aa9efba26929522"}, + {file = "matplotlib-3.5.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b19a761b948e939a9e20173aaae76070025f0024fc8f7ba08bef22a5c8573afc"}, + {file = "matplotlib-3.5.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6803299cbf4665eca14428d9e886de62e24f4223ac31ab9c5d6d5339a39782c7"}, + {file = "matplotlib-3.5.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14334b9902ec776461c4b8c6516e26b450f7ebe0b3ef8703bf5cdfbbaecf774a"}, + {file = "matplotlib-3.5.1.tar.gz", hash = "sha256:b2e9810e09c3a47b73ce9cab5a72243a1258f61e7900969097a817232246ce1c"}, +] +nose = [ + {file = "nose-1.3.7-py2-none-any.whl", hash = "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a"}, + {file = "nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac"}, + {file = "nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"}, +] +numpy = [ + {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, + {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, + {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, + {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, + {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, + {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, + {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, + {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, + {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pandas = [ + {file = "pandas-1.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bf23a3b54d128b50f4f9d4675b3c1857a688cc6731a32f931837d72effb2698d"}, + {file = "pandas-1.1.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a780260afc88268a9d3ac3511d8f494fdcf637eece62fb9eb656a63d53eb7ca"}, + {file = "pandas-1.1.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b61080750d19a0122469ab59b087380721d6b72a4e7d962e4d7e63e0c4504814"}, + {file = "pandas-1.1.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:0de3ddb414d30798cbf56e642d82cac30a80223ad6fe484d66c0ce01a84d6f2f"}, + {file = "pandas-1.1.5-cp36-cp36m-win32.whl", hash = "sha256:70865f96bb38fec46f7ebd66d4b5cfd0aa6b842073f298d621385ae3898d28b5"}, + {file = "pandas-1.1.5-cp36-cp36m-win_amd64.whl", hash = "sha256:19a2148a1d02791352e9fa637899a78e371a3516ac6da5c4edc718f60cbae648"}, + {file = "pandas-1.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26fa92d3ac743a149a31b21d6f4337b0594b6302ea5575b37af9ca9611e8981a"}, + {file = "pandas-1.1.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c16d59c15d946111d2716856dd5479221c9e4f2f5c7bc2d617f39d870031e086"}, + {file = "pandas-1.1.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3be7a7a0ca71a2640e81d9276f526bca63505850add10206d0da2e8a0a325dae"}, + {file = "pandas-1.1.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:573fba5b05bf2c69271a32e52399c8de599e4a15ab7cec47d3b9c904125ab788"}, + {file = "pandas-1.1.5-cp37-cp37m-win32.whl", hash = "sha256:21b5a2b033380adbdd36b3116faaf9a4663e375325831dac1b519a44f9e439bb"}, + {file = "pandas-1.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:24c7f8d4aee71bfa6401faeba367dd654f696a77151a8a28bc2013f7ced4af98"}, + {file = "pandas-1.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2860a97cbb25444ffc0088b457da0a79dc79f9c601238a3e0644312fcc14bf11"}, + {file = "pandas-1.1.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5008374ebb990dad9ed48b0f5d0038124c73748f5384cc8c46904dace27082d9"}, + {file = "pandas-1.1.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2c2f7c670ea4e60318e4b7e474d56447cf0c7d83b3c2a5405a0dbb2600b9c48e"}, + {file = "pandas-1.1.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0a643bae4283a37732ddfcecab3f62dd082996021b980f580903f4e8e01b3c5b"}, + {file = "pandas-1.1.5-cp38-cp38-win32.whl", hash = "sha256:5447ea7af4005b0daf695a316a423b96374c9c73ffbd4533209c5ddc369e644b"}, + {file = "pandas-1.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:4c62e94d5d49db116bef1bd5c2486723a292d79409fc9abd51adf9e05329101d"}, + {file = "pandas-1.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:731568be71fba1e13cae212c362f3d2ca8932e83cb1b85e3f1b4dd77d019254a"}, + {file = "pandas-1.1.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c61c043aafb69329d0f961b19faa30b1dab709dd34c9388143fc55680059e55a"}, + {file = "pandas-1.1.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2b1c6cd28a0dfda75c7b5957363333f01d370936e4c6276b7b8e696dd500582a"}, + {file = "pandas-1.1.5-cp39-cp39-win32.whl", hash = "sha256:c94ff2780a1fd89f190390130d6d36173ca59fcfb3fe0ff596f9a56518191ccb"}, + {file = "pandas-1.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:edda9bacc3843dfbeebaf7a701763e68e741b08fccb889c003b0a52f0ee95782"}, + {file = "pandas-1.1.5.tar.gz", hash = "sha256:f10fc41ee3c75a474d3bdf68d396f10782d013d7f67db99c0efbfd0acb99701b"}, +] +pillow = [ + {file = "Pillow-9.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea"}, + {file = "Pillow-9.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e"}, + {file = "Pillow-9.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3"}, + {file = "Pillow-9.1.0-cp310-cp310-win32.whl", hash = "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160"}, + {file = "Pillow-9.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033"}, + {file = "Pillow-9.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2"}, + {file = "Pillow-9.1.0-cp37-cp37m-win32.whl", hash = "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244"}, + {file = "Pillow-9.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e"}, + {file = "Pillow-9.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5"}, + {file = "Pillow-9.1.0-cp38-cp38-win32.whl", hash = "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a"}, + {file = "Pillow-9.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331"}, + {file = "Pillow-9.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8"}, + {file = "Pillow-9.1.0-cp39-cp39-win32.whl", hash = "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58"}, + {file = "Pillow-9.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, + {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, +] +pyparsing = [ + {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, + {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pytz = [ + {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, + {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +scipy = [ + {file = "scipy-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a15a1f3fc0abff33e792d6049161b7795909b40b97c6cc2934ed54384017ab76"}, + {file = "scipy-1.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e79570979ccdc3d165456dd62041d9556fb9733b86b4b6d818af7a0afc15f092"}, + {file = "scipy-1.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a423533c55fec61456dedee7b6ee7dce0bb6bfa395424ea374d25afa262be261"}, + {file = "scipy-1.6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:33d6b7df40d197bdd3049d64e8e680227151673465e5d85723b3b8f6b15a6ced"}, + {file = "scipy-1.6.1-cp37-cp37m-win32.whl", hash = "sha256:6725e3fbb47da428794f243864f2297462e9ee448297c93ed1dcbc44335feb78"}, + {file = "scipy-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:5fa9c6530b1661f1370bcd332a1e62ca7881785cc0f80c0d559b636567fab63c"}, + {file = "scipy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd50daf727f7c195e26f27467c85ce653d41df4358a25b32434a50d8870fc519"}, + {file = "scipy-1.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f46dd15335e8a320b0fb4685f58b7471702234cba8bb3442b69a3e1dc329c345"}, + {file = "scipy-1.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0e5b0ccf63155d90da576edd2768b66fb276446c371b73841e3503be1d63fb5d"}, + {file = "scipy-1.6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2481efbb3740977e3c831edfd0bd9867be26387cacf24eb5e366a6a374d3d00d"}, + {file = "scipy-1.6.1-cp38-cp38-win32.whl", hash = "sha256:68cb4c424112cd4be886b4d979c5497fba190714085f46b8ae67a5e4416c32b4"}, + {file = "scipy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:5f331eeed0297232d2e6eea51b54e8278ed8bb10b099f69c44e2558c090d06bf"}, + {file = "scipy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8a51d33556bf70367452d4d601d1742c0e806cd0194785914daf19775f0e67"}, + {file = "scipy-1.6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:83bf7c16245c15bc58ee76c5418e46ea1811edcc2e2b03041b804e46084ab627"}, + {file = "scipy-1.6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:794e768cc5f779736593046c9714e0f3a5940bc6dcc1dba885ad64cbfb28e9f0"}, + {file = "scipy-1.6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5da5471aed911fe7e52b86bf9ea32fb55ae93e2f0fac66c32e58897cfb02fa07"}, + {file = "scipy-1.6.1-cp39-cp39-win32.whl", hash = "sha256:8e403a337749ed40af60e537cc4d4c03febddcc56cd26e774c9b1b600a70d3e4"}, + {file = "scipy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a5193a098ae9f29af283dcf0041f762601faf2e595c0db1da929875b7570353f"}, + {file = "scipy-1.6.1.tar.gz", hash = "sha256:c4fceb864890b6168e79b0e714c585dbe2fd4222768ee90bc1aa0f8218691b11"}, +] +setuptools-scm = [ + {file = "setuptools_scm-6.4.2-py3-none-any.whl", hash = "sha256:acea13255093849de7ccb11af9e1fb8bde7067783450cee9ef7a93139bddf6d4"}, + {file = "setuptools_scm-6.4.2.tar.gz", hash = "sha256:6833ac65c6ed9711a4d5d2266f8024cfa07c533a0e55f4c12f6eff280a5a9e30"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typing-extensions = [ + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, +] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..db065e97 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "wfdb" +version = "4.0.0a0" +description = "The WFDB Python package: tools for reading, writing, and processing physiologic signals and annotations." +authors = ["The Laboratory for Computational Physiology "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.7" +numpy = "^1.10.1" +scipy = "^1.0.0" +pandas = "^1.0.0" +matplotlib = "^3.3.4" +requests = "^2.8.1" +nose = {version = "^1.3.7", optional = true} + +[tool.poetry.extras] +dev = ["nose"] + +# Do NOT use [tool.poetry.dev-dependencies]. See: https://github.com/python-poetry/poetry/issues/3514 + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f9bb9b08..00000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -matplotlib==3.3.4 -nose==1.3.7 -numpy==1.18.5 -pandas==1.0.3 -requests==2.23.0 -scipy==1.4.1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 2549bc8f..00000000 --- a/setup.py +++ /dev/null @@ -1,106 +0,0 @@ -"""The WFDB Python Toolbox. -See: https://www.physionet.org/physiotools/wfdb.shtml -""" - -# Always prefer setuptools over distutils -from setuptools import setup, find_packages -# To use a consistent encoding -from codecs import open -from os import path - -here = path.abspath(path.dirname(__file__)) - -# Get the long description from the README file -with open(path.join(here, 'README.md'), encoding='utf-8') as f: - long_description = f.read() - -# Get the version number from the version.py file -with open('wfdb/version.py') as f: - __version__ = f.read().split()[-1].strip("'") - -setup( - name='wfdb', - - # Versions should comply with PEP440. For a discussion on single-sourcing - # the version across setup.py and the project code, see - # https://packaging.python.org/en/latest/single_source_version.html - version=__version__, - - description='The WFDB Python Toolbox', - long_description=long_description, - long_description_content_type="text/markdown", - - # The project's main homepage. - url='https://github.com/MIT-LCP/wfdb-python', - - # Author details - author='The Laboratory for Computational Physiology', - author_email='support@physionet.org', - - # Choose your license - license='MIT', - - # What does your project relate to? - keywords='WFDB clinical waveform', - - # You can just specify the packages manually here if your project is - # simple. Or you can use find_packages(). - # packages=find_packages(exclude=['contrib', 'docs', 'tests']), - packages=find_packages(), - - # Alternatively, if you want to distribute just a my_module.py, uncomment - # this: - # py_modules=["my_module"], - - # List run-time dependencies here. These will be installed by pip when - # your project is installed. For an analysis of "install_requires" vs pip's - # requirements files see: - # https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/?highlight=requirements - install_requires=[ - 'matplotlib>=3.3.4', - 'numpy>=1.10.1', - 'pandas>=0.17.0', - 'requests>=2.8.1', - 'scipy>=0.17.0', - ], - - # List additional groups of dependencies here (e.g. development - # dependencies). You can install these using the following syntax, - # for example: - # $ pip install -e .[dev,test] - extras_require={ - 'test': ['nose>=1.3.7'] - }, - - # If there are data files included in your packages that need to be - # installed, specify them here. If using Python 2.6 or less, then these - # have to be included in MANIFEST.in as well. - # package_data={'wfdb': ['wfdb.config'], - # }, - - # Although 'package_data' is the preferred approach, in some case you may - # need to place data files outside of your packages. See: - # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa - # In this case, 'data_file' will be installed into '/my_data' - # data_files=[('my_data', ['data/data_file'])], - # data_files=[('config', ['wfdb.config'])], - - # To provide executable scripts, use entry points in preference to the - # "scripts" keyword. Entry points provide cross-platform support and allow - # pip to create the appropriate form of executable for the target platform. - # entry_points={ - # 'console_scripts': [ - # 'sample=sample:main', - # ], - # }, - - # Add ways to quickly filter project - classifiers=[ - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "License :: OSI Approved :: MIT License" - ], -) diff --git a/wfdb/version.py b/wfdb/version.py index da4564dd..79fb7745 100644 --- a/wfdb/version.py +++ b/wfdb/version.py @@ -1 +1 @@ -__version__ = '3.4.1' +__version__ = '4.0.0a0' From 34059488f7fd3be3e04390df17b796dc073ba086 Mon Sep 17 00:00:00 2001 From: Chen Date: Wed, 20 Apr 2022 07:51:01 -0700 Subject: [PATCH 046/316] Replace nose with pytest and add Python 3.10 to testing matrix (#363) * Replace nose with pytest * Update workflow to use pytest and include Python 3.10 * Add test notes --- .github/workflows/run-tests.yml | 8 +- README.md | 11 ++ poetry.lock | 218 +++++++++++++++++++++++++++++--- pyproject.toml | 5 +- 4 files changed, 221 insertions(+), 21 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e77a93dc..18a39019 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -30,7 +30,7 @@ jobs: python -m pip install --upgrade pip poetry pip install ".[dev]" - name: Run tests - run: nosetests + run: pytest - name: Validate poetry file run: poetry check @@ -47,7 +47,7 @@ jobs: python3-pandas \ python3-requests \ python3-scipy \ - python3-nose \ + python3-pytest \ git # Note: "actions/checkout@v2" requires libstdc++6:amd64 to be @@ -58,4 +58,4 @@ jobs: - name: Run tests run: | - nosetests3 + pytest-3 diff --git a/README.md b/README.md index 42d83e03..fc5b29cc 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,17 @@ To upload a new distribution to PyPI: poetry publish ``` +### Tests + +Run tests using pytest: + +```sh +pytest +# Distribute tests across multiple cores. +# https://github.com/pytest-dev/pytest-xdist +pytest -n auto +``` + ## Citing When using this resource, please cite the software [publication](https://physionet.org/content/wfdb-python/) oh PhysioNet. diff --git a/poetry.lock b/poetry.lock index 706379ee..716a1543 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,25 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + [[package]] name = "certifi" version = "2021.10.8" @@ -17,6 +39,14 @@ python-versions = ">=3.5.0" [package.extras] unicode_backport = ["unicodedata2"] +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "cycler" version = "0.11.0" @@ -25,6 +55,17 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +testing = ["pre-commit"] + [[package]] name = "fonttools" version = "4.32.0" @@ -54,6 +95,31 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "importlib-metadata" +version = "4.11.3" +description = "Read metadata from Python packages" +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = true +python-versions = "*" + [[package]] name = "kiwisolver" version = "1.4.2" @@ -84,14 +150,6 @@ pyparsing = ">=2.2.1" python-dateutil = ">=2.7" setuptools_scm = ">=4" -[[package]] -name = "nose" -version = "1.3.7" -description = "nose extends unittest to make testing easier" -category = "main" -optional = true -python-versions = "*" - [[package]] name = "numpy" version = "1.21.1" @@ -139,6 +197,29 @@ python-versions = ">=3.7" docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pyparsing" version = "3.0.8" @@ -150,6 +231,58 @@ python-versions = ">=3.6.8" [package.extras] diagrams = ["railroad-diagrams", "jinja2"] +[[package]] +name = "pytest" +version = "7.1.1" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-forked" +version = "1.4.0" +description = "run tests in isolated forked subprocesses" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +py = "*" +pytest = ">=3.10" + +[[package]] +name = "pytest-xdist" +version = "2.5.0" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" +pytest-forked = "*" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -251,15 +384,35 @@ brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "zipp" +version = "3.8.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = true +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + [extras] -dev = ["nose"] +dev = ["pytest", "pytest-xdist"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "1cfc9fad8f7f149573d271b2f21a0c5ef5d84bfbb7400e5dcdde083310fd33f0" +content-hash = "c6c4a7b3fae4ba46a2ca543ab32f536b734eb47dd014c907f7f3ce18347c4c38" [metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, @@ -268,10 +421,18 @@ charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] cycler = [ {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, ] +execnet = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] fonttools = [ {file = "fonttools-4.32.0-py3-none-any.whl", hash = "sha256:b038d1a0dee0079de7ade57071e2e2aced6e35bd697de244ac62938b2b1628c1"}, {file = "fonttools-4.32.0.zip", hash = "sha256:59a90de72149893167e3d552ae2402c6874e006b9adc3feaf5f6d706fe20d392"}, @@ -280,6 +441,14 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] +importlib-metadata = [ + {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"}, + {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] kiwisolver = [ {file = "kiwisolver-1.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e395ece147f0692ca7cdb05a028d31b83b72c369f7b4a2c1798f4b96af1e3d8"}, {file = "kiwisolver-1.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b7f50a1a25361da3440f07c58cd1d79957c2244209e4f166990e770256b6b0b"}, @@ -362,11 +531,6 @@ matplotlib = [ {file = "matplotlib-3.5.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14334b9902ec776461c4b8c6516e26b450f7ebe0b3ef8703bf5cdfbbaecf774a"}, {file = "matplotlib-3.5.1.tar.gz", hash = "sha256:b2e9810e09c3a47b73ce9cab5a72243a1258f61e7900969097a817232246ce1c"}, ] -nose = [ - {file = "nose-1.3.7-py2-none-any.whl", hash = "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a"}, - {file = "nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac"}, - {file = "nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"}, -] numpy = [ {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, @@ -467,10 +631,30 @@ pillow = [ {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, ] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] pyparsing = [ {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, ] +pytest = [ + {file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, + {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, +] +pytest-forked = [ + {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, + {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, +] +pytest-xdist = [ + {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, + {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, +] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -524,3 +708,7 @@ urllib3 = [ {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] +zipp = [ + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, +] diff --git a/pyproject.toml b/pyproject.toml index db065e97..f9c849b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,10 +12,11 @@ scipy = "^1.0.0" pandas = "^1.0.0" matplotlib = "^3.3.4" requests = "^2.8.1" -nose = {version = "^1.3.7", optional = true} +pytest = {version = "^7.1.1", optional = true} +pytest-xdist = {version = "^2.5.0", optional = true} [tool.poetry.extras] -dev = ["nose"] +dev = ["pytest", "pytest-xdist"] # Do NOT use [tool.poetry.dev-dependencies]. See: https://github.com/python-poetry/poetry/issues/3514 From 3b408f23f50889c7d1c6fee9329c76b739b9f667 Mon Sep 17 00:00:00 2001 From: cx1111 Date: Tue, 26 Apr 2022 07:42:16 -0700 Subject: [PATCH 047/316] Add pylint and black. Apply formatting (#364) * Install pylint and black * Add black config and pylintrc file * Apply black formatting * Add black format to workflow --- .github/workflows/run-tests.yml | 2 + docs/conf.py | 64 +- poetry.lock | 360 +++- pylintrc | 564 +++++++ pyproject.toml | 8 +- tests/__init__.py | 2 +- tests/test_annotation.py | 204 ++- tests/test_plot.py | 22 +- tests/test_processing.py | 170 +- tests/test_record.py | 529 +++--- tests/test_url.py | 103 +- wfdb/__init__.py | 46 +- wfdb/io/__init__.py | 47 +- wfdb/io/_header.py | 501 +++--- wfdb/io/_signal.py | 851 ++++++---- wfdb/io/_url.py | 174 +- wfdb/io/annotation.py | 1726 +++++++++++++------- wfdb/io/download.py | 108 +- wfdb/io/record.py | 2721 +++++++++++++++++++------------ wfdb/io/tff.py | 114 +- wfdb/plot/plot.py | 358 ++-- wfdb/processing/__init__.py | 16 +- wfdb/processing/basic.py | 57 +- wfdb/processing/evaluate.py | 202 ++- wfdb/processing/hr.py | 32 +- wfdb/processing/peaks.py | 96 +- wfdb/processing/qrs.py | 377 +++-- wfdb/version.py | 2 +- 28 files changed, 6373 insertions(+), 3083 deletions(-) create mode 100644 pylintrc diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 18a39019..dc4d76a1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -33,6 +33,8 @@ jobs: run: pytest - name: Validate poetry file run: poetry check + - name: Format files + run: black . test-deb10-i386: runs-on: ubuntu-latest diff --git a/docs/conf.py b/docs/conf.py index 9f9758db..1a236b0b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,16 +20,19 @@ import os import sys -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # For dependencies from unittest.mock import MagicMock + + class Mock(MagicMock): @classmethod def __getattr__(cls, name): - return MagicMock() + return MagicMock() -MOCK_MODULES = ['numpy', 'matplotlib', 'matplotlib.pyplot', 'pandas', 'scipy'] + +MOCK_MODULES = ["numpy", "matplotlib", "matplotlib.pyplot", "pandas", "scipy"] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) # -- General configuration ------------------------------------------------ @@ -41,26 +44,26 @@ def __getattr__(cls, name): # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc'] +extensions = ["sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'wfdb' -copyright = '2018, MIT Lab for Computational Physiology' -author = 'MIT Lab for Computational Physiology' +project = "wfdb" +copyright = "2018, MIT Lab for Computational Physiology" +author = "MIT Lab for Computational Physiology" -with open('../wfdb/version.py') as f: +with open("../wfdb/version.py") as f: __version__ = f.read().split()[-1].strip("'") # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -81,10 +84,10 @@ def __getattr__(cls, name): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -95,7 +98,7 @@ def __getattr__(cls, name): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'classic' +html_theme = "classic" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -106,7 +109,7 @@ def __getattr__(cls, name): # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -114,9 +117,9 @@ def __getattr__(cls, name): # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { - '**': [ - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', + "**": [ + "relations.html", # needs 'show_related': True theme option to display + "searchbox.html", ] } @@ -124,7 +127,7 @@ def __getattr__(cls, name): # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'wfdbdoc' +htmlhelp_basename = "wfdbdoc" # -- Options for LaTeX output --------------------------------------------- @@ -133,15 +136,12 @@ def __getattr__(cls, name): # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -151,8 +151,7 @@ def __getattr__(cls, name): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'wfdb.tex', 'wfdb Documentation', - author, 'manual'), + (master_doc, "wfdb.tex", "wfdb Documentation", author, "manual"), ] @@ -160,10 +159,7 @@ def __getattr__(cls, name): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'wfdb', 'wfdb Documentation', - [author], 1) -] +man_pages = [(master_doc, "wfdb", "wfdb Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -172,7 +168,13 @@ def __getattr__(cls, name): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'wfdb', 'wfdb Documentation', - author, 'wfdb', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "wfdb", + "wfdb Documentation", + author, + "wfdb", + "One line description of project.", + "Miscellaneous", + ), ] diff --git a/poetry.lock b/poetry.lock index 716a1543..8734e175 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,17 @@ +[[package]] +name = "astroid" +version = "2.11.3" +description = "An abstract syntax tree for Python with inference support." +category = "main" +optional = true +python-versions = ">=3.6.2" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = ">=1.11,<2" + [[package]] name = "atomicwrites" version = "1.4.0" @@ -20,6 +34,29 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +[[package]] +name = "black" +version = "22.3.0" +description = "The uncompromising code formatter." +category = "main" +optional = true +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2021.10.8" @@ -39,6 +76,18 @@ python-versions = ">=3.5.0" [package.extras] unicode_backport = ["unicodedata2"] +[[package]] +name = "click" +version = "8.1.2" +description = "Composable command line interface toolkit" +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + [[package]] name = "colorama" version = "0.4.4" @@ -55,6 +104,17 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "dill" +version = "0.3.4" +description = "serialize all of python" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*" + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + [[package]] name = "execnet" version = "1.9.0" @@ -68,19 +128,20 @@ testing = ["pre-commit"] [[package]] name = "fonttools" -version = "4.32.0" +version = "4.33.0" description = "Tools to manipulate font files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=14.0.0)", "xattr"] +all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "uharfbuzz (>=0.23.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=14.0.0)", "xattr"] graphite = ["lz4 (>=1.7.4.2)"] interpolatable = ["scipy", "munkres"] lxml = ["lxml (>=4.0,<5)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] symfont = ["sympy"] type1 = ["xattr"] ufo = ["fs (>=2.2.0,<3)"] @@ -120,6 +181,20 @@ category = "main" optional = true python-versions = "*" +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "main" +optional = true +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + [[package]] name = "kiwisolver" version = "1.4.2" @@ -131,6 +206,14 @@ python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.8\""} +[[package]] +name = "lazy-object-proxy" +version = "1.7.1" +description = "A fast and thorough lazy object proxy." +category = "main" +optional = true +python-versions = ">=3.6" + [[package]] name = "matplotlib" version = "3.5.1" @@ -150,6 +233,22 @@ pyparsing = ">=2.2.1" python-dateutil = ">=2.7" setuptools_scm = ">=4" +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "main" +optional = true +python-versions = ">=3.6" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "main" +optional = true +python-versions = "*" + [[package]] name = "numpy" version = "1.21.1" @@ -185,6 +284,14 @@ pytz = ">=2017.2" [package.extras] test = ["pytest (>=4.0.2)", "pytest-xdist", "hypothesis (>=3.58)"] +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "main" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [[package]] name = "pillow" version = "9.1.0" @@ -197,6 +304,18 @@ python-versions = ">=3.7" docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = true +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + [[package]] name = "pluggy" version = "1.0.0" @@ -220,6 +339,27 @@ category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pylint" +version = "2.13.7" +description = "python code static checker" +category = "main" +optional = true +python-versions = ">=3.6.2" + +[package.dependencies] +astroid = ">=2.11.3,<=2.12.0-dev0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +dill = ">=0.2" +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +testutil = ["gitpython (>3)"] + [[package]] name = "pyparsing" version = "3.0.8" @@ -363,6 +503,14 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "typed-ast" +version = "1.5.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "main" +optional = true +python-versions = ">=3.6" + [[package]] name = "typing-extensions" version = "4.2.0" @@ -384,6 +532,14 @@ brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "wrapt" +version = "1.14.0" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [[package]] name = "zipp" version = "3.8.0" @@ -397,14 +553,18 @@ docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [extras] -dev = ["pytest", "pytest-xdist"] +dev = ["pytest", "pytest-xdist", "pylint", "black"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "c6c4a7b3fae4ba46a2ca543ab32f536b734eb47dd014c907f7f3ce18347c4c38" +content-hash = "a5fb02dcee08f3bbddd7e75c3b5a58b6c82f8d86337d8e9d6deddc0df53fcaf4" [metadata.files] +astroid = [ + {file = "astroid-2.11.3-py3-none-any.whl", hash = "sha256:f1af57483cd17e963b2eddce8361e00fc593d1520fe19948488e94ff6476bd71"}, + {file = "astroid-2.11.3.tar.gz", hash = "sha256:4e5ba10571e197785e312966ea5efb2f5783176d4c1a73fa922d474ae2be59f7"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -413,6 +573,31 @@ attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] +black = [ + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, +] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, @@ -421,6 +606,10 @@ charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] +click = [ + {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"}, + {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"}, +] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, @@ -429,13 +618,17 @@ cycler = [ {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, ] +dill = [ + {file = "dill-0.3.4-py2.py3-none-any.whl", hash = "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f"}, + {file = "dill-0.3.4.zip", hash = "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675"}, +] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] fonttools = [ - {file = "fonttools-4.32.0-py3-none-any.whl", hash = "sha256:b038d1a0dee0079de7ade57071e2e2aced6e35bd697de244ac62938b2b1628c1"}, - {file = "fonttools-4.32.0.zip", hash = "sha256:59a90de72149893167e3d552ae2402c6874e006b9adc3feaf5f6d706fe20d392"}, + {file = "fonttools-4.33.0-py3-none-any.whl", hash = "sha256:14b94aab22439b9bd46c8f44b057b1d2f4b7ebe86556f9b15a1b25694bb12519"}, + {file = "fonttools-4.33.0.zip", hash = "sha256:65d14ab1fe70cbd2f18fca538d98bd45d73e9b065defb843da71dc3c454deb45"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -449,6 +642,10 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] kiwisolver = [ {file = "kiwisolver-1.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e395ece147f0692ca7cdb05a028d31b83b72c369f7b4a2c1798f4b96af1e3d8"}, {file = "kiwisolver-1.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b7f50a1a25361da3440f07c58cd1d79957c2244209e4f166990e770256b6b0b"}, @@ -494,6 +691,45 @@ kiwisolver = [ {file = "kiwisolver-1.4.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42f6ef9b640deb6f7d438e0a371aedd8bef6ddfde30683491b2e6f568b4e884e"}, {file = "kiwisolver-1.4.2.tar.gz", hash = "sha256:7f606d91b8a8816be476513a77fd30abe66227039bd6f8b406c348cb0247dcc9"}, ] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, + {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, +] matplotlib = [ {file = "matplotlib-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:456cc8334f6d1124e8ff856b42d2cc1c84335375a16448189999496549f7182b"}, {file = "matplotlib-3.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a77906dc2ef9b67407cec0bdbf08e3971141e535db888974a915be5e1e3efc6"}, @@ -531,6 +767,14 @@ matplotlib = [ {file = "matplotlib-3.5.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14334b9902ec776461c4b8c6516e26b450f7ebe0b3ef8703bf5cdfbbaecf774a"}, {file = "matplotlib-3.5.1.tar.gz", hash = "sha256:b2e9810e09c3a47b73ce9cab5a72243a1258f61e7900969097a817232246ce1c"}, ] +mccabe = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] numpy = [ {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, @@ -591,6 +835,10 @@ pandas = [ {file = "pandas-1.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:edda9bacc3843dfbeebaf7a701763e68e741b08fccb889c003b0a52f0ee95782"}, {file = "pandas-1.1.5.tar.gz", hash = "sha256:f10fc41ee3c75a474d3bdf68d396f10782d013d7f67db99c0efbfd0acb99701b"}, ] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] pillow = [ {file = "Pillow-9.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea"}, {file = "Pillow-9.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652"}, @@ -631,6 +879,10 @@ pillow = [ {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, ] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, @@ -639,6 +891,10 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +pylint = [ + {file = "pylint-2.13.7-py3-none-any.whl", hash = "sha256:13ddbbd8872c804574149e81197c28877eba75224ba6b76cd8652fc31df55c1c"}, + {file = "pylint-2.13.7.tar.gz", hash = "sha256:911d3a97c808f7554643bcc5416028cfdc42eae34ed129b150741888c688d5d5"}, +] pyparsing = [ {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, @@ -700,6 +956,32 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +typed-ast = [ + {file = "typed_ast-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ad3b48cf2b487be140072fb86feff36801487d4abb7382bb1929aaac80638ea"}, + {file = "typed_ast-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:542cd732351ba8235f20faa0fc7398946fe1a57f2cdb289e5497e1e7f48cfedb"}, + {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc2c11ae59003d4a26dda637222d9ae924387f96acae9492df663843aefad55"}, + {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd5df1313915dbd70eaaa88c19030b441742e8b05e6103c631c83b75e0435ccc"}, + {file = "typed_ast-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:e34f9b9e61333ecb0f7d79c21c28aa5cd63bec15cb7e1310d7d3da6ce886bc9b"}, + {file = "typed_ast-1.5.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f818c5b81966d4728fec14caa338e30a70dfc3da577984d38f97816c4b3071ec"}, + {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3042bfc9ca118712c9809201f55355479cfcdc17449f9f8db5e744e9625c6805"}, + {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fff9fdcce59dc61ec1b317bdb319f8f4e6b69ebbe61193ae0a60c5f9333dc49"}, + {file = "typed_ast-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8e0b8528838ffd426fea8d18bde4c73bcb4167218998cc8b9ee0a0f2bfe678a6"}, + {file = "typed_ast-1.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef1d96ad05a291f5c36895d86d1375c0ee70595b90f6bb5f5fdbee749b146db"}, + {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed44e81517364cb5ba367e4f68fca01fba42a7a4690d40c07886586ac267d9b9"}, + {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f60d9de0d087454c91b3999a296d0c4558c1666771e3460621875021bf899af9"}, + {file = "typed_ast-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9e237e74fd321a55c90eee9bc5d44be976979ad38a29bbd734148295c1ce7617"}, + {file = "typed_ast-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee852185964744987609b40aee1d2eb81502ae63ee8eef614558f96a56c1902d"}, + {file = "typed_ast-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27e46cdd01d6c3a0dd8f728b6a938a6751f7bd324817501c15fb056307f918c6"}, + {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d64dabc6336ddc10373922a146fa2256043b3b43e61f28961caec2a5207c56d5"}, + {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8cdf91b0c466a6c43f36c1964772918a2c04cfa83df8001ff32a89e357f8eb06"}, + {file = "typed_ast-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:9cc9e1457e1feb06b075c8ef8aeb046a28ec351b1958b42c7c31c989c841403a"}, + {file = "typed_ast-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e20d196815eeffb3d76b75223e8ffed124e65ee62097e4e73afb5fec6b993e7a"}, + {file = "typed_ast-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37e5349d1d5de2f4763d534ccb26809d1c24b180a477659a12c4bde9dd677d74"}, + {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f1a27592fac87daa4e3f16538713d705599b0a27dfe25518b80b6b017f0a6d"}, + {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8831479695eadc8b5ffed06fdfb3e424adc37962a75925668deeb503f446c0a3"}, + {file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"}, + {file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"}, +] typing-extensions = [ {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, @@ -708,6 +990,72 @@ urllib3 = [ {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] +wrapt = [ + {file = "wrapt-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5"}, + {file = "wrapt-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330"}, + {file = "wrapt-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c"}, + {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561"}, + {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa"}, + {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a"}, + {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131"}, + {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8"}, + {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763"}, + {file = "wrapt-1.14.0-cp310-cp310-win32.whl", hash = "sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff"}, + {file = "wrapt-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3"}, + {file = "wrapt-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0"}, + {file = "wrapt-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425"}, + {file = "wrapt-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48"}, + {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb"}, + {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e"}, + {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3"}, + {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8"}, + {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd"}, + {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036"}, + {file = "wrapt-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8"}, + {file = "wrapt-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06"}, + {file = "wrapt-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4"}, + {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80"}, + {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce"}, + {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279"}, + {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653"}, + {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0"}, + {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9"}, + {file = "wrapt-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68"}, + {file = "wrapt-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3"}, + {file = "wrapt-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d"}, + {file = "wrapt-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38"}, + {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7"}, + {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1"}, + {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8"}, + {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd"}, + {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe"}, + {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0"}, + {file = "wrapt-1.14.0-cp38-cp38-win32.whl", hash = "sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f"}, + {file = "wrapt-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e"}, + {file = "wrapt-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1"}, + {file = "wrapt-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4"}, + {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758"}, + {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d"}, + {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b"}, + {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6"}, + {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0"}, + {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c"}, + {file = "wrapt-1.14.0-cp39-cp39-win32.whl", hash = "sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350"}, + {file = "wrapt-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc"}, + {file = "wrapt-1.14.0.tar.gz", hash = "sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311"}, +] zipp = [ {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..3253109f --- /dev/null +++ b/pylintrc @@ -0,0 +1,564 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Files or directories to be skipped. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns=^\.# + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + pylint.extensions.check_elif, + pylint.extensions.bad_builtin, + pylint.extensions.docparams, + pylint.extensions.for_any_all, + pylint.extensions.set_membership, + pylint.extensions.code_style, + pylint.extensions.overlapping_exceptions, + pylint.extensions.typing, + pylint.extensions.redefined_variable_type, + pylint.extensions.comparison_placement, + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-allow-list= + +# Minimum supported python version +py-version = 3.7.2 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +# confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + use-symbolic-message-instead, + useless-suppression, + fixme + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" + +disable= + attribute-defined-outside-init, + duplicate-code, + invalid-name, + missing-docstring, + protected-access, + too-few-public-methods, + # handled by black + format, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables 'fatal', 'error', 'warning', 'refactor', 'convention' +# and 'info', which contain the number of messages in each category, as +# well as 'statement', which is the total number of statements analyzed. This +# score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Activate the evaluation score. +score=yes + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Signatures are removed from the similarity computation +ignore-signatures=no + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.* + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Maximum number of lines in a module +max-module-lines=2000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,}$ + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,}$ + +# Regular expression which can overwrite the naming style set by typevar-naming-style. +#typevar-rgx= + +# Regular expression which should only match function or class names that do +# not require a docstring. Use ^(?!__init__$)_ to also check __init__. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# List of decorators that define properties, such as abc.abstractproperty. +property-classes=abc.abstractproperty + + +[TYPECHECK] + +# Regex pattern to define which classes are considered mixins if ignore-mixin- +# members is set to 'yes' +mixin-class-rgx=.*MixIn + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=SQLObject, optparse.Values, thread._local, _thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent,argparse.Namespace + +# List of decorators that create context managers from functions, such as +# contextlib.contextmanager. +contextmanager-decorators=contextlib.contextmanager + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=10 + +# Maximum number of locals for function / method body +max-locals=25 + +# Maximum number of return / yield for function / method body +max-returns=11 + +# Maximum number of branch for function / method body +max-branches=27 + +# Maximum number of statements in function / method body +max-statements=100 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# List of qualified class names to ignore when counting class parents (see R0901). +ignored-parents= + +# Maximum number of attributes for a class (see R0902). +max-attributes=11 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=25 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# List of regular expressions of class ancestor names to +# ignore when counting public methods (see R0903). +exclude-too-few-public-methods= + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp,__post_init__ + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception + + +[TYPING] + +# Set to ``no`` if the app / library does **NOT** need to support runtime +# introspection of type annotations. If you use type annotations +# **exclusively** for type checking of an application, you're probably fine. +# For libraries, evaluate if some users what to access the type hints at +# runtime first, e.g., through ``typing.get_type_hints``. Applies to Python +# versions 3.7 - 3.9 +runtime-typing = no + + +[DEPRECATED_BUILTINS] + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,input + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[CODE_STYLE] + +# Max line length for which to sill emit suggestions. Used to prevent optional +# suggestions which would get split by a code formatter (e.g., black). Will +# default to the setting for ``max-line-length``. +#max-line-length-suggestions= diff --git a/pyproject.toml b/pyproject.toml index f9c849b5..96469450 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,12 +14,18 @@ matplotlib = "^3.3.4" requests = "^2.8.1" pytest = {version = "^7.1.1", optional = true} pytest-xdist = {version = "^2.5.0", optional = true} +pylint = {version = "^2.13.7", optional = true} +black = {version = "^22.3.0", optional = true} [tool.poetry.extras] -dev = ["pytest", "pytest-xdist"] +dev = ["pytest", "pytest-xdist", "pylint", "black"] # Do NOT use [tool.poetry.dev-dependencies]. See: https://github.com/python-poetry/poetry/issues/3514 +[tool.black] +line-length = 80 +target-version = ['py37'] + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/tests/__init__.py b/tests/__init__.py index 65ad25c1..b280897f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,7 +8,7 @@ def setup_module(): # Raise exceptions for arithmetic errors, except underflow global _np_error_state _np_error_state = np.seterr() - np.seterr('raise', under='ignore') + np.seterr("raise", under="ignore") def teardown_module(): diff --git a/tests/test_annotation.py b/tests/test_annotation.py index c7d0f4d3..d4b2bcb1 100644 --- a/tests/test_annotation.py +++ b/tests/test_annotation.py @@ -21,60 +21,76 @@ def test_1(self): Target file created with: rdann -r sample-data/100 -a atr > ann-1 """ - annotation = wfdb.rdann('sample-data/100', 'atr') - + annotation = wfdb.rdann("sample-data/100", "atr") # This is not the fault of the script. The annotation file specifies a # length 3 - annotation.aux_note[0] = '(N' + annotation.aux_note[0] = "(N" # aux_note field with a null written after '(N' which the script correctly picks up. I am just # getting rid of the null in this unit test to compare with the regexp output below which has # no null to detect in the output text file of rdann. # Target data from WFDB software package - lines = tuple(open('tests/target-output/ann-1', 'r')) + lines = tuple(open("tests/target-output/ann-1", "r")) nannot = len(lines) target_time = [None] * nannot - target_sample = np.empty(nannot, dtype='object') + target_sample = np.empty(nannot, dtype="object") target_symbol = [None] * nannot - target_subtype = np.empty(nannot, dtype='object') - target_chan = np.empty(nannot, dtype='object') - target_num = np.empty(nannot, dtype='object') + target_subtype = np.empty(nannot, dtype="object") + target_chan = np.empty(nannot, dtype="object") + target_num = np.empty(nannot, dtype="object") target_aux_note = [None] * nannot RXannot = re.compile( - '[ \t]*(?P